Skip to content

Release/2.1.0 - WIP#119

Closed
JJJ wants to merge 183 commits into
masterfrom
release/2.1.0
Closed

Release/2.1.0 - WIP#119
JJJ wants to merge 183 commits into
masterfrom
release/2.1.0

Conversation

@JJJ

@JJJ JJJ commented Aug 31, 2021

Copy link
Copy Markdown
Collaborator

Work in progress!

JJJ and others added 29 commits August 30, 2021 21:24
* Fix return types in Query
* Docs: improvents recommended by phpstan-wordpress.

Props szepeviktor.

Co-authored-by: Viktor Szépe <viktor@szepe.net>
This change ensures that, going forward, this methods parameter signature better matches the other _item() methods.

This should be /relatively/ safe to do thanks to it being private. I've looked at all of the projects I'm aware of that use Berlin, and they are all uneffected.
This refactors the Table::create() SQL generator to explode an array of parts, which should make it easier to make more edits to later.
* Column: improvements to $pattern
* Always a string (no false)
* Add link to PHP docs
* Update description to remove "string replace"
* Set UUID to %s
* Base: prevent double prefixing in apply_prefix()
* Column: correct inline doc
* Column: rename private references from pattern to format.
* Autoloader: minor code cleanup
* Column: various improvements:
* Tons of inline & block docs
* Smarter default class values
* Column::args are saved during parse_args() for later reuse
* Prefer get_object_vars() over another array of args
* Add "extra" support to special_args()
* Add is_ methods for some other types
* Add is_extra() method for comparing extra values
* Add sanitize_extra() for allowing specific values
* Improve fallback support in sanitize_pattern()
* Improve fallback support in sanitize_validation()
* Remove function_exists check for gmdate() from validate_datetime()
* Legitimize validate_numeric() and use where appropriate
* Improve get_create_string() with support for binary, more types, null, etc...
* Base: introduce sanitize_column_name()
* Allows upper-case letters in table and column names.
* Swaps out sanitize_key() usage for a custom preg_replace: '/^[a-zA-Z0-9_\-]+$/'
* Table: use Base::sanitize_column_name()
* Also fix return value inline comment in count()
* Column: introduce validate() and validate_int()
* validate() centralizes column value validation into the most logical location, and validate_int() allows for falling back to $default in a way that intval obviously could not.
* Base: update regex.
* Base: add stash_args() method
* Also avoid errors in apply_prefix() if not a string.
* Column: use stash_args()
* Also bail early if no arguments to parse.
* Schema: add support for indexes.
* Move filters into methods and their own section
* Improve docs, and add missing docs
* Default values for item_names to prevent fatals
* Add setup() method and move set_ methods out of __construct() and into it
* Minimize touches to this->columns for future Schema/Structure work
* Remove assumptions that primary column must be an int that uses absint/intval - see #124
* Add some todo's for MySQL 8 improvements
* Improve support for CURRENT_TIMESTAMP in relevant columns
* Add get_columns_field_by() to retrieve a single field from a matching array of values to a single key - primarily used for getting an array of column patterns when querying, to return an array of formats for sprintf()
* Improve readability of do_action_ref_array() calls
* Clean-up get_item_ids()
* Rename parse_where() to parse_query_vars()
* Add parse_where() and parse_join() – likely get renamed in the future
* Use wp_parse_list() instead of wp_parse_id_list() - likely needs its own handler
* Prevent fatals from return values of get_search_sql()
* Abstract repeated code into new get_in_sql() method to escape/prepare/format IN (%s) SQL
* Introduce parse_query_var() and use it in place of repeated query_vars[] touches - attempts to internally parse comma separated strings (might remove)
* Introduce undocumented $column->by check to allow a column to not be queried directly by its name
* Refactor parse_query_vars() to improve its internal patterns, for future abstraction
* Fallback in parse_fields() and parse_groupby() to prevent fatal errors
* Introduce parse_single_orderby, parse_limits, parse_join, and parse_where - refactor parse_orderby
* Some minor clean-up to shape_items()
* Bail early in get_item_fields() to avoid trying to filter empty fields
* Introduce validate_item_field() to call $column->validate(), and use it inside shape_item_id() and more. This centralizes validation and ensures they always return the same results.
* Update add_item() and update_item() to skip database if $save fails validation
* Update copy_item() to shape the item ID, as it is not done inside of get_item_raw()
* Update delete_item() to match other item changes above
* All _item() functions use get_columns_field_by() to get patterns to send into wpdb queries for proper formatting (including delete_all_item_meta) - see #137
* Update validate_item() to use validate_item_field()
* Use shape_item_id() in update_item_cache() - also use is_scalar() in place of is_numeric() when making assumptions about the shape of the primary column
* Stop shaping the ID inside of get_non_cached_ids(), as item IDs are (or should be) previously shaped
* Gut get_results() and make it use the query() method - this needs more work
* Query: Introduce filter_search_columns()
* Switch it to using apply_filters_ref_array() - minor back-compat break
* Add direct Schema support
* Get columns directly from Schema
* Deprecate $columns var
* Introduce set_query_clause_defaults() for allowing the query & request clauses to be updated easier
* Add keys to query & request clauses
* Refactor the way that counts & searches are parsed
* Always include columns when count & groupby are used together
* Pass query_vars into more parse_ methods to further abstract their usages for future un-privating
* Override query_vars in parse_query() when counting
* Introduce parse_count()
* Rename parse_where/join to _clauses() suffix
* Pass arguments into default_item(), and use array_combine()
* Swap some var orders in prime_item_caches()
* All: update @copyright and README
* Remove $columns
* Improve query parsing to allow reuse for second COUNT(*) query_clause overrides
* Add support for SELECT & EXPLAIN clauses
* Add several new methods to abstract out newly repeated behaviors
* Add is_valid_column() and get_query_var() and get_column_name_alias() to help with repeated code patterns
* Add parse_query_vars() again, to help abstract only the parsing part
* Use get_meta_type() when updating meta data
* Update prime_item_caches() to not bail early so it can continue on and try updating meta data
* Base: minor refactor to magic methods, and is_success()
* Query: MySQL 8 support
* Remove $columns
* Improve query parsing to allow reuse for second COUNT(*) query_clause overrides
* Add support for SELECT & EXPLAIN clauses
* Add several new methods to abstract out newly repeated behaviors
* Add is_valid_column() and get_query_var() and get_column_name_alias() to help with repeated code patterns
* Add parse_query_vars() again, to help abstract only the parsing part
* Use get_meta_type() when updating meta data
* Update prime_item_caches() to not bail early so it can continue on and try updating meta data
* Resolve PHPStan Level 0 errors
* Fix boolean handling
* Revert change for get_sql
* Make $index public
This ends up being easier to understand than following the code backwards.
* Query: Add support for query handlers.
* Break apart parse_where() into multiple methods
* These new methods return an array of where & join clauses
* Those clauses get merged together to maintain backwards compatibility
* Query: normalize Join/Where order.
* Query: graduate "join" to first-class clause.
* Schema: these need to stay protected
* Query: remove array_flip from get_column_names
* Query: docs
* All: improve code consistency
* Bail early with specific (non retval) values
* Improve inline docs
* Remove some unuseful references
@szepeviktor

