Skip to content

Commit 6dc64bf

Browse files
committed
Support setting AUTO_INCREMENT via CREATE TABLE / ALTER TABLE
Implement the AUTO_INCREMENT table option in CREATE TABLE and ALTER TABLE by writing the requested value as seq - 1 into SQLite's sqlite_sequence. As in MySQL, the effective counter is clamped to MAX(col) so it never drops below an existing row, and the option is silently ignored when the table has no AUTO_INCREMENT column. Also skip the full table rebuild in ALTER TABLE when the statement only carries table options (no alterListItem) so that pure AUTO_INCREMENT = N changes are instant rather than a no-op full-table copy.
1 parent 47325ab commit 6dc64bf

2 files changed

Lines changed: 183 additions & 2 deletions

File tree

packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2378,6 +2378,9 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
23782378
foreach ( $constraint_queries as $query ) {
23792379
$this->execute_sqlite_query( $query );
23802380
}
2381+
2382+
// Apply AUTO_INCREMENT = N table option, if any.
2383+
$this->apply_auto_increment_table_option( $table_is_temporary, $table_name, $node );
23812384
}
23822385

23832386
/**
@@ -2451,8 +2454,18 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
24512454
}
24522455
}
24532456

2454-
$this->information_schema_builder->record_alter_table( $node );
2455-
$this->recreate_table_from_information_schema( $table_is_temporary, $table_name, $column_map );
2457+
/*
2458+
* Skip the expensive table rebuild when the statement only carries
2459+
* table options (e.g. ALTER TABLE t AUTO_INCREMENT = N). These don't
2460+
* change the schema, so the recreate would be a pointless full copy.
2461+
*/
2462+
if ( count( $node->get_descendant_nodes( 'alterListItem' ) ) > 0 ) {
2463+
$this->information_schema_builder->record_alter_table( $node );
2464+
$this->recreate_table_from_information_schema( $table_is_temporary, $table_name, $column_map );
2465+
}
2466+
2467+
// Apply AUTO_INCREMENT = N table option, if any.
2468+
$this->apply_auto_increment_table_option( $table_is_temporary, $table_name, $node );
24562469

24572470
// @TODO: Consider using a "fast path" for ALTER TABLE statements that
24582471
// consist only of operations that SQLite's ALTER TABLE supports.
@@ -4916,6 +4929,83 @@ private function recreate_table_from_information_schema(
49164929
// @TODO: Triggers and views.
49174930
}
49184931

