From cb73ba728c98d36a7e6a1e3a39fdfc1c6b8177d2 Mon Sep 17 00:00:00 2001 From: Matt Harrison Date: Wed, 17 Dec 2025 17:32:29 -0500 Subject: [PATCH 1/2] Failing test for ticket 38921 --- tests/phpunit/includes/utils.php | 5 +++++ tests/phpunit/tests/db.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/tests/phpunit/includes/utils.php b/tests/phpunit/includes/utils.php index 014f4b83c2301..d22d38540a879 100644 --- a/tests/phpunit/includes/utils.php +++ b/tests/phpunit/includes/utils.php @@ -571,6 +571,11 @@ public function __construct() { public function __call( $name, $arguments ) { return call_user_func_array( array( $this, $name ), $arguments ); } + + public function reset_cache_values() { + $this->table_charset = array(); + $this->col_meta = array(); + } } /** diff --git a/tests/phpunit/tests/db.php b/tests/phpunit/tests/db.php index 22eeb235e4819..3fda47b22620f 100644 --- a/tests/phpunit/tests/db.php +++ b/tests/phpunit/tests/db.php @@ -2486,4 +2486,35 @@ public function test_check_connection_returns_true_when_there_is_a_connection() $this->assertTrue( $wpdb->check_connection( false ) ); } + + /** + * Tests if the column charset doesn't throw errors with the `pre_get_table_charset` filter. + * + * @ticket 38921 + */ + public function test_get_table_and_column_charset_with_pre_get_table_charset_filter() { + global $wpdb; + $expected_charset = $wpdb->get_col_charset( $wpdb->posts, 'post_content' ); + + self::$_wpdb->reset_cache_values(); + + add_filter( 'pre_get_table_charset', array( $this, 'change_table_charset_callback' ), 10, 2 ); + $table_charset = self::$_wpdb->get_table_charset( $wpdb->posts ); + $column_charset = self::$_wpdb->get_col_charset( $wpdb->posts, 'post_content' ); + remove_filter( 'pre_get_table_charset', array( $this, 'change_table_charset_callback' ), 10, 2 ); + + $this->assertSame( 'fake_charset', $table_charset ); + $this->assertSame( $expected_charset, $column_charset ); + } + + /** + * Callback for the `pre_get_table_charset` filter. + * + * @param string $charset The table's character set. + * @param string $table The name of the table. + * @return string $charset The table's character set. + */ + public function change_table_charset_callback( $charset, $table ) { + return 'fake_charset'; + } } From f45fe8602714a25d9fae70d9344d1102f292c05e Mon Sep 17 00:00:00 2001 From: Matt Harrison Date: Wed, 17 Dec 2025 17:33:45 -0500 Subject: [PATCH 2/2] Change how column meta is pulled so it uses a get_cols_meta function It previously used a cached values array throughout the code which relied on get_table_charset being run to prime the cache. If the pre_get_table_charset was set then the cache would never be primed and cache checks would throw errors. Fixes #38921 and #59836 --- src/wp-includes/class-wpdb.php | 175 +++++++++++++++++++++++---------- 1 file changed, 124 insertions(+), 51 deletions(-) diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index 23c865b87d817..6fde31d891c30 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -3227,21 +3227,15 @@ protected function get_table_charset( $table ) { } $charsets = array(); - $columns = array(); + $columns = $this->get_cols_meta( $table ); - $table_parts = explode( '.', $table ); - $table = '`' . implode( '`.`', $table_parts ) . '`'; - $results = $this->get_results( "SHOW FULL COLUMNS FROM $table" ); - if ( ! $results ) { - return new WP_Error( 'wpdb_get_table_charset_failure', __( 'Could not retrieve table charset.' ) ); + if ( is_wp_error( $columns ) ) { + return $columns; } - - foreach ( $results as $column ) { - $columns[ strtolower( $column->Field ) ] = $column; + if ( empty( $columns ) ) { + $columns = array(); } - $this->col_meta[ $tablekey ] = $columns; - foreach ( $columns as $column ) { if ( ! empty( $column->Collation ) ) { list( $charset ) = explode( '_', $column->Collation ); @@ -3291,6 +3285,109 @@ protected function get_table_charset( $table ) { return $charset; } + /** + * Retrieves the column metadata for the given column. + * + * @since 7.0.0 + * + * @param string $table Table name. + * @return object|false|WP_Error Column meta set as an object. False if the column was + * not found. WP_Error object if there was an error. + */ + public function get_cols_meta( $table ) { + $tablekey = strtolower( $table ); + + /** + * Filters the columns meta value before the DB is checked. + * + * Passing a non-null value to the filter will short-circuit + * checking the DB for the meta, returning that value instead. + * + * @since 7.0.0 + * + * @param object[]|null|false|WP_Error $meta The meta to use. Default null. + * @param string $table The name of the table being checked. + */ + $col_meta = apply_filters( 'pre_get_cols_meta', null, $table ); + if ( null !== $col_meta ) { + return $col_meta; + } + + // Skip this entirely if this isn't a MySQL database. + if ( empty( $this->is_mysql ) ) { + return false; + } + + // If no column information, fetch it from the database. + if ( empty( $this->col_meta[ $tablekey ] ) ) { + $columns = array(); + + $table_parts = explode( '.', $table ); + $table = '`' . implode( '`.`', $table_parts ) . '`'; + $results = $this->get_results( "SHOW FULL COLUMNS FROM $table" ); + if ( ! $results ) { + return new WP_Error( 'wpdb_get_cols_meta_failure', __( 'Could not retrieve column metadata.' ) ); + } + + foreach ( $results as $column ) { + $columns[ strtolower( $column->Field ) ] = $column; + } + + $this->col_meta[ $tablekey ] = $columns; + } + + // If this table data doesn't exist, return false. + if ( empty( $this->col_meta[ $tablekey ] ) ) { + return false; + } + + return $this->col_meta[ $tablekey ]; + } + + /** + * Retrieves the column metadata for the given column. + * + * @since 7.0.0 + * + * @param string $table Table name. + * @param string $column Column name. + * @return object|false|WP_Error Column character set as a string. False if the column has + * no character set. WP_Error object if there was an error. + */ + public function get_col_meta( $table, $column ) { + $columnkey = strtolower( $column ); + + /** + * Filters the column meta value before the DB is checked. + * + * Passing a non-null value to the filter will short-circuit + * checking the DB for the meta, returning that value instead. + * + * @since 7.0.0 + * + * @param object|null|false|WP_Error $meta The meta to use. Default null. + * @param string $table The name of the table being checked. + * @param string $column The name of the column being checked. + */ + $col_meta = apply_filters( 'pre_get_col_meta', null, $table, $column ); + if ( null !== $col_meta ) { + return $col_meta; + } + + $cols_meta = $this->get_cols_meta( $table ); + + if ( is_wp_error( $cols_meta ) ) { + return $cols_meta; + } + + // If this column doesn't exist, return false. + if ( empty( $cols_meta[ $columnkey ] ) ) { + return false; + } + + return $cols_meta[ $columnkey ]; + } + /** * Retrieves the character set for the given column. * @@ -3302,9 +3399,6 @@ protected function get_table_charset( $table ) { * no character set. WP_Error object if there was an error. */ public function get_col_charset( $table, $column ) { - $tablekey = strtolower( $table ); - $columnkey = strtolower( $column ); - /** * Filters the column charset value before the DB is checked. * @@ -3322,35 +3416,23 @@ public function get_col_charset( $table, $column ) { return $charset; } - // Skip this entirely if this isn't a MySQL database. - if ( empty( $this->is_mysql ) ) { - return false; - } - - if ( empty( $this->table_charset[ $tablekey ] ) ) { - // This primes column information for us. - $table_charset = $this->get_table_charset( $table ); - if ( is_wp_error( $table_charset ) ) { - return $table_charset; - } - } + $col_meta = $this->get_col_meta( $table, $column ); - // If still no column information, return the table charset. - if ( empty( $this->col_meta[ $tablekey ] ) ) { - return $this->table_charset[ $tablekey ]; + if ( is_wp_error( $col_meta ) ) { + return $col_meta; } // If this column doesn't exist, return the table charset. - if ( empty( $this->col_meta[ $tablekey ][ $columnkey ] ) ) { - return $this->table_charset[ $tablekey ]; + if ( empty( $col_meta ) ) { + return $this->get_table_charset( $table ); } // Return false when it's not a string column. - if ( empty( $this->col_meta[ $tablekey ][ $columnkey ]->Collation ) ) { + if ( empty( $col_meta->Collation ) ) { return false; } - list( $charset ) = explode( '_', $this->col_meta[ $tablekey ][ $columnkey ]->Collation ); + list( $charset ) = explode( '_', $col_meta->Collation ); return $charset; } @@ -3372,27 +3454,17 @@ public function get_col_charset( $table, $column ) { * } */ public function get_col_length( $table, $column ) { - $tablekey = strtolower( $table ); - $columnkey = strtolower( $column ); - - // Skip this entirely if this isn't a MySQL database. - if ( empty( $this->is_mysql ) ) { - return false; - } + $col_meta = $this->get_col_meta( $table, $column ); - if ( empty( $this->col_meta[ $tablekey ] ) ) { - // This primes column information for us. - $table_charset = $this->get_table_charset( $table ); - if ( is_wp_error( $table_charset ) ) { - return $table_charset; - } + if ( is_wp_error( $col_meta ) ) { + return $col_meta; } - if ( empty( $this->col_meta[ $tablekey ][ $columnkey ] ) ) { + if ( empty( $col_meta ) ) { return false; } - $typeinfo = explode( '(', $this->col_meta[ $tablekey ][ $columnkey ]->Type ); + $typeinfo = explode( '(', $col_meta->Type ); $type = strtolower( $typeinfo[0] ); if ( ! empty( $typeinfo[1] ) ) { @@ -3510,8 +3582,9 @@ protected function check_safe_collation( $query ) { return true; } - $table = strtolower( $table ); - if ( empty( $this->col_meta[ $table ] ) ) { + $cols_meta = $this->get_cols_meta( $table ); + + if ( is_wp_error( $cols_meta ) || empty( $cols_meta ) ) { return false; } @@ -3525,7 +3598,7 @@ protected function check_safe_collation( $query ) { 'utf8mb4_general_ci', ); - foreach ( $this->col_meta[ $table ] as $col ) { + foreach ( $cols_meta as $col ) { if ( empty( $col->Collation ) ) { continue; }