Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
aristath committed Sep 4, 2024
2 parents 3c5455a + 91008f0 commit a2661f9
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 16 deletions.
2 changes: 1 addition & 1 deletion load.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: SQLite Database Integration
* Description: SQLite database driver drop-in.
* Author: The WordPress Team
* Version: 2.1.14
* Version: 2.1.15
* Requires PHP: 7.0
* Textdomain: sqlite-database-integration
*
Expand Down
2 changes: 1 addition & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.14
Stable tag: 2.1.15
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: performance, database
Expand Down
227 changes: 225 additions & 2 deletions tests/WP_SQLite_Translator_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -1102,14 +1102,14 @@ public function testColumnWithOnUpdate() {
'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",
'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 rowid = NEW.rowid;\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",
'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 rowid = NEW.rowid;\n\t\t\tEND",
),
),
$results
Expand Down Expand Up @@ -1176,6 +1176,150 @@ public function testColumnWithOnUpdate() {
$this->assertNull( $result[0]->updated_at );
}

public function testColumnWithOnUpdateAndNoIdField() {
// CREATE TABLE with ON UPDATE
$this->assertQuery(
'CREATE TABLE _tmp_table (
name varchar(20) NOT NULL,
created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP
);'
);

// on INSERT, no timestamps are expected
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('aaa')" );
$result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'aaa'" );
$this->assertNull( $result[0]->created_at );

// on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS
$this->assertQuery( "UPDATE _tmp_table SET name = 'bbb' WHERE name = 'aaa'" );
$result = $this->assertQuery( "SELECT * FROM _tmp_table WHERE name = 'bbb'" );
$this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at );
}

public function testColumnWithOnUpdateAndAutoincrementPrimaryKey() {
// CREATE TABLE with ON UPDATE, AUTO_INCREMENT, and PRIMARY KEY
$this->assertQuery(
'CREATE TABLE _tmp_table (
id int(11) NOT NULL AUTO_INCREMENT,
created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);'
);

// 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 );

// 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 );
}

public function testChangeColumnWithOnUpdate() {
// CREATE TABLE with ON UPDATE
$this->assertQuery(
'CREATE TABLE _tmp_table (
id int(11) NOT NULL,
created_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' => '',
),
),
$results
);

// no ON UPDATE is set
$this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' );
$this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' );
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' );
$this->assertNull( $result[0]->created_at );

// CHANGE COLUMN to add ON UPDATE
$this->assertQuery(
'ALTER TABLE _tmp_table CHANGE COLUMN created_at 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
);

// now, ON UPDATE SHOULD BE SET
$this->assertQuery( 'UPDATE _tmp_table SET id = 1 WHERE id = 1' );
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' );
$this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at );

// change column to remove ON UPDATE
$this->assertQuery(
'ALTER TABLE _tmp_table CHANGE COLUMN created_at created_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' => '',
),
),
$results
);

// now, no timestamp is expected
$this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (2)' );
$this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 2' );
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' );
$this->assertNull( $result[0]->created_at );
}

public function testAlterTableWithColumnFirstAndAfter() {
$this->assertQuery(
"CREATE TABLE _tmp_table (
Expand Down Expand Up @@ -3133,6 +3277,85 @@ public function testCurrentTimestamp() {
$this->assertQuery( 'DELETE FROM _dates WHERE option_value = CURRENT_TIMESTAMP()' );
}

public function testGroupByHaving() {
$this->assertQuery(
'CREATE TABLE _tmp_table (
name varchar(20)
);'
);

$this->assertQuery(
"INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')"
);

$result = $this->assertQuery(
'SELECT name, COUNT(*) as count FROM _tmp_table GROUP BY name HAVING COUNT(*) > 1'
);
$this->assertEquals(
array(
(object) array(
'name' => 'b',
'count' => '2',
),
(object) array(
'name' => 'c',
'count' => '3',
),
),
$result
);
}

public function testHavingWithoutGroupBy() {
$this->assertQuery(
'CREATE TABLE _tmp_table (
name varchar(20)
);'
);

$this->assertQuery(
"INSERT INTO _tmp_table VALUES ('a'), ('b'), ('b'), ('c'), ('c'), ('c')"
);

// HAVING condition satisfied
$result = $this->assertQuery(
"SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 1"
);
$this->assertEquals(
array(
(object) array(
':param0' => 'T',
),
),
$result
);

// HAVING condition not satisfied
$result = $this->assertQuery(
"SELECT 'T' FROM _tmp_table HAVING COUNT(*) > 100"
);
$this->assertEquals(
array(),
$result
);

// DISTINCT ... HAVING, where only some results meet the HAVING condition
$result = $this->assertQuery(
'SELECT DISTINCT name FROM _tmp_table HAVING COUNT(*) > 1'
);
$this->assertEquals(
array(
(object) array(
'name' => 'b',
),
(object) array(
'name' => 'c',
),
),
$result
);
}