Copy link
Copy Markdown
Contributor

Once this was a gift for a WP plugin developer: https://gist.github.com/szepeviktor/ddb1bfd12d93accd318cc081637956ec

JJJ and others added 28 commits May 26, 2026 10:37
…ly correct

- Query: drop `is_string($item_shape) &&` (get_current_string guarantees string|null)
- Query: simplify `is_scalar($found_items_val) ? (int)... : 0` → `(int)$found_items_val`
- Meta: drop both `is_scalar($type)` ternaries (type is already a valid string at those points)
- In/NotIn/By: replace three is_string two-liners per file with (string) casts at call site
- Search: same for get_quoted_column_name_aliased and get_item_name_plural callers
- Lifecycle: add get_current_string(), get_current_array(), get_current_int() typed accessors

No behaviour change — these guards all handled conditions that cannot occur in the
established call paths. Casting at the call site makes the type contract explicit
without adding defensive middleware.
PHPCS now exits 0 with zero errors and zero warnings across all 42 source
files. PHPStan level 8 remains clean.

Excluded from phpcs.xml (intentional deviations):
- WordPress.Files.FileName.*: PSR-4 PascalCase filenames
- WordPress.Arrays.ArrayKeySpacingRestrictions: $arr[ 'key' ] spacing style
- Squiz.Commenting.FunctionComment.IncorrectTypeHint: PHPDoc uses list<T> /
  array<K,V> generics; PHP runtime type hint is always array
