Release/3.0.0 - WIP#147
Merged
Merged
Conversation
* Traits * Parsers (yuck) * Operators (yuck) A bunch of things are broken here.
Thanks, Claude <3
Query: parse_where_parsers() - Fix $args['table_alias'] -> $args['primary_alias']; 'table_alias' key does not exist, so every Meta/Date/Compare clause alias was null - Uncomment and expand the $qv narrowing line; Meta, Date, and Compare parsers were receiving the full $query_vars blob instead of their own sub-array, producing no SQL for any meta/date/compare query - Add is_array() guard to narrowing so Search stays on full $query_vars; without it, a scalar 'search' string would be passed as the query array - Change Search parser 'default' from null to ''; null fed through to the random sentinel value, which meant every query ran a LIKE search for the sentinel string regardless of whether the user searched for anything Meta: get_sql_for_clause() - Remove early return $retval that blocked the entire method body - Remove debug lines ($hello / var_dump) - Fix hardcoded $db->postmeta -> $this->meta_table and post_ID -> $this->meta_column in the NOT EXISTS subquery builder; would have generated invalid SQL for any non-post object type - Initialize $subquery_alias, $meta_compare_string_start, and $meta_compare_string_end to '' before the if ($neg) block - Rename $operator to $regex_op in REGEXP/RLIKE cases to avoid shadowing the outer $operator object - Remove dead $operator = $meta_compare_key assignment in NOT REGEXP case - Move $retval definition before the get_db() bail so bail can return it - Add missing if (empty($db)) guard - Inline $_meta_type assignment using null coalescing operator - Fix get_first_keys() to return the array directly (parameter was unused) - Docblock: @param Query $caller -> @param \BerlinDB\Database\Query|null - Docblock: @return string[] -> @return array with @type string[] per key - Fix stale comments ("sooo", "Already exists?") Compare: extend Base instead of Meta - Compare::get_sql_for_clause() uses only trait/Base methods; it does not use $this->meta_table, $this->meta_column, or any other Meta property - Inheriting Meta silently dragged in Meta::get_sql()'s _get_meta_table() lookup, which returns false for any custom table type, killing all compare queries with no error - Change extends Meta -> extends Base; no other changes needed By: fix copy-paste file docblock title ("In Query Var" -> "By Query Var") Compare, Date: correct file-level @SInCE from 1.0.0 to 3.0.0; these classes are new in 3.0.0 and did not exist in earlier releases NotIn: align file title to "Not In Query Var Parser Class" to match the naming convention of all other parser files
* Yoda conditions * Whitespace * Docs * Update @SInCE tags to 3.0.0
Refactor and harden Schema item API, layout, and internals. - Fix legacy schema hydration path in setup_items by correctly using item values - Add and organize generic item API methods near add_item: add/get/has/set/remove for collection-level operations - Keep column/index convenience wrappers grouped in Item Helpers - Improve class structure with clearer section boundaries: - Public Item Core, Private Internals, Item Helpers, Validators - Add schema validation safeguards used by CREATE TABLE generation: duplicate names, missing names, unknown index columns, primary key conflicts - Gate get_create_table_string() on schema validity and skip empty SQL fragments - Introduce reusable create_item() helper to centralize item instantiation logic - Reduce repeated work in setup and validation paths for cleaner/faster internals - Preserve backward compatibility and public behavior while improving readability and maintainability
- Update copyright year to 2021-2026 - Expand class docblock to describe Column/Index collections, legacy hydration, validation, and SQL generation - Add override note to $column and $index property docs - Type $columns/$indexes as Column[]/Index[] and document legacy support - Rewrite sunrise()/init() docs to describe Traits\Boot lifecycle role - Add periods to all inline comments missing them - Expand @param for clear() to document the empty-string-clears-all behavior - Update all @return types to use concrete types (Column|false, Index[], string[], etc.) throughout public and private methods - Document the 'primary' name alias in get_item(), remove_item(), get_index(), has_index(), and remove_index() docblocks - Fix get_item_class() @return from "Class object" to class name string - Rewrite get_items_create_string() @return to describe actual output - List all seven validation checks in get_validation_errors() docblock - Document the normalize/trim/regex pipeline in normalize_item_name() - Document accepted aliases in validate_item_type() - Extract private is_primary_index() helper to consolidate the repeated strtolower(trim($item->type)) === 'primary' check from get_item(), remove_item(), and get_validation_errors()
Centralizes the logic and allows it to be reused in several files consistently.
szepeviktor
reviewed
May 14, 2026
Includes regression fixes and general improvements to make work in weirder Docker environments.
Contributor
|
PR sent #190 |
Rename `set_alias()` to `set_table_alias()`, make `get_table_name()` protected, add `get_table_alias()`, and use the accessors when building aliased SQL fragments.
…hreading - Normalize first_letters() through the shared sanitizer and add tests for its behavior. - Update sanitize_column_name() phpdoc to match the other sanitizer methods. - Remove get_sql() positional argument plumbing across Query, Meta, and Parser so parsers use direct query access instead of threaded context arrays. - This simplifies the call chain, reduces coupling, and keeps parser behavior tied to the active query object rather than passing context through multiple layers.
Probably future home of some Platform detection code.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.