From 81a6da3ba5df638fb63264dbf4780e7091d8a487 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Tue, 26 Nov 2024 18:24:34 +0100 Subject: [PATCH] Handle specifics of the CREATE TABLE statement --- .../sqlite-ast/class-wp-sqlite-driver.php | 145 +++++++++++++++++- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index e6938949..6bf99395 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -780,9 +780,7 @@ private function execute_mysql_query( WP_Parser_Node $ast ) { $subtree = $ast->get_child_node(); switch ( $subtree->rule_name ) { case 'createTable': - $query = $this->translate( $ast ); - $this->execute_sqlite_query( $query ); - $this->set_result_from_affected_rows(); + $this->execute_create_table_statement( $ast ); break; default: throw new Exception( @@ -845,6 +843,94 @@ private function execute_update_statement( WP_Parser_Node $node ): void { $this->set_result_from_affected_rows(); } + private function execute_create_table_statement( WP_Parser_Node $node ): void { + $element_list = $node->get_descendant_node( 'tableElementList' ); + if ( null === $element_list ) { + $query = $this->translate( $node ); + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows(); + return; + } + + /* + * We need to handle some differences between MySQL and SQLite: + * + * 1. Inline index definitions: + * + * In MySQL, we can define an index inline with a column definition. + * In SQLite, we need to define indexes separately, using extra queries. + * + * 2. Column and constraint definition order: + * + * In MySQL, column and constraint definitions can be arbitrarily mixed. + * In SQLite, column definitions must come first, followed by constraints. + * + * 2. Auto-increment: + * + * In MySQL, there can at most one AUTO_INCREMENT column, and it must be + * a PRIMARY KEY, or the first column in a multi-column KEY. + * + * In SQLite, there can at most one AUTOINCREMENT column, and it must be + * a PRIMARY KEY, defined inline on a single column. + * + * Therefore, the following valid MySQL construct is not supported: + * CREATE TABLE t ( a INT AUTO_INCREMENT, b INT, PRIMARY KEY (a, b) ); + * @TODO: Support it with a single-column PK and a multi-column UNIQUE KEY. + */ + + // Collect column, index, and constraint nodes. + $columns = array(); + $constraints = array(); + $indexes = array(); + $has_autoincrement = false; + $primary_key_constraint = null; // Does not include inline PK definition. + + foreach ( $element_list->get_descendant_nodes( 'columnDefinition' ) as $child ) { + if ( null !== $child->get_descendant_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) { + $has_autoincrement = true; + } + // @TODO: Collect inline index definitions. + $columns[] = $child; + } + + foreach ( $element_list->get_descendant_nodes( 'tableConstraintDef' ) as $child ) { + if ( null !== $child->get_descendant_token( WP_MySQL_Lexer::PRIMARY_SYMBOL ) ) { + $primary_key_constraint = $child; + } else { + $constraints[] = $child; + } + } + + /* + * If we have a PRIMARY KEY constraint: + * 1. Without auto-increment, we can put it back to the list of constraints. + * 2. With auto-increment, we need to later move it to the column definition. + */ + if ( null !== $primary_key_constraint ) { + if ( ! $has_autoincrement ) { + $constraints[] = $primary_key_constraint; + } elseif ( count( $primary_key_constraint->get_descendant_nodes( 'keyPart' ) ) > 1 ) { + throw $this->not_supported_exception( + 'Composite primary key with AUTO_INCREMENT' + ); + } + } + + $query_parts = array( 'CREATE' ); + foreach ( $node->get_child_node()->get_children() as $child ) { + if ( $child instanceof WP_Parser_Node && 'tableElementList' === $child->rule_name ) { + $query_parts[] = $this->translate_sequence( array_merge( $columns, $constraints ), ' , ' ); + } else { + $query_parts[] = $this->translate( $child ); + } + } + + // @TODO: Execute queries for inline index definitions. + + $this->execute_sqlite_query( implode( ' ', $query_parts ) ); + $this->set_result_from_affected_rows(); + } + private function translate( $ast ) { if ( null === $ast ) { return null; @@ -915,6 +1001,54 @@ private function translate( $ast ) { // When we have no value, it's reasonable to use NULL. return 'NULL'; + case 'fieldDefinition': + /* + * In SQLite, there is the a quirk for backward compatibility: + * 1. INTEGER PRIMARY KEY creates an alias of ROWID. + * 2. INT PRIMARY KEY will not alias of ROWID. + * + * Therefore, we want to: + * 1. Use INTEGER PRIMARY KEY for when we have AUTOINCREMENT. + * 2. Use INT PRIMARY KEY otherwise. + */ + $has_primary_key = $ast->get_descendant_token( WP_MySQL_Lexer::KEY_SYMBOL ) !== null; + $has_autoincrement = $ast->get_descendant_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) !== null; + if ( $has_primary_key ) { + $children = $ast->get_children(); + $data_type_node = array_shift( $children ); + $data_type = $this->translate( $data_type_node ); + if ( 'INTEGER' === $data_type ) { + $data_type = $has_autoincrement ? 'INTEGER' : 'INT'; + } + $definition = $data_type . ' ' . $this->translate_sequence( $children ); + } else { + $definition = $this->translate_sequence( $ast->get_children() ); + } + + /* + * In SQLite, AUTOINCREMENT must always be preceded by PRIMARY KEY. + * Therefore, we remove both PRIMARY KEY and AUTOINCREMENT from + * column attributes, and append them here in SQLite-friendly way. + */ + if ( $has_autoincrement ) { + return $definition . ' PRIMARY KEY AUTOINCREMENT'; + } elseif ( $has_primary_key ) { + return $definition . ' PRIMARY KEY'; + } + return $definition; + case 'columnAttribute': + case 'gcolAttribute': + /* + * Remove PRIMARY KEY and AUTOINCREMENT from the column attributes. + * They are handled in the "fieldDefinition" node. + */ + if ( $ast->has_child_token( WP_MySQL_Lexer::KEY_SYMBOL ) ) { + return null; + } + if ( $ast->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) { + return null; + } + return $this->translate_sequence( $ast->get_children() ); default: return $this->translate_sequence( $ast->get_children() ); } @@ -936,10 +1070,11 @@ private function translate_token( WP_MySQL_Token $token ) { private function translate_sequence( array $nodes, string $separator = ' ' ): string { $parts = array(); foreach ( $nodes as $node ) { - if ( null === $node ) { + $translated = null === $node ? null : $this->translate( $node ); + if ( null === $translated ) { continue; } - $parts[] = $this->translate( $node ); + $parts[] = $translated; } return implode( $separator, $parts ); }