4932+
/**
4933+
* Apply the AUTO_INCREMENT table option from a CREATE TABLE or ALTER TABLE
4934+
* statement by adjusting the row in SQLite's "sqlite_sequence" table.
4935+
*
4936+
* @param bool $table_is_temporary Whether the table is temporary.
4937+
* @param string $table_name The table name.
4938+
* @param WP_Parser_Node $node The "createStatement" or "alterStatement" AST node.
4939+
*/
4940+
private function apply_auto_increment_table_option(
4941+
bool $table_is_temporary,
4942+
string $table_name,
4943+
WP_Parser_Node $node
4944+
): void {
4945+
// Find the last AUTO_INCREMENT = N option (MySQL uses the last one).
4946+
$value = null;
4947+
foreach ( $node->get_descendant_nodes( 'createTableOption' ) as $option ) {
4948+
if ( ! $option->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
4949+
continue;
4950+
}
4951+
$number_node = $option->get_first_child_node( 'ulonglong_number' );
4952+
if ( null === $number_node ) {
4953+
continue;
4954+
}
4955+
$value = (int) $number_node->get_first_descendant_token()->get_value();
4956+
}
4957+
if ( null === $value ) {
4958+
return;
4959+
}
4960+
4961+
// Find the AUTO_INCREMENT column.
4962+
$columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' );
4963+
$auto_column = $this->execute_sqlite_query(
4964+
sprintf(
4965+
"SELECT column_name FROM %s
4966+
WHERE table_schema = ?
4967+
AND table_name = ?
4968+
AND extra = 'auto_increment'",
4969+
$this->quote_sqlite_identifier( $columns_table )
4970+
),
4971+
array( $this->get_saved_db_name(), $table_name )
4972+
)->fetchColumn();
4973+
if ( false === $auto_column ) {
4974+
return;
4975+
}
4976+
4977+
/*
4978+
* Prepare an expression for the sequence value.
4979+
* 1. Use N - 1. MySQL stores the next value, SQLite the last one.
4980+
* 2. Clamp to MAX(col) like MySQL (we can't go below existing values).
4981+
*
4982+
* The value is inlined as an integer literal because PDO binds PHP ints
4983+
* as TEXT, and SQLite's type affinity ranks TEXT above INTEGER in MAX().
4984+
*/
4985+
$schema = $table_is_temporary ? 'temp' : 'main';
4986+
$seq_expr = sprintf(
4987+
'MAX(%d, COALESCE((SELECT MAX(%s) FROM %s), 0))',
4988+
$value - 1,
4989+
$this->quote_sqlite_identifier( $auto_column ),
4990+
$this->quote_sqlite_identifier( $table_name )
4991+
);
4992+
4993+
// Update the value in the "sqlite_sequence" table.
4994+
$updated = $this->execute_sqlite_query(
4995+
sprintf( 'UPDATE %s.sqlite_sequence SET seq = %s WHERE name = ?', $schema, $seq_expr ),
4996+
array( $table_name )
4997+
)->rowCount();
4998+
4999+
// If the sequence value does not exist yet, insert a new row.
5000+
// SQLite reports matched (not affected) rows, so the 0 check is safe.
5001+
if ( 0 === $updated ) {
5002+
$this->execute_sqlite_query(
5003+
sprintf( 'INSERT INTO %s.sqlite_sequence (name, seq) VALUES (?, %s)', $schema, $seq_expr ),
5004+
array( $table_name )
5005+
);
5006+
}
5007+
}
5008+
49195009
/**
49205010
* Translate a MySQL SHOW LIKE ... or SHOW WHERE ... condition to SQLite.
49215011
*

packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Metadata_Tests.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,97 @@ public function testShowCreateTableAutoIncrementOnTemporaryTable(): void {
768768
);
769769
}
770770

771+
public function testCreateTableAutoIncrementOption(): void {
772+
// AUTO_INCREMENT = N at CREATE TABLE time seeds the counter, and the
773+
// next insert uses that value.
774+
$this->assertQuery(
775+
'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT=100'
776+
);
777+
$this->assertSame( '100', $this->getAutoIncrement( 't' ) );
778+
779+
$this->assertQuery( "INSERT INTO t (name) VALUES ('a')" );
780+
$this->assertSame( '100', $this->assertQuery( 'SELECT id FROM t' )[0]->id );
781+
$this->assertSame( '101', $this->getAutoIncrement( 't' ) );
782+
783+
// SHOW CREATE TABLE reflects the seeded counter.
784+
$this->assertStringContainsString(
785+
'AUTO_INCREMENT=101',
786+
$this->getCreateTable( 't' )
787+
);
788+
789+
// AUTO_INCREMENT = N option is silently ignored without an AI column.
790+
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT) AUTO_INCREMENT=500' );
791+
$this->assertSame( null, $this->getAutoIncrement( 'plain' ) );
792+
}
793+
794+
public function testAlterTableAutoIncrementOption(): void {
795+
$this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
796+
797+
// Set the counter on an empty table.
798+
$this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 50' );
799+
$this->assertSame( '50', $this->getAutoIncrement( 't' ) );
800+
$this->assertQuery( "INSERT INTO t (name) VALUES ('a')" );
801+
$this->assertSame( '50', $this->assertQuery( 'SELECT id FROM t' )[0]->id );
802+
803+
// Raising the counter above MAX(id) takes effect immediately.
804+
$this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 200' );
805+
$this->assertSame( '200', $this->getAutoIncrement( 't' ) );
806+
807+
// Lowering the counter at or below MAX(id) clamps to MAX(id) + 1.
808+
$this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 1' );
809+
$this->assertSame( '51', $this->getAutoIncrement( 't' ) );
810+
}
811+
812+
public function testAlterTableAutoIncrementOptionMixedWithAlterItems(): void {
813+
$this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
814+
$this->assertQuery( "INSERT INTO t (name) VALUES ('a'), ('b')" );
815+
816+
// AUTO_INCREMENT option alongside an ADD COLUMN action: both apply.
817+
$this->assertQuery( 'ALTER TABLE t ADD COLUMN extra TEXT, AUTO_INCREMENT = 500' );
818+
$this->assertSame( '500', $this->getAutoIncrement( 't' ) );
819+
820+
$result = $this->assertQuery( "SHOW COLUMNS FROM t LIKE 'extra'" );
821+
$this->assertCount( 1, $result );
822+
823+
// Rows are preserved through the ALTER TABLE rewrite.
824+
$this->assertCount( 2, $this->assertQuery( 'SELECT * FROM t' ) );
825+
}
826+
827+
public function testAlterTableAutoIncrementOptionWithoutAutoIncrementColumn(): void {
828+
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT)' );
829+
830+
// Silently ignored without an AI column, matching MySQL.
831+
$this->assertQuery( 'ALTER TABLE plain AUTO_INCREMENT = 42' );
832+
$this->assertSame( null, $this->getAutoIncrement( 'plain' ) );
833+
}
834+
835+
public function testAlterTableAutoIncrementOptionAfterTruncate(): void {
836+
$this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
837+
$this->assertQuery( "INSERT INTO t (name) VALUES ('a'), ('b'), ('c')" );
838+
839+
// TRUNCATE resets the counter to 1, and AUTO_INCREMENT = N reseeds it.
840+
$this->assertQuery( 'TRUNCATE TABLE t' );
841+
$this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 1000' );
842+
$this->assertSame( '1000', $this->getAutoIncrement( 't' ) );
843+
844+
$this->assertQuery( "INSERT INTO t (name) VALUES ('x')" );
845+
$this->assertSame( '1000', $this->assertQuery( 'SELECT id FROM t' )[0]->id );
846+
}
847+
848+
public function testAlterTableAutoIncrementOptionOnTemporaryTable(): void {
849+
// The persistent and temporary tables use separate sqlite_sequence
850+
// stores and must not influence each other.
851+
$this->assertQuery( 'CREATE TABLE main_tbl (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
852+
$this->assertQuery( 'CREATE TEMPORARY TABLE tmp (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
853+
854+
$this->assertQuery( 'ALTER TABLE tmp AUTO_INCREMENT = 77' );
855+
$this->assertStringContainsString( 'AUTO_INCREMENT=77', $this->getCreateTable( 'tmp' ) );
856+
$this->assertStringNotContainsString( 'AUTO_INCREMENT=', $this->getCreateTable( 'main_tbl' ) );
857+
858+
$this->assertQuery( "INSERT INTO tmp (name) VALUES ('x')" );
859+
$this->assertSame( '77', $this->assertQuery( 'SELECT id FROM tmp' )[0]->id );
860+
}
861+
771862
private function getAutoIncrement( string $table_name ): ?string {
772863
$result = $this->assertQuery(
773864
sprintf( "SHOW TABLE STATUS LIKE '%s'", $table_name )

0 commit comments

Comments
 (0)