diff --git a/CHANGELOG.md b/CHANGELOG.md index e06f773..7f598a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,13 @@ Notable changes to BerlinDB are documented here. `get_create_table_strings()` builder and retains its released zero-argument signature. `Table::create()` appends enforced foreign keys in inline mode, so existing Schema overrides remain compatible. - Column compatibility is limited to APIs predating 3.0: `is_numeric()` retains - its parameterless signature, with explicit type checks on `is_numeric_type()`, - and `validate_datetime()`, `validate_decimal()`, and `validate_uuid()` remain - public. The 3.0-only Column predicates and cast-aware `get_name_sql()` keep their - expanded signatures. Cast-aware operator rendering uses `get_sql_with_cast()`; - custom operators must override that method to customize explicit casts. + Cast-aware operator rendering uses `get_sql_with_cast()`; custom operators must + override it to customize explicit casts. The comparison operator classes + moved under `Comparisons\` + retain aliases at their released `Operators\*` names. Removed newly added native return types from selected released untyped extension - methods while retaining their PHPDoc contracts. The deliberate lifecycle and - strict-config changes below remain separate migration requirements. + methods while retaining their PHPDoc contracts. The intentional `parse_args()` + and Column signature changes are documented below. - Extends the plural write verbs `update_items()` / `delete_items()` to composite-key tables (#241, following the singular verbs in #234). A query-var filter now resolves to @@ -590,8 +588,17 @@ Notable changes to BerlinDB are documented here. query now runs AFTER `init()`; an old `init()` override that expected query results should move that work after `parent::consume_args()` or to `sunset()`. - The `parse_args()` construction hook (`Boot`/`Query`, 3.0.0) is renamed to - `consume_args()`; `parse_args()` is now a `wp_parse_args()`-style array helper. - Rename any override of the leftover-args hook to `consume_args()`. + `consume_args()`; `parse_args()` is now a `wp_parse_args()`-style array helper + with an optional `$defaults` parameter. Rename any override of the leftover-args + hook to `consume_args()` before upgrading: retaining a one-argument override + causes a PHP declaration fatal. +- `Column::is_json()`, `is_bool()`, `is_date_time()`, `is_int()`, `is_decimal()`, + `is_text()`, and `is_binary()` now accept an optional type argument. Subclasses + overriding one of these methods must add the optional parameter before upgrading + or PHP will reject the subclass declaration. +- `Column::get_name_sql()` now accepts an optional `$cast` argument. Subclasses + overriding it must add the optional parameter before upgrading or PHP will reject + the subclass declaration. - Configuration is strict by default — keys outside the declared config surface (`get_config_callbacks()`) are dropped and logged. Declaring a custom property alone does not register it as configuration. Override `is_strict_config()` diff --git a/CLAUDE.md b/CLAUDE.md index d6888d5..b955d2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,7 +46,7 @@ hand-rolled queries — never more surprising. vendor/bin/phpcs ``` 5. **Don't invent APIs.** If unsure how something behaves, search `src/` and - `tests/` — the source and its 1753 test methods are the source of truth, ahead + `tests/` — the source and its 1755 test methods are the source of truth, ahead of memory or training data. (PHPUnit reports more cases: data providers expand methods at run time.) 6. **Keep changes focused and tested.** Bug fixes and new behavior ship with diff --git a/autoloader.php b/autoloader.php index 9fa0945..3f5aa7b 100644 --- a/autoloader.php +++ b/autoloader.php @@ -31,8 +31,31 @@ static function ( $class_name = '' ) { 'BerlinDB\\Database\\Table' => 'BerlinDB\\Database\\Kern\\Table', ); - if ( isset( $legacy_kern_classes[ $class_name ] ) ) { - $target = $legacy_kern_classes[ $class_name ]; + $legacy_operator_classes = array( + 'BerlinDB\\Database\\Operators\\Base' => 'BerlinDB\\Database\\Operators\\Comparisons\\Base', + 'BerlinDB\\Database\\Operators\\Between' => 'BerlinDB\\Database\\Operators\\Comparisons\\Between', + 'BerlinDB\\Database\\Operators\\Equal' => 'BerlinDB\\Database\\Operators\\Comparisons\\Equal', + 'BerlinDB\\Database\\Operators\\Exists' => 'BerlinDB\\Database\\Operators\\Comparisons\\Exists', + 'BerlinDB\\Database\\Operators\\GreaterThan' => 'BerlinDB\\Database\\Operators\\Comparisons\\GreaterThan', + 'BerlinDB\\Database\\Operators\\GreaterThanOrEqual' => 'BerlinDB\\Database\\Operators\\Comparisons\\GreaterThanOrEqual', + 'BerlinDB\\Database\\Operators\\In' => 'BerlinDB\\Database\\Operators\\Comparisons\\In', + 'BerlinDB\\Database\\Operators\\LessThan' => 'BerlinDB\\Database\\Operators\\Comparisons\\LessThan', + 'BerlinDB\\Database\\Operators\\LessThanOrEqual' => 'BerlinDB\\Database\\Operators\\Comparisons\\LessThanOrEqual', + 'BerlinDB\\Database\\Operators\\Like' => 'BerlinDB\\Database\\Operators\\Comparisons\\Like', + 'BerlinDB\\Database\\Operators\\NotBetween' => 'BerlinDB\\Database\\Operators\\Comparisons\\NotBetween', + 'BerlinDB\\Database\\Operators\\NotEqual' => 'BerlinDB\\Database\\Operators\\Comparisons\\NotEqual', + 'BerlinDB\\Database\\Operators\\NotExists' => 'BerlinDB\\Database\\Operators\\Comparisons\\NotExists', + 'BerlinDB\\Database\\Operators\\NotIn' => 'BerlinDB\\Database\\Operators\\Comparisons\\NotIn', + 'BerlinDB\\Database\\Operators\\NotLike' => 'BerlinDB\\Database\\Operators\\Comparisons\\NotLike', + 'BerlinDB\\Database\\Operators\\NotRegexp' => 'BerlinDB\\Database\\Operators\\Comparisons\\NotRegexp', + 'BerlinDB\\Database\\Operators\\Regexp' => 'BerlinDB\\Database\\Operators\\Comparisons\\Regexp', + 'BerlinDB\\Database\\Operators\\Rlike' => 'BerlinDB\\Database\\Operators\\Comparisons\\Rlike', + ); + + $legacy_classes = array_merge( $legacy_kern_classes, $legacy_operator_classes ); + + if ( isset( $legacy_classes[ $class_name ] ) ) { + $target = $legacy_classes[ $class_name ]; $strip = str_replace( 'BerlinDB\\', '', $target ); $name = str_replace( '\\', DIRECTORY_SEPARATOR, $strip ); $file = sprintf( '%1$s/src/%2$s.php', __DIR__, $name ); @@ -76,13 +99,13 @@ class_alias( $target, $class_name ); require_once $file; /* - * Eagerly register this Kern class's legacy alias, if it has one. PHP's + * Eagerly register this class's legacy alias, if it has one. PHP's * instanceof operator does not trigger autoloading, so an `instanceof * \BerlinDB\Database\Index` check would resolve to false until the alias * name was loaded some other way. Creating the alias as soon as the Kern * class loads keeps legacy type checks (instanceof / is_a) correct. */ - $legacy_alias = array_search( $class_name, $legacy_kern_classes, true ); + $legacy_alias = array_search( $class_name, $legacy_classes, true ); if ( ( false !== $legacy_alias ) && ! class_exists( $legacy_alias, false ) ) { class_alias( $class_name, $legacy_alias ); diff --git a/src/Database/Kern/Column.php b/src/Database/Kern/Column.php index 3d3d41c..37bbf2d 100644 --- a/src/Database/Kern/Column.php +++ b/src/Database/Kern/Column.php @@ -2430,18 +2430,16 @@ private function get_datetime_default_sql(): string { * When $alias is provided it is quoted and prepended, producing the fully * qualified form used in WHERE and SELECT clauses: `alias`.`column`. * - * When $cast is a valid CAST target the reference is wrapped in - * CAST( ... AS $cast ). $cast is sanitized here (sanitize_sql_cast_type()), so - * this public helper does not trust its caller - an invalid value is safely - * ignored (no cast). Casting is opt-in and never applied by default. CHAR is a - * real target (string-semantics comparison), not a no-op. + * A valid CAST target wraps the reference in CAST( ... AS $cast ). The cast is + * sanitized here; an invalid value is safely ignored. CHAR is a real target + * for string-semantics comparisons, not a no-op. * * @since 3.0.0 + * @since 3.1.0 Added the $cast parameter. * - * @param string $alias Optional. Table alias to prefix. Default empty (no alias). - * @param string $cast Optional. A CAST target; sanitized internally (invalid => no cast). Default empty. - * - * @return string Quoted SQL reference, e.g. `alias`.`column` or `column`. + * @param string $alias Optional. Table alias to prefix. Default empty. + * @param string $cast Optional. A CAST target. Default empty. + * @return string Quoted, optionally cast SQL reference. */ public function get_name_sql( string $alias = '', string $cast = '' ): string { @@ -2516,8 +2514,8 @@ public function get_sql_cast_type(): string { * * Without a cast, this returns the $type_category property (set explicitly or * inferred from the declared type by sanitize_type_category). An optional CAST - * overrides it - mirroring get_name_sql(), so the category matches the SQL that - * will actually render: a SIGNED/DECIMAL cast is 'numeric', a DATETIME cast is + * overrides it - mirroring get_name_sql(), so the category matches the + * SQL that will actually render: a SIGNED/DECIMAL cast is 'numeric', a DATETIME cast is * 'date', etc. * * @since 3.1.0 diff --git a/tests/Database/Kern/Column/ColumnTest.php b/tests/Database/Kern/Column/ColumnTest.php index 9a94681..088e076 100644 --- a/tests/Database/Kern/Column/ColumnTest.php +++ b/tests/Database/Kern/Column/ColumnTest.php @@ -191,6 +191,23 @@ public function test_is_date_time_returns_false_for_varchar() { $this->assertFalse( $column->is_date_time() ); } + /** + * Explicit type checks use the new helpers without changing released signatures. + * + * @since 3.1.0 + */ + public function test_type_predicate_helpers_accept_explicit_types() { + $column = new Column(); + + $this->assertTrue( $column->is_json( 'json' ) ); + $this->assertTrue( $column->is_bool( 'bool' ) ); + $this->assertTrue( $column->is_date_time( 'timestamp' ) ); + $this->assertTrue( $column->is_int( 'bigint' ) ); + $this->assertTrue( $column->is_decimal( 'decimal' ) ); + $this->assertTrue( $column->is_text( 'varchar' ) ); + $this->assertTrue( $column->is_binary( 'blob' ) ); + } + /** * Test that is_bounded_string returns true for a varchar column. * @@ -2438,7 +2455,7 @@ public function test_temporal_type_predicates() { } /** - * Test that get_name_sql wraps the reference in CAST only when a cast is given. + * Test that get_name_sql wraps the reference when a cast is given. * * @since 3.1.0 */ diff --git a/tests/Database/ReleasedSubclassContractTest.php b/tests/Database/ReleasedSubclassContractTest.php index aa762bb..9aeb64f 100644 --- a/tests/Database/ReleasedSubclassContractTest.php +++ b/tests/Database/ReleasedSubclassContractTest.php @@ -167,6 +167,9 @@ public function get_sql( Column $col, string $alias = '', $value = null ): strin } } +/** An operator extending the class name shipped in 3.0. */ +class ReleasedOperatorClassName extends \BerlinDB\Database\Operators\Equal {} + /** * Check extension loading and dispatch, not only reflection signatures. * @@ -233,6 +236,44 @@ public function test_casts_use_cast_aware_renderer(): void { $this->assertSame( 'CAST(`total` AS SIGNED) = 1', ( new ReleasedOperatorOverride() )->get_sql_with_cast( $column, '', 1, 'SIGNED' ) ); } + /** + * Comparison operator class names shipped in 3.0 remain loadable. + * + * @since 3.1.0 + */ + public function test_released_operator_class_names_remain_loadable(): void { + $operators = array( + 'Base', + 'Between', + 'Equal', + 'Exists', + 'GreaterThan', + 'GreaterThanOrEqual', + 'In', + 'LessThan', + 'LessThanOrEqual', + 'Like', + 'NotBetween', + 'NotEqual', + 'NotExists', + 'NotIn', + 'NotLike', + 'NotRegexp', + 'Regexp', + 'Rlike', + ); + + foreach ( $operators as $operator ) { + $released = 'BerlinDB\\Database\\Operators\\' . $operator; + $current = 'BerlinDB\\Database\\Operators\\Comparisons\\' . $operator; + + $this->assertTrue( class_exists( $released ) ); + $this->assertTrue( is_a( $released, $current, true ) ); + } + + $this->assertInstanceOf( Equal::class, new ReleasedOperatorClassName() ); + } + /** * Validators that predate 3.0 remain callable by plugins. *