@@ -3515,9 +3515,55 @@ private function translate_query_expression( WP_Parser_Node $node ): string {
35153515 * @return string|null
35163516 */
35173517 private function translate_query_specification ( WP_Parser_Node $ node ): string {
3518+ $ from = $ node ->get_first_child_node ( 'fromClause ' );
35183519 $ group_by = $ node ->get_first_child_node ( 'groupByClause ' );
35193520 $ having = $ node ->get_first_child_node ( 'havingClause ' );
35203521
3522+ /*
3523+ * Check if the query may possibly read from an information schema table
3524+ * using a "*" wildcard, such as "SELECT *", "SELECT t.*", and similar.
3525+ * If that's the case, we'll need to expand the wildcard to a list of
3526+ * column names and inject the configured database name dynamically.
3527+ */
3528+ if ( $ from && $ from ->has_child_node ( 'tableReferenceList ' ) ) {
3529+ $ select_item_list = $ node ->get_first_child_node ( 'selectItemList ' );
3530+ $ table_reference_list = $ from ->get_first_child_node ( 'tableReferenceList ' );
3531+
3532+ // Check if the query contains any wildcards.
3533+ $ has_wildcard = $ select_item_list ->has_child_token ( WP_MySQL_Lexer::MULT_OPERATOR );
3534+ if ( ! $ has_wildcard ) {
3535+ foreach ( $ select_item_list ->get_child_nodes () as $ select_item ) {
3536+ if ( $ select_item ->has_child_node ( 'tableWild ' ) ) {
3537+ $ has_wildcard = true ;
3538+ break ;
3539+ }
3540+ }
3541+ }
3542+
3543+ if ( $ has_wildcard ) {
3544+ $ table_refs = $ table_reference_list ->get_descendant_nodes ( 'tableRef ' );
3545+
3546+ // Check if the query may reference any information schema tables.
3547+ // This check is approximate, as it also descends into subqueries.
3548+ $ references_information_schema = false ;
3549+ foreach ( $ table_refs as $ table_ref ) {
3550+ $ references_information_schema = str_starts_with (
3551+ strtolower ( $ this ->translate ( $ table_ref ) ),
3552+ self ::RESERVED_PREFIX . 'mysql_information_schema_ '
3553+ );
3554+ if ( $ references_information_schema ) {
3555+ break ;
3556+ }
3557+ }
3558+
3559+ // We have both wildcards and information schema tables.
3560+ // Let's expand the wildcards to a list of columns.
3561+ if ( $ references_information_schema ) {
3562+ return $ this ->translate_query_specification_with_information_schema_wildcards ( $ node );
3563+ }
3564+ }
3565+ }
3566+
35213567 /*
35223568 * When the GROUP BY or HAVING clause is present, we need to disambiguate
35233569 * the items to ensure they don't cause an "ambiguous column name" error.
@@ -3592,6 +3638,83 @@ private function translate_query_specification( WP_Parser_Node $node ): string {
35923638 return $ this ->translate_sequence ( $ node ->get_children () );
35933639 }
35943640
3641+ /**
3642+ * Translate a query specification with information schema wildcards to SQLite.
3643+ *
3644+ * When a SELECT item contains wildcards, such as "SELECT *" or "SELECT t.*",
3645+ * and the query references an information schema table, we need to expand the
3646+ * wildcards to a list of columns and inject the configured database name.
3647+ *
3648+ * @param WP_Parser_Node $node The "querySpecification" AST node.
3649+ * @return string The translated value.
3650+ */
3651+ private function translate_query_specification_with_information_schema_wildcards ( WP_Parser_Node $ node ): string {
3652+ $ select_item_list = $ node ->get_first_child_node ( 'selectItemList ' );
3653+ $ from = $ node ->get_first_child_node ( 'fromClause ' );
3654+ $ table_reference_list = $ from ->get_first_child_node ( 'tableReferenceList ' );
3655+
3656+ // Collect all tables used in the query.
3657+ $ table_alias_map = $ this ->create_table_reference_map ( $ table_reference_list );
3658+
3659+ // Translate the SELECT item list, expanding wildcards that are targeting
3660+ // the information schema tables, and replacing the database name with
3661+ // the configured database name.
3662+ $ transformed_list = array ();
3663+ foreach ( $ select_item_list ->get_children () as $ select_item ) {
3664+ if ( $ select_item instanceof WP_MySQL_Token ) {
3665+ // For a global wildcard ("SELECT *"), we need to expand all tables.
3666+ if ( WP_MySQL_Lexer::MULT_OPERATOR === $ select_item ->id ) {
3667+ foreach ( $ table_alias_map as $ table_alias => $ table_data ) {
3668+ $ transformed_list [] = $ this ->expand_wildcard ( $ table_data ['table_name ' ], $ table_alias );
3669+ }
3670+ }
3671+ } elseif ( $ select_item ->has_child_node ( 'tableWild ' ) ) {
3672+ // For a table wildcard ("SELECT t.*"), we expand the given table.
3673+ $ table_wild = $ select_item ->get_first_child_node ( 'tableWild ' );
3674+ $ identifiers = $ table_wild ->get_child_nodes ( 'identifier ' );
3675+
3676+ // Do not expand the wildcard if the identifier contains a database
3677+ // name and targets a different database than "information_schema".
3678+ if (
3679+ 2 === count ( $ identifiers )
3680+ && '`information_schema` ' !== strtolower ( $ this ->translate ( $ identifiers [0 ] ) )
3681+ ) {
3682+ $ transformed_list [] = $ this ->translate ( $ select_item );
3683+ continue ;
3684+ }
3685+
3686+ // Do not expand the wildcard if the identifier has no database
3687+ // name and the current database is not "information_schema".
3688+ if (
3689+ 1 === count ( $ identifiers )
3690+ && 'information_schema ' !== $ this ->db_name
3691+ ) {
3692+ $ transformed_list [] = $ this ->translate ( $ select_item );
3693+ continue ;
3694+ }
3695+
3696+ // Expand the wildcard.
3697+ $ last_identifier = end ( $ identifiers );
3698+ $ alias = $ this ->unquote_sqlite_identifier ( $ this ->translate ( $ last_identifier ) );
3699+ $ table_name = $ table_alias_map [ $ alias ]['table_name ' ];
3700+ $ transformed_list [] = $ this ->expand_wildcard ( $ table_name , $ alias );
3701+ } else {
3702+ $ transformed_list [] = $ this ->translate ( $ select_item );
3703+ }
3704+ }
3705+
3706+ // Translate node children, replacing the SELECT list with the transformed one.
3707+ $ parts = array ();
3708+ foreach ( $ node ->get_children () as $ child ) {
3709+ if ( $ child instanceof WP_Parser_Node && 'selectItemList ' === $ child ->rule_name ) {
3710+ $ parts [] = implode ( ', ' , $ transformed_list );
3711+ } else {
3712+ $ parts [] = $ this ->translate ( $ child );
3713+ }
3714+ }
3715+ return implode ( ' ' , $ parts );
3716+ }
3717+
35953718 /**
35963719 * Translate a MySQL simple expression to SQLite.
35973720 *
@@ -4002,6 +4125,57 @@ private function inject_configured_database_name( string $column_name ): string
40024125 );
40034126 }
40044127
4128+ /**
4129+ * Expand a SELECT wildcard to a list of columns.
4130+ *
4131+ * This method expands wildcards such as "SELECT *", "SELECT t.*", and similar,
4132+ * to an explicit list of all columns in the table. When the wildcard targets
4133+ * an information schema table, the configured database name will be injected.
4134+ *
4135+ * For example, the following query:
4136+ *
4137+ * SELECT * FROM information_schema.tables t
4138+ *
4139+ * Will be expanded to:
4140+ *
4141+ * SELECT t.TABLE_CATALOG, 'database_name' AS TABLE_SCHEMA, t.TABLE_NAME, ...
4142+ * FROM information_schema.tables t
4143+ *
4144+ * @param string $table_name The name of the table to expand the wildcard for.
4145+ * @param string $table_alias The alias of the table to expand the wildcard for.
4146+ * @return string The expanded and translated list of columns.
4147+ */
4148+ private function expand_wildcard ( string $ table_name , string $ table_alias ): string {
4149+ // We need to fetch the SQLite column information, because the information
4150+ // schema tables don't contain records for the information schema itself.
4151+ $ result = $ this ->execute_sqlite_query (
4152+ 'SELECT name FROM pragma_table_info(?) ' ,
4153+ array ( $ table_name )
4154+ );
4155+
4156+ // List all columns in the table, replacing columns targeting database
4157+ // name columns with the configured database name.
4158+ $ columns = $ result ->fetchAll ( PDO ::FETCH_COLUMN );
4159+ $ expanded_list = array ();
4160+ foreach ( $ columns as $ column ) {
4161+ $ fully_qualified_column = sprintf (
4162+ '%s.%s ' ,
4163+ $ this ->quote_sqlite_identifier ( $ table_alias ),
4164+ $ this ->quote_sqlite_identifier ( $ column )
4165+ );
4166+ if ( $ this ->is_information_schema_db_column ( $ column ) ) {
4167+ $ expanded_list [] = sprintf (
4168+ '%s AS %s ' ,
4169+ $ this ->inject_configured_database_name ( $ fully_qualified_column ),
4170+ strtoupper ( $ column ),
4171+ );
4172+ } else {
4173+ $ expanded_list [] = $ fully_qualified_column ;
4174+ }
4175+ }
4176+ return implode ( ', ' , $ expanded_list );
4177+ }
4178+
40054179 /**
40064180 * Recreate an existing table using data in the information schema.
40074181 *
0 commit comments