Skip to content

Commit

Permalink
Handle specifics of the CREATE TABLE statement
Browse files Browse the repository at this point in the history
  • Loading branch information
JanJakes committed Nov 26, 2024
1 parent 70744b7 commit 81a6da3
Showing 1 changed file with 140 additions and 5 deletions.
145 changes: 140 additions & 5 deletions wp-includes/sqlite-ast/class-wp-sqlite-driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() );
}
Expand All @@ -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 );
}
Expand Down

0 comments on commit 81a6da3

Please sign in to comment.