Skip to content

Commit b489eb7

Browse files
committed
Inject database name in queries dynamically
1 parent 3951308 commit b489eb7

2 files changed

Lines changed: 197 additions & 8 deletions

File tree

tests/WP_SQLite_Driver_Tests.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9244,4 +9244,111 @@ public function testCheckConstraintNotEnforced(): void {
92449244
$result[0]->{'Create Table'}
92459245
);
92469246
}
9247+
9248+
public function testDynamicDatabaseName(): void {
9249+
// Create a setter for the private property "$db_name".
9250+
$set_db_name = Closure::bind(
9251+
function ( $name ) {
9252+
$this->main_db_name = $name;
9253+
},
9254+
$this->engine,
9255+
WP_SQLite_Driver::class
9256+
);
9257+
9258+
// Default database name.
9259+
$result = $this->assertQuery( 'SELECT schema_name FROM information_schema.schemata ORDER BY schema_name' );
9260+
$this->assertEquals(
9261+
array(
9262+
(object) array( 'SCHEMA_NAME' => 'information_schema' ),
9263+
(object) array( 'SCHEMA_NAME' => 'wp' ),
9264+
),
9265+
$result
9266+
);
9267+
9268+
// Change the database name.
9269+
$set_db_name( 'wp_test_new' );
9270+
$result = $this->assertQuery( 'SELECT schema_name FROM information_schema.schemata ORDER BY schema_name' );
9271+
$this->assertEquals(
9272+
array(
9273+
(object) array( 'SCHEMA_NAME' => 'information_schema' ),
9274+
(object) array( 'SCHEMA_NAME' => 'wp_test_new' ),
9275+
),
9276+
$result
9277+
);
9278+
}
9279+
9280+
public function testDynamicDatabaseNameComplexScenario(): void {
9281+
// Create a setter for the private property "$db_name".
9282+
$set_db_name = Closure::bind(
9283+
function ( $name ) {
9284+
$this->main_db_name = $name;
9285+
},
9286+
$this->engine,
9287+
WP_SQLite_Driver::class
9288+
);
9289+
9290+
$this->assertQuery( 'CREATE TABLE t (id INT, db_name TEXT)' );
9291+
$this->assertQuery( 'INSERT INTO t (id, db_name) VALUES (1, "wp")' );
9292+
$this->assertQuery( 'INSERT INTO t (id, db_name) VALUES (2, "wp_test_new")' );
9293+
$this->assertQuery( 'INSERT INTO t (id, db_name) VALUES (3, "other")' );
9294+
9295+
$set_db_name( 'wp_test_new' );
9296+
9297+
$result = $this->assertQuery(
9298+
"SELECT sub.id, sub.table_schema, sub.table_name, sub.column_name
9299+
FROM (
9300+
SELECT * FROM information_schema.columns c
9301+
JOIN t ON t.db_name = CONCAT(COALESCE(c.table_schema, 'default'), '')
9302+
JOIN information_schema.schemata s ON s.schema_name = c.table_schema
9303+
WHERE c.table_name = 't'
9304+
) sub
9305+
ORDER BY ordinal_position"
9306+
);
9307+
$this->assertEquals(
9308+
array(
9309+
(object) array(
9310+
'id' => '2',
9311+
'TABLE_SCHEMA' => 'wp_test_new',
9312+
'TABLE_NAME' => 't',
9313+
'COLUMN_NAME' => 'id',
9314+
),
9315+
(object) array(
9316+
'id' => '2',
9317+
'TABLE_SCHEMA' => 'wp_test_new',
9318+
'TABLE_NAME' => 't',
9319+
'COLUMN_NAME' => 'db_name',
9320+
),
9321+
),
9322+
$result
9323+
);
9324+
}
9325+
9326+
public function testDynamicDatabaseNameWithWildcards(): void {
9327+
// Create a setter for the private property "$db_name".
9328+
$set_db_name = Closure::bind(
9329+
function ( $name ) {
9330+
$this->main_db_name = $name;
9331+
},
9332+
$this->engine,
9333+
WP_SQLite_Driver::class
9334+
);
9335+
9336+
// Default database name.
9337+
$result = $this->assertQuery(
9338+
'SELECT * FROM information_schema.schemata s'
9339+
);
9340+
$this->assertEquals( 'information_schema', $result[0]->SCHEMA_NAME );
9341+
$this->assertEquals( 'wp', $result[1]->SCHEMA_NAME );
9342+
9343+
// Default database name.
9344+
$set_db_name( 'wp_test_new' );
9345+
$result = $this->assertQuery(
9346+
'SELECT s.*
9347+
FROM information_schema.schemata s
9348+
LEFT JOIN information_schema.tables t ON t.table_schema = s.schema_name
9349+
ORDER BY s.schema_name'
9350+
);
9351+
$this->assertEquals( 'information_schema', $result[0]->SCHEMA_NAME );
9352+
$this->assertEquals( 'wp_test_new', $result[1]->SCHEMA_NAME );
9353+
}
92479354
}

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

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3005,6 +3005,8 @@ private function translate( $node ): ?string {
30053005
return $this->translate_query_expression( $node );
30063006
case 'querySpecification':
30073007
return $this->translate_query_specification( $node );
3008+
case 'tableRef':
3009+
return $this->translate_table_ref( $node );
30083010
case 'qualifiedIdentifier':
30093011
case 'tableRefWithWildcard':
30103012
$parts = $node->get_descendant_nodes( 'identifier' );
@@ -3404,14 +3406,7 @@ private function translate_qualified_identifier(
34043406

34053407
// Database-level object name (table, view, procedure, trigger, etc.).
34063408
if ( null !== $object_node ) {
3407-
if ( $is_information_schema ) {
3408-
$object_name = $this->unquote_sqlite_identifier(
3409-
$this->translate_sequence( $object_node->get_children() )
3410-
);
3411-
$parts[] = $this->information_schema_builder->get_table_name( false, $object_name );
3412-
} else {
3413-
$parts[] = $this->translate( $object_node );
3414-
}
3409+
$parts[] = $this->translate( $object_node );
34153410
}
34163411

34173412
// Object child name (column, index, etc.).
@@ -3939,6 +3934,93 @@ public function translate_select_item( WP_Parser_Node $node ): string {
39393934
return sprintf( '%s AS %s', $item, $alias );
39403935
}
39413936

3937+
/**
3938+
* Translate a MySQL table reference to SQLite.
3939+
*
3940+
* When the table reference targets an information schema table, we replace
3941+
* it with a subquery, injecting the configured database name dynamically.
3942+
*
3943+
* For example, the following query:
3944+
*
3945+
* SELECT *, t.*, t.table_schema FROM information_schema.tables t
3946+
*
3947+
* Will be translated to:
3948+
*
3949+
* SELECT *, `t`.*, `t`.`table_schema` FROM (
3950+
* SELECT
3951+
* `TABLE_CATALOG`,
3952+
* IIF(`TABLE_SCHEMA` = 'information_schema', `TABLE_SCHEMA`, 'database_name') AS `TABLE_SCHEMA`,
3953+
* `TABLE_NAME`,
3954+
* ...
3955+
* FROM `_wp_sqlite_mysql_information_schema_tables` AS `tables`
3956+
* ) `t`
3957+
*
3958+
* The same logic will be applied to table references in JOIN clauses as well.
3959+
*
3960+
* @param WP_Parser_Node $node The "tableRef" AST node.
3961+
* @return string The translated value.
3962+
* @throws WP_SQLite_Driver_Exception When the translation fails.
3963+
*/
3964+
public function translate_table_ref( WP_Parser_Node $node ): string {
3965+
// Information schema is currently accessible only in read-only queries.
3966+
if ( ! $this->is_readonly ) {
3967+
return $this->translate_sequence( $node->get_children() );
3968+
}
3969+
3970+
// The table reference is in "<schema>.<table>" or "<table>" format.
3971+
$parts = $node->get_descendant_nodes( 'identifier' );
3972+
$table = array_pop( $parts );
3973+
$schema = array_pop( $parts );
3974+
3975+
$schema_name = $schema ? $this->unquote_sqlite_identifier( $this->translate( $schema ) ) : null;
3976+
$table_name = $this->unquote_sqlite_identifier( $this->translate( $table ) );
3977+
3978+
// When the table reference targets an information schema table,
3979+
// we need to inject the configured database name dynamically.
3980+
if (
3981+
( null === $schema_name && 'information_schema' === $this->db_name )
3982+
|| ( null !== $schema_name && 'information_schema' === strtolower( $schema_name ) )
3983+
) {
3984+
$table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table_name );
3985+
$sqlite_table_name = $this->information_schema_builder->get_table_name( $table_is_temporary, $table_name );
3986+
3987+
// We need to fetch the SQLite column information, because the information
3988+
// schema tables don't contain records for the information schema itself.
3989+
$columns = $this->execute_sqlite_query(
3990+
'SELECT name FROM pragma_table_info(?)',
3991+
array( $sqlite_table_name )
3992+
)->fetchAll( PDO::FETCH_COLUMN );
3993+
3994+
// List all columns in the table, replacing columns targeting database
3995+
// name columns with the configured database name.
3996+
$expanded_list = array();
3997+
foreach ( $columns as $column ) {
3998+
$quoted_column = $this->quote_sqlite_identifier( $column );
3999+
if ( str_contains( strtolower( $column ), 'schema' ) ) {
4000+
$expanded_list[] = sprintf(
4001+
"IIF(%s = 'information_schema', %s, %s) AS %s",
4002+
$quoted_column,
4003+
$quoted_column,
4004+
$this->connection->quote( $this->main_db_name ),
4005+
strtoupper( $quoted_column )
4006+
);
4007+
} else {
4008+
$expanded_list[] = $quoted_column;
4009+
}
4010+
}
4011+
$column_list = implode( ', ', $expanded_list );
4012+
4013+
// Compose information schema subquery.
4014+
return sprintf(
4015+
'(SELECT %s FROM %s AS %s)',
4016+
$column_list,
4017+
$this->quote_sqlite_identifier( $sqlite_table_name ),
4018+
$this->quote_sqlite_identifier( $table_name )
4019+
);
4020+
}
4021+
return $this->translate_sequence( $node->get_children() );
4022+
}
4023+
39424024
/**
39434025
* Recreate an existing table using data in the information schema.
39444026
*

0 commit comments

Comments
 (0)