Skip to content

Commit 65afd67

Browse files
committed
Add basic support for SELECT statements, add unit tests
1 parent 3449b0b commit 65afd67

File tree

3 files changed

+243
-12
lines changed

3 files changed

+243
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
4+
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token-factory.php';
5+
require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-token.php';
6+
7+
use PHPUnit\Framework\TestCase;
8+
use WIP\WP_SQLite_Driver;
9+
10+
class WP_SQLite_Driver_Translation_Tests extends TestCase {
11+
const GRAMMAR_PATH = __DIR__ . '/../wp-includes/mysql/mysql-grammar.php';
12+
13+
/**
14+
* @var WP_Parser_Grammar
15+
*/
16+
private static $grammar;
17+
18+
public static function setUpBeforeClass(): void {
19+
self::$grammar = new WP_Parser_Grammar( include self::GRAMMAR_PATH );
20+
}
21+
22+
public function testSelect(): void {
23+
$this->assertQuery(
24+
'SELECT 1',
25+
'SELECT 1'
26+
);
27+
28+
$this->assertQuery(
29+
'SELECT * FROM "t"',
30+
'SELECT * FROM t'
31+
);
32+
33+
$this->assertQuery(
34+
'SELECT "c" FROM "t"',
35+
'SELECT c FROM t'
36+
);
37+
38+
$this->assertQuery(
39+
'SELECT ALL "c" FROM "t"',
40+
'SELECT ALL c FROM t'
41+
);
42+
43+
$this->assertQuery(
44+
'SELECT DISTINCT "c" FROM "t"',
45+
'SELECT DISTINCT c FROM t'
46+
);
47+
48+
$this->assertQuery(
49+
'SELECT "c1" , "c2" FROM "t"',
50+
'SELECT c1, c2 FROM t'
51+
);
52+
53+
$this->assertQuery(
54+
'SELECT "t"."c" FROM "t"',
55+
'SELECT t.c FROM t'
56+
);
57+
58+
$this->assertQuery(
59+
'SELECT "c1" FROM "t" WHERE "c2" = \'abc\'',
60+
"SELECT c1 FROM t WHERE c2 = 'abc'"
61+
);
62+
63+
$this->assertQuery(
64+
'SELECT "c" FROM "t" GROUP BY "c"',
65+
'SELECT c FROM t GROUP BY c'
66+
);
67+
68+
$this->assertQuery(
69+
'SELECT "c" FROM "t" ORDER BY "c" ASC',
70+
'SELECT c FROM t ORDER BY c ASC'
71+
);
72+
73+
$this->assertQuery(
74+
'SELECT "c" FROM "t" LIMIT 10',
75+
'SELECT c FROM t LIMIT 10'
76+
);
77+
78+
$this->assertQuery(
79+
'SELECT "c" FROM "t" GROUP BY "c" HAVING COUNT ( "c" ) > 1',
80+
'SELECT c FROM t GROUP BY c HAVING COUNT(c) > 1'
81+
);
82+
83+
$this->assertQuery(
84+
'SELECT * FROM "t1" LEFT JOIN "t2" ON "t1"."id" = "t2"."t1_id" WHERE "t1"."name" = \'abc\'',
85+
"SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'"
86+
);
87+
}
88+
89+
private function assertQuery( $expected, string $query ): void {
90+
$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
91+
$driver->query( $query );
92+
93+
$executed_queries = array_column( $driver->executed_sqlite_queries, 'sql' );
94+
if ( count( $executed_queries ) > 2 ) {
95+
// Remove BEGIN and COMMIT/ROLLBACK queries.
96+
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
97+
}
98+
99+
if ( ! is_array( $expected ) ) {
100+
$expected = array( $expected );
101+
}
102+
$this->assertSame( $expected, $executed_queries );
103+
}
104+
}

tests/tools/dump-sqlite-query.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,15 @@
2020

2121
$query = "SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id WHERE t1.name = 'abc'";
2222

23-
echo $driver->query( $query );
23+
$driver->query( $query );
24+
25+
$executed_queries = $driver->executed_sqlite_queries;
26+
if ( count( $executed_queries ) > 2 ) {
27+
// Remove BEGIN and COMMIT/ROLLBACK queries.
28+
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
29+
}
30+
31+
foreach ( $executed_queries as $executed_query ) {
32+
printf( "Query: %s\n", $executed_query['sql'] );
33+
printf( "Params: %s\n", json_encode( $executed_query['params'] ) );
34+
}

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

+127-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
use SQLite3;
1111
use WP_MySQL_Lexer;
1212
use WP_MySQL_Parser;
13+
use WP_MySQL_Token;
1314
use WP_Parser_Grammar;
15+
use WP_Parser_Node;
1416
use WP_SQLite_PDO_User_Defined_Functions;
1517

