Skip to content

Commit b8e24f4

Browse files
committed
Inject database name in SELECT wildcards dynamically
1 parent 14b8820 commit b8e24f4

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

tests/WP_SQLite_Driver_Tests.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9321,4 +9321,32 @@ function ( $name ) {
93219321
$result
93229322
);
93239323
}
9324+
9325+
public function testDynamicDatabaseNameWithWildcards(): void {
9326+
// Create a setter for the private property "$db_name".
9327+
$set_db_name = Closure::bind(
9328+
function ( $name ) {
9329+
$this->main_db_name = $name;
9330+
},
9331+
$this->engine,
9332+
WP_SQLite_Driver::class
9333+
);
9334+
9335+
// Default database name.
9336+
$result = $this->assertQuery(
9337+
'SELECT * FROM information_schema.schemata s'
9338+
);
9339+
$this->assertEquals( 'information_schema', $result[0]->SCHEMA_NAME );
9340+
$this->assertEquals( 'wp', $result[1]->SCHEMA_NAME );
9341+
9342+
// Default database name.
9343+
$result = $this->assertQuery(
9344+
'SELECT s.*
9345+
FROM information_schema.schemata s
9346+
LEFT JOIN information_schema.tables t ON t.table_schema = s.schema_name
9347+
ORDER BY s.schema_name'
9348+
);
9349+
$this->assertEquals( 'information_schema', $result[0]->SCHEMA_NAME );
9350+
$this->assertEquals( 'wp', $result[1]->SCHEMA_NAME );
9351+
}
93249352
}

wp-includes/sqlite-ast/class-wp-sqlite-driver.php

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)