Skip to content

Commit d2c99e7

Browse files
JanJakesadamziel
andauthored
Better support for SET statements and MySQL variables (#214)
This pull request implements **better support for MySQL `SET` statements**. It also implements additional related functionality and resolves some related issues: - **Depth-first traversal:** All node querying APIs use depth-first pre-order traversal. This traversal produces a natural ordering that corresponds to the original parsed input. It also aligns with CSS selector behavior. This is useful in situations when we need to reconstruct or retrieve a fragment of the original input with tokens in the correct order. It's used to preserve correct returned MySQL column names (see the next item). - **Correct result column names:** When no explicit column alias is provided in a select item, the driver will now ensure that the returned column name is equivalent to what MySQL infers from the input. For example, if we translate `CONCAT('a', 'b')` to `('a' || 'b')`, we need to use the original `CONCAT('a', 'b')` string as the column name. In other words, `SELECT CONCAT('a', 'b')` will be translated to `SELECT ('a' || 'b') AS ’CONCAT('a', 'b')’`. This is also needed when selecting user and system variables (see the items below). - **SET NAMES/CHARSET:**`SET NAMES`, `SET CHARSET`, and `SET CHARACTER SET` are a no-op now, with a `TODO` to implement some validation logic in #192. - **User variables:** Reading and writing of user variables is now implemented (`SET @my_var = ...`, `SELECT @my_var`). - **System variables:** Reading and writing of session system variables is now implemented (`SET time_zone = ...`, `SET @@time_zone = ...`, , `SET @@session.time_zone = ...`, `SELECT @@time_zone`, `SELECT @@session.time_zone`, etc.). The variables are not yet interpreted in any way, and they are not merged with global system variables at the moment. --------- Co-authored-by: Adam Zieliński <adam@adamziel.com>
1 parent c5fd397 commit d2c99e7

7 files changed

Lines changed: 618 additions & 107 deletions

phpunit.xml.dist

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
<directory suffix=".php">tests/</directory>
1818
<!-- Exclude test tools. -->
1919
<exclude>tests/tools</exclude>
20-
<!-- Exclude new parser tests for now. -->
21-
<exclude>tests/parser</exclude>
2220
</testsuite>
2321
</testsuites>
2422
</phpunit>

tests/WP_SQLite_Driver_Metadata_Tests.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,10 @@ public function testCountTables() {
2323
$this->assertQuery( 'CREATE TABLE t2 (id INT)' );
2424

2525
$result = $this->assertQuery( "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'wp'" );
26-
$this->assertEquals( array( (object) array( 'COUNT ( * )' => '2' ) ), $result );
26+
$this->assertEquals( array( (object) array( 'COUNT(*)' => '2' ) ), $result );
2727

2828
$result = $this->assertQuery( "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'other'" );
29-
$this->assertEquals( array( (object) array( 'COUNT ( * )' => '0' ) ), $result );
30-
31-
// @TODO: The result key should be "COUNT(*)" instead of "COUNT ( * )".
32-
// The spacing was probably inserted by the translator.
29+
$this->assertEquals( array( (object) array( 'COUNT(*)' => '0' ) ), $result );
3330
}
3431

3532
public function testInformationSchemaTables() {

tests/WP_SQLite_Driver_Tests.php

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6020,4 +6020,222 @@ public function testDatabaseNameMismatchWithExistingInformationSchemaTableData()
60206020
$this->expectExceptionMessage( "Incorrect database name. The database was created with name 'db-one', but 'db-two' is used in the current session." );
60216021
new WP_SQLite_Driver( $connection, 'db-two' );
60226022
}
6023+
6024+
public function testSelectColumnNames(): void {
6025+
$this->assertQuery( 'CREATE TABLE t (id INT, name VARCHAR(255))' );
6026+
$this->assertQuery( 'INSERT INTO t (id, name) VALUES (1, "John"), (2, "Jane")' );
6027+
6028+
// Columns (no explicit alias).
6029+
$result = $this->assertQuery( 'SELECT id, name FROM t' );
6030+
$this->assertSame( array( 'id', 'name' ), array_keys( (array) $result[0] ) );
6031+
6032+
// Columns with an explicit alias.
6033+
$result = $this->assertQuery( 'SELECT id AS alias_id, name AS alias_name FROM t' );
6034+
$this->assertSame( array( 'alias_id', 'alias_name' ), array_keys( (array) $result[0] ) );
6035+
6036+
// Expressions (no explicit alias).
6037+
$result = $this->assertQuery( 'SELECT id + 1, (2 + 3) FROM t' );
6038+
$this->assertSame( array( 'id + 1', '(2 + 3)' ), array_keys( (array) $result[0] ) );
6039+
6040+
// Expressions with an explicit alias.
6041+
$result = $this->assertQuery( 'SELECT id + 1 AS alias_id, (2 + 3) AS alias_numbers FROM t' );
6042+
$this->assertSame( array( 'alias_id', 'alias_numbers' ), array_keys( (array) $result[0] ) );
6043+
6044+
// Function calls (no explicit alias).
6045+
$result = $this->assertQuery( "SELECT CONCAT('a', 'b')" );
6046+
$this->assertSame( array( "CONCAT('a', 'b')" ), array_keys( (array) $result[0] ) );
6047+
6048+
// Function calls with an explicit alias.
6049+
$result = $this->assertQuery( "SELECT CONCAT('a', 'b') AS alias_concat" );
6050+
$this->assertSame( array( 'alias_concat' ), array_keys( (array) $result[0] ) );
6051+
}
6052+
6053+
public function testSetStatement(): void {
6054+
$this->assertQuery( 'SET NAMES utf8mb4' );
6055+
$this->assertQuery( 'SET CHARSET utf8mb4' );
6056+
$this->assertQuery( 'SET CHARACTER SET utf8mb4' );
6057+
}
6058+
6059+
public function testSessionSystemVariables(): void {
6060+
$this->assertQuery( "SET character_set_client = 'latin1'" );
6061+
$result = $this->assertQuery( 'SELECT @@character_set_client' );
6062+
$this->assertSame( 'latin1', $result[0]->{'@@character_set_client'} );
6063+
6064+
$this->assertQuery( "SET @@character_set_client = 'utf8mb3'" );
6065+
$result = $this->assertQuery( 'SELECT @@character_set_client' );
6066+
$this->assertSame( 'utf8mb3', $result[0]->{'@@character_set_client'} );
6067+
6068+
$this->assertQuery( "SET @@session.character_set_client = 'utf8mb4'" );
6069+
$result = $this->assertQuery( 'SELECT @@session.character_set_client' );
6070+
$this->assertSame( 'utf8mb4', $result[0]->{'@@session.character_set_client'} );
6071+
}
6072+
6073+
public function testSystemVariablesWithKeywords(): void {
6074+
$this->assertQuery( 'SET default_storage_engine = InnoDB' );
6075+
$result = $this->assertQuery( 'SELECT @@default_storage_engine' );
6076+
$this->assertSame( 'InnoDB', $result[0]->{'@@default_storage_engine'} );
6077+
6078+
$this->assertQuery( 'SET default_collation_for_utf8mb4 = utf8mb4_0900_ai_ci' );
6079+
$result = $this->assertQuery( 'SELECT @@default_collation_for_utf8mb4' );
6080+
$this->assertSame( 'utf8mb4_0900_ai_ci', $result[0]->{'@@default_collation_for_utf8mb4'} );
6081+
6082+
$this->assertQuery( 'SET resultset_metadata = FULL' );
6083+
$result = $this->assertQuery( 'SELECT @@resultset_metadata' );
6084+
$this->assertSame( 'FULL', $result[0]->{'@@resultset_metadata'} );
6085+
6086+
$this->assertQuery( 'SET session_track_gtids = OWN_GTID' );
6087+
$result = $this->assertQuery( 'SELECT @@session_track_gtids' );
6088+
$this->assertSame( 'OWN_GTID', $result[0]->{'@@session_track_gtids'} );
6089+
6090+
$this->assertQuery( 'SET session_track_transaction_info = STATE' );
6091+
$result = $this->assertQuery( 'SELECT @@session_track_transaction_info' );
6092+
$this->assertSame( 'STATE', $result[0]->{'@@session_track_transaction_info'} );
6093+
6094+
$this->assertQuery( 'SET transaction_isolation = SERIALIZABLE' );
6095+
$result = $this->assertQuery( 'SELECT @@transaction_isolation' );
6096+
$this->assertSame( 'SERIALIZABLE', $result[0]->{'@@transaction_isolation'} );
6097+
6098+
$this->assertQuery( 'SET use_secondary_engine = FORCED' );
6099+
$result = $this->assertQuery( 'SELECT @@use_secondary_engine' );
6100+
$this->assertSame( 'FORCED', $result[0]->{'@@use_secondary_engine'} );
6101+
}
6102+
6103+
public function testSystemVariablesWithBooleanValues(): void {
6104+
$this->assertQuery( 'SET autocommit = ON, big_tables = OFF' );
6105+
$result = $this->assertQuery( 'SELECT @@autocommit, @@big_tables' );
6106+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6107+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6108+
6109+
$this->assertQuery( 'SET autocommit = on, big_tables = off' );
6110+
$result = $this->assertQuery( 'SELECT @@autocommit, @@big_tables' );
6111+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6112+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6113+
6114+
$this->assertQuery( "SET autocommit = 'ON', big_tables = 'OFF'" );
6115+
$result = $this->assertQuery( 'SELECT @@autocommit, @@big_tables' );
6116+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6117+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6118+
6119+
$this->assertQuery( "SET autocommit = 'on', big_tables = 'off'" );
6120+
$result = $this->assertQuery( 'SELECT @@autocommit, @@big_tables' );
6121+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6122+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6123+
6124+
$this->assertQuery( 'SET autocommit = TRUE, big_tables = FALSE' );
6125+
$result = $this->assertQuery( 'SELECT @@autocommit, @@big_tables' );
6126+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6127+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6128+
6129+
$this->assertQuery( 'SET autocommit = true, big_tables = false' );
6130+
$result = $this->assertQuery( 'SELECT @@autocommit, @@big_tables' );
6131+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6132+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6133+
6134+
$this->assertQuery( 'SET autocommit = 1, big_tables = 0' );
6135+
$result = $this->assertQuery( 'SELECT @@autocommit, @@big_tables' );
6136+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6137+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6138+
}
6139+
6140+
public function testSystemVariablesWithOnOffValues(): void {
6141+
$this->assertQuery( 'SET autocommit = ON' );
6142+
$result = $this->assertQuery( 'SELECT @@autocommit' );
6143+
$this->assertSame( '1', $result[0]->{'@@autocommit'} );
6144+
6145+
$this->assertQuery( 'SET big_tables = OFF' );
6146+
$result = $this->assertQuery( 'SELECT @@big_tables' );
6147+
$this->assertSame( '0', $result[0]->{'@@big_tables'} );
6148+
6149+
$this->assertQuery( 'SET end_markers_in_json = ON' );
6150+
$result = $this->assertQuery( 'SELECT @@end_markers_in_json' );
6151+
$this->assertSame( '1', $result[0]->{'@@end_markers_in_json'} );
6152+
6153+
$this->assertQuery( 'SET explicit_defaults_for_timestamp = OFF' );
6154+
$result = $this->assertQuery( 'SELECT @@explicit_defaults_for_timestamp' );
6155+
$this->assertSame( '0', $result[0]->{'@@explicit_defaults_for_timestamp'} );
6156+
6157+
$this->assertQuery( 'SET keep_files_on_create = ON' );
6158+
$result = $this->assertQuery( 'SELECT @@keep_files_on_create' );
6159+
$this->assertSame( '1', $result[0]->{'@@keep_files_on_create'} );
6160+
6161+
$this->assertQuery( 'SET old_alter_table = OFF' );
6162+
$result = $this->assertQuery( 'SELECT @@old_alter_table' );
6163+
$this->assertSame( '0', $result[0]->{'@@old_alter_table'} );
6164+
6165+
$this->assertQuery( 'SET print_identified_with_as_hex = ON' );
6166+
$result = $this->assertQuery( 'SELECT @@print_identified_with_as_hex' );
6167+
$this->assertSame( '1', $result[0]->{'@@print_identified_with_as_hex'} );
6168+
6169+
$this->assertQuery( 'SET require_row_format = OFF' );
6170+
$result = $this->assertQuery( 'SELECT @@require_row_format' );
6171+
$this->assertSame( '0', $result[0]->{'@@require_row_format'} );
6172+
6173+
$this->assertQuery( 'SET select_into_disk_sync = ON' );
6174+
$result = $this->assertQuery( 'SELECT @@select_into_disk_sync' );
6175+
$this->assertSame( '1', $result[0]->{'@@select_into_disk_sync'} );
6176+
6177+
$this->assertQuery( 'SET session_track_gtids = OFF' );
6178+
$result = $this->assertQuery( 'SELECT @@session_track_gtids' );
6179+
// @TODO: For session_track_gtids, the value should be OFF, not 0.
6180+
//$this->assertSame( 'OFF', $result[0]->{'@@session_track_gtids'} );
6181+
6182+
$this->assertQuery( 'SET session_track_schema = ON' );
6183+
$result = $this->assertQuery( 'SELECT @@session_track_schema' );
6184+
$this->assertSame( '1', $result[0]->{'@@session_track_schema'} );
6185+
6186+
$this->assertQuery( 'SET session_track_state_change = OFF' );
6187+
$result = $this->assertQuery( 'SELECT @@session_track_state_change' );
6188+
$this->assertSame( '0', $result[0]->{'@@session_track_state_change'} );
6189+
6190+
$this->assertQuery( 'SET session_track_transaction_info = OFF' );
6191+
$result = $this->assertQuery( 'SELECT @@session_track_transaction_info' );
6192+
// @TODO: For session_track_transaction_info, the value should be OFF, not 0.
6193+
//$this->assertSame( 'OFF', $result[0]->{'@@session_track_transaction_info'} );
6194+
6195+
$this->assertQuery( 'SET show_create_table_skip_secondary_engine = ON' );
6196+
$result = $this->assertQuery( 'SELECT @@show_create_table_skip_secondary_engine' );
6197+
$this->assertSame( '1', $result[0]->{'@@show_create_table_skip_secondary_engine'} );
6198+
6199+
$this->assertQuery( 'SET show_create_table_verbosity = OFF' );
6200+
$result = $this->assertQuery( 'SELECT @@show_create_table_verbosity' );
6201+
$this->assertSame( '0', $result[0]->{'@@show_create_table_verbosity'} );
6202+
6203+
$this->assertQuery( 'SET sql_auto_is_null = ON' );
6204+
$result = $this->assertQuery( 'SELECT @@sql_auto_is_null' );
6205+
$this->assertSame( '1', $result[0]->{'@@sql_auto_is_null'} );
6206+
6207+
$this->assertQuery( 'SET sql_big_selects = OFF' );
6208+
$result = $this->assertQuery( 'SELECT @@sql_big_selects' );
6209+
$this->assertSame( '0', $result[0]->{'@@sql_big_selects'} );
6210+
6211+
$this->assertQuery( 'SET sql_buffer_result = ON' );
6212+
$result = $this->assertQuery( 'SELECT @@sql_buffer_result' );
6213+
$this->assertSame( '1', $result[0]->{'@@sql_buffer_result'} );
6214+
6215+
$this->assertQuery( 'SET sql_safe_updates = OFF' );
6216+
$result = $this->assertQuery( 'SELECT @@sql_safe_updates' );
6217+
$this->assertSame( '0', $result[0]->{'@@sql_safe_updates'} );
6218+
6219+
$this->assertQuery( 'SET sql_warnings = ON' );
6220+
$result = $this->assertQuery( 'SELECT @@sql_warnings' );
6221+
$this->assertSame( '1', $result[0]->{'@@sql_warnings'} );
6222+
6223+
$this->assertQuery( 'SET transaction_read_only = OFF' );
6224+
$result = $this->assertQuery( 'SELECT @@transaction_read_only' );
6225+
$this->assertSame( '0', $result[0]->{'@@transaction_read_only'} );
6226+
}
6227+
6228+
public function testUserVariables(): void {
6229+
$this->assertQuery( 'SET @my_var = 1' );
6230+
$result = $this->assertQuery( 'SELECT @my_var' );
6231+
$this->assertEquals( 1, $result[0]->{'@my_var'} );
6232+
6233+
$this->assertQuery( 'SET @my_var = @my_var + 1' );
6234+
$result = $this->assertQuery( 'SELECT @my_var' );
6235+
$this->assertEquals( 2, $result[0]->{'@my_var'} );
6236+
6237+
$this->assertQuery( 'SET @my_var = @my_var + 1' );
6238+
$result = $this->assertQuery( 'SELECT @my_var' );
6239+
$this->assertEquals( 3, $result[0]->{'@my_var'} );
6240+
}
60236241
}

