From 3c5455afcab06f5d74a20a77fe97a6ef72a19de4 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Wed, 14 Aug 2024 18:49:58 +0300 Subject: [PATCH 1/8] v2.1.14 (#155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jeroen P Co-authored-by: Rostislav Wolný <1082140+costasovo@users.noreply.github.com> Co-authored-by: Brandon Payton Co-authored-by: Alex Kirk Co-authored-by: Jan Jakeš Co-authored-by: Bero Co-authored-by: Antony Agrios --- load.php | 2 +- readme.txt | 2 +- tests/WP_SQLite_Query_Tests.php | 21 + tests/WP_SQLite_Translator_Tests.php | 633 +++++++++++++++++- .../sqlite/class-wp-sqlite-translator.php | 204 +++++- 5 files changed, 854 insertions(+), 8 deletions(-) diff --git a/load.php b/load.php index 11fc477f..511db936 100644 --- a/load.php +++ b/load.php @@ -3,7 +3,7 @@ * Plugin Name: SQLite Database Integration * Description: SQLite database driver drop-in. * Author: The WordPress Team - * Version: 2.1.13 + * Version: 2.1.14 * Requires PHP: 7.0 * Textdomain: sqlite-database-integration * diff --git a/readme.txt b/readme.txt index 3e81d465..d9fee569 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Contributors: wordpressdotorg, aristath Requires at least: 6.4 Tested up to: 6.6.1 Requires PHP: 7.0 -Stable tag: 2.1.13 +Stable tag: 2.1.14 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: performance, database diff --git a/tests/WP_SQLite_Query_Tests.php b/tests/WP_SQLite_Query_Tests.php index 72329feb..ff6e0aa4 100644 --- a/tests/WP_SQLite_Query_Tests.php +++ b/tests/WP_SQLite_Query_Tests.php @@ -537,6 +537,27 @@ public function testOnDuplicateKeyWithUnnamedKeys() { ); } + public function testOnCreateTableIfNotExistsWithIndexAdded() { + $this->assertQuery( + 'CREATE TABLE IF not EXISTS `test` ( + `id` INT, + `name` VARCHAR(255), + `other` VARCHAR(255), + PRIMARY KEY (id), + UNIQUE KEY (name) + );' + ); + $this->assertQuery( + 'CREATE TABLE if NOT ExisTS `test` ( + `id` INT, + `name` VARCHAR(255), + `other` VARCHAR(255), + PRIMARY KEY (id), + UNIQUE KEY (name) + );' + ); + } + public function testShowColumns() { $query = 'SHOW COLUMNS FROM wp_posts'; diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index f7f7242f..1ca25d51 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -871,7 +871,7 @@ enum_column ENUM('a', 'b', 'c') NOT NULL DEFAULT 'a', ); } - public function testAlterTableAddColumn() { + public function testAlterTableAddAndDropColumn() { $result = $this->assertQuery( "CREATE TABLE _tmp_table ( name varchar(20) NOT NULL default '' @@ -905,6 +905,90 @@ public function testAlterTableAddColumn() { ), $results ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table ADD `column2` int;' ); + $this->assertEquals( '', $this->engine->get_error_message() ); + $this->assertEquals( 1, $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'column', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'column2', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP COLUMN `column`;' ); + $this->assertEquals( '', $this->engine->get_error_message() ); + $this->assertEquals( 1, $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'column2', + 'Type' => 'int', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + $result = $this->assertQuery( 'ALTER TABLE _tmp_table DROP `column2`;' ); + $this->assertEquals( '', $this->engine->get_error_message() ); + $this->assertEquals( 1, $result ); + + $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( + array( + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); } public function testAlterTableAddNotNullVarcharColumn() { @@ -943,6 +1027,428 @@ public function testAlterTableAddNotNullVarcharColumn() { ); } + public function testColumnWithOnUpdate() { + // CREATE TABLE with ON UPDATE + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + id int(11) NOT NULL, + created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP + );' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + // ADD COLUMN with ON UPDATE + $this->assertQuery( + 'ALTER TABLE _tmp_table ADD COLUMN updated_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'updated_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + // assert ON UPDATE triggers + $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" ); + $this->assertEquals( + array( + (object) array( + 'type' => 'trigger', + 'name' => '___tmp_table_created_at_on_update__', + 'tbl_name' => '_tmp_table', + 'rootpage' => '0', + 'sql' => "CREATE TRIGGER \"___tmp_table_created_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"created_at\" = CURRENT_TIMESTAMP WHERE id = NEW.id;\n\t\t\tEND", + ), + (object) array( + 'type' => 'trigger', + 'name' => '___tmp_table_updated_at_on_update__', + 'tbl_name' => '_tmp_table', + 'rootpage' => '0', + 'sql' => "CREATE TRIGGER \"___tmp_table_updated_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"updated_at\" = CURRENT_TIMESTAMP WHERE id = NEW.id;\n\t\t\tEND", + ), + ), + $results + ); + + // on INSERT, no timestamps are expected + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' ); + $this->assertNull( $result[0]->created_at ); + $this->assertNull( $result[0]->updated_at ); + + // on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS + $this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated_at ); + + // drop ON UPDATE + $this->assertQuery( + 'ALTER TABLE _tmp_table + CHANGE created_at created_at timestamp NULL, + CHANGE COLUMN updated_at updated_at timestamp NULL' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'created_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'updated_at', + 'Type' => 'timestamp', + 'Null' => 'YES', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + ), + $results + ); + + // assert ON UPDATE triggers are removed + $results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" ); + $this->assertEquals( array(), $results ); + + // now, no timestamps are expected + $this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (10)' ); + $this->assertQuery( 'UPDATE _tmp_table SET id = 11 WHERE id = 10' ); + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 11' ); + $this->assertNull( $result[0]->created_at ); + $this->assertNull( $result[0]->updated_at ); + } + + public function testAlterTableWithColumnFirstAndAfter() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + id int(11) NOT NULL, + name varchar(20) NOT NULL default '' + );" + ); + + // ADD COLUMN with FIRST + $this->assertQuery( + "ALTER TABLE _tmp_table ADD COLUMN new_first_column VARCHAR(255) NOT NULL DEFAULT '' FIRST" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // ADD COLUMN with AFTER + $this->assertQuery( + "ALTER TABLE _tmp_table ADD COLUMN new_column VARCHAR(255) NOT NULL DEFAULT '' AFTER id" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // CHANGE with FIRST + $this->assertQuery( + "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' FIRST" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // CHANGE with AFTER + $this->assertQuery( + "ALTER TABLE _tmp_table CHANGE id id int(11) NOT NULL DEFAULT '0' AFTER name" + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'name', + 'Type' => 'varchar(20)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_first_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new_column', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + } + + public function testAlterTableWithMultiColumnFirstAndAfter() { + $this->assertQuery( + 'CREATE TABLE _tmp_table ( + id int(11) NOT NULL + );' + ); + + // ADD COLUMN + $this->assertQuery( + 'ALTER TABLE _tmp_table + ADD COLUMN new1 varchar(255) NOT NULL, + ADD COLUMN new2 varchar(255) NOT NULL FIRST, + ADD COLUMN new3 varchar(255) NOT NULL AFTER new1' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new1', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new2', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new3', + 'Type' => 'varchar(255)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + + // CHANGE + $this->assertQuery( + 'ALTER TABLE _tmp_table + CHANGE new1 new1 int(11) NOT NULL FIRST, + CHANGE new2 new2 int(11) NOT NULL, + CHANGE new3 new3 int(11) NOT NULL AFTER new2' + ); + $results = $this->assertQuery( 'DESCRIBE _tmp_table;' ); + $this->assertEquals( + array( + (object) array( + 'Field' => 'id', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '0', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new1', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => null, + 'Extra' => '', + ), + (object) array( + 'Field' => 'new2', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + (object) array( + 'Field' => 'new3', + 'Type' => 'int(11)', + 'Null' => 'NO', + 'Key' => '', + 'Default' => '', + 'Extra' => '', + ), + ), + $results + ); + } + public function testAlterTableAddIndex() { $result = $this->assertQuery( "CREATE TABLE _tmp_table ( @@ -2389,6 +2895,91 @@ public function testTranslatesUtf8SELECT() { $this->assertQuery( 'DELETE FROM _options' ); } + public function testTranslateLikeBinaryAndGlob() { + // Create a temporary table for testing + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name varchar(20) NOT NULL default '' + );" + ); + + // Insert data into the table + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('second');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('%special%');" ); + $this->assertQuery( 'INSERT INTO _tmp_table (name) VALUES (NULL);' ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special%chars');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special_chars');" ); + $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('special\\chars');" ); + + // Test case-sensitive LIKE BINARY + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test case-sensitive LIKE BINARY with wildcard % + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f%'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test case-sensitive LIKE BINARY with wildcard _ + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'f_rst'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test case-insensitive LIKE + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE 'FIRST'" ); + $this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST' + + // Test mixed case with LIKE BINARY + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'First'" ); + $this->assertCount( 0, $result ); + + // Test no matches with LIKE BINARY + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'third'" ); + $this->assertCount( 0, $result ); + + // Test GLOB equivalent for case-sensitive matching with wildcard + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f*'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test GLOB with single character wildcard + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'f?rst'" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + // Test GLOB with no matches + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'S*'" ); + $this->assertCount( 0, $result ); + + // Test GLOB case sensitivity with LIKE and GLOB + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'first';" ); + $this->assertCount( 1, $result ); // Should only match 'first' + + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name GLOB 'FIRST';" ); + $this->assertCount( 1, $result ); // Should only match 'FIRST' + + // Test NULL comparison with LIKE BINARY + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY 'first';" ); + $this->assertCount( 1, $result ); + $this->assertEquals( 'first', $result[0]->name ); + + $result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE name LIKE BINARY NULL;' ); + $this->assertCount( 0, $result ); // NULL comparison should return no results + + // Test pattern with special characters using LIKE BINARY + $result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name LIKE BINARY '%special%';" ); + $this->assertCount( 4, $result ); + $this->assertEquals( '%special%', $result[0]->name ); + $this->assertEquals( 'special%chars', $result[1]->name ); + $this->assertEquals( 'special_chars', $result[2]->name ); + $this->assertEquals( 'specialchars', $result[3]->name ); + } + public function testOnConflictReplace() { $this->assertQuery( "CREATE TABLE _tmp_table ( @@ -2502,6 +3093,46 @@ public function testDefaultNullValue() { ); } + public function testCurrentTimestamp() { + // SELECT + $results = $this->assertQuery( + 'SELECT + current_timestamp AS t1, + CURRENT_TIMESTAMP AS t2, + current_timestamp() AS t3, + CURRENT_TIMESTAMP() AS t4' + ); + $this->assertIsArray( $results ); + $this->assertCount( 1, $results ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t1 ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t2 ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t3 ); + + // INSERT + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', CURRENT_TIMESTAMP())" + ); + $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); + $this->assertCount( 1, $results ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t ); + + // UPDATE + $this->assertQuery( 'UPDATE _dates SET option_value = NULL' ); + $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); + $this->assertCount( 1, $results ); + $this->assertEmpty( $results[0]->t ); + + $this->assertQuery( 'UPDATE _dates SET option_value = CURRENT_TIMESTAMP()' ); + $results = $this->assertQuery( 'SELECT option_value AS t FROM _dates' ); + $this->assertCount( 1, $results ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $results[0]->t ); + + // DELETE + // We can only assert that the query passes. It is not guaranteed that we'll actually + // delete the existing record, as the delete query could fall into a different second. + $this->assertQuery( 'DELETE FROM _dates WHERE option_value = CURRENT_TIMESTAMP()' ); + } + /** * @dataProvider mysqlVariablesToTest */ diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 489a5d59..46bbeb79 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -421,6 +421,11 @@ public function __construct( $pdo = null ) { $this->pdo->query( 'PRAGMA foreign_keys = ON' ); } $this->pdo->query( 'PRAGMA encoding="UTF-8";' ); + + $valid_journal_modes = array( 'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF' ); + if ( defined( 'SQLITE_JOURNAL_MODE' ) && in_array( SQLITE_JOURNAL_MODE, $valid_journal_modes, true ) ) { + $this->pdo->query( 'PRAGMA journal_mode = ' . SQLITE_JOURNAL_MODE ); + } } /** @@ -767,7 +772,23 @@ public function get_return_value() { * @throws Exception If the query is not supported. */ private function execute_mysql_query( $query ) { - $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; + $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; + + // SQLite does not support CURRENT_TIMESTAMP() calls with parentheses. + // Since CURRENT_TIMESTAMP() can appear in most types of SQL queries, + // let's remove the parentheses globally before further processing. + foreach ( $tokens as $i => $token ) { + if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type && 'CURRENT_TIMESTAMP' === $token->keyword ) { + $paren_open = $tokens[ $i + 1 ] ?? null; + $paren_close = $tokens[ $i + 2 ] ?? null; + if ( WP_SQLite_Token::TYPE_OPERATOR === $paren_open->type && '(' === $paren_open->value + && WP_SQLite_Token::TYPE_OPERATOR === $paren_close->type && ')' === $paren_close->value ) { + unset( $tokens[ $i + 1 ], $tokens[ $i + 2 ] ); + } + } + } + $tokens = array_values( $tokens ); + $this->rewriter = new WP_SQLite_Query_Rewriter( $tokens ); $this->query_type = $this->rewriter->peek()->value; @@ -859,6 +880,7 @@ private function execute_create_table() { $table = $this->parse_create_table(); $definitions = array(); + $on_updates = array(); foreach ( $table->fields as $field ) { /* * Do not include the inline PRIMARY KEY definition @@ -872,6 +894,10 @@ private function execute_create_table() { } $definitions[] = $this->make_sqlite_field_definition( $field ); + if ( $field->on_update ) { + $on_updates[ $field->name ] = $field->on_update; + } + $this->update_data_type_cache( $table->name, $field->name, @@ -890,6 +916,8 @@ private function execute_create_table() { ')' ); + $if_not_exists = preg_match( '/\bIF\s+NOT\s+EXISTS\b/i', $create_query ) ? 'IF NOT EXISTS' : ''; + $this->execute_sqlite_query( $create_query ); $this->results = $this->last_exec_returned; $this->return_value = $this->results; @@ -902,7 +930,7 @@ private function execute_create_table() { } $index_name = $this->generate_index_name( $table->name, $constraint->name ); $this->execute_sqlite_query( - "CREATE $unique INDEX \"$index_name\" ON \"{$table->name}\" (\"" . implode( '", "', $constraint->columns ) . '")' + "CREATE $unique INDEX $if_not_exists \"$index_name\" ON \"{$table->name}\" (\"" . implode( '", "', $constraint->columns ) . '")' ); $this->update_data_type_cache( $table->name, @@ -910,6 +938,10 @@ private function execute_create_table() { $constraint->value ); } + + foreach ( $on_updates as $column => $on_update ) { + $this->add_column_on_update_current_timestamp( $table->name, $column ); + } } /** @@ -1047,6 +1079,7 @@ private function parse_mysql_create_table_field() { $result->default = false; $result->auto_increment = false; $result->primary_key = false; + $result->on_update = false; $field_name_token = $this->rewriter->skip(); // Field name. $this->rewriter->add( new WP_SQLite_Token( "\n", WP_SQLite_Token::TYPE_WHITESPACE ) ); @@ -1101,6 +1134,22 @@ private function parse_mysql_create_table_field() { continue; } + if ( + $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'ON UPDATE' ) + ) && $this->rewriter->peek()->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'CURRENT_TIMESTAMP' ) + ) + ) { + $this->rewriter->skip(); + $result->on_update = true; + continue; + } + if ( $this->is_create_table_field_terminator( $token, $definition_depth ) ) { $this->rewriter->add( $token ); break; @@ -2019,6 +2068,7 @@ private function translate_expression( $token ) { || $this->translate_regexp_functions( $token ) || $this->capture_group_by( $token ) || $this->translate_ungrouped_having( $token ) + || $this->translate_like_binary( $token ) || $this->translate_like_escape( $token ) || $this->translate_left_function( $token ) ); @@ -2543,6 +2593,57 @@ private function translate_regexp_functions( $token ) { } return true; } + /** + * Translate LIKE BINARY to SQLite equivalent using GLOB. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_like_binary( $token ): bool { + if ( ! $token->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'LIKE' ) ) ) { + return false; + } + + $next = $this->rewriter->peek_nth( 2 ); + if ( ! $next || ! $next->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'BINARY' ) ) ) { + return false; + } + + $this->rewriter->skip(); // Skip 'LIKE' + $this->rewriter->skip(); // Skip 'BINARY' + + $pattern_token = $this->rewriter->peek(); + $this->rewriter->skip(); // Skip the pattern token + + $this->rewriter->add( new WP_SQLite_Token( 'GLOB', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + + $escaped_pattern = $this->escape_like_to_glob( $pattern_token->value ); + $this->rewriter->add( new WP_SQLite_Token( $escaped_pattern, WP_SQLite_Token::TYPE_STRING ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + + return true; + } + + /** + * Escape LIKE pattern to GLOB pattern. + * + * @param string $pattern The LIKE pattern. + * @return string The escaped GLOB pattern. + */ + private function escape_like_to_glob( $pattern ) { + // Remove surrounding quotes + $pattern = trim( $pattern, "'\"" ); + + $pattern = str_replace( '%', '*', $pattern ); + $pattern = str_replace( '_', '?', $pattern ); + + // No need to escape special characters in this case + // because GLOB doesn't require escaping in the same way LIKE does + // Return the pattern wrapped in single quotes + return "'" . $pattern . "'"; + } /** * Detect GROUP BY. @@ -2920,9 +3021,14 @@ private function execute_alter() { $op_subject = strtoupper( $op_raw_subject ); $mysql_index_type = $this->normalize_mysql_index_type( $op_subject ); $is_index_op = (bool) $mysql_index_type; + $on_update = false; - if ( 'ADD' === $op_type && 'COLUMN' === $op_subject ) { - $column_name = $this->rewriter->consume()->value; + if ( 'ADD' === $op_type && ! $is_index_op ) { + if ( 'COLUMN' === $op_subject ) { + $column_name = $this->rewriter->consume()->value; + } else { + $column_name = $op_subject; + } $skip_mysql_data_type_parts = $this->skip_mysql_data_type(); $sqlite_data_type = $skip_mysql_data_type_parts[0]; @@ -2935,12 +3041,67 @@ private function execute_alter() { WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE ) ); + + $comma = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + + // Handle "ON UPDATE CURRENT_TIMESTAMP". + $on_update_token = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => array( 'ON UPDATE' ), + ) + ); + + if ( $on_update_token && ( ! $comma || $on_update_token->position < $comma->position ) ) { + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => array( 'ON UPDATE' ), + ) + ); + if ( $this->rewriter->peek()->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'CURRENT_TIMESTAMP' ) + ) ) { + $this->rewriter->drop_last(); + $this->rewriter->skip(); + $on_update = $column_name; + } + } + + // Drop "FIRST" and "AFTER ", as these are not supported in SQLite. + $column_position = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => array( 'FIRST', 'AFTER' ), + ) + ); + + if ( $column_position && ( ! $comma || $column_position->position < $comma->position ) ) { + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => array( 'FIRST', 'AFTER' ), + ) + ); + $this->rewriter->drop_last(); + if ( 'AFTER' === strtoupper( $column_position->value ) ) { + $this->rewriter->skip(); + } + } + $this->update_data_type_cache( $this->table_name, $column_name, $mysql_data_type ); - } elseif ( 'DROP' === $op_type && 'COLUMN' === $op_subject ) { + } elseif ( 'DROP' === $op_type && ! $is_index_op ) { $this->rewriter->consume_all(); } elseif ( 'CHANGE' === $op_type ) { // Parse the new column definition. @@ -3165,9 +3326,17 @@ private function execute_alter() { ) ); $this->rewriter->drop_last(); + + $on_update_trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $this->table_name, $op_subject ); + $this->execute_sqlite_query( "DROP TRIGGER IF EXISTS \"$on_update_trigger_name\"" ); + $this->execute_sqlite_query( $this->rewriter->get_updated_query() ); + + if ( $on_update ) { + $this->add_column_on_update_current_timestamp( $this->table_name, $on_update ); + } } while ( $comma ); $this->results = 1; @@ -4218,4 +4387,29 @@ private function generate_index_name( $table, $original_index_name ) { // to allow easier splitting on __ later. return preg_replace( '/_{2,}/', '_', $table ) . '__' . $original_index_name; } + + /** + * @param string $table + * @param string $column + */ + private function add_column_on_update_current_timestamp( $table, $column ) { + $trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $table, $column ); + $this->execute_sqlite_query( + "CREATE TRIGGER \"$trigger_name\" + AFTER UPDATE ON \"$table\" + FOR EACH ROW + BEGIN + UPDATE \"$table\" SET \"$column\" = CURRENT_TIMESTAMP WHERE id = NEW.id; + END" + ); + } + + /** + * @param string $table + * @param string $column + * @return string + */ + private function get_column_on_update_current_timestamp_trigger_name( $table, $column ) { + return "__{$table}_{$column}_on_update__"; + } } From e43782e9b02c27de7dc258059b2100311a357133 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 11 Dec 2024 16:50:06 +0100 Subject: [PATCH 2/8] Add test that reproduces issue with invalid empty date default --- tests/WP_SQLite_Translator_Tests.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 20bc152e..8a13bbb1 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -290,6 +290,28 @@ public function testShowCreateTable1() { ); } + public function testShowCreateTableWithEmptyDatetimeDefault() { + $this->assertQuery( + "CREATE TABLE _tmp_table ( + ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + created_at datetime NOT NULL + );" + ); + + $this->assertQuery( + 'SHOW CREATE TABLE _tmp_table;' + ); + $results = $this->engine->get_query_results(); + + $this->assertEquals( + "CREATE TABLE _tmp_table ( + `ID` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL, + `created_at` datetime NOT NULL +);", + $results[0]->{'Create Table'} + ); + } + public function testShowCreateTableQuoted() { $this->assertQuery( "CREATE TABLE _tmp_table ( From 056bb1881b39f1290bc1cf9a5f87cb4ebc1bb668 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 11 Dec 2024 16:52:01 +0100 Subject: [PATCH 3/8] Remove trailing space --- tests/WP_SQLite_Translator_Tests.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 8a13bbb1..854221cb 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -294,7 +294,7 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { $this->assertQuery( "CREATE TABLE _tmp_table ( ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - created_at datetime NOT NULL + created_at datetime NOT NULL );" ); @@ -306,7 +306,7 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { $this->assertEquals( "CREATE TABLE _tmp_table ( `ID` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL, - `created_at` datetime NOT NULL + `created_at` datetime NOT NULL );", $results[0]->{'Create Table'} ); From 674bb6ff2fb3645b31dce4f222d7c6ae416fffd9 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 11 Dec 2024 17:35:09 +0100 Subject: [PATCH 4/8] Add more cases to the test --- tests/WP_SQLite_Translator_Tests.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 854221cb..781f9586 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -294,7 +294,11 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { $this->assertQuery( "CREATE TABLE _tmp_table ( ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, - created_at datetime NOT NULL + timestamp1 datetime NOT NULL, + timestamp2 date NOT NULL, + timestamp3 time NOT NULL, + timestamp4 timestamp NOT NULL, + timestamp5 year NOT NULL );" ); @@ -306,7 +310,11 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { $this->assertEquals( "CREATE TABLE _tmp_table ( `ID` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL, - `created_at` datetime NOT NULL + `timestamp1` datetime NOT NULL, + `timestamp2` date NOT NULL, + `timestamp3` time NOT NULL, + `timestamp4` timestamp NOT NULL, + `timestamp5` year NOT NULL );", $results[0]->{'Create Table'} ); From 7835a5e29dba55a61609dc7c0928b982ae8adbae Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 11 Dec 2024 17:35:29 +0100 Subject: [PATCH 5/8] Ensure that for empty default date fields there is no default --- tests/WP_SQLite_Translator_Tests.php | 9 ++++---- .../sqlite/class-wp-sqlite-translator.php | 21 +++++++++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 781f9586..6801ea32 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -293,7 +293,7 @@ public function testShowCreateTable1() { public function testShowCreateTableWithEmptyDatetimeDefault() { $this->assertQuery( "CREATE TABLE _tmp_table ( - ID BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL, + ID BIGINT PRIMARY KEY AUTO_INCREMENT, timestamp1 datetime NOT NULL, timestamp2 date NOT NULL, timestamp3 time NOT NULL, @@ -308,13 +308,14 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { $results = $this->engine->get_query_results(); $this->assertEquals( - "CREATE TABLE _tmp_table ( - `ID` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL, + "CREATE TABLE `_tmp_table` ( + `ID` bigint AUTO_INCREMENT, `timestamp1` datetime NOT NULL, `timestamp2` date NOT NULL, `timestamp3` time NOT NULL, `timestamp4` timestamp NOT NULL, - `timestamp5` year NOT NULL + `timestamp5` year NOT NULL, + PRIMARY KEY (`ID`) );", $results[0]->{'Create Table'} ); diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 705b3970..61e3f008 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -3693,16 +3693,17 @@ protected function get_column_definitions( $table_name, $columns ) { $auto_increment_column = $this->get_autoincrement_column( $table_name ); $column_definitions = array(); foreach ( $columns as $column ) { + $mysql_type = $this->get_cached_mysql_data_type( $table_name, $column->name ); $is_auto_incr = $auto_increment_column && strtolower( $auto_increment_column ) === strtolower( $column->name ); $definition = array(); $definition[] = '`' . $column->name . '`'; - $definition[] = $this->get_cached_mysql_data_type( $table_name, $column->name ) ?? $column->name; + $definition[] = $mysql_type ?? $column->name; if ( '1' === $column->notnull ) { $definition[] = 'NOT NULL'; } - if ( null !== $column->dflt_value && '' !== $column->dflt_value && ! $is_auto_incr ) { + if ( $this->column_has_default( $column, $mysql_type ) && ! $is_auto_incr ) { $definition[] = 'DEFAULT ' . $column->dflt_value; } @@ -3858,6 +3859,22 @@ function ( $row ) use ( $name_map ) { ); } + /** + * Checks if column should define the default. + * + * @param stdClass $column The table column + * @param string $mysql_type The MySQL data type + * + * @return boolean If column should have a default definition. + */ + private function column_has_default( $column, $mysql_type ) { + if ( null !== $column->dflt_value && '' !== $column->dflt_value && ! in_array( strtolower( $mysql_type ), array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true ) ) { + return true; + } + + return false; + } + /** * Consumes data types from the query. * From 7f43961b038997cfd6a39759bf69a864c63ff735 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 11 Dec 2024 17:56:31 +0100 Subject: [PATCH 6/8] Fix lint issues --- wp-includes/sqlite/class-wp-sqlite-translator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 61e3f008..5f92810d 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -3693,7 +3693,7 @@ protected function get_column_definitions( $table_name, $columns ) { $auto_increment_column = $this->get_autoincrement_column( $table_name ); $column_definitions = array(); foreach ( $columns as $column ) { - $mysql_type = $this->get_cached_mysql_data_type( $table_name, $column->name ); + $mysql_type = $this->get_cached_mysql_data_type( $table_name, $column->name ); $is_auto_incr = $auto_increment_column && strtolower( $auto_increment_column ) === strtolower( $column->name ); $definition = array(); $definition[] = '`' . $column->name . '`'; @@ -3867,7 +3867,7 @@ function ( $row ) use ( $name_map ) { * * @return boolean If column should have a default definition. */ - private function column_has_default( $column, $mysql_type ) { + private function column_has_default( $column, $mysql_type ) { if ( null !== $column->dflt_value && '' !== $column->dflt_value && ! in_array( strtolower( $mysql_type ), array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true ) ) { return true; } From e81a812433af96db54156bdd405028a782c0f2f4 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 11 Dec 2024 17:59:12 +0100 Subject: [PATCH 7/8] Fix one more lint issue --- tests/WP_SQLite_Translator_Tests.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 6801ea32..dc2e96fe 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -292,14 +292,14 @@ public function testShowCreateTable1() { public function testShowCreateTableWithEmptyDatetimeDefault() { $this->assertQuery( - "CREATE TABLE _tmp_table ( + 'CREATE TABLE _tmp_table ( ID BIGINT PRIMARY KEY AUTO_INCREMENT, timestamp1 datetime NOT NULL, timestamp2 date NOT NULL, timestamp3 time NOT NULL, timestamp4 timestamp NOT NULL, timestamp5 year NOT NULL - );" + );' ); $this->assertQuery( @@ -308,7 +308,7 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { $results = $this->engine->get_query_results(); $this->assertEquals( - "CREATE TABLE `_tmp_table` ( + 'CREATE TABLE `_tmp_table` ( `ID` bigint AUTO_INCREMENT, `timestamp1` datetime NOT NULL, `timestamp2` date NOT NULL, @@ -316,7 +316,7 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { `timestamp4` timestamp NOT NULL, `timestamp5` year NOT NULL, PRIMARY KEY (`ID`) -);", +);', $results[0]->{'Create Table'} ); } From 86b43fdb1a07162533362a0e292335235c18fc5a Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 18 Dec 2024 17:26:55 +0100 Subject: [PATCH 8/8] Fxi support for valid date field default values --- tests/WP_SQLite_Translator_Tests.php | 20 ++++++++++++++----- .../sqlite/class-wp-sqlite-translator.php | 17 +++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index dc2e96fe..b93afe30 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -292,14 +292,19 @@ public function testShowCreateTable1() { public function testShowCreateTableWithEmptyDatetimeDefault() { $this->assertQuery( - 'CREATE TABLE _tmp_table ( + "CREATE TABLE _tmp_table ( ID BIGINT PRIMARY KEY AUTO_INCREMENT, timestamp1 datetime NOT NULL, timestamp2 date NOT NULL, timestamp3 time NOT NULL, timestamp4 timestamp NOT NULL, - timestamp5 year NOT NULL - );' + timestamp5 year NOT NULL, + notempty1 datetime DEFAULT '1999-12-12 12:12:12', + notempty2 date DEFAULT '1999-12-12', + notempty3 time DEFAULT '12:12:12', + notempty4 year DEFAULT '2024', + notempty5 timestamp DEFAULT '1734539165', + );" ); $this->assertQuery( @@ -308,15 +313,20 @@ public function testShowCreateTableWithEmptyDatetimeDefault() { $results = $this->engine->get_query_results(); $this->assertEquals( - 'CREATE TABLE `_tmp_table` ( + "CREATE TABLE `_tmp_table` ( `ID` bigint AUTO_INCREMENT, `timestamp1` datetime NOT NULL, `timestamp2` date NOT NULL, `timestamp3` time NOT NULL, `timestamp4` timestamp NOT NULL, `timestamp5` year NOT NULL, + `notempty1` datetime DEFAULT '1999-12-12 12:12:12', + `notempty2` date DEFAULT '1999-12-12', + `notempty3` time DEFAULT '12:12:12', + `notempty4` year DEFAULT '2024', + `notempty5` timestamp DEFAULT '1734539165', PRIMARY KEY (`ID`) -);', +);", $results[0]->{'Create Table'} ); } diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 5f92810d..0f61a7c7 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -3868,11 +3868,22 @@ function ( $row ) use ( $name_map ) { * @return boolean If column should have a default definition. */ private function column_has_default( $column, $mysql_type ) { - if ( null !== $column->dflt_value && '' !== $column->dflt_value && ! in_array( strtolower( $mysql_type ), array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true ) ) { - return true; + if ( null === $column->dflt_value ) { + return false; } - return false; + if ( '' === $column->dflt_value ) { + return false; + } + + if ( + in_array( strtolower( $mysql_type ), array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true ) && + "''" === $column->dflt_value + ) { + return false; + } + + return true; } /**