/**
* @dataProvider mysqlVariablesToTest
*/
Expand Down
37 changes: 25 additions & 12 deletions wp-includes/sqlite/class-wp-sqlite-translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2659,23 +2659,19 @@ private function capture_group_by( $token ) {
! $token->matches(
WP_SQLite_Token::TYPE_KEYWORD,
WP_SQLite_Token::FLAG_KEYWORD_RESERVED,
array( 'GROUP' )
array( 'GROUP BY' )
)
) {
return false;
}
$next = $this->rewriter->peek_nth( 2 )->value;
if ( 'BY' !== strtoupper( $next ?? '' ) ) {
return false;
}

$this->has_group_by = true;

return false;
}

/**
* Translate WHERE something HAVING something to WHERE something AND something.
* Translate HAVING without GROUP BY to GROUP BY 1 HAVING.
*
* @param WP_SQLite_Token $token The token to translate.
*
Expand All @@ -2694,8 +2690,15 @@ private function translate_ungrouped_having( $token ) {
if ( $this->has_group_by ) {
return false;
}
$this->rewriter->skip();
$this->rewriter->add( new WP_SQLite_Token( 'AND', WP_SQLite_Token::TYPE_KEYWORD ) );

// GROUP BY is missing, add "GROUP BY 1" before the HAVING clause.
$having = $this->rewriter->skip();
$this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) );
$this->rewriter->add( new WP_SQLite_Token( 'GROUP BY', WP_SQLite_Token::TYPE_KEYWORD ) );
$this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) );
$this->rewriter->add( new WP_SQLite_Token( '1', WP_SQLite_Token::TYPE_NUMBER ) );
$this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) );
$this->rewriter->add( $having );

return true;
}
Expand Down Expand Up @@ -3115,6 +3118,10 @@ private function execute_alter() {
$new_field->mysql_data_type
);

// Drop ON UPDATE trigger by the old column name.
$on_update_trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $this->table_name, $from_name );
$this->execute_sqlite_query( "DROP TRIGGER IF EXISTS \"$on_update_trigger_name\"" );

/*
* In SQLite, there is no direct equivalent to the CHANGE COLUMN
* statement from MySQL. We need to do a bit of work to emulate it.
Expand Down Expand Up @@ -3235,6 +3242,11 @@ private function execute_alter() {
);
}

// Add the ON UPDATE trigger if needed.
if ( $new_field->on_update ) {
$this->add_column_on_update_current_timestamp( $this->table_name, $new_field->name );
}

if ( ',' === $alter_terminator->token ) {
/*
* If the terminator was a comma,
Expand Down Expand Up @@ -3327,9 +3339,6 @@ 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()
);
Expand Down Expand Up @@ -4394,12 +4403,16 @@ private function generate_index_name( $table, $original_index_name ) {
*/
private function add_column_on_update_current_timestamp( $table, $column ) {
$trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $table, $column );

// The trigger wouldn't work for virtual and "WITHOUT ROWID" tables,
// but currently that can't happen as we're not creating such tables.
// See: https://www.sqlite.org/rowidtable.html
$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;
UPDATE \"$table\" SET \"$column\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;
END"
);
}
Expand Down

0 comments on commit a2661f9

Please sign in to comment.