tests/WP_SQLite_Driver_Translation_Tests.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1261,7 +1261,7 @@ public function testSystemVariables(): void {
12611261

12621262
public function testConcatFunction(): void {
12631263
$this->assertQuery(
1264-
"SELECT ('a' || 'b' || 'c')",
1264+
"SELECT ('a' || 'b' || 'c') AS `CONCAT(\"a\", \"b\", \"c\")`",
12651265
'SELECT CONCAT("a", "b", "c")'
12661266
);
12671267
}

tests/parser/WP_Parser_Node_Tests.php

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,21 @@ public function testEmptyChildren(): void {
3535
}
3636

3737
public function testNodeTree(): void {
38+
$input = 'SELECT 1 + 2, 2';
39+
3840
// Prepare nodes and tokens.
3941
$root = new WP_Parser_Node( 1, 'root' );
4042
$n_keyword = new WP_Parser_Node( 2, 'keyword' );
4143
$n_expr_a = new WP_Parser_Node( 3, 'expr' );
4244
$n_expr_b = new WP_Parser_Node( 3, 'expr' );
4345
$n_expr_c = new WP_Parser_Node( 3, 'expr' );
44-
$t_select = new WP_Parser_Token( 100, 'SELECT' );
45-
$t_comma = new WP_Parser_Token( 200, ',' );
46-
$t_plus = new WP_Parser_Token( 300, '+' );
47-
$t_one = new WP_Parser_Token( 400, '1' );
48-
$t_two_a = new WP_Parser_Token( 400, '2' );
49-
$t_two_b = new WP_Parser_Token( 400, '2' );
50-
$t_eof = new WP_Parser_Token( 500, '' );
46+
$t_select = new WP_Parser_Token( 100, 0, 6, $input );
47+
$t_comma = new WP_Parser_Token( 200, 12, 1, $input );
48+
$t_plus = new WP_Parser_Token( 300, 9, 1, $input );
49+
$t_one = new WP_Parser_Token( 400, 7, 1, $input );
50+
$t_two_a = new WP_Parser_Token( 400, 11, 1, $input );
51+
$t_two_b = new WP_Parser_Token( 400, 14, 1, $input );
52+
$t_eof = new WP_Parser_Token( 500, 15, 0, $input );
5153

5254
// Prepare a tree.
5355
//
@@ -102,38 +104,36 @@ public function testNodeTree(): void {
102104
$this->assertSame( array(), $root->get_child_tokens( 100 ) );
103105

104106
// Test single descendant methods.
105-
// @TODO: Consider breadth-first search vs depth-first search.
106107
$this->assertSame( $n_keyword, $root->get_first_descendant_node() );
107108
$this->assertSame( $n_expr_a, $root->get_first_descendant_node( 'expr' ) );
108109
$this->assertSame( null, $root->get_first_descendant_node( 'root' ) );
109-
$this->assertSame( $t_comma, $root->get_first_descendant_token() );
110+
$this->assertSame( $t_select, $root->get_first_descendant_token() );
110111
$this->assertSame( $t_one, $root->get_first_descendant_token( 400 ) );
111112
$this->assertSame( null, $root->get_first_descendant_token( 123 ) );
112113

113114
// Test multiple descendant methods.
114-
// @TODO: Consider breadth-first search vs depth-first search.
115115
$this->assertSame(
116-
array( $n_keyword, $n_expr_a, $t_comma, $n_expr_b, $t_eof, $t_select, $t_one, $t_plus, $n_expr_c, $t_two_a, $t_two_b ),
116+
array( $n_keyword, $t_select, $n_expr_a, $t_one, $t_plus, $n_expr_c, $t_two_b, $t_comma, $n_expr_b, $t_two_a, $t_eof ),
117117
$root->get_descendants()
118118
);
119119
$this->assertSame(
120-
array( $n_keyword, $n_expr_a, $n_expr_b, $n_expr_c ),
120+
array( $n_keyword, $n_expr_a, $n_expr_c, $n_expr_b ),
121121
$root->get_descendant_nodes()
122122
);
123123
$this->assertSame(
124-
array( $n_expr_a, $n_expr_b, $n_expr_c ),
124+
array( $n_expr_a, $n_expr_c, $n_expr_b ),
125125
$root->get_descendant_nodes( 'expr' )
126126
);
127127
$this->assertSame(
128128
array(),
129129
$root->get_descendant_nodes( 'root' )
130130
);
131131
$this->assertSame(
132-
array( $t_comma, $t_eof, $t_select, $t_one, $t_plus, $t_two_a, $t_two_b ),
132+
array( $t_select, $t_one, $t_plus, $t_two_b, $t_comma, $t_two_a, $t_eof ),
133133
$root->get_descendant_tokens()
134134
);
135135
$this->assertSame(
136-
array( $t_one, $t_two_a, $t_two_b ),
136+
array( $t_one, $t_two_b, $t_two_a ),
137137
$root->get_descendant_tokens( 400 )
138138
);
139139
$this->assertSame(

0 commit comments

Comments
 (0)