Skip to content

Commit ceee84d

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 ceee84d

2 files changed

Lines changed: 235 additions & 73 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: 143 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -639,17 +639,11 @@ public function testInformationSchemaTablesAutoIncrement(): void {
639639
$result = $this->assertQuery( "SELECT * FROM information_schema.tables WHERE table_name = 't'" );
640640
$this->assertSame( '1', $result[0]->AUTO_INCREMENT );
641641

642-
// After inserts, the stored value advances, and a backtick-quoted
643-
// projection returns the same result as SELECT *.
642+
// After inserts, the sequence advances.
644643
$this->assertQuery( "INSERT INTO t (name) VALUES ('a'), ('b'), ('c')" );
645644
$result = $this->assertQuery( "SELECT * FROM information_schema.tables WHERE table_name = 't'" );
646645
$this->assertSame( '4', $result[0]->AUTO_INCREMENT );
647646

648-
$result = $this->assertQuery(
649-
"SELECT table_name, `auto_increment` FROM information_schema.tables WHERE table_name = 't'"
650-
);
651-
$this->assertSame( '4', $result[0]->AUTO_INCREMENT );
652-
653647
// DELETE preserves the counter.
654648
$this->assertQuery( 'DELETE FROM t' );
655649
$result = $this->assertQuery( "SELECT * FROM information_schema.tables WHERE table_name = 't'" );
@@ -661,41 +655,60 @@ public function testInformationSchemaTablesAutoIncrement(): void {
661655
$this->assertSame( '1', $result[0]->AUTO_INCREMENT );
662656
}
663657

664-
public function testShowTableStatusAutoIncrement() {
658+
public function testInformationSchemaTablesFilterByAutoIncrement(): void {
659+
$this->assertQuery( 'CREATE TABLE low (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
660+
$this->assertQuery( 'CREATE TABLE high (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
661+
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT)' );
662+
$this->assertQuery( "INSERT INTO low (name) VALUES ('a')" );
663+
$this->assertQuery( "INSERT INTO high (name) VALUES ('a'), ('b'), ('c'), ('d'), ('e')" );
664+
665+
$result = $this->assertQuery(
666+
'SELECT TABLE_NAME FROM information_schema.tables WHERE `AUTO_INCREMENT` > 3'
667+
);
668+
$this->assertCount( 1, $result );
669+
$this->assertSame( 'high', $result[0]->TABLE_NAME );
670+
671+
$result = $this->assertQuery(
672+
'SELECT TABLE_NAME FROM information_schema.tables WHERE `AUTO_INCREMENT` IS NULL'
673+
);
674+
$this->assertCount( 1, $result );
675+
$this->assertSame( 'plain', $result[0]->TABLE_NAME );
676+
}
677+
678+
public function testShowTableStatusAutoIncrement(): void {
665679
// A non-AUTO_INCREMENT table reports NULL.
666680
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT)' );
667-
$this->assertSame( null, $this->getAutoIncrement( 'plain' ) );
681+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 'plain'" );
682+
$this->assertSame( null, $result[0]->Auto_increment );
668683

669684
// A fresh AUTO_INCREMENT table reports 1.
670685
$this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
671-
$this->assertSame( '1', $this->getAutoIncrement( 't' ) );
686+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
687+
$this->assertSame( '1', $result[0]->Auto_increment );
672688

673-
// After inserts, Auto_increment is the next value.
689+
// After inserts, the sequence advances.
674690
$this->assertQuery( "INSERT INTO t (name) VALUES ('a'), ('b'), ('c')" );
675-
$this->assertSame( '4', $this->getAutoIncrement( 't' ) );
676-
677-
// DELETE must preserve the counter, matching InnoDB behavior.
678-
$this->assertQuery( 'DELETE FROM t WHERE id = 3' );
679-
$this->assertSame( '4', $this->getAutoIncrement( 't' ) );
691+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
692+
$this->assertSame( '4', $result[0]->Auto_increment );
680693

681-
// DELETE of all rows still preserves the counter.
694+
// DELETE preserves the counter.
682695
$this->assertQuery( 'DELETE FROM t' );
683-
$this->assertSame( '4', $this->getAutoIncrement( 't' ) );
696+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
697+
$this->assertSame( '4', $result[0]->Auto_increment );
684698

685699
// TRUNCATE resets the counter.
686700
$this->assertQuery( 'TRUNCATE TABLE t' );
687-
$this->assertSame( '1', $this->getAutoIncrement( 't' ) );
701+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
702+
$this->assertSame( '1', $result[0]->Auto_increment );
688703
}
689704

690-
public function testShowTableStatusFilterByAutoIncrement() {
705+
public function testShowTableStatusFilterByAutoIncrement(): void {
691706
$this->assertQuery( 'CREATE TABLE low (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
692707
$this->assertQuery( 'CREATE TABLE high (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
693708
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT)' );
694709
$this->assertQuery( "INSERT INTO low (name) VALUES ('a')" );
695710
$this->assertQuery( "INSERT INTO high (name) VALUES ('a'), ('b'), ('c'), ('d'), ('e')" );
696711

697-
// WHERE must filter on the computed Auto_increment alias, not on the
698-
// always-NULL underlying info_schema column.
699712
$result = $this->assertQuery( 'SHOW TABLE STATUS WHERE `Auto_increment` > 3' );
700713
$this->assertCount( 1, $result );
701714
$this->assertSame( 'high', $result[0]->Name );
@@ -706,78 +719,137 @@ public function testShowTableStatusFilterByAutoIncrement() {
706719
}
707720

708721
public function testShowCreateTableAutoIncrement(): void {
709-
// No AUTO_INCREMENT=N on a non-AI table.
722+
// No AUTO_INCREMENT=N on a non-AUTO_INCREMENT table.
710723
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT)' );
711-
$this->assertStringNotContainsString(
712-
'AUTO_INCREMENT=',
713-
$this->getCreateTable( 'plain' )
714-
);
724+
$result = $this->assertQuery( 'SHOW CREATE TABLE plain' );
725+
$this->assertStringNotContainsString( 'AUTO_INCREMENT=', $result[0]->{'Create Table'} );
715726

716-
// No AUTO_INCREMENT=N on a fresh AI table (counter has not advanced).
727+
// No AUTO_INCREMENT=N on a fresh AUTO_INCREMENT table (sequence not advanced).
717728
$this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
718-
$this->assertStringNotContainsString(
719-
'AUTO_INCREMENT=',
720-
$this->getCreateTable( 't' )
721-
);
729+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
730+
$this->assertStringNotContainsString( 'AUTO_INCREMENT=', $result[0]->{'Create Table'} );
722731

723-
// After inserts, AUTO_INCREMENT=<next value> is emitted.
732+
// After inserts, the sequence advances.
724733
$this->assertQuery( "INSERT INTO t (name) VALUES ('a'), ('b'), ('c')" );
725-
$this->assertStringContainsString(
726-
'AUTO_INCREMENT=4',
727-
$this->getCreateTable( 't' )
728-
);
734+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
735+
$this->assertStringContainsString( 'AUTO_INCREMENT=4', $result[0]->{'Create Table'} );
729736

730737
// DELETE preserves the counter.
731738
$this->assertQuery( 'DELETE FROM t' );
732-
$this->assertStringContainsString(
733-
'AUTO_INCREMENT=4',
734-
$this->getCreateTable( 't' )
735-
);
739+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
740+
$this->assertStringContainsString( 'AUTO_INCREMENT=4', $result[0]->{'Create Table'} );
736741

737-
// TRUNCATE resets the counter; AUTO_INCREMENT=N is omitted again.
742+
// TRUNCATE resets the counter.
738743
$this->assertQuery( 'TRUNCATE TABLE t' );
739-
$this->assertStringNotContainsString(
740-
'AUTO_INCREMENT=',
741-
$this->getCreateTable( 't' )
744+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
745+
$this->assertStringNotContainsString( 'AUTO_INCREMENT=', $result[0]->{'Create Table'} );
746+
}
747+
748+
749+
public function testCreateTableSetAutoIncrement(): void {
750+
$this->assertQuery(
751+
'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT=100'
742752
);
753+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
754+
$this->assertSame( '100', $result[0]->Auto_increment );
755+
756+
// INSERT advances the sequence.
757+
$this->assertQuery( "INSERT INTO t (name) VALUES ('a')" );
758+
$this->assertSame( '100', $this->assertQuery( 'SELECT id FROM t' )[0]->id );
759+
760+
// SHOW TABLE STATUS
761+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
762+
$this->assertSame( '101', $result[0]->Auto_increment );
763+
764+
// SHOW CREATE TABLE
765+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
766+
$this->assertStringContainsString( 'AUTO_INCREMENT=101', $result[0]->{'Create Table'} );
767+
768+
// Without an AUTO_INCREMENT column, the option is ignored.
769+
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT) AUTO_INCREMENT=500' );
770+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 'plain'" );
771+
$this->assertSame( null, $result[0]->Auto_increment );
743772
}
744773

745-
public function testShowCreateTableAutoIncrementOnTemporaryTable(): void {
746-
// Temporary tables live in SQLite's `temp` schema with their own
747-
// `sqlite_sequence`. Make sure the counter is read from the right one
748-
// even when a persistent AI table in `main` might shadow it.
774+
public function testAlterTableSetAutoIncrement(): void {
775+
$this->assertQuery( 'CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
776+
777+
// Set the sequence value on an empty table.
778+
$this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 50' );
779+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
780+
$this->assertSame( '50', $result[0]->Auto_increment );
781+
782+
// INSERT advances the sequence.
783+
$this->assertQuery( "INSERT INTO t (name) VALUES ('a')" );
784+
$this->assertSame( '50', $this->assertQuery( 'SELECT id FROM t' )[0]->id );
785+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
786+
$this->assertSame( '51', $result[0]->Auto_increment );
787+
788+
// Update the sequence value.
789+
$this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 200' );
790+
791+
// SHOW TABLE STATUS
792+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
793+
$this->assertSame( '200', $result[0]->Auto_increment );
794+
795+
// SHOW CREATE TABLE
796+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
797+
$this->assertStringContainsString( 'AUTO_INCREMENT=200', $result[0]->{'Create Table'} );
798+
799+
// Lowering the counter at or below MAX(id) clamps to MAX(id) + 1.
800+
$this->assertQuery( 'ALTER TABLE t AUTO_INCREMENT = 1' );
801+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 't'" );
802+
$this->assertSame( '51', $result[0]->Auto_increment );
803+
804+
// Without an AUTO_INCREMENT column, the option is ignored.
805+
$this->assertQuery( 'CREATE TABLE plain (id INT, name TEXT) AUTO_INCREMENT=500' );
806+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 'plain'" );
807+
$this->assertSame( null, $result[0]->Auto_increment );
808+
}
809+
810+
public function testTemporaryTableAutoIncrement(): void {
811+
/*
812+
* Temporary tables live in SQLite's "temp" schema with their own
813+
* sqlite_sequence, independent of the persistent one in "main".
814+
* SHOW TABLE STATUS and information_schema.tables list only persistent
815+
* tables, so the temporary sequence is verified through SHOW CREATE
816+
* TABLE and the values produced by the next INSERT.
817+
*/
749818
$this->assertQuery( 'CREATE TABLE main_tbl (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
750819
$this->assertQuery( "INSERT INTO main_tbl (name) VALUES ('m')" );
751820

821+
// A fresh temp AUTO_INCREMENT table has no AUTO_INCREMENT=N in SHOW CREATE TABLE.
752822
$this->assertQuery( 'CREATE TEMPORARY TABLE tmp (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT)' );
753-
$this->assertStringNotContainsString(
754-
'AUTO_INCREMENT=',
755-
$this->getCreateTable( 'tmp' )
756-
);
823+
$result = $this->assertQuery( 'SHOW CREATE TABLE tmp' );
824+
$this->assertStringNotContainsString( 'AUTO_INCREMENT=', $result[0]->{'Create Table'} );
757825

826+
// After inserts, the temp sequence advances.
758827
$this->assertQuery( "INSERT INTO tmp (name) VALUES ('a'), ('b')" );
759-
$this->assertStringContainsString(
760-
'AUTO_INCREMENT=3',
761-
$this->getCreateTable( 'tmp' )
762-
);
828+
$result = $this->assertQuery( 'SHOW CREATE TABLE tmp' );
829+
$this->assertStringContainsString( 'AUTO_INCREMENT=3', $result[0]->{'Create Table'} );
763830

764-
// The persistent table's counter must remain independent.
765-
$this->assertStringContainsString(
766-
'AUTO_INCREMENT=2',
767-
$this->getCreateTable( 'main_tbl' )
768-
);
769-
}
831+
// ALTER TABLE AUTO_INCREMENT = N updates the temp sequence, and the
832+
// new value is used by the next insert.
833+
$this->assertQuery( 'ALTER TABLE tmp AUTO_INCREMENT = 77' );
834+
$result = $this->assertQuery( 'SHOW CREATE TABLE tmp' );
835+
$this->assertStringContainsString( 'AUTO_INCREMENT=77', $result[0]->{'Create Table'} );
770836

771-
private function getAutoIncrement( string $table_name ): ?string {
772-
$result = $this->assertQuery(
773-
sprintf( "SHOW TABLE STATUS LIKE '%s'", $table_name )
837+
$this->assertQuery( "INSERT INTO tmp (name) VALUES ('x')" );
838+
$this->assertSame( '77', $this->assertQuery( "SELECT id FROM tmp WHERE name = 'x'" )[0]->id );
839+
840+
// CREATE TEMPORARY TABLE ... AUTO_INCREMENT = N seeds the temp sequence.
841+
$this->assertQuery(
842+
'CREATE TEMPORARY TABLE tmp2 (id INT AUTO_INCREMENT PRIMARY KEY, name TEXT) AUTO_INCREMENT=500'
774843
);
775-
return $result[0]->Auto_increment;
776-
}
844+
$result = $this->assertQuery( 'SHOW CREATE TABLE tmp2' );
845+
$this->assertStringContainsString( 'AUTO_INCREMENT=500', $result[0]->{'Create Table'} );
846+
847+
$this->assertQuery( "INSERT INTO tmp2 (name) VALUES ('x')" );
848+
$this->assertSame( '500', $this->assertQuery( 'SELECT id FROM tmp2' )[0]->id );
777849

778-
private function getCreateTable( string $table_name ): string {
779-
$result = $this->assertQuery( sprintf( 'SHOW CREATE TABLE %s', $table_name ) );
780-
return $result[0]->{'Create Table'};
850+
// The persistent table's sequence must remain unaffected throughout.
851+
$result = $this->assertQuery( "SHOW TABLE STATUS LIKE 'main_tbl'" );
852+
$this->assertSame( '2', $result[0]->Auto_increment );
781853
}
782854

783855
public function testShowFullColumns(): void {

0 commit comments

Comments
 (0)