Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()`
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 27 additions & 4 deletions autoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down
20 changes: 9 additions & 11 deletions src/Database/Kern/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion tests/Database/Kern/Column/ColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
*/
Expand Down
41 changes: 41 additions & 0 deletions tests/Database/ReleasedSubclassContractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
Loading