- Squiz.PHP.CommentedOutCode: too many false positives on PHPStan @var
  annotations, MySQL keyword labels, and URL comments

Key changes:
- Restored $arr[ 'key' ] bracket spacing across all src/ files (phpcbf had
  stripped it during earlier auto-fix passes)
- Added proper short descriptions to property docblocks in all 17 operator
  and 7 parser concrete subclasses
- Changed inline /** @var Type $var */ type assertions to use phpcs:ignore
  MissingShort (must stay as /** */ — PHPStan only narrows on doc comments)
- Added @param descriptions to 87 bare @param lines; added trailing periods
  to 20 param descriptions missing them
- Replaced mt_rand() with wp_rand() in Column.php UUID generation
- Added phpcs:ignore for MySQL-returned properties (Msg_text, Checksum) in
  Table.php that cannot be renamed
- Collapsed multi-line ternaries to single lines where phpcs:ignore was needed
- Capitalized long/short descriptions; added periods to inline comments;
  fixed block comment endings and spacing issues
…_cache()

$last_changed was the last ephemeral property not consolidated into $current[].
With the old EDD/SC guard already removed (fix for #160), the
property served no cross-call purpose — set_last_changed() was called and read
back within the same two lines of update_last_changed_cache(). Inline it as a
local variable and delete the property and method entirely.

Adds QueryCacheTest::test_cache_is_invalidated_after_delete to cover the #160
regression scenario: delete an item, re-query on the same instance, expect an
empty result rather than the stale cached row.
…ion hooks

Five tests across three files, all documenting previously uncovered behaviour:

- QueryCrudTest: get_item_by() returns false for '0' and 0 column values —
  documents the empty($column_value) guard (empty('0') and empty(0) are both
  true in PHP, so these bail before the database regardless of table contents)

- QueryFilterTest: orderby => '' falls back to the primary column via
  parse_single_orderby() → get_primary_column_name(), returns a non-empty
  result set in ID order

- QueryTransitionTest (new): transition hook fires on add_item() with 'new'
  as old_value (WordPress new-item convention); fires on update_item() when
  status changes; does not fire when status is unchanged (array_diff bail)
copy_item() passed the full raw row to add_item() without stripping the UUID.
Column::validate_uuid() preserves any existing valid UUID unchanged ("UUIDs
should never change once they are set"), so every copy silently inherited the
original's UUID — two rows, one identifier.

Fix: unset 'uuid' from $save before the $data override merge. This lets
add_item() generate a fresh UUID via validate_uuid(). A UUID explicitly
provided in the $data override array is still respected (restored by the merge).

Adds test_copy_item_generates_distinct_uuid() to QueryCrudTest to pin the
corrected behaviour and catch any regression.
Introduce a small Log trait for structured in-memory diagnostic logging without adding another WordPress-specific dependency. The trait provides protected log collection, public log access/clearing helpers, and a no-op write_log() bridge for projects that want to forward entries to debug.log, error_log(), Monolog, Query Monitor, or another writer.

Compose Log into the shared Base trait so BerlinDB core objects can use it consistently, and add focused tests covering stored entries, level filtering, clearing, writer bridging, and empty-entry guards.

(Also update Trait doc-block text to be shorter)
write_log() is always called with a real entry — the array $entry = array()
default implied it could meaningfully be called with no args, which it cannot.
Remove the default from both the trait and the LogTestSubject override.

Add three tests that were absent from the initial pass:
- get_logs() returns [] before any entries are recorded
- entries are returned in insertion order
- clear_logs($level) re-indexes remaining entries so keys stay sequential
…, call sites

Adds a machine-readable $code field to every log entry so callers and
readers can match on stable event identifiers independently of message
text.

get_logs() and clear_logs() now accept an $args field/value filter and
a 'and'/'or' operator, backed by the new protected filter_logs() and
log_matches() helpers (both now carry explicit return type hints).

Adds four semantic log helpers that build consistent entries from
common diagnostic scenarios:
- log_empty_value()
- log_class_not_found()
- log_class_instantiation_failed()
- log_method_not_found()

Each helper accepts an optional $caller array (callable-style [object,
method]) and a $context override array whose special keys (code,
message, level) promote to the entry top level so callers can
customise without rebuilding the full entry. Supporting internals:
get_log_code(), get_log_caller_context(), get_log_class_short_name(),
normalize_log_key().

First real call sites land in Query::set_schema() and
Table::set_schema(), covering the four failure modes (empty schema,
class not found, instantiation failure, missing required method) with
stable event codes (query_schema_* / table_schema_*). Downstream
callers already guard with is_callable() so a partial schema object
does not crash; the log entry surfaces the misconfiguration for
debugging without aborting the boot sequence.

LogTest expanded to 10 assertions covering the new $code field, AND/OR
field filtering, clear_logs() re-indexing, write_log() bridge, and
empty-input guards. QuerySchemaLogTest and TableSchemaLogTest added to
verify each set_schema() failure path records the correct log code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a lightweight Log trait that gives every BerlinDB kernel
class (Query, Table, Column, Schema, Index, Row) a shared, structured
in-memory log. Entries carry a stable machine-readable code, level,
human-readable message, key-value context, timestamp, and source class.

Core API:
- log() — protected; normalises level/code, trims message, guards
  empties, calls the write_log() hook so subclasses can bridge to any
  external log destination without overriding the storage logic.
- get_logs( $args, $operator ) — returns all entries or those matching
  field/value pairs in AND (default) or OR mode.
- clear_logs( $args, $operator ) — removes matching entries (or all)
  and re-indexes the remaining array.
- write_log( $entry ) — no-op hook; override to forward to error_log(),
  Monolog, Query Monitor, etc.

filter_logs() / log_matches() carry explicit :array / :bool return
types and the full entry shape in their docblocks so PHPStan can track
the specific array shape through filtering without widening it.

First real call sites land in Query::set_schema() and
Table::set_schema(), each logging a single structured entry under the
stable code query_schema_unavailable / table_schema_unavailable when
the schema class is empty, missing, throws on construction, or lacks
the required method. Downstream callers already guard with is_callable()
so a partial schema object does not abort the boot sequence; the log
entry surfaces the misconfiguration for debugging.

Tests: LogTest (10), QuerySchemaLogTest (3), TableSchemaLogTest (3).

phpcs.xml updated from WordPress to WordPress-Core to align with
phpcs.xml.dist, and with the standard exclusion set from that baseline:
  - Generic.Files.OneObjectStructurePerFile excluded for tests/
    (multiple helper classes per file is the normal test pattern)
  - WordPress.DB.DirectDatabaseQuery / SlowDBQuery excluded for tests/
    (integration tests intentionally issue direct queries)
  - Squiz.Commenting.FunctionComment, DocComment.ShortNotCapital,
    DocComment.LongNotCapital, and related noise sniffs excluded globally
    (consistent with phpcs.xml.dist and eliminates ~150 pre-existing
    false positives in the test suite that were blocking CI)

PHPStan level 8: 0 errors. PHPCS: 0 errors. PHPUnit: 498/498.

Closes #153

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add coverage for PHP's same-family protected property behavior, where
a subclass can read a protected property from another object in the same
inheritance tree without invoking __get().

Also add Query-specific coverage showing the practical BerlinDB result:
external table_name access resolves through get_table_name(), while
sibling Query subclass access reads the raw configured/prefixed property
value directly.

Refs #46
Phase 1 — Schema object injection (Query + Table)

$table_schema on Query and $schema on Table now accept either a class
name string (the existing subclass pattern, unchanged) or a live Schema
instance. set_schema() checks instanceof first and short-circuits; the
existing string path is untouched. This makes both Boot-based
constructor injection and direct property assignment work:

  new Query( array( 'table_schema' => $schema ) )
  new Table( array( 'schema'       => $schema ) )

No existing subclass is affected — the instanceof guard only fires when
a Schema object is present.

Phase 2 — MySQL introspection factories

Column::from_mysql( array $row )
  Maps a single SHOW COLUMNS row (Field, Type, Null, Key, Default,
  Extra) to a fully constructed Column. The type string is parsed with
  a single regex into base_type, length, unsigned, and zerofill. Null,
  Key, and Default map to allow_null, primary, and default. Passing
  empty strings for pattern, cast, and validate triggers the existing
  auto-inference in sanitize_pattern(), sanitize_cast(), and
  sanitize_validation(), so the correct cast and format are inferred
  from the column type at construction time.

Schema::from_table( string $table )
  Queries SHOW COLUMNS FROM $table via the get_db() wrapper (no global
  $wpdb), maps each row through Column::from_mysql(), and returns a
  ready Schema. Returns an empty Schema if the table does not exist or
  the DB interface is unavailable.

Together these close the loop: a live MySQL table can now be reverse-
engineered into a working Query in a few lines:

  $schema = Schema::from_table( $wpdb->prefix . 'posts' );
  $query  = new Query( array( 'table_schema' => $schema ) );

PHPStan level 8: 0 errors. PHPCS: 0 errors. PHPUnit: 500/500.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two new test classes covering the MySQL introspection factories introduced
in the previous commit. ColumnFromMysqlTest (33 tests) exercises from_mysql()
with hand-crafted SHOW COLUMNS rows and documents all sanitization side-effects
— type and extra are normalised to uppercase, string defaults collapse to '' due
to the validate-callback ordering constraint in sanitize_args(). SchemaFromTableTest
(27 integration tests) issues live SHOW COLUMNS queries against wp_posts and
wp_users and asserts column count, type, primary-key, date_query, and ordering
invariants that hold across all supported WordPress versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Column::sanitize_default() was discarding string defaults because it called
validate($fallback) with only one argument, leaving the method's own $fallback
parameter at its empty-string default. Passing the value in both positions —
validate($value, $value) — preserves it when no validate callback is registered
at construction time (as is the case during from_mysql() introspection).

Environment::get_db_global() is a new static accessor that holds the DB
resolution logic; get_db() now delegates to it instead of duplicating it.
Schema::from_table() uses the static accessor directly (eliminating the
throwaway-instance smell) and wraps the SHOW COLUMNS query in
suppress_errors() so a missing table silently returns an empty Schema rather
than printing an HTML error block to the page.

Test suite: ColumnFromMysqlTest now builds shared column fixtures in
setUpBeforeClass() instead of repeating the same six-key array nine times;
the varchar-default assertion is corrected to 'publish' and the missing-key
assertion is corrected to false; SchemaFromTableTest drops the manual
suppress_errors() wrappers that are no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Abstracts the database contract behind a Connection interface
(Interfaces/Connection.php) with a Wpdb adapter wrapping \wpdb and a
NullConnection adapter implementing the Null Object pattern. The
Environment trait now always returns a Connection — never false — so
the ~50 empty($db) early-return guards scattered across Query, Table,
Schema, Parsers, Operators, and Traits were all dead code and have been
removed. NullConnection::get_table_prefix() is a pass-through, which
preserves the pre-guard fallback behaviour in get_table_name() at zero
cost. PHPStan level 8 clean, PHPCS clean, 560/560 tests passing.
get_db_global() now caches the Wpdb adapter in a static array keyed by
global name. spl_object_id() guards the cache entry so a replaced $wpdb
instance (e.g. in tests) correctly yields a fresh adapter rather than
reusing the stale one. Because the adapter holds a reference to the
underlying \wpdb object rather than a snapshot, prefix mutations from
switch_to_blog() are visible through the cached adapter with no extra
work — the seven new EnvironmentTest cases cover this explicitly,
including a direct assertion that $wpdb->prefix mutations are reflected
after a cache hit.
Renames the protected accessor on the Environment trait from get_db()
to db() — shorter and consistent with non-get_ accessor conventions.
All 48 call sites are updated: the "// Get the database interface." /
"$db = $this->db();" two-line preamble is removed and uses are inlined
as $this->db()->method(). A deprecated get_db() alias is retained on
the trait so existing subclasses (EDD, Sugar Calendar, etc.) continue
to compile without changes. Two justified exceptions remain: Schema::from_table()
is static so it uses $db = self::get_db_global(), and Search::get_search_sql()
keeps a local $db because array_map requires an object reference for its
callable argument. Static adapter cache makes every inlined call a single
array lookup at no meaningful cost.

Fixes #69.
Closes #139. Adds 'cache_results' (default true) to query_var_defaults,
mirroring WP_Query/WP_Term_Query's pattern. When false, the result-list
cache is neither read nor written for that query, giving callers a
per-query escape hatch without resorting to wp_cache_flush(). The var is
excluded from cache key generation so cached and uncached calls for the
same args share the same key slot.
NullConnectionTest (PHPUnit\TestCase): verifies all Connection interface
methods return inert/safe values and that set_table_prefix/register_table
are no-ops. WpdbTest (WPIntegration\TestCase): verifies delegation to
the live $wpdb instance for prepare, esc_like, property accessors, and
the dynamic table-prefix registry, with state restoration after each
case. QueryGetResultsTest (PHPUnit\TestCase): uses a query subject that
captures args without a database to cover the three arg-mapping paths in
get_results(). ErrorTest (PHPUnit\TestCase): data-provider-driven coverage
of is_success() for all failure sentinels (false, null, 0), positive
values, and the WP_Error stash.
Use ALTER TABLE ... RENAME TO for Table::rename() so wpdb treats the
operation as DDL and returns a usable success value. Also prevent
Boot::parse_args() from restoring its internal args stash through
set_vars() when merging constructor defaults.

Add coverage for table copy, rename, repair, Boot argument stashing, and
existing Query hook smoke tests.
Add direct tests for Parser::get_cast_for_type() and sanitize_query()
behavior around invalid numeric children and OR relation tracking.

This complements the existing concrete parser integration coverage without
changing production code.
Add a CI workflow with separate PHPStan, PHPCS, and PHPUnit jobs for
pull requests and pushes to main, trunk, and release branches.

Allow the Docker PHPUnit runner to skip its built-in PHPCS step via
SKIP_PHPCS so CI job names map cleanly to the work they perform, while
preserving local test-runner behavior by default.
Add export-ignore rules so Composer dist archives only include the
runtime package files: license, readme, autoloader, composer metadata,
and src.

Declare PHP 8.1 as the supported minimum, update the Composer platform
to 8.1, and refresh the lock file metadata. PHP 8.0 is close at runtime,
but the current dev/test dependency stack requires PHP 8.1+.

Remove the stale hardcoded package version and add GitHub support links.

Expand PHPUnit CI to run on PHP 8.1 and 8.2, and give PHPStan a 1G
memory limit for more reliable CI runs.

Verified with composer validate, PHPCS, PHPStan, PHPUnit on PHP 8.1 and
PHP 8.2, and a Composer archive inspection.
Rewrite the README around installation, requirements, quick-start usage,
documentation links, and development workflow.

Add changelog, contribution guide, security policy, release checklist,
issue templates, pull request template, and markdownlint configuration.

Add Packagist-friendly Composer metadata and tighten export-ignore rules
so Composer archives stay focused on runtime package files.

Add a manual Pre-Release workflow that runs static checks, supported
PHPUnit lanes, changelog validation, and Composer archive inspection.
@JJJ

JJJ commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

I'm going to close this PR.

Everything in this branch is also in the 3.0.0 branch and tag.

Thanks to everyone for helping 💞

@JJJ JJJ closed this May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants