diff --git a/tests/WP_SQLite_Driver_Tests.php b/tests/WP_SQLite_Driver_Tests.php index 51411385..8307b2cc 100644 --- a/tests/WP_SQLite_Driver_Tests.php +++ b/tests/WP_SQLite_Driver_Tests.php @@ -11238,4 +11238,28 @@ public function testVersionFunction(): void { $result = $this->engine->query( 'SELECT VERSION()' ); $this->assertSame( '8.0.38', $result[0]->{'VERSION()'} ); } + + /** + * Test CREATE TABLE with DEFAULT (now()) - GitHub issue #300 + * Tests that DEFAULT with function calls in parentheses works correctly in AST driver. + * + * @see https://github.com/WordPress/sqlite-database-integration/issues/300 + */ + public function testCreateTableWithDefaultNowFunction(): void { + // Test the exact SQL from the issue + $this->assertQuery( + 'CREATE TABLE `test_now_default` ( + `id` int NOT NULL, + `updated` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;' + ); + + // Insert a row to verify the default value works + $this->assertQuery( 'INSERT INTO test_now_default (id) VALUES (1)' ); + $result = $this->assertQuery( 'SELECT * FROM test_now_default WHERE id = 1' ); + $this->assertCount( 1, $result ); + + // Verify the updated timestamp was set (should match YYYY-MM-DD HH:MM:SS format) + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated ); + } } diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index a892fe72..e73368d0 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -3510,4 +3510,41 @@ public static function mysqlVariablesToTest() { array( '@@sEssIOn.sqL_moDe' ), ); } + + /** + * Test CREATE TABLE with DEFAULT (now()) - GitHub issue #300 + * Tests that DEFAULT with function calls in parentheses works correctly. + */ + public function testCreateTableWithDefaultNowFunction() { + // Test the exact SQL from the issue + $this->assertQuery( + 'CREATE TABLE `test_now_default` ( + `id` int NOT NULL, + `updated` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;' + ); + + // Verify the table was created successfully + $results = $this->assertQuery( 'DESCRIBE test_now_default;' ); + $this->assertCount( 2, $results ); + + // Verify the updated column has the correct properties + $updated_field = $results[1]; + $this->assertEquals( 'updated', $updated_field->Field ); + $this->assertEquals( 'timestamp', $updated_field->Type ); + $this->assertEquals( 'NO', $updated_field->Null ); + + // Insert a row to verify the default value works + $this->assertQuery( 'INSERT INTO test_now_default (id) VALUES (1)' ); + $result = $this->assertQuery( 'SELECT * FROM test_now_default WHERE id = 1' ); + $this->assertCount( 1, $result ); + + // Verify the updated timestamp was set (should match YYYY-MM-DD HH:MM:SS format) + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated ); + + // Test ON UPDATE trigger works + $this->assertQuery( 'UPDATE test_now_default SET id = 2 WHERE id = 1' ); + $result = $this->assertQuery( 'SELECT * FROM test_now_default WHERE id = 2' ); + $this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated ); + } } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php index 252573fd..34396816 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php @@ -2088,6 +2088,25 @@ private function get_column_default( WP_Parser_Node $node ): ?string { return $this->get_value( $signed_literal ); } + // DEFAULT (expression) - MySQL 8.0.13+ supports exprWithParentheses + $expr_with_parens = $default_attr->get_first_child_node( 'exprWithParentheses' ); + if ( $expr_with_parens ) { + // For now, only support simple function calls like (now()), (CURRENT_TIMESTAMP) + // Check if it's (now()) or (NOW()) + $now_tokens = $expr_with_parens->get_descendant_tokens( WP_MySQL_Lexer::NOW_SYMBOL ); + if ( ! empty( $now_tokens ) ) { + return 'CURRENT_TIMESTAMP'; + } + + // Check if it's (CURRENT_TIMESTAMP) or (CURRENT_TIMESTAMP()) + $current_ts_tokens = $expr_with_parens->get_descendant_tokens( WP_MySQL_Lexer::CURRENT_TIMESTAMP_SYMBOL ); + if ( ! empty( $current_ts_tokens ) ) { + return 'CURRENT_TIMESTAMP'; + } + + // For any other complex expressions, throw an exception + throw new Exception( 'DEFAULT values with complex expressions are not yet supported. Only (now()) and (CURRENT_TIMESTAMP) are currently supported.' ); + } throw new Exception( 'DEFAULT values with expressions are not yet supported.' ); } diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index c0ac1b85..c7582210 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1149,7 +1149,28 @@ private function parse_mysql_create_table_field() { WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, array( 'DEFAULT' ) ) ) { - $result->default = $this->rewriter->consume()->token; + // Consume the next token (could be a value, opening paren, etc.) + $default_token = $this->rewriter->consume(); + $result->default = $default_token->token; + + // Check if the default value is wrapped in parentheses (for function calls like (now())) + if ( $default_token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( '(' ) ) ) { + // Track parenthesis depth to consume the complete expression + $paren_depth = 1; + $default_value = '('; + + while ( $paren_depth > 0 && ( $next_token = $this->rewriter->consume() ) ) { + $default_value .= $next_token->token; + + if ( $next_token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( '(' ) ) ) { + ++$paren_depth; + } elseif ( $next_token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')' ) ) ) { + --$paren_depth; + } + } + + $result->default = $default_value; + } continue; }