Skip to content

Commit b8b4500

Browse files
committed
Add basic support for CREATE TABLE, implement data types
1 parent 00ec46d commit b8b4500

File tree

4 files changed

+267
-11
lines changed

4 files changed

+267
-11
lines changed

Diff for: tests/WP_SQLite_Driver_Translation_Tests.php

+132-1
Original file line numberDiff line numberDiff line change
@@ -179,16 +179,147 @@ public function testDelete(): void {
179179
);
180180
}
181181

182+
public function testCreateTable(): void {
183+
$this->assertQuery(
184+
'CREATE TABLE "t" ( "id" INTEGER )',
185+
'CREATE TABLE t (id INT)'
186+
);
187+
188+
$this->assertQuery(
189+
'CREATE TABLE "t" ( "id" INTEGER , "name" TEXT , "score" REAL DEFAULT 0.0 )',
190+
'CREATE TABLE t (id INT, name TEXT, score FLOAT DEFAULT 0.0)'
191+
);
192+
193+
$this->assertQuery(
194+
'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT )',
195+
'CREATE TABLE t (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT)'
196+
);
197+
198+
$this->assertQuery(
199+
'CREATE TABLE IF NOT EXISTS "t" ( "id" INTEGER )',
200+
'CREATE TABLE IF NOT EXISTS t (id INT)'
201+
);
202+
}
203+
204+
public function testDataTypes(): void {
205+
// Numeric data types.
206+
$this->assertQuery(
207+
'CREATE TABLE "t" ( "i1" INTEGER , "i2" INTEGER , "i3" INTEGER )',
208+
'CREATE TABLE t (i1 BIT, i2 BOOL, i3 BOOLEAN)'
209+
);
210+
211+
$this->assertQuery(
212+
'CREATE TABLE "t" ( "i1" INTEGER , "i2" INTEGER , "i3" INTEGER , "i4" INTEGER , "i5" INTEGER , "i6" INTEGER )',
213+
'CREATE TABLE t (i1 TINYINT, i2 SMALLINT, i3 MEDIUMINT, i4 INT, i5 INTEGER, i6 BIGINT)'
214+
);
215+
216+
$this->assertQuery(
217+
'CREATE TABLE "t" ( "f1" REAL , "f2" REAL , "f3" REAL , "f4" REAL )',
218+
'CREATE TABLE t (f1 FLOAT, f2 DOUBLE, f3 DOUBLE PRECISION, f4 REAL)'
219+
);
220+
221+
$this->assertQuery(
222+
'CREATE TABLE "t" ( "f1" REAL , "f2" REAL , "f3" REAL , "f4" REAL )',
223+
'CREATE TABLE t (f1 DECIMAL, f2 DEC, f3 FIXED, f4 NUMERIC)'
224+
);
225+
226+
// String data types.
227+
$this->assertQuery(
228+
'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT , "c4" TEXT )',
229+
'CREATE TABLE t (c1 CHAR, c2 VARCHAR(255), c3 CHAR VARYING(255), c4 CHARACTER VARYING(255))'
230+
);
231+
232+
$this->assertQuery(
233+
'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT )',
234+
'CREATE TABLE t (c1 NATIONAL CHAR, c2 NCHAR)'
235+
);
236+
237+
$this->assertQuery(
238+
'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT )',
239+
'CREATE TABLE t (c1 NCHAR VARCHAR(255), c2 NCHAR VARYING(255), c3 NVARCHAR(255))'
240+
);
241+
242+
$this->assertQuery(
243+
'CREATE TABLE "t" ( "c1" TEXT , "c2" TEXT , "c3" TEXT )',
244+
'CREATE TABLE t (c1 NATIONAL VARCHAR(255), c2 NATIONAL CHAR VARYING(255), c3 NATIONAL CHARACTER VARYING(255))'
245+
);
246+
247+
$this->assertQuery(
248+
'CREATE TABLE "t" ( "t1" TEXT , "t2" TEXT , "t3" TEXT , "t4" TEXT )',
249+
'CREATE TABLE t (t1 TINYTEXT, t2 TEXT, t3 MEDIUMTEXT, t4 LONGTEXT)'
250+
);
251+
252+
$this->assertQuery(
253+
'CREATE TABLE "t" ( "e" TEXT )',
254+
'CREATE TABLE t (e ENUM("a", "b", "c"))'
255+
);
256+
257+
// Date and time data types.
258+
$this->assertQuery(
259+
'CREATE TABLE "t" ( "d" TEXT , "t" TEXT , "dt" TEXT , "ts" TEXT , "y" TEXT )',
260+
'CREATE TABLE t (d DATE, t TIME, dt DATETIME, ts TIMESTAMP, y YEAR)'
261+
);
262+
263+
// Binary data types.
264+
$this->assertQuery(
265+
'CREATE TABLE "t" ( "b" INTEGER , "v" BLOB )',
266+
'CREATE TABLE t (b BINARY, v VARBINARY(255))'
267+
);
268+
269+
$this->assertQuery(
270+
'CREATE TABLE "t" ( "b1" BLOB , "b2" BLOB , "b3" BLOB , "b4" BLOB )',
271+
'CREATE TABLE t (b1 TINYBLOB, b2 BLOB, b3 MEDIUMBLOB, b4 LONGBLOB)'
272+
);
273+
274+
// Spatial data types.
275+
$this->assertQuery(
276+
'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT , "g3" TEXT , "g4" TEXT )',
277+
'CREATE TABLE t (g1 GEOMETRY, g2 POINT, g3 LINESTRING, g4 POLYGON)'
278+
);
279+
280+
$this->assertQuery(
281+
'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT , "g3" TEXT )',
282+
'CREATE TABLE t (g1 MULTIPOINT, g2 MULTILINESTRING, g3 MULTIPOLYGON)'
283+
);
284+
285+
$this->assertQuery(
286+
'CREATE TABLE "t" ( "g1" TEXT , "g2" TEXT )',
287+
'CREATE TABLE t (g1 GEOMCOLLECTION, g2 GEOMETRYCOLLECTION)'
288+
);
289+
290+
// SERIAL
291+
$this->assertQuery(
292+
'CREATE TABLE "t" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE )',
293+
'CREATE TABLE t (id SERIAL)'
294+
);
295+
}
296+
182297
private function assertQuery( $expected, string $query ): void {
183298
$driver = new WP_SQLite_Driver( new PDO( 'sqlite::memory:' ) );
184299
$driver->query( $query );
185300

301+
// Check for SQLite syntax errors.
302+
// This ensures that invalid SQLite syntax will always fail, even if it
303+
// was the expected result. It prevents us from using wrong assertions.
304+
$error = $driver->get_error_message();
305+
if ( $error && preg_match( '/(SQLSTATE\[HY000].+syntax error\.)/i', $error, $matches ) ) {
306+
$this->fail( 'SQLite syntax error: ' . $matches[1] );
307+
}
308+
186309
$executed_queries = array_column( $driver->executed_sqlite_queries, 'sql' );
310+
311+
// Remove BEGIN and COMMIT/ROLLBACK queries.
187312
if ( count( $executed_queries ) > 2 ) {
188-
// Remove BEGIN and COMMIT/ROLLBACK queries.
189313
$executed_queries = array_values( array_slice( $executed_queries, 1, -1, true ) );
190314
}
191315

316+
// Remove "select changes()" executed after some queries.
317+
if (
318+
count( $executed_queries ) > 1
319+
&& 'select changes()' === $executed_queries[ count( $executed_queries ) - 1 ] ) {
320+
array_pop( $executed_queries );
321+
}
322+
192323
if ( ! is_array( $expected ) ) {
193324
$expected = array( $expected );
194325
}

Diff for: tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ class WP_MySQL_Server_Suite_Parser_Tests extends TestCase {
1717
'SELECT 1 /*!99999 /* */ */' => true,
1818
'select 1ea10.1a20,1e+ 1e+10 from 1ea10' => true,
1919
"聠聡聢聣聤聬聭聮聯聰聲聽隆垄拢陇楼卤潞禄录陆戮 聶職聳聴\n0聲5\n1聲5\n2聲5\n3聲5\n4\n\nSET NAMES gb18030" => true,
20-
'CREATE TABLE t1 (g GEOMCOLLECTION)' => true,
2120
"alter user mysqltest_7@ identified by 'systpass'" => true,
2221
"SELECT 'a%' LIKE 'a!%' ESCAPE '!', 'a%' LIKE 'a!' || '%' ESCAPE '!'" => true,
2322
"SELECT 'a%' NOT LIKE 'a!%' ESCAPE '!', 'a%' NOT LIKE 'a!' || '%' ESCAPE '!'" => true,

Diff for: wp-includes/mysql/class-wp-mysql-lexer.php

+4
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,7 @@ class WP_MySQL_Lexer {
929929
const SECONDARY_ENGINE_ATTRIBUTE_SYMBOL = 849;
930930
const JSON_VALUE_SYMBOL = 850;
931931
const RETURNING_SYMBOL = 851;
932+
const GEOMCOLLECTION_SYMBOL = 852;
932933

933934
// Comments
934935
const COMMENT = 900;
@@ -1155,6 +1156,7 @@ class WP_MySQL_Lexer {
11551156
'FUNCTION' => self::FUNCTION_SYMBOL,
11561157
'GENERAL' => self::GENERAL_SYMBOL,
11571158
'GENERATED' => self::GENERATED_SYMBOL,
1159+
'GEOMCOLLECTION' => self::GEOMCOLLECTION_SYMBOL,
11581160
'GEOMETRY' => self::GEOMETRY_SYMBOL,
11591161
'GEOMETRYCOLLECTION' => self::GEOMETRYCOLLECTION_SYMBOL,
11601162
'GET' => self::GET_SYMBOL,
@@ -1810,6 +1812,7 @@ class WP_MySQL_Lexer {
18101812
self::FIELDS_SYMBOL => self::COLUMNS_SYMBOL,
18111813
self::FLOAT4_SYMBOL => self::FLOAT_SYMBOL,
18121814
self::FLOAT8_SYMBOL => self::DOUBLE_SYMBOL,
1815+
self::GEOMCOLLECTION_SYMBOL => self::GEOMETRYCOLLECTION_SYMBOL,
18131816
self::INT1_SYMBOL => self::TINYINT_SYMBOL,
18141817
self::INT2_SYMBOL => self::SMALLINT_SYMBOL,
18151818
self::INT3_SYMBOL => self::MEDIUMINT_SYMBOL,
@@ -1936,6 +1939,7 @@ class WP_MySQL_Lexer {
19361939
self::FAILED_LOGIN_ATTEMPTS_SYMBOL => 80019,
19371940
self::FIRST_VALUE_SYMBOL => 80000,
19381941
self::FOLLOWING_SYMBOL => 80000,
1942+
self::GEOMCOLLECTION_SYMBOL => 80000,
19391943
self::GET_MASTER_PUBLIC_KEY_SYMBOL => 80000,
19401944
self::GET_SOURCE_PUBLIC_KEY_SYMBOL => 80000,
19411945
self::GROUPING_SYMBOL => 80000,

Diff for: wp-includes/sqlite-ast/class-wp-sqlite-driver.php

+131-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,65 @@ class WP_SQLite_Driver {
2121
const SQLITE_BUSY = 5;
2222
const SQLITE_LOCKED = 6;
2323

24+
const DATA_TYPE_MAP = array(
25+
// Numeric data types:
26+
WP_MySQL_Lexer::BIT_SYMBOL => 'INTEGER',
27+
WP_MySQL_Lexer::BOOL_SYMBOL => 'INTEGER',
28+
WP_MySQL_Lexer::BOOLEAN_SYMBOL => 'INTEGER',
29+
WP_MySQL_Lexer::TINYINT_SYMBOL => 'INTEGER',
30+
WP_MySQL_Lexer::SMALLINT_SYMBOL => 'INTEGER',
31+
WP_MySQL_Lexer::MEDIUMINT_SYMBOL => 'INTEGER',
32+
WP_MySQL_Lexer::INT_SYMBOL => 'INTEGER',
33+
WP_MySQL_Lexer::INTEGER_SYMBOL => 'INTEGER',
34+
WP_MySQL_Lexer::BIGINT_SYMBOL => 'INTEGER',
35+
WP_MySQL_Lexer::FLOAT_SYMBOL => 'REAL',
36+
WP_MySQL_Lexer::DOUBLE_SYMBOL => 'REAL',
37+
WP_MySQL_Lexer::REAL_SYMBOL => 'REAL',
38+
WP_MySQL_Lexer::DECIMAL_SYMBOL => 'REAL',
39+
WP_MySQL_Lexer::DEC_SYMBOL => 'REAL',
40+
WP_MySQL_Lexer::FIXED_SYMBOL => 'REAL',
41+
WP_MySQL_Lexer::NUMERIC_SYMBOL => 'REAL',
42+
43+
// String data types:
44+
WP_MySQL_Lexer::CHAR_SYMBOL => 'TEXT',
45+
WP_MySQL_Lexer::VARCHAR_SYMBOL => 'TEXT',
46+
WP_MySQL_Lexer::NCHAR_SYMBOL => 'TEXT',
47+
WP_MySQL_Lexer::NVARCHAR_SYMBOL => 'TEXT',
48+
WP_MySQL_Lexer::TINYTEXT_SYMBOL => 'TEXT',
49+
WP_MySQL_Lexer::TEXT_SYMBOL => 'TEXT',
50+
WP_MySQL_Lexer::MEDIUMTEXT_SYMBOL => 'TEXT',
51+
WP_MySQL_Lexer::LONGTEXT_SYMBOL => 'TEXT',
52+
WP_MySQL_Lexer::ENUM_SYMBOL => 'TEXT',
53+
54+
// Date and time data types:
55+
WP_MySQL_Lexer::DATE_SYMBOL => 'TEXT',
56+
WP_MySQL_Lexer::TIME_SYMBOL => 'TEXT',
57+
WP_MySQL_Lexer::DATETIME_SYMBOL => 'TEXT',
58+
WP_MySQL_Lexer::TIMESTAMP_SYMBOL => 'TEXT',
59+
WP_MySQL_Lexer::YEAR_SYMBOL => 'TEXT',
60+
61+
// Binary data types:
62+
WP_MySQL_Lexer::BINARY_SYMBOL => 'INTEGER',
63+
WP_MySQL_Lexer::VARBINARY_SYMBOL => 'BLOB',
64+
WP_MySQL_Lexer::TINYBLOB_SYMBOL => 'BLOB',
65+
WP_MySQL_Lexer::BLOB_SYMBOL => 'BLOB',
66+
WP_MySQL_Lexer::MEDIUMBLOB_SYMBOL => 'BLOB',
67+
WP_MySQL_Lexer::LONGBLOB_SYMBOL => 'BLOB',
68+
69+
// Spatial data types:
70+
WP_MySQL_Lexer::GEOMETRY_SYMBOL => 'TEXT',
71+
WP_MySQL_Lexer::POINT_SYMBOL => 'TEXT',
72+
WP_MySQL_Lexer::LINESTRING_SYMBOL => 'TEXT',
73+
WP_MySQL_Lexer::POLYGON_SYMBOL => 'TEXT',
74+
WP_MySQL_Lexer::MULTIPOINT_SYMBOL => 'TEXT',
75+
WP_MySQL_Lexer::MULTILINESTRING_SYMBOL => 'TEXT',
76+
WP_MySQL_Lexer::MULTIPOLYGON_SYMBOL => 'TEXT',
77+
WP_MySQL_Lexer::GEOMCOLLECTION_SYMBOL => 'TEXT',
78+
WP_MySQL_Lexer::GEOMETRYCOLLECTION_SYMBOL => 'TEXT',
79+
80+
// SERIAL, SET, and JSON types are handled in the translation process.
81+
);
82+
2483
const DATA_TYPES_CACHE_TABLE = '_mysql_data_types_cache';
2584

2685
const CREATE_DATA_TYPES_CACHE_TABLE = 'CREATE TABLE IF NOT EXISTS _mysql_data_types_cache (
@@ -716,6 +775,25 @@ private function execute_mysql_query( WP_Parser_Node $ast ) {
716775
$this->execute_sqlite_query( $query );
717776
$this->set_result_from_affected_rows();
718777
break;
778+
case 'createStatement':
779+
$this->query_type = 'CREATE';
780+
$subtree = $ast->get_child_node();
781+
switch ( $subtree->rule_name ) {
782+
case 'createTable':
783+
$query = $this->translate( $ast );
784+
$this->execute_sqlite_query( $query );
785+
$this->set_result_from_affected_rows();
786+
break;
787+
default:
788+
throw new Exception(
789+
sprintf(
790+
'Unsupported statement type: "%s" > "%s"',
791+
$ast->rule_name,
792+
$subtree->rule_name
793+
)
794+
);
795+
}
796+
break;
719797
default:
720798
throw new Exception( sprintf( 'Unsupported statement type: "%s"', $ast->rule_name ) );
721799
}
@@ -739,19 +817,51 @@ private function translate( $ast ) {
739817
case 'qualifiedIdentifier':
740818
case 'dotIdentifier':
741819
return $this->translate_sequence( $ast->get_children(), '' );
820+
case 'identifierKeyword':
821+
return '"' . $this->translate( $ast->get_child() ) . '"';
742822
case 'textStringLiteral':
743-
if ( $ast->has_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT ) ) {
744-
return WP_SQLite_Token_Factory::double_quoted_value(
745-
$ast->get_child_token( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT )->value
746-
)->value;
823+
$token = $ast->get_child_token();
824+
if ( WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $token->id ) {
825+
return WP_SQLite_Token_Factory::double_quoted_value( $token->value )->value;
826+
}
827+
if ( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $token->id ) {
828+
return WP_SQLite_Token_Factory::raw( $token->value )->value;
829+
}
830+
throw $this->invalid_input_exception();
831+
case 'dataType':
832+
case 'nchar':
833+
$child = $ast->get_child();
834+
if ( $child instanceof WP_Parser_Node ) {
835+
return $this->translate( $child );
836+
}
837+
838+
// Handle optional prefixes (data type is the second token):
839+
// 1. LONG VARCHAR, LONG CHAR(ACTER) VARYING, LONG VARBINARY.
840+
// 2. NATIONAL CHAR, NATIONAL VARCHAR, NATIONAL CHAR(ACTER) VARYING.
841+
if ( WP_MySQL_Lexer::LONG_SYMBOL === $child->id ) {
842+
$child = $ast->get_child_tokens()[1] ?? null;
843+
} elseif ( WP_MySQL_Lexer::NATIONAL_SYMBOL === $child->id ) {
844+
$child = $ast->get_child_tokens()[1] ?? null;
845+
}
846+
847+
if ( null === $child ) {
848+
throw $this->invalid_input_exception();
747849
}
748-
if ( $ast->has_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT ) ) {
749-
return WP_SQLite_Token_Factory::raw(
750-
$ast->get_child_token( WP_MySQL_Lexer::SINGLE_QUOTED_TEXT )->value
751-
)->value;
850+
851+
$type = self::DATA_TYPE_MAP[ $child->id ] ?? null;
852+
if ( null !== $type ) {
853+
return $type;
854+
}
855+
856+
// SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
857+
if ( WP_MySQL_Lexer::SERIAL_SYMBOL === $child->id ) {
858+
return 'INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE';
752859
}
753-
// Fall through to the default case.
754860

861+
// @TODO: Handle SET and JSON.
862+
throw $this->not_supported_exception(
863+
sprintf( 'data type: %s', $child->value )
864+
);
755865
default:
756866
return $this->translate_sequence( $ast->get_children() );
757867
}
@@ -763,6 +873,8 @@ private function translate_token( WP_MySQL_Token $token ) {
763873
return null;
764874
case WP_MySQL_Lexer::IDENTIFIER:
765875
return '"' . trim( $token->value, '`"' ) . '"';
876+
case WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL:
877+
return 'AUTOINCREMENT';
766878
default:
767879
return $token->value;
768880
}
@@ -912,4 +1024,14 @@ private function set_error( $line, $function_name, $message ) {
9121024
$this->error_messages[] = $message;
9131025
$this->is_error = true;
9141026
}
1027+
1028+
private function invalid_input_exception() {
1029+
throw new Exception( 'MySQL query syntax error.' );
1030+
}
1031+
1032+
private function not_supported_exception( string $cause ): Exception {
1033+
return new Exception(
1034+
sprintf( 'MySQL query not supported. Cause: %s', $cause )
1035+
);
1036+
}
9151037
}

0 commit comments

Comments
 (0)