From 9cdd372bc2c8c60ce5a66246cc62ec00c9fad0c6 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 10 Sep 2026 19:50:41 -0400 Subject: [PATCH 1/6] Accept proven transactional table backends --- .../PostIdentityReservations.php | 28 +++++++++++-- .../PostIdentityReservationsTest.php | 40 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php b/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php index 7efd3981c..b4499b7c0 100644 --- a/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php +++ b/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php @@ -213,9 +213,7 @@ public function validate_schema() { $this->table_name ) ); - if ( 'INNODB' !== strtoupper( (string) $engine ) ) { - return new \WP_Error( 'identity_schema_engine', 'Post identity reservation table must use InnoDB.' ); - } + $uses_innodb = 'INNODB' === strtoupper( (string) $engine ); $required_columns = array( 'identity_hash' => array( @@ -324,6 +322,9 @@ public function validate_schema() { if ( ! $has_nonunique_post_id || $has_unique_post_id ) { return new \WP_Error( 'identity_schema_post_index', 'Post identity reservation post_id requires a nonunique index.' ); } + if ( ! $uses_innodb && ! $this->supports_transactional_tables( array( $this->table_name, $this->wpdb->posts ) ) ) { + return new \WP_Error( 'identity_schema_engine', 'Post identity reservation table must use InnoDB.' ); + } return true; } @@ -705,6 +706,9 @@ protected function verify_transactional_storage() { return new \WP_Error( 'identity_storage_invalid', 'Post identity storage has an invalid table name.' ); } } + if ( $this->supports_transactional_tables( $tables ) ) { + return true; + } $placeholders = implode( ',', array_fill( 0, count( $tables ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching @@ -729,6 +733,24 @@ protected function verify_transactional_storage() { return true; } + /** + * Ask an optional database implementation to prove one atomic table set. + * + * The capability is deliberately exact: it may replace the InnoDB engine + * check only when every table in this reservation operation participates. + */ + private function supports_transactional_tables( array $tables ): bool { + if ( ! method_exists( $this->wpdb, 'supports_transactional_tables' ) ) { + return false; + } + + try { + return true === $this->wpdb->supports_transactional_tables( $tables ); + } catch ( \Throwable ) { + return false; + } + } + /** @return int[] */ protected function find_legacy_candidates( array $identity ): array { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching diff --git a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php index 59ab10524..d940f88f4 100644 --- a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php +++ b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php @@ -138,6 +138,46 @@ public function test_nontransactional_reservation_table_fails_closed(): void { } } + public function test_exact_transactional_table_capability_can_replace_the_engine_check(): void { + global $wpdb; + + $this->assertNotFalse( $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ENGINE=MyISAM', $this->repository->get_table_name() ) ) ); + $original = $wpdb; + $capable = new class( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ) extends \wpdb { + public array $supported_tables = array(); + public bool $throws = false; + + public function supports_transactional_tables( array $tables ): bool { + if ( $this->throws ) { + throw new \RuntimeException( 'Unsupported transactional table capability.' ); + } + return $tables === $this->supported_tables; + } + }; + $capable->set_prefix( $original->prefix ); + $wpdb = $capable; + + try { + $repository = new PostIdentityReservations(); + $capable->supported_tables = array( $repository->get_table_name() ); + $incomplete = $repository->validate_schema(); + $this->assertWPError( $incomplete ); + $this->assertSame( 'identity_schema_engine', $incomplete->get_error_code() ); + + $capable->throws = true; + $thrown = $repository->validate_schema(); + $this->assertWPError( $thrown ); + $this->assertSame( 'identity_schema_engine', $thrown->get_error_code() ); + + $capable->throws = false; + $capable->supported_tables = array( $repository->get_table_name(), $capable->posts ); + $this->assertTrue( $repository->validate_schema() ); + } finally { + $wpdb = $original; + $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ENGINE=InnoDB', $this->repository->get_table_name() ) ); + } + } + public function test_create_table_repairs_myisam_to_innodb(): void { global $wpdb; From 14f5198a8237a11ed62a189633c8ddf969df2282 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 10 Sep 2026 20:07:23 -0400 Subject: [PATCH 2/6] Cover transactional capability failure modes --- .../PostIdentityReservationsTest.php | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php index d940f88f4..02349b0d0 100644 --- a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php +++ b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php @@ -121,9 +121,10 @@ public function test_missing_table_fails_identity_write_closed(): void { } } - public function test_nontransactional_reservation_table_fails_closed(): void { + public function test_nontransactional_reservation_table_fails_closed_without_the_capability(): void { global $wpdb; + $this->assertFalse( method_exists( $wpdb, 'supports_transactional_tables' ) ); $changed = $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ENGINE=MyISAM', $this->repository->get_table_name() ) ); $this->assertNotFalse( $changed ); try { @@ -138,20 +139,21 @@ public function test_nontransactional_reservation_table_fails_closed(): void { } } - public function test_exact_transactional_table_capability_can_replace_the_engine_check(): void { + /** This fake wpdb covers consumer routing, not native capability conformance. */ + public function test_exact_transactional_table_capability_routes_only_a_complete_valid_schema(): void { global $wpdb; $this->assertNotFalse( $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ENGINE=MyISAM', $this->repository->get_table_name() ) ) ); $original = $wpdb; $capable = new class( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ) extends \wpdb { public array $supported_tables = array(); - public bool $throws = false; + public mixed $result = false; - public function supports_transactional_tables( array $tables ): bool { - if ( $this->throws ) { + public function supports_transactional_tables( array $tables ) { + if ( 'throw' === $this->result ) { throw new \RuntimeException( 'Unsupported transactional table capability.' ); } - return $tables === $this->supported_tables; + return $tables === $this->supported_tables ? $this->result : false; } }; $capable->set_prefix( $original->prefix ); @@ -164,17 +166,27 @@ public function supports_transactional_tables( array $tables ): bool { $this->assertWPError( $incomplete ); $this->assertSame( 'identity_schema_engine', $incomplete->get_error_code() ); - $capable->throws = true; + $capable->supported_tables = array( $repository->get_table_name(), $capable->posts ); + $capable->result = 1; + $strict_false = $repository->validate_schema(); + $this->assertWPError( $strict_false ); + $this->assertSame( 'identity_schema_engine', $strict_false->get_error_code() ); + + $capable->result = 'throw'; $thrown = $repository->validate_schema(); $this->assertWPError( $thrown ); $this->assertSame( 'identity_schema_engine', $thrown->get_error_code() ); - $capable->throws = false; - $capable->supported_tables = array( $repository->get_table_name(), $capable->posts ); + $capable->result = true; $this->assertTrue( $repository->validate_schema() ); + + $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i DROP COLUMN completed_at', $repository->get_table_name() ) ); + $invalid_schema = $repository->validate_schema(); + $this->assertWPError( $invalid_schema ); + $this->assertSame( 'identity_schema_columns', $invalid_schema->get_error_code() ); } finally { $wpdb = $original; - $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ENGINE=InnoDB', $this->repository->get_table_name() ) ); + PostIdentityReservations::create_table(); } } From 8a58e51416556c34d37b269a7d1197d94755f1ab Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 10 Sep 2026 20:45:35 -0400 Subject: [PATCH 3/6] test(database): avoid wpdb result property conflict --- .../PostIdentityReservationsTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php index 02349b0d0..be3895d6b 100644 --- a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php +++ b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php @@ -147,13 +147,13 @@ public function test_exact_transactional_table_capability_routes_only_a_complete $original = $wpdb; $capable = new class( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ) extends \wpdb { public array $supported_tables = array(); - public mixed $result = false; + public mixed $capability_result = false; public function supports_transactional_tables( array $tables ) { - if ( 'throw' === $this->result ) { + if ( 'throw' === $this->capability_result ) { throw new \RuntimeException( 'Unsupported transactional table capability.' ); } - return $tables === $this->supported_tables ? $this->result : false; + return $tables === $this->supported_tables ? $this->capability_result : false; } }; $capable->set_prefix( $original->prefix ); @@ -167,17 +167,17 @@ public function supports_transactional_tables( array $tables ) { $this->assertSame( 'identity_schema_engine', $incomplete->get_error_code() ); $capable->supported_tables = array( $repository->get_table_name(), $capable->posts ); - $capable->result = 1; + $capable->capability_result = 1; $strict_false = $repository->validate_schema(); $this->assertWPError( $strict_false ); $this->assertSame( 'identity_schema_engine', $strict_false->get_error_code() ); - $capable->result = 'throw'; + $capable->capability_result = 'throw'; $thrown = $repository->validate_schema(); $this->assertWPError( $thrown ); $this->assertSame( 'identity_schema_engine', $thrown->get_error_code() ); - $capable->result = true; + $capable->capability_result = true; $this->assertTrue( $repository->validate_schema() ); $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i DROP COLUMN completed_at', $repository->get_table_name() ) ); From 7349c52667f5b1bd612be9115b43f435bbbc655d Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 10 Sep 2026 20:48:37 -0400 Subject: [PATCH 4/6] test(database): delegate capability fake queries --- .../PostIdentityReservationsTest.php | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php index be3895d6b..9ee75d4ce 100644 --- a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php +++ b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php @@ -145,10 +145,32 @@ public function test_exact_transactional_table_capability_routes_only_a_complete $this->assertNotFalse( $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ENGINE=MyISAM', $this->repository->get_table_name() ) ) ); $original = $wpdb; - $capable = new class( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ) extends \wpdb { + $capable = new class( $original ) extends \wpdb { + private \wpdb $delegate; public array $supported_tables = array(); public mixed $capability_result = false; + public function __construct( \wpdb $delegate ) { + $this->delegate = $delegate; + $this->set_prefix( $delegate->prefix ); + } + + public function prepare( $query, ...$args ) { + return $this->delegate->prepare( $query, ...$args ); + } + + public function query( $query ) { + return $this->delegate->query( $query ); + } + + public function get_var( $query = null, $x = 0, $y = 0 ) { + return $this->delegate->get_var( $query, $x, $y ); + } + + public function get_results( $query = null, $output = OBJECT ) { + return $this->delegate->get_results( $query, $output ); + } + public function supports_transactional_tables( array $tables ) { if ( 'throw' === $this->capability_result ) { throw new \RuntimeException( 'Unsupported transactional table capability.' ); @@ -156,7 +178,6 @@ public function supports_transactional_tables( array $tables ) { return $tables === $this->supported_tables ? $this->capability_result : false; } }; - $capable->set_prefix( $original->prefix ); $wpdb = $capable; try { From 4432c2433dbce2985654cc88f57dc3d5abc411f0 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 10 Sep 2026 20:51:10 -0400 Subject: [PATCH 5/6] test(database): preserve capability fake table prefix --- .../PostIdentityReservationsTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php index 9ee75d4ce..776dcef48 100644 --- a/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php +++ b/tests/Unit/Core/Database/PostIdentityReservations/PostIdentityReservationsTest.php @@ -151,8 +151,10 @@ public function test_exact_transactional_table_capability_routes_only_a_complete public mixed $capability_result = false; public function __construct( \wpdb $delegate ) { - $this->delegate = $delegate; - $this->set_prefix( $delegate->prefix ); + $this->delegate = $delegate; + $this->prefix = $delegate->prefix; + $this->base_prefix = $delegate->base_prefix; + $this->posts = $delegate->posts; } public function prepare( $query, ...$args ) { From b6fe3762ea92a31b30d63cb72607dfaaa8582b4f Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 10 Sep 2026 21:34:04 -0400 Subject: [PATCH 6/6] style(database): align capability schema query --- .../PostIdentityReservations/PostIdentityReservations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php b/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php index b4499b7c0..d56f6c130 100644 --- a/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php +++ b/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php @@ -206,7 +206,7 @@ public function validate_schema() { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $database = (string) $this->wpdb->get_var( 'SELECT DATABASE()' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $engine = $this->wpdb->get_var( + $engine = $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s', $database,