From 0e012a5800f3b9b543e343887fc644a9a57ae3c0 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 22 Sep 2026 17:36:32 -0500 Subject: [PATCH 1/3] Compatibility: preserve released declarations and operator names. --- CHANGELOG.md | 28 ++-- CLAUDE.md | 2 +- autoloader.php | 31 +++- src/Database/Diff/ColumnNormalizer.php | 4 +- src/Database/Kern/Column.php | 138 ++++++++++++++---- src/Database/Kern/Query.php | 2 +- src/Database/Operands/Column.php | 2 +- src/Database/Presets/Column/Serial.php | 2 +- src/Database/Traits/Arguments.php | 23 ++- src/Database/Traits/Configuration.php | 6 +- src/Database/Traits/Operator.php | 2 +- src/Database/Traits/Query/Clauses.php | 4 +- src/Database/Traits/Query/Crud.php | 2 +- src/Database/Traits/Query/Execution.php | 2 +- src/Database/Traits/Query/Variables.php | 4 +- tests/Database/Kern/Column/ColumnTest.php | 29 +++- .../Database/ReleasedSubclassContractTest.php | 92 +++++++++++- .../Database/Traits/BaseSanitizationTest.php | 22 +-- 18 files changed, 315 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e06f7733..116ae490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,18 @@ 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. + Column's released `is_*()` predicates retain their parameterless signatures; + explicit type checks use the new `is_type_*()` helpers. `get_name_sql()` keeps + its released alias-only signature, while explicit casts use + `get_name_sql_with_cast()`. Cast-aware operator rendering uses + `get_sql_with_cast()`; custom operators must override that method 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. `parse_args()` also retains its + released one-argument declaration as a compatibility seam; internal argument + parsing uses `parse_args_to_array()`. The deliberate lifecycle and strict-config + changes below remain separate migration requirements. - 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 @@ -571,7 +574,7 @@ Notable changes to BerlinDB are documented here. a normalizer fails closed by returning a `query_filter_short_circuit` directive. - Reduces WordPress coupling: reimplements `wp_validate_boolean()`, `absint()`, and (filter-free) `sanitize_key()` in the `Sanitizer` trait and (filter-free) - `wp_parse_args()` as `Base::parse_args()`, and uses native PHP CSPRNG for UUIDs + `wp_parse_args()` as `Base::parse_args_to_array()`, and uses native PHP CSPRNG for UUIDs and random integers instead of `wp_rand()`. - Removes the internal `Parser::caller()` indirection in favor of direct, type-checked calls. @@ -590,8 +593,11 @@ 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()`. Rename any override of the leftover-args hook to + `consume_args()`. The old one-argument `parse_args()` declaration remains so a + released subclass can load, but construction no longer calls it; the new + `parse_args_to_array()` helper handles `wp_parse_args()`-style input parsing and + array merging. The old `parse_args()` method is deprecated. - 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 d6888d5e..b955d2d6 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 9fa09452..3f5aa7ba 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/Diff/ColumnNormalizer.php b/src/Database/Diff/ColumnNormalizer.php index 783a2cd8..fcb02e6f 100644 --- a/src/Database/Diff/ColumnNormalizer.php +++ b/src/Database/Diff/ColumnNormalizer.php @@ -111,8 +111,8 @@ private function signature( Column $column ): array { return array( 'type' => $canonical, - 'length' => $column->is_int( $canonical ) ? 0 : (int) $column->length, - 'scale' => $column->is_decimal( $canonical ) ? (int) $column->scale : 0, + 'length' => $column->is_type_int( $canonical ) ? 0 : (int) $column->length, + 'scale' => $column->is_type_decimal( $canonical ) ? (int) $column->scale : 0, 'nullable' => ! empty( $column->allow_null ), 'unsigned' => $is_numeric && ! empty( $column->unsigned ), 'zerofill' => $is_numeric && ! empty( $column->zerofill ), diff --git a/src/Database/Kern/Column.php b/src/Database/Kern/Column.php index 3d3d41cf..83f7998d 100644 --- a/src/Database/Kern/Column.php +++ b/src/Database/Kern/Column.php @@ -945,10 +945,20 @@ public function get_active_presets(): array { * Return if a column type is JSON. * * @since 3.0.0 - * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if json type only. */ - public function is_json( $type = '' ) { + public function is_json() { + return $this->is_type_json( $this->type ); + } + + /** + * Return if a supplied column type is JSON. + * + * @since 3.1.0 + * @param string $type Type string to test. + * @return bool True if json type only. + */ + public function is_type_json( $type ) { return $this->is_type( array( 'json', @@ -961,10 +971,20 @@ public function is_json( $type = '' ) { * Return if a column type is a bool. * * @since 3.0.0 - * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if bool type only. */ - public function is_bool( $type = '' ) { + public function is_bool() { + return $this->is_type_bool( $this->type ); + } + + /** + * Return if a supplied column type is a bool. + * + * @since 3.1.0 + * @param string $type Type string to test. + * @return bool True if bool type only. + */ + public function is_type_bool( $type ) { return $this->is_type( array( 'bool', @@ -977,10 +997,20 @@ public function is_bool( $type = '' ) { * Return if a column type is a date. * * @since 3.0.0 - * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if any date or time. */ - public function is_date_time( $type = '' ) { + public function is_date_time() { + return $this->is_type_date_time( $this->type ); + } + + /** + * Return if a supplied column type is a date or time. + * + * @since 3.1.0 + * @param string $type Type string to test. + * @return bool True if any date or time. + */ + public function is_type_date_time( $type ) { return $this->is_type( array( 'date', @@ -1040,10 +1070,20 @@ public function is_year( $type = '' ) { * Return if a column type is an integer. * * @since 3.0.0 - * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if int. */ - public function is_int( $type = '' ) { + public function is_int() { + return $this->is_type_int( $this->type ); + } + + /** + * Return if a supplied column type is an integer. + * + * @since 3.1.0 + * @param string $type Type string to test. + * @return bool True if int. + */ + public function is_type_int( $type ) { return $this->is_type( array( 'tinyint', @@ -1060,10 +1100,20 @@ public function is_int( $type = '' ) { * Return if a column type is decimal. * * @since 3.0.0 - * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if float. */ - public function is_decimal( $type = '' ) { + public function is_decimal() { + return $this->is_type_decimal( $this->type ); + } + + /** + * Return if a supplied column type is decimal. + * + * @since 3.1.0 + * @param string $type Type string to test. + * @return bool True if float. + */ + public function is_type_decimal( $type ) { return $this->is_type( array( 'float', @@ -1083,7 +1133,7 @@ public function is_decimal( $type = '' ) { * @return bool True if bit, int, or float. */ public function is_numeric() { - return $this->is_numeric_type(); + return $this->is_type_numeric( $this->type ); } /** @@ -1092,10 +1142,10 @@ public function is_numeric() { * Consider using is_int() or is_decimal() for improved specificity. * * @since 3.1.0 - * @param string $type Optional type string to test. Defaults to $this->type. + * @param string $type Type string to test. * @return bool True if bit, int, or float. */ - public function is_numeric_type( $type = '' ) { + public function is_type_numeric( $type ) { return $this->is_type( array( @@ -1124,10 +1174,20 @@ public function is_numeric_type( $type = '' ) { * For binary strings (blobs) use is_binary(). * * @since 3.0.0 - * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if text. */ - public function is_text( $type = '' ) { + public function is_text() { + return $this->is_type_text( $this->type ); + } + + /** + * Return if a supplied column type is a string. + * + * @since 3.1.0 + * @param string $type Type string to test. + * @return bool True if text. + */ + public function is_type_text( $type ) { return $this->is_type( array( @@ -1149,10 +1209,20 @@ public function is_text( $type = '' ) { * Return if a column type is binary. * * @since 3.0.0 - * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if binary. */ - public function is_binary( $type = '' ) { + public function is_binary() { + return $this->is_type_binary( $this->type ); + } + + /** + * Return if a supplied column type is binary. + * + * @since 3.1.0 + * @param string $type Type string to test. + * @return bool True if binary. + */ + public function is_type_binary( $type ) { return $this->is_type( array( @@ -1352,7 +1422,7 @@ private function is_extra( $extras = '', $extra = '' ): bool { * @return array */ private function sanitize_capabilities( $caps = array() ): array { - return $this->parse_args( + return $this->parse_args_to_array( $caps, array( 'select' => 'exist', @@ -2430,20 +2500,30 @@ 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. - * * @since 3.0.0 * * @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`. + * @return string Quoted SQL reference, e.g. `alias`.`column`. + */ + public function get_name_sql( string $alias = '' ): string { + return $this->get_name_sql_with_cast( $alias ); + } + + /** + * Return the backtick-quoted column name with an optional SQL cast. + * + * 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.1.0 + * + * @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 { + public function get_name_sql_with_cast( string $alias = '', string $cast = '' ): string { // Quote the column name. $quoted = $this->quote_identifier( $this->name ); @@ -2516,8 +2596,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_with_cast(), 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/src/Database/Kern/Query.php b/src/Database/Kern/Query.php index 32ce82e9..51597c2e 100644 --- a/src/Database/Kern/Query.php +++ b/src/Database/Kern/Query.php @@ -937,7 +937,7 @@ protected function get_aggregate_functions(): array { public function get_results( $cols = array(), $where_cols = array(), $limit = 25, $offset = null, $output = OBJECT ) { // Parse arguments. - $r = $this->parse_args( + $r = $this->parse_args_to_array( $where_cols, array( 'fields' => $cols, diff --git a/src/Database/Operands/Column.php b/src/Database/Operands/Column.php index 397c413a..f031ad18 100644 --- a/src/Database/Operands/Column.php +++ b/src/Database/Operands/Column.php @@ -97,7 +97,7 @@ protected function init( array $args ): void { * @return string */ public function get_sql(): string { - return $this->column->get_name_sql( $this->alias, $this->cast ); + return $this->column->get_name_sql_with_cast( $this->alias, $this->cast ); } /** diff --git a/src/Database/Presets/Column/Serial.php b/src/Database/Presets/Column/Serial.php index 988f7e03..9fba5dad 100644 --- a/src/Database/Presets/Column/Serial.php +++ b/src/Database/Presets/Column/Serial.php @@ -102,7 +102,7 @@ public function set_args( array $args, Column $column ): array { } // Both forms promote an integer type to an auto-increment primary key. - if ( $column->is_int( $args[ 'type' ] ?? '' ) ) { + if ( $column->is_type_int( $args[ 'type' ] ?? '' ) ) { $args[ 'allow_null' ] = false; $args[ 'default' ] = false; $args[ 'primary' ] = true; diff --git a/src/Database/Traits/Arguments.php b/src/Database/Traits/Arguments.php index 7b990ec8..4eca0199 100644 --- a/src/Database/Traits/Arguments.php +++ b/src/Database/Traits/Arguments.php @@ -62,7 +62,26 @@ protected function set_vars( $args = array() ): void { } /** - * Merge an arguments value over a set of defaults. + * Parse an arguments value using the released extension signature. + * + * Kept as a compatibility seam for subclasses that overrode the construction + * hook shipped in 3.0. Internal calls use parse_args_to_array() so a legacy + * override cannot alter the new construction pipeline. + * + * @since 3.0.0 + * @since 3.1.0 No longer used as the construction hook. + * @deprecated 3.1.0 Use parse_args_to_array() to parse arguments, or + * consume_args() to handle construction arguments. + * + * @param array|object|string $args Value to parse. + * @return array + */ + protected function parse_args( $args = array() ) { + return $this->parse_args_to_array( $args ); + } + + /** + * Parse an arguments value into an array. * * A dependency-free reimplementation of WordPress's wp_parse_args(): accepts * an array, an object (read via get_object_vars()), or a URL-style query @@ -75,7 +94,7 @@ protected function set_vars( $args = array() ): void { * @param array $defaults Defaults to merge under $args. * @return array */ - protected function parse_args( $args = array(), $defaults = array() ): array { + protected function parse_args_to_array( $args = array(), $defaults = array() ): array { // Normalize $args to an array. if ( is_object( $args ) ) { diff --git a/src/Database/Traits/Configuration.php b/src/Database/Traits/Configuration.php index 22be76d7..33c3cacc 100644 --- a/src/Database/Traits/Configuration.php +++ b/src/Database/Traits/Configuration.php @@ -33,7 +33,7 @@ * - is_strict_config(): reject unknown config keys instead of passing them on. * - special_args() / validate_args(): force/validate values before set_vars(). * - * Requires the host to provide set_vars(), parse_args(), and log() (Traits\Base, + * Requires the host to provide set_vars(), parse_args_to_array(), and log() (Traits\Base, * which pulls Log). Declared via @method (not abstract methods) so tooling sees * the dependency without colliding with the real methods when a class composes * both. @@ -41,7 +41,7 @@ * @since 3.1.0 * * @method void set_vars( array $args = [] ) - * @method array parse_args( array|object|string $args = [], array $defaults = [] ) + * @method array parse_args_to_array( array|object|string $args = [], array $defaults = [] ) * @method void log( string $level, string $code, string $message, array $context = [] ) * @method list get_boot_reserved_vars() * @method list get_lifecycle_reserved_vars() @@ -139,7 +139,7 @@ protected function configure( array $args = array() ): array { */ $reserved = array_flip( $this->get_reserved_vars() ); $defaults = array_diff_key( $this->args[ 'class' ], $reserved ); - $r = $this->parse_args( $args, $defaults ); + $r = $this->parse_args_to_array( $args, $defaults ); // Force special-type args, set them, then validate & set. $r = $this->special_args( $r ); diff --git a/src/Database/Traits/Operator.php b/src/Database/Traits/Operator.php index e4de6e7e..87b5436a 100644 --- a/src/Database/Traits/Operator.php +++ b/src/Database/Traits/Operator.php @@ -406,6 +406,6 @@ private function get_comparison_sql( Column $col, string $alias = '', $value = n } // Assemble and return the full expression, optionally casting the column. - return $col->get_name_sql( $alias, $cast ) . ' ' . $this->get_sql_compare() . ' ' . $value_sql; + return $col->get_name_sql_with_cast( $alias, $cast ) . ' ' . $this->get_sql_compare() . ' ' . $value_sql; } } diff --git a/src/Database/Traits/Query/Clauses.php b/src/Database/Traits/Query/Clauses.php index e7a79a6b..87e56ddc 100644 --- a/src/Database/Traits/Query/Clauses.php +++ b/src/Database/Traits/Query/Clauses.php @@ -53,7 +53,7 @@ private function parse_query_vars( $query_vars = array() ): array { } // Parse arguments. - $r = $this->parse_args( $query_vars ); + $r = $this->parse_args_to_array( $query_vars ); // Parse $query_vars. $join_where = $this->parse_join_where( $r ); @@ -772,7 +772,7 @@ private function parse_query_clauses( $clauses = array() ): array { } // Default return value. - $retval = $this->parse_args( $clauses ); + $retval = $this->parse_args_to_array( $clauses ); // Return array of clauses. return $retval; diff --git a/src/Database/Traits/Query/Crud.php b/src/Database/Traits/Query/Crud.php index 2cde4843..50bb8118 100644 --- a/src/Database/Traits/Query/Crud.php +++ b/src/Database/Traits/Query/Crud.php @@ -1079,7 +1079,7 @@ private function reduce_item( $method = 'update', $item = array() ): array { private function default_item( $args = array() ): array { // Parse arguments. - $r = $this->parse_args( $args ); + $r = $this->parse_args_to_array( $args ); // Get the column names and their defaults. $names = $this->get_column_names( $r ); diff --git a/src/Database/Traits/Query/Execution.php b/src/Database/Traits/Query/Execution.php index 135b4a8b..f883c8ae 100644 --- a/src/Database/Traits/Query/Execution.php +++ b/src/Database/Traits/Query/Execution.php @@ -607,7 +607,7 @@ private function count_found_items( $item_ids ): int { $overrides[ 'groupby' ] = ''; } - $r = $this->parse_args( $overrides, $this->get_current_array( 'request_clauses' ) ); + $r = $this->parse_args_to_array( $overrides, $this->get_current_array( 'request_clauses' ) ); // Build and filter the found-items query. $query = $this->filter_found_items_query( $this->parse_request_clauses( $r ) ); diff --git a/src/Database/Traits/Query/Variables.php b/src/Database/Traits/Query/Variables.php index 644dba27..386d8069 100644 --- a/src/Database/Traits/Query/Variables.php +++ b/src/Database/Traits/Query/Variables.php @@ -152,10 +152,10 @@ public function get_query_var_default_value(): string { private function parse_query( $query = array() ): void { // Stash the raw query args before any defaults are merged in. - $this->set_current( 'query_var_originals', $this->parse_args( $query ) ); + $this->set_current( 'query_var_originals', $this->parse_args_to_array( $query ) ); // Setup the $query_vars parsed var. - $this->query_vars = $this->parse_args( + $this->query_vars = $this->parse_args_to_array( $this->get_current_array( 'query_var_originals' ), $this->query_var_defaults ); diff --git a/tests/Database/Kern/Column/ColumnTest.php b/tests/Database/Kern/Column/ColumnTest.php index 9a946815..f7e0d205 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_type_json( 'json' ) ); + $this->assertTrue( $column->is_type_bool( 'bool' ) ); + $this->assertTrue( $column->is_type_date_time( 'timestamp' ) ); + $this->assertTrue( $column->is_type_int( 'bigint' ) ); + $this->assertTrue( $column->is_type_decimal( 'decimal' ) ); + $this->assertTrue( $column->is_type_text( 'varchar' ) ); + $this->assertTrue( $column->is_type_binary( 'blob' ) ); + } + /** * Test that is_bounded_string returns true for a varchar column. * @@ -2363,7 +2380,7 @@ public function test_get_type_category() { $this->assertSame( 'time', $varchar->get_type_category( 'TIME' ) ); /* - * The cast is normalized like get_name_sql(): a sloppy-but-valid cast is + * The cast is normalized like get_name_sql_with_cast(): a sloppy-but-valid cast is * honored, and an invalid cast is ignored (the declared type decides). */ $this->assertSame( 'numeric', $varchar->get_type_category( ' signed ' ) ); @@ -2438,11 +2455,11 @@ 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_with_cast wraps the reference when a cast is given. * * @since 3.1.0 */ - public function test_get_name_sql_optionally_casts() { + public function test_get_name_sql_with_cast_optionally_casts() { $column = new Column( array( 'name' => 'total', @@ -2455,13 +2472,13 @@ public function test_get_name_sql_optionally_casts() { $this->assertSame( '`a`.`total`', $column->get_name_sql( 'a' ) ); // With a cast: wrapped. - $this->assertSame( 'CAST(`a`.`total` AS SIGNED)', $column->get_name_sql( 'a', 'SIGNED' ) ); + $this->assertSame( 'CAST(`a`.`total` AS SIGNED)', $column->get_name_sql_with_cast( 'a', 'SIGNED' ) ); // CHAR is a real cast target (string-semantics comparison / LIKE). - $this->assertSame( 'CAST(`a`.`total` AS CHAR)', $column->get_name_sql( 'a', 'CHAR' ) ); + $this->assertSame( 'CAST(`a`.`total` AS CHAR)', $column->get_name_sql_with_cast( 'a', 'CHAR' ) ); // Invalid cast is sanitized away at this public boundary (no cast). - $this->assertSame( '`a`.`total`', $column->get_name_sql( 'a', 'nonsense' ) ); + $this->assertSame( '`a`.`total`', $column->get_name_sql_with_cast( 'a', 'nonsense' ) ); } /** diff --git a/tests/Database/ReleasedSubclassContractTest.php b/tests/Database/ReleasedSubclassContractTest.php index aa762bb3..73348379 100644 --- a/tests/Database/ReleasedSubclassContractTest.php +++ b/tests/Database/ReleasedSubclassContractTest.php @@ -63,6 +63,33 @@ public function get_indexes() { * @since 3.1.0 */ class ReleasedColumnOverrides extends Column { + /** @var int */ + public $parse_args_calls = 0; + + /** @inheritDoc */ + public function is_json() { + return parent::is_json(); + } + + /** @inheritDoc */ + public function is_bool() { + return parent::is_bool(); + } + + /** @inheritDoc */ + public function is_date_time() { + return parent::is_date_time(); + } + + /** @inheritDoc */ + public function is_int() { + return parent::is_int(); + } + + /** @inheritDoc */ + public function is_decimal() { + return parent::is_decimal(); + } /** * Preserve the released predicate override. @@ -74,6 +101,27 @@ public function is_numeric() { return parent::is_numeric(); } + /** @inheritDoc */ + public function is_text() { + return parent::is_text(); + } + + /** @inheritDoc */ + public function is_binary() { + return parent::is_binary(); + } + + /** @inheritDoc */ + public function get_name_sql( string $alias = '' ): string { + return parent::get_name_sql( $alias ); + } + + /** @inheritDoc */ + protected function parse_args( $args = array() ) { + ++$this->parse_args_calls; + return parent::parse_args( $args ); + } + /** * Preserve the released success check override. * @@ -167,6 +215,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. * @@ -201,7 +252,8 @@ public function test_sql_helpers_honor_released_overrides(): void { 'type' => 'int', ) ); - $this->assertSame( 'CAST(`total` AS SIGNED)', $column->get_name_sql( '', 'SIGNED' ) ); + $this->assertSame( 'CAST(`total` AS SIGNED)', $column->get_name_sql_with_cast( '', 'SIGNED' ) ); + $this->assertSame( 0, $column->parse_args_calls ); $this->assertSame( 'custom = 1', ( new ReleasedOperatorOverride() )->get_sql_with_cast( $column, '', 1 ) ); $this->assertNotEmpty( ( new ReleasedQueryOverrides() )->get_columns() ); } @@ -233,6 +285,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. * diff --git a/tests/Database/Traits/BaseSanitizationTest.php b/tests/Database/Traits/BaseSanitizationTest.php index 8d12e8c7..c4566e03 100644 --- a/tests/Database/Traits/BaseSanitizationTest.php +++ b/tests/Database/Traits/BaseSanitizationTest.php @@ -161,7 +161,7 @@ public function get_sanitized_key( $key ) { } /** - * Public access to protected parse_args method. + * Public access to protected parse_args_to_array method. * * @since 3.1.0 * @@ -169,8 +169,8 @@ public function get_sanitized_key( $key ) { * @param array $defaults * @return array */ - public function get_parsed_args( $args = array(), $defaults = array() ) { - return $this->parse_args( $args, $defaults ); + public function get_parsed_args_array( $args = array(), $defaults = array() ) { + return $this->parse_args_to_array( $args, $defaults ); } } @@ -802,12 +802,12 @@ public function test_sanitize_key() { } /** - * parse_args() mirrors wp_parse_args(): merges an array/object/query-string + * parse_args_to_array() mirrors wp_parse_args(): merges an array/object/query-string * over defaults (filter-free). * * @since 3.1.0 */ - public function test_parse_args() { + public function test_parse_args_to_array() { // Array over defaults - passed values win, defaults fill the rest. $this->assertSame( @@ -815,7 +815,7 @@ public function test_parse_args() { 'a' => 1, 'b' => 2, ), - $this->helper->get_parsed_args( + $this->helper->get_parsed_args_array( array( 'a' => 1 ), array( 'a' => 0, @@ -825,13 +825,13 @@ public function test_parse_args() { ); // Array with no defaults passes through. - $this->assertSame( array( 'x' => 1 ), $this->helper->get_parsed_args( array( 'x' => 1 ) ) ); + $this->assertSame( array( 'x' => 1 ), $this->helper->get_parsed_args_array( array( 'x' => 1 ) ) ); // Empty args with defaults yields the defaults. - $this->assertSame( array( 'd' => 4 ), $this->helper->get_parsed_args( array(), array( 'd' => 4 ) ) ); + $this->assertSame( array( 'd' => 4 ), $this->helper->get_parsed_args_array( array(), array( 'd' => 4 ) ) ); // Object input is read via get_object_vars(). - $this->assertSame( array( 'k' => 'v' ), $this->helper->get_parsed_args( (object) array( 'k' => 'v' ) ) ); + $this->assertSame( array( 'k' => 'v' ), $this->helper->get_parsed_args_array( (object) array( 'k' => 'v' ) ) ); // Query-string input is parsed with parse_str(). $this->assertSame( @@ -839,7 +839,7 @@ public function test_parse_args() { 'a' => '1', 'b' => '2', ), - $this->helper->get_parsed_args( 'a=1&b=2' ) + $this->helper->get_parsed_args_array( 'a=1&b=2' ) ); // Query-string over defaults. @@ -848,7 +848,7 @@ public function test_parse_args() { 'a' => '1', 'c' => '3', ), - $this->helper->get_parsed_args( + $this->helper->get_parsed_args_array( 'a=1', array( 'a' => '0', From c6a9782f447382b2008be00203d5714f30544ce7 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 22 Sep 2026 20:24:24 -0500 Subject: [PATCH 2/3] Compatibility: document deliberate 3.1 signature changes. --- CHANGELOG.md | 27 +++-- src/Database/Diff/ColumnNormalizer.php | 4 +- src/Database/Kern/Column.php | 106 +++--------------- src/Database/Kern/Query.php | 2 +- src/Database/Presets/Column/Serial.php | 2 +- src/Database/Traits/Arguments.php | 23 +--- src/Database/Traits/Configuration.php | 6 +- src/Database/Traits/Query/Clauses.php | 4 +- src/Database/Traits/Query/Crud.php | 2 +- src/Database/Traits/Query/Execution.php | 2 +- src/Database/Traits/Query/Variables.php | 4 +- tests/Database/Kern/Column/ColumnTest.php | 14 +-- .../Database/ReleasedSubclassContractTest.php | 44 -------- .../Database/Traits/BaseSanitizationTest.php | 22 ++-- 14 files changed, 64 insertions(+), 198 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 116ae490..6a33a3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,18 +12,14 @@ 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's released `is_*()` predicates retain their parameterless signatures; - explicit type checks use the new `is_type_*()` helpers. `get_name_sql()` keeps - its released alias-only signature, while explicit casts use - `get_name_sql_with_cast()`. Cast-aware operator rendering uses + Column's `get_name_sql()` keeps its released alias-only signature, while + explicit casts use `get_name_sql_with_cast()`. Cast-aware operator rendering uses `get_sql_with_cast()`; custom operators must override that method 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. `parse_args()` also retains its - released one-argument declaration as a compatibility seam; internal argument - parsing uses `parse_args_to_array()`. The deliberate lifecycle and strict-config - changes below remain separate migration requirements. + methods while retaining their PHPDoc contracts. The intentional `parse_args()` + and Column type-predicate 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 @@ -574,7 +570,7 @@ Notable changes to BerlinDB are documented here. a normalizer fails closed by returning a `query_filter_short_circuit` directive. - Reduces WordPress coupling: reimplements `wp_validate_boolean()`, `absint()`, and (filter-free) `sanitize_key()` in the `Sanitizer` trait and (filter-free) - `wp_parse_args()` as `Base::parse_args_to_array()`, and uses native PHP CSPRNG for UUIDs + `wp_parse_args()` as `Base::parse_args()`, and uses native PHP CSPRNG for UUIDs and random integers instead of `wp_rand()`. - Removes the internal `Parser::caller()` indirection in favor of direct, type-checked calls. @@ -593,11 +589,14 @@ 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()`. Rename any override of the leftover-args hook to - `consume_args()`. The old one-argument `parse_args()` declaration remains so a - released subclass can load, but construction no longer calls it; the new - `parse_args_to_array()` helper handles `wp_parse_args()`-style input parsing and - array merging. The old `parse_args()` method is deprecated. + `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. - 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/src/Database/Diff/ColumnNormalizer.php b/src/Database/Diff/ColumnNormalizer.php index fcb02e6f..783a2cd8 100644 --- a/src/Database/Diff/ColumnNormalizer.php +++ b/src/Database/Diff/ColumnNormalizer.php @@ -111,8 +111,8 @@ private function signature( Column $column ): array { return array( 'type' => $canonical, - 'length' => $column->is_type_int( $canonical ) ? 0 : (int) $column->length, - 'scale' => $column->is_type_decimal( $canonical ) ? (int) $column->scale : 0, + 'length' => $column->is_int( $canonical ) ? 0 : (int) $column->length, + 'scale' => $column->is_decimal( $canonical ) ? (int) $column->scale : 0, 'nullable' => ! empty( $column->allow_null ), 'unsigned' => $is_numeric && ! empty( $column->unsigned ), 'zerofill' => $is_numeric && ! empty( $column->zerofill ), diff --git a/src/Database/Kern/Column.php b/src/Database/Kern/Column.php index 83f7998d..e859ed59 100644 --- a/src/Database/Kern/Column.php +++ b/src/Database/Kern/Column.php @@ -945,20 +945,10 @@ public function get_active_presets(): array { * Return if a column type is JSON. * * @since 3.0.0 + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if json type only. */ - public function is_json() { - return $this->is_type_json( $this->type ); - } - - /** - * Return if a supplied column type is JSON. - * - * @since 3.1.0 - * @param string $type Type string to test. - * @return bool True if json type only. - */ - public function is_type_json( $type ) { + public function is_json( $type = '' ) { return $this->is_type( array( 'json', @@ -971,20 +961,10 @@ public function is_type_json( $type ) { * Return if a column type is a bool. * * @since 3.0.0 + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if bool type only. */ - public function is_bool() { - return $this->is_type_bool( $this->type ); - } - - /** - * Return if a supplied column type is a bool. - * - * @since 3.1.0 - * @param string $type Type string to test. - * @return bool True if bool type only. - */ - public function is_type_bool( $type ) { + public function is_bool( $type = '' ) { return $this->is_type( array( 'bool', @@ -997,20 +977,10 @@ public function is_type_bool( $type ) { * Return if a column type is a date. * * @since 3.0.0 + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if any date or time. */ - public function is_date_time() { - return $this->is_type_date_time( $this->type ); - } - - /** - * Return if a supplied column type is a date or time. - * - * @since 3.1.0 - * @param string $type Type string to test. - * @return bool True if any date or time. - */ - public function is_type_date_time( $type ) { + public function is_date_time( $type = '' ) { return $this->is_type( array( 'date', @@ -1070,20 +1040,10 @@ public function is_year( $type = '' ) { * Return if a column type is an integer. * * @since 3.0.0 + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if int. */ - public function is_int() { - return $this->is_type_int( $this->type ); - } - - /** - * Return if a supplied column type is an integer. - * - * @since 3.1.0 - * @param string $type Type string to test. - * @return bool True if int. - */ - public function is_type_int( $type ) { + public function is_int( $type = '' ) { return $this->is_type( array( 'tinyint', @@ -1100,20 +1060,10 @@ public function is_type_int( $type ) { * Return if a column type is decimal. * * @since 3.0.0 + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if float. */ - public function is_decimal() { - return $this->is_type_decimal( $this->type ); - } - - /** - * Return if a supplied column type is decimal. - * - * @since 3.1.0 - * @param string $type Type string to test. - * @return bool True if float. - */ - public function is_type_decimal( $type ) { + public function is_decimal( $type = '' ) { return $this->is_type( array( 'float', @@ -1133,7 +1083,7 @@ public function is_type_decimal( $type ) { * @return bool True if bit, int, or float. */ public function is_numeric() { - return $this->is_type_numeric( $this->type ); + return $this->is_numeric_type(); } /** @@ -1142,10 +1092,10 @@ public function is_numeric() { * Consider using is_int() or is_decimal() for improved specificity. * * @since 3.1.0 - * @param string $type Type string to test. + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if bit, int, or float. */ - public function is_type_numeric( $type ) { + public function is_numeric_type( $type = '' ) { return $this->is_type( array( @@ -1174,20 +1124,10 @@ public function is_type_numeric( $type ) { * For binary strings (blobs) use is_binary(). * * @since 3.0.0 + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if text. */ - public function is_text() { - return $this->is_type_text( $this->type ); - } - - /** - * Return if a supplied column type is a string. - * - * @since 3.1.0 - * @param string $type Type string to test. - * @return bool True if text. - */ - public function is_type_text( $type ) { + public function is_text( $type = '' ) { return $this->is_type( array( @@ -1209,20 +1149,10 @@ public function is_type_text( $type ) { * Return if a column type is binary. * * @since 3.0.0 + * @param string $type Optional type string to test. Defaults to $this->type. * @return bool True if binary. */ - public function is_binary() { - return $this->is_type_binary( $this->type ); - } - - /** - * Return if a supplied column type is binary. - * - * @since 3.1.0 - * @param string $type Type string to test. - * @return bool True if binary. - */ - public function is_type_binary( $type ) { + public function is_binary( $type = '' ) { return $this->is_type( array( @@ -1422,7 +1352,7 @@ private function is_extra( $extras = '', $extra = '' ): bool { * @return array */ private function sanitize_capabilities( $caps = array() ): array { - return $this->parse_args_to_array( + return $this->parse_args( $caps, array( 'select' => 'exist', diff --git a/src/Database/Kern/Query.php b/src/Database/Kern/Query.php index 51597c2e..32ce82e9 100644 --- a/src/Database/Kern/Query.php +++ b/src/Database/Kern/Query.php @@ -937,7 +937,7 @@ protected function get_aggregate_functions(): array { public function get_results( $cols = array(), $where_cols = array(), $limit = 25, $offset = null, $output = OBJECT ) { // Parse arguments. - $r = $this->parse_args_to_array( + $r = $this->parse_args( $where_cols, array( 'fields' => $cols, diff --git a/src/Database/Presets/Column/Serial.php b/src/Database/Presets/Column/Serial.php index 9fba5dad..988f7e03 100644 --- a/src/Database/Presets/Column/Serial.php +++ b/src/Database/Presets/Column/Serial.php @@ -102,7 +102,7 @@ public function set_args( array $args, Column $column ): array { } // Both forms promote an integer type to an auto-increment primary key. - if ( $column->is_type_int( $args[ 'type' ] ?? '' ) ) { + if ( $column->is_int( $args[ 'type' ] ?? '' ) ) { $args[ 'allow_null' ] = false; $args[ 'default' ] = false; $args[ 'primary' ] = true; diff --git a/src/Database/Traits/Arguments.php b/src/Database/Traits/Arguments.php index 4eca0199..7b990ec8 100644 --- a/src/Database/Traits/Arguments.php +++ b/src/Database/Traits/Arguments.php @@ -62,26 +62,7 @@ protected function set_vars( $args = array() ): void { } /** - * Parse an arguments value using the released extension signature. - * - * Kept as a compatibility seam for subclasses that overrode the construction - * hook shipped in 3.0. Internal calls use parse_args_to_array() so a legacy - * override cannot alter the new construction pipeline. - * - * @since 3.0.0 - * @since 3.1.0 No longer used as the construction hook. - * @deprecated 3.1.0 Use parse_args_to_array() to parse arguments, or - * consume_args() to handle construction arguments. - * - * @param array|object|string $args Value to parse. - * @return array - */ - protected function parse_args( $args = array() ) { - return $this->parse_args_to_array( $args ); - } - - /** - * Parse an arguments value into an array. + * Merge an arguments value over a set of defaults. * * A dependency-free reimplementation of WordPress's wp_parse_args(): accepts * an array, an object (read via get_object_vars()), or a URL-style query @@ -94,7 +75,7 @@ protected function parse_args( $args = array() ) { * @param array $defaults Defaults to merge under $args. * @return array */ - protected function parse_args_to_array( $args = array(), $defaults = array() ): array { + protected function parse_args( $args = array(), $defaults = array() ): array { // Normalize $args to an array. if ( is_object( $args ) ) { diff --git a/src/Database/Traits/Configuration.php b/src/Database/Traits/Configuration.php index 33c3cacc..22be76d7 100644 --- a/src/Database/Traits/Configuration.php +++ b/src/Database/Traits/Configuration.php @@ -33,7 +33,7 @@ * - is_strict_config(): reject unknown config keys instead of passing them on. * - special_args() / validate_args(): force/validate values before set_vars(). * - * Requires the host to provide set_vars(), parse_args_to_array(), and log() (Traits\Base, + * Requires the host to provide set_vars(), parse_args(), and log() (Traits\Base, * which pulls Log). Declared via @method (not abstract methods) so tooling sees * the dependency without colliding with the real methods when a class composes * both. @@ -41,7 +41,7 @@ * @since 3.1.0 * * @method void set_vars( array $args = [] ) - * @method array parse_args_to_array( array|object|string $args = [], array $defaults = [] ) + * @method array parse_args( array|object|string $args = [], array $defaults = [] ) * @method void log( string $level, string $code, string $message, array $context = [] ) * @method list get_boot_reserved_vars() * @method list get_lifecycle_reserved_vars() @@ -139,7 +139,7 @@ protected function configure( array $args = array() ): array { */ $reserved = array_flip( $this->get_reserved_vars() ); $defaults = array_diff_key( $this->args[ 'class' ], $reserved ); - $r = $this->parse_args_to_array( $args, $defaults ); + $r = $this->parse_args( $args, $defaults ); // Force special-type args, set them, then validate & set. $r = $this->special_args( $r ); diff --git a/src/Database/Traits/Query/Clauses.php b/src/Database/Traits/Query/Clauses.php index 87e56ddc..e7a79a6b 100644 --- a/src/Database/Traits/Query/Clauses.php +++ b/src/Database/Traits/Query/Clauses.php @@ -53,7 +53,7 @@ private function parse_query_vars( $query_vars = array() ): array { } // Parse arguments. - $r = $this->parse_args_to_array( $query_vars ); + $r = $this->parse_args( $query_vars ); // Parse $query_vars. $join_where = $this->parse_join_where( $r ); @@ -772,7 +772,7 @@ private function parse_query_clauses( $clauses = array() ): array { } // Default return value. - $retval = $this->parse_args_to_array( $clauses ); + $retval = $this->parse_args( $clauses ); // Return array of clauses. return $retval; diff --git a/src/Database/Traits/Query/Crud.php b/src/Database/Traits/Query/Crud.php index 50bb8118..2cde4843 100644 --- a/src/Database/Traits/Query/Crud.php +++ b/src/Database/Traits/Query/Crud.php @@ -1079,7 +1079,7 @@ private function reduce_item( $method = 'update', $item = array() ): array { private function default_item( $args = array() ): array { // Parse arguments. - $r = $this->parse_args_to_array( $args ); + $r = $this->parse_args( $args ); // Get the column names and their defaults. $names = $this->get_column_names( $r ); diff --git a/src/Database/Traits/Query/Execution.php b/src/Database/Traits/Query/Execution.php index f883c8ae..135b4a8b 100644 --- a/src/Database/Traits/Query/Execution.php +++ b/src/Database/Traits/Query/Execution.php @@ -607,7 +607,7 @@ private function count_found_items( $item_ids ): int { $overrides[ 'groupby' ] = ''; } - $r = $this->parse_args_to_array( $overrides, $this->get_current_array( 'request_clauses' ) ); + $r = $this->parse_args( $overrides, $this->get_current_array( 'request_clauses' ) ); // Build and filter the found-items query. $query = $this->filter_found_items_query( $this->parse_request_clauses( $r ) ); diff --git a/src/Database/Traits/Query/Variables.php b/src/Database/Traits/Query/Variables.php index 386d8069..644dba27 100644 --- a/src/Database/Traits/Query/Variables.php +++ b/src/Database/Traits/Query/Variables.php @@ -152,10 +152,10 @@ public function get_query_var_default_value(): string { private function parse_query( $query = array() ): void { // Stash the raw query args before any defaults are merged in. - $this->set_current( 'query_var_originals', $this->parse_args_to_array( $query ) ); + $this->set_current( 'query_var_originals', $this->parse_args( $query ) ); // Setup the $query_vars parsed var. - $this->query_vars = $this->parse_args_to_array( + $this->query_vars = $this->parse_args( $this->get_current_array( 'query_var_originals' ), $this->query_var_defaults ); diff --git a/tests/Database/Kern/Column/ColumnTest.php b/tests/Database/Kern/Column/ColumnTest.php index f7e0d205..ca34aa09 100644 --- a/tests/Database/Kern/Column/ColumnTest.php +++ b/tests/Database/Kern/Column/ColumnTest.php @@ -199,13 +199,13 @@ public function test_is_date_time_returns_false_for_varchar() { public function test_type_predicate_helpers_accept_explicit_types() { $column = new Column(); - $this->assertTrue( $column->is_type_json( 'json' ) ); - $this->assertTrue( $column->is_type_bool( 'bool' ) ); - $this->assertTrue( $column->is_type_date_time( 'timestamp' ) ); - $this->assertTrue( $column->is_type_int( 'bigint' ) ); - $this->assertTrue( $column->is_type_decimal( 'decimal' ) ); - $this->assertTrue( $column->is_type_text( 'varchar' ) ); - $this->assertTrue( $column->is_type_binary( 'blob' ) ); + $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' ) ); } /** diff --git a/tests/Database/ReleasedSubclassContractTest.php b/tests/Database/ReleasedSubclassContractTest.php index 73348379..006aec10 100644 --- a/tests/Database/ReleasedSubclassContractTest.php +++ b/tests/Database/ReleasedSubclassContractTest.php @@ -63,33 +63,6 @@ public function get_indexes() { * @since 3.1.0 */ class ReleasedColumnOverrides extends Column { - /** @var int */ - public $parse_args_calls = 0; - - /** @inheritDoc */ - public function is_json() { - return parent::is_json(); - } - - /** @inheritDoc */ - public function is_bool() { - return parent::is_bool(); - } - - /** @inheritDoc */ - public function is_date_time() { - return parent::is_date_time(); - } - - /** @inheritDoc */ - public function is_int() { - return parent::is_int(); - } - - /** @inheritDoc */ - public function is_decimal() { - return parent::is_decimal(); - } /** * Preserve the released predicate override. @@ -101,27 +74,11 @@ public function is_numeric() { return parent::is_numeric(); } - /** @inheritDoc */ - public function is_text() { - return parent::is_text(); - } - - /** @inheritDoc */ - public function is_binary() { - return parent::is_binary(); - } - /** @inheritDoc */ public function get_name_sql( string $alias = '' ): string { return parent::get_name_sql( $alias ); } - /** @inheritDoc */ - protected function parse_args( $args = array() ) { - ++$this->parse_args_calls; - return parent::parse_args( $args ); - } - /** * Preserve the released success check override. * @@ -253,7 +210,6 @@ public function test_sql_helpers_honor_released_overrides(): void { ) ); $this->assertSame( 'CAST(`total` AS SIGNED)', $column->get_name_sql_with_cast( '', 'SIGNED' ) ); - $this->assertSame( 0, $column->parse_args_calls ); $this->assertSame( 'custom = 1', ( new ReleasedOperatorOverride() )->get_sql_with_cast( $column, '', 1 ) ); $this->assertNotEmpty( ( new ReleasedQueryOverrides() )->get_columns() ); } diff --git a/tests/Database/Traits/BaseSanitizationTest.php b/tests/Database/Traits/BaseSanitizationTest.php index c4566e03..8d12e8c7 100644 --- a/tests/Database/Traits/BaseSanitizationTest.php +++ b/tests/Database/Traits/BaseSanitizationTest.php @@ -161,7 +161,7 @@ public function get_sanitized_key( $key ) { } /** - * Public access to protected parse_args_to_array method. + * Public access to protected parse_args method. * * @since 3.1.0 * @@ -169,8 +169,8 @@ public function get_sanitized_key( $key ) { * @param array $defaults * @return array */ - public function get_parsed_args_array( $args = array(), $defaults = array() ) { - return $this->parse_args_to_array( $args, $defaults ); + public function get_parsed_args( $args = array(), $defaults = array() ) { + return $this->parse_args( $args, $defaults ); } } @@ -802,12 +802,12 @@ public function test_sanitize_key() { } /** - * parse_args_to_array() mirrors wp_parse_args(): merges an array/object/query-string + * parse_args() mirrors wp_parse_args(): merges an array/object/query-string * over defaults (filter-free). * * @since 3.1.0 */ - public function test_parse_args_to_array() { + public function test_parse_args() { // Array over defaults - passed values win, defaults fill the rest. $this->assertSame( @@ -815,7 +815,7 @@ public function test_parse_args_to_array() { 'a' => 1, 'b' => 2, ), - $this->helper->get_parsed_args_array( + $this->helper->get_parsed_args( array( 'a' => 1 ), array( 'a' => 0, @@ -825,13 +825,13 @@ public function test_parse_args_to_array() { ); // Array with no defaults passes through. - $this->assertSame( array( 'x' => 1 ), $this->helper->get_parsed_args_array( array( 'x' => 1 ) ) ); + $this->assertSame( array( 'x' => 1 ), $this->helper->get_parsed_args( array( 'x' => 1 ) ) ); // Empty args with defaults yields the defaults. - $this->assertSame( array( 'd' => 4 ), $this->helper->get_parsed_args_array( array(), array( 'd' => 4 ) ) ); + $this->assertSame( array( 'd' => 4 ), $this->helper->get_parsed_args( array(), array( 'd' => 4 ) ) ); // Object input is read via get_object_vars(). - $this->assertSame( array( 'k' => 'v' ), $this->helper->get_parsed_args_array( (object) array( 'k' => 'v' ) ) ); + $this->assertSame( array( 'k' => 'v' ), $this->helper->get_parsed_args( (object) array( 'k' => 'v' ) ) ); // Query-string input is parsed with parse_str(). $this->assertSame( @@ -839,7 +839,7 @@ public function test_parse_args_to_array() { 'a' => '1', 'b' => '2', ), - $this->helper->get_parsed_args_array( 'a=1&b=2' ) + $this->helper->get_parsed_args( 'a=1&b=2' ) ); // Query-string over defaults. @@ -848,7 +848,7 @@ public function test_parse_args_to_array() { 'a' => '1', 'c' => '3', ), - $this->helper->get_parsed_args_array( + $this->helper->get_parsed_args( 'a=1', array( 'a' => '0', From ac343db10de119d5717faaed36b39b57e7772b18 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 22 Sep 2026 20:38:40 -0500 Subject: [PATCH 3/3] Column: retain cast argument in get_name_sql(). --- CHANGELOG.md | 12 ++++++----- src/Database/Kern/Column.php | 20 ++++--------------- src/Database/Operands/Column.php | 2 +- src/Database/Traits/Operator.php | 2 +- tests/Database/Kern/Column/ColumnTest.php | 12 +++++------ .../Database/ReleasedSubclassContractTest.php | 7 +------ 6 files changed, 20 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a33a3ff..7f598a2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +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's `get_name_sql()` keeps its released alias-only signature, while - explicit casts use `get_name_sql_with_cast()`. Cast-aware operator rendering uses - `get_sql_with_cast()`; custom operators must override that method to customize - explicit casts. The comparison operator classes moved under `Comparisons\` + 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 intentional `parse_args()` - and Column type-predicate signature changes are documented below. + 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 @@ -597,6 +596,9 @@ Notable changes to BerlinDB are documented here. `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/src/Database/Kern/Column.php b/src/Database/Kern/Column.php index e859ed59..37bbf2dd 100644 --- a/src/Database/Kern/Column.php +++ b/src/Database/Kern/Column.php @@ -2430,30 +2430,18 @@ 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`. * - * @since 3.0.0 - * - * @param string $alias Optional. Table alias to prefix. Default empty (no alias). - * - * @return string Quoted SQL reference, e.g. `alias`.`column`. - */ - public function get_name_sql( string $alias = '' ): string { - return $this->get_name_sql_with_cast( $alias ); - } - - /** - * Return the backtick-quoted column name with an optional SQL cast. - * * 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.1.0 + * @since 3.0.0 + * @since 3.1.0 Added the $cast parameter. * * @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_with_cast( string $alias = '', string $cast = '' ): string { + public function get_name_sql( string $alias = '', string $cast = '' ): string { // Quote the column name. $quoted = $this->quote_identifier( $this->name ); @@ -2526,7 +2514,7 @@ 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_with_cast(), so the category matches the + * 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. * diff --git a/src/Database/Operands/Column.php b/src/Database/Operands/Column.php index f031ad18..397c413a 100644 --- a/src/Database/Operands/Column.php +++ b/src/Database/Operands/Column.php @@ -97,7 +97,7 @@ protected function init( array $args ): void { * @return string */ public function get_sql(): string { - return $this->column->get_name_sql_with_cast( $this->alias, $this->cast ); + return $this->column->get_name_sql( $this->alias, $this->cast ); } /** diff --git a/src/Database/Traits/Operator.php b/src/Database/Traits/Operator.php index 87b5436a..e4de6e7e 100644 --- a/src/Database/Traits/Operator.php +++ b/src/Database/Traits/Operator.php @@ -406,6 +406,6 @@ private function get_comparison_sql( Column $col, string $alias = '', $value = n } // Assemble and return the full expression, optionally casting the column. - return $col->get_name_sql_with_cast( $alias, $cast ) . ' ' . $this->get_sql_compare() . ' ' . $value_sql; + return $col->get_name_sql( $alias, $cast ) . ' ' . $this->get_sql_compare() . ' ' . $value_sql; } } diff --git a/tests/Database/Kern/Column/ColumnTest.php b/tests/Database/Kern/Column/ColumnTest.php index ca34aa09..088e076c 100644 --- a/tests/Database/Kern/Column/ColumnTest.php +++ b/tests/Database/Kern/Column/ColumnTest.php @@ -2380,7 +2380,7 @@ public function test_get_type_category() { $this->assertSame( 'time', $varchar->get_type_category( 'TIME' ) ); /* - * The cast is normalized like get_name_sql_with_cast(): a sloppy-but-valid cast is + * The cast is normalized like get_name_sql(): a sloppy-but-valid cast is * honored, and an invalid cast is ignored (the declared type decides). */ $this->assertSame( 'numeric', $varchar->get_type_category( ' signed ' ) ); @@ -2455,11 +2455,11 @@ public function test_temporal_type_predicates() { } /** - * Test that get_name_sql_with_cast wraps the reference when a cast is given. + * Test that get_name_sql wraps the reference when a cast is given. * * @since 3.1.0 */ - public function test_get_name_sql_with_cast_optionally_casts() { + public function test_get_name_sql_optionally_casts() { $column = new Column( array( 'name' => 'total', @@ -2472,13 +2472,13 @@ public function test_get_name_sql_with_cast_optionally_casts() { $this->assertSame( '`a`.`total`', $column->get_name_sql( 'a' ) ); // With a cast: wrapped. - $this->assertSame( 'CAST(`a`.`total` AS SIGNED)', $column->get_name_sql_with_cast( 'a', 'SIGNED' ) ); + $this->assertSame( 'CAST(`a`.`total` AS SIGNED)', $column->get_name_sql( 'a', 'SIGNED' ) ); // CHAR is a real cast target (string-semantics comparison / LIKE). - $this->assertSame( 'CAST(`a`.`total` AS CHAR)', $column->get_name_sql_with_cast( 'a', 'CHAR' ) ); + $this->assertSame( 'CAST(`a`.`total` AS CHAR)', $column->get_name_sql( 'a', 'CHAR' ) ); // Invalid cast is sanitized away at this public boundary (no cast). - $this->assertSame( '`a`.`total`', $column->get_name_sql_with_cast( 'a', 'nonsense' ) ); + $this->assertSame( '`a`.`total`', $column->get_name_sql( 'a', 'nonsense' ) ); } /** diff --git a/tests/Database/ReleasedSubclassContractTest.php b/tests/Database/ReleasedSubclassContractTest.php index 006aec10..9aeb64f9 100644 --- a/tests/Database/ReleasedSubclassContractTest.php +++ b/tests/Database/ReleasedSubclassContractTest.php @@ -74,11 +74,6 @@ public function is_numeric() { return parent::is_numeric(); } - /** @inheritDoc */ - public function get_name_sql( string $alias = '' ): string { - return parent::get_name_sql( $alias ); - } - /** * Preserve the released success check override. * @@ -209,7 +204,7 @@ public function test_sql_helpers_honor_released_overrides(): void { 'type' => 'int', ) ); - $this->assertSame( 'CAST(`total` AS SIGNED)', $column->get_name_sql_with_cast( '', 'SIGNED' ) ); + $this->assertSame( 'CAST(`total` AS SIGNED)', $column->get_name_sql( '', 'SIGNED' ) ); $this->assertSame( 'custom = 1', ( new ReleasedOperatorOverride() )->get_sql_with_cast( $column, '', 1 ) ); $this->assertNotEmpty( ( new ReleasedQueryOverrides() )->get_columns() ); }