diff --git a/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php b/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php index 7efd3981c..d56f6c130 100644 --- a/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php +++ b/inc/Core/Database/PostIdentityReservations/PostIdentityReservations.php @@ -206,16 +206,14 @@ 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, $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..776dcef48 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,6 +139,80 @@ public function test_nontransactional_reservation_table_fails_closed(): 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( $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->prefix = $delegate->prefix; + $this->base_prefix = $delegate->base_prefix; + $this->posts = $delegate->posts; + } + + 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.' ); + } + return $tables === $this->supported_tables ? $this->capability_result : false; + } + }; + $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->supported_tables = array( $repository->get_table_name(), $capable->posts ); + $capable->capability_result = 1; + $strict_false = $repository->validate_schema(); + $this->assertWPError( $strict_false ); + $this->assertSame( 'identity_schema_engine', $strict_false->get_error_code() ); + + $capable->capability_result = 'throw'; + $thrown = $repository->validate_schema(); + $this->assertWPError( $thrown ); + $this->assertSame( 'identity_schema_engine', $thrown->get_error_code() ); + + $capable->capability_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; + PostIdentityReservations::create_table(); + } + } + public function test_create_table_repairs_myisam_to_innodb(): void { global $wpdb;