1618
class WP_SQLite_Driver {
@@ -502,17 +504,6 @@ public function get_return_value() {
502504
return $this->return_value;
503505
}
504506

505-
/**
506-
* Executes a MySQL query in SQLite.
507-
*
508-
* @param string $query The query.
509-
*
510-
* @throws Exception If the query is not supported.
511-
*/
512-
private function execute_mysql_query( $query ) {
513-
//@TODO: Implement the query translation.
514-
}
515-
516507
/**
517508
* Executes a query in SQLite.
518509
*
@@ -681,6 +672,93 @@ public function rollback() {
681672
return $this->last_exec_returned;
682673
}
683674

675+
/**
676+
* Executes a MySQL query in SQLite.
677+
*
678+
* @param string $query The query.
679+
*
680+
* @throws Exception If the query is not supported.
681+
*/
682+
private function execute_mysql_query( WP_Parser_Node $ast ) {
683+
if ( 'query' !== $ast->rule_name ) {
684+
throw new Exception( sprintf( 'Expected "query" node, got: "%s"', $ast->rule_name ) );
685+
}
686+
687+
$children = $ast->get_child_nodes();
688+
if ( count( $children ) !== 1 ) {
689+
throw new Exception( sprintf( 'Expected 1 child, got: %d', count( $children ) ) );
690+
}
691+
692+
$ast = $children[0]->get_child_node();
693+
switch ( $ast->rule_name ) {
694+
case 'selectStatement':
695+
$this->query_type = 'SELECT';
696+
$query = $this->translate( $ast->get_child() );
697+
$stmt = $this->execute_sqlite_query( $query );
698+
$this->set_results_from_fetched_data(
699+
$stmt->fetchAll( $this->pdo_fetch_mode )
700+
);
701+
break;
702+
default:
703+
throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
704+
}
705+
}
706+
707+
private function translate( $ast ) {
708+
if ( null === $ast ) {
709+
return null;
710+
}
711+
712+
if ( $ast instanceof WP_MySQL_Token ) {
713+
return $this->translate_token( $ast );
714+
}
715+
716+
if ( ! $ast instanceof WP_Parser_Node ) {
717+
throw new Exception( 'translate_query only accepts WP_MySQL_Token and WP_Parser_Node instances' );
718+
}
719+
720+
$rule_name = $ast->rule_name;
721+
switch ( $rule_name ) {
722+
case 'qualifiedIdentifier':
723+
case 'dotIdentifier':
724+
return $this->translate_sequence( $ast->get_children(), '' );
725+
case 'textStringLiteral':
726+
if ( $ast->has_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ) {
727+
return WP_SQLite_Token_Factory::double_quoted_value(
728+
$ast->get_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->value
729+
)->value;
730+
}
731+
if ( $ast->has_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ) {
732+
return WP_SQLite_Token_Factory::raw(
733+
$ast->get_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->value
734+
)->value;
735+
}
736+
// Fall through to the default case.
737+
738+
default:
739+
return $this->translate_sequence( $ast->get_children() );
740+
}
741+
}
742+
743+
private function translate_token( WP_MySQL_Token $token ) {
744+
switch ( $token->id ) {
745+
case WP_MySQL_Lexer::EOF:
746+
return null;
747+
case WP_MySQL_Lexer::IDENTIFIER:
748+
return '"' . trim( $token->value, '`"' ) . '"';
749+
default:
750+
return $token->value;
751+
}
752+
}
753+
754+
private function translate_sequence( array $nodes, string $separator = ' ' ): string {
755+
$parts = array();
756+
foreach ( $nodes as $node ) {
757+
$parts[] = $this->translate( $node );
758+
}
759+
return implode( $separator, $parts );
760+
}
761+
684762
/**
685763
* This method makes database directory and .htaccess file.
686764
*
@@ -745,6 +823,44 @@ private function flush() {
745823
$this->executed_sqlite_queries = array();
746824
}
747825

826+
/**
827+
* Method to set the results from the fetched data.
828+
*
829+
* @param array $data The data to set.
830+
*/
831+
private function set_results_from_fetched_data( $data ) {
832+
if ( null === $this->results ) {
833+
$this->results = $data;
834+
}
835+
if ( is_array( $this->results ) ) {
836+
$this->num_rows = count( $this->results );
837+
$this->last_select_found_rows = count( $this->results );
838+
}
839+
$this->return_value = $this->results;
840+
}
841+
842+
/**
843+
* Method to set the results from the affected rows.
844+
*
845+
* @param int|null $override Override the affected rows.
846+
*/
847+
private function set_result_from_affected_rows( $override = null ) {
848+
/*
849+
* SELECT CHANGES() is a workaround for the fact that
850+
* $stmt->rowCount() returns "0" (zero) with the
851+
* SQLite driver at all times.
852+
* Source: https://www.php.net/manual/en/pdostatement.rowcount.php
853+
*/
854+
if ( null === $override ) {
855+
$this->affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0];
856+
} else {
857+
$this->affected_rows = $override;
858+
}
859+
$this->return_value = $this->affected_rows;
860+
$this->num_rows = $this->affected_rows;
861+
$this->results = $this->affected_rows;
862+
}
863+
748864
/**
749865
* Error handler.
750866
*

0 commit comments

Comments
 (0)