Skip to content

Commit c214cda

Browse files
authored
Add support for AUTO_INCREMENT value management (#367)
## Summary Adds support for **reporting** and **adjusting** `AUTO_INCREMENT` counters, which the driver previously ignored. ### Reporting `AUTO_INCREMENT` values (commit 1) `SHOW TABLE STATUS`, `SHOW CREATE TABLE`, and `INFORMATION_SCHEMA.TABLES` now compute the `Auto_increment` value live from `sqlite_sequence` rather than returning `NULL`. The counter is preserved past `DELETE` and reset by `TRUNCATE`, matching InnoDB semantics. ### Adjusting `AUTO_INCREMENT` values (commit 2) Three MySQL forms are now honored: 1. `CREATE TABLE t (...) AUTO_INCREMENT = N` — seeds the counter. 2. `ALTER TABLE t AUTO_INCREMENT = N` — instant; no table rebuild. 3. `ALTER TABLE t <actions>, AUTO_INCREMENT = N` — applied alongside other alter items. The requested value is written as `N - 1` into `sqlite_sequence` (MySQL stores the next value, SQLite the last used), clamped to `MAX(col)` so the effective counter never drops below an existing row — matching MySQL. When the table has no `AUTO_INCREMENT` column, the option is silently ignored, matching MySQL. **Not covered:** `SET @@auto_increment_increment` / `SET @@auto_increment_offset`. These change the *step* and *offset* of generated values, not the counter, and SQLite's `AUTOINCREMENT` always steps by 1 — so emulating non-1 increments would require generating IDs in PHP for every insert. Out of scope here. ## Notes - `sqlite_sequence` has no `UNIQUE` constraint on `name`, so native upsert is unavailable; the row is maintained with `UPDATE`-then-`INSERT` (second query only runs when the first matches nothing). - The clamp expression is inlined as an integer literal rather than a bound parameter. PDO binds PHP ints as `TEXT`, and SQLite's type affinity ranks `TEXT` above `INTEGER` in `MAX()`, which would silently produce wrong results. - `ALTER TABLE` with *only* table options (no `alterListItem`) now skips the unnecessary full-table copy. Fixes #114 and #140.
1 parent bd461a2 commit c214cda

3 files changed

Lines changed: 423 additions & 39 deletions

File tree

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

Lines changed: 200 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2410,6 +2410,9 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void {
24102410
foreach ( $constraint_queries as $query ) {
24112411
$this->execute_sqlite_query( $query );
24122412
}
2413+
2414+
// Apply AUTO_INCREMENT = N table option, if any.
2415+
$this->apply_auto_increment_table_option( $table_is_temporary, $table_name, $node );
24132416
}
24142417

24152418
/**
@@ -2483,8 +2486,18 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void {
24832486
}
24842487
}
24852488

2486-
$this->information_schema_builder->record_alter_table( $node );
2487-
$this->recreate_table_from_information_schema( $table_is_temporary, $table_name, $column_map );
2489+
/*
2490+
* Skip the expensive table rebuild when the statement only carries
2491+
* table options (e.g. ALTER TABLE t AUTO_INCREMENT = N). These don't
2492+
* change the schema, so the recreate would be a pointless full copy.
2493+
*/
2494+
if ( count( $node->get_descendant_nodes( 'alterListItem' ) ) > 0 ) {
2495+
$this->information_schema_builder->record_alter_table( $node );
2496+
$this->recreate_table_from_information_schema( $table_is_temporary, $table_name, $column_map );
2497+
}
2498+
2499+
// Apply AUTO_INCREMENT = N table option, if any.
2500+
$this->apply_auto_increment_table_option( $table_is_temporary, $table_name, $node );
24882501

24892502
// @TODO: Consider using a "fast path" for ALTER TABLE statements that
24902503
// consist only of operations that SQLite's ALTER TABLE supports.
@@ -2990,41 +3003,64 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo
29903003
// LIKE and WHERE clauses.
29913004
$like_or_where = $node->get_first_child_node( 'likeOrWhere' );
29923005
if ( null !== $like_or_where ) {
2993-
$condition = $this->translate_show_like_or_where_condition( $like_or_where, 'table_name' );
3006+
$condition = $this->translate_show_like_or_where_condition( $like_or_where, 'Name' );
29943007
}
29953008

2996-
// Fetch table information.
2997-
$tables_tables = $this->information_schema_builder->get_table_name(
2998-
false, // SHOW TABLE STATUS lists only non-temporary tables.
2999-
'tables'
3009+
// SHOW TABLE STATUS lists only non-temporary tables.
3010+
$tables_table = $this->information_schema_builder->get_table_name( false, 'tables' );
3011+
$columns_table = $this->information_schema_builder->get_table_name( false, 'columns' );
3012+
3013+
// Compose a subquery to compute auto-increment values.
3014+
$has_sequence_table = (bool) $this->execute_sqlite_query(
3015+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'"
3016+
)->fetchColumn();
3017+
3018+
$auto_increment_subquery = sprintf(
3019+
"(
3020+
SELECT COALESCE(s.seq + 1, 1)
3021+
FROM %s AS c
3022+
%s
3023+
WHERE c.extra = 'auto_increment'
3024+
AND c.table_schema = t.table_schema
3025+
AND c.table_name = t.table_name
3026+
)",
3027+
$this->quote_sqlite_identifier( $columns_table ),
3028+
$has_sequence_table
3029+
? 'LEFT JOIN main.sqlite_sequence AS s ON s.name = c.table_name'
3030+
: 'LEFT JOIN (SELECT 0 AS seq) AS s'
30003031
);
3001-
$query = sprintf(
3002-
'SELECT
3003-
table_name AS `Name`,
3004-
engine AS `Engine`,
3005-
version AS `Version`,
3006-
row_format AS `Row_format`,
3007-
table_rows AS `Rows`,
3008-
avg_row_length AS `Avg_row_length`,
3009-
data_length AS `Data_length`,
3010-
max_data_length AS `Max_data_length`,
3011-
index_length AS `Index_length`,
3012-
data_free AS `Data_free`,
3013-
auto_increment AS `Auto_increment`,
3014-
create_time AS `Create_time`,
3015-
update_time AS `Update_time`,
3016-
check_time AS `Check_time`,
3017-
table_collation AS `Collation`,
3018-
checksum AS `Checksum`,
3019-
create_options AS `Create_options`,
3020-
table_comment AS `Comment`
3021-
FROM %s
3022-
WHERE table_schema = ? %s
3023-
ORDER BY table_name',
3024-
$this->quote_sqlite_identifier( $tables_tables ),
3032+
3033+
$query = sprintf(
3034+
'SELECT * FROM (
3035+
SELECT
3036+
table_name AS `Name`,
3037+
engine AS `Engine`,
3038+
version AS `Version`,
3039+
row_format AS `Row_format`,
3040+
table_rows AS `Rows`,
3041+
avg_row_length AS `Avg_row_length`,
3042+
data_length AS `Data_length`,
3043+
max_data_length AS `Max_data_length`,
3044+
index_length AS `Index_length`,
3045+
data_free AS `Data_free`,
3046+
%s AS `Auto_increment`,
3047+
create_time AS `Create_time`,
3048+
update_time AS `Update_time`,
3049+
check_time AS `Check_time`,
3050+
table_collation AS `Collation`,
3051+
checksum AS `Checksum`,
3052+
create_options AS `Create_options`,
3053+
table_comment AS `Comment`
3054+
FROM %s AS t
3055+
WHERE table_schema = ?
3056+
)
3057+
WHERE 1 %s
3058+
ORDER BY `Name`',
3059+
$auto_increment_subquery,
3060+
$this->quote_sqlite_identifier( $tables_table ),
30253061
$condition ?? ''
30263062
);
3027-
$params = array(
3063+
$params = array(
30283064
$this->get_saved_db_name( $database ),
30293065
);
30303066

@@ -4729,7 +4765,7 @@ public function translate_table_ref( WP_Parser_Node $node ): string {
47294765
$table_name = $this->unquote_sqlite_identifier( $this->translate( $table ) );
47304766

47314767
// When the table reference targets an information schema table,
4732-
// we need to inject the configured database name dynamically.
4768+
// we need to inject some additional values dynamically.
47334769
if (
47344770
( null === $schema_name && 'information_schema' === $this->db_name )
47354771
|| ( null !== $schema_name && 'information_schema' === strtolower( $schema_name ) )
@@ -4775,14 +4811,40 @@ public function translate_table_ref( WP_Parser_Node $node ): string {
47754811
$expanded_list = array();
47764812
foreach ( $columns as $column ) {
47774813
$quoted_column = $this->quote_sqlite_identifier( $column );
4778-
if ( isset( $information_schema_db_column_map[ strtoupper( $column ) ] ) ) {
4814+
if ( isset( $information_schema_db_column_map[ $column ] ) ) {
4815+
// Replace the database name with the configured database name.
47794816
$expanded_list[] = sprintf(
47804817
"CASE WHEN %s = 'information_schema' THEN %s ELSE %s END AS %s",
47814818
$quoted_column,
47824819
$quoted_column,
47834820
$this->quote_sqlite_value( $this->main_db_name ),
4784-
strtoupper( $quoted_column )
4821+
$quoted_column
47854822
);
4823+
} elseif ( 'tables' === $table_name && 'AUTO_INCREMENT' === $column ) {
4824+
// Inject the auto-increment values.
4825+
$columns_table = $this->information_schema_builder->get_table_name( false, 'columns' );
4826+
$has_sequence_table = (bool) $this->execute_sqlite_query(
4827+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'"
4828+
)->fetchColumn();
4829+
4830+
$auto_increment_subquery = sprintf(
4831+
"(
4832+
SELECT COALESCE(s.seq + 1, 1)
4833+
FROM %s AS c
4834+
%s
4835+
WHERE c.extra = 'auto_increment'
4836+
AND c.table_schema = %s.table_schema
4837+
AND c.table_name = %s.table_name
4838+
)",
4839+
$this->quote_sqlite_identifier( $columns_table ),
4840+
$has_sequence_table
4841+
? 'LEFT JOIN main.sqlite_sequence AS s ON s.name = c.table_name'
4842+
: 'LEFT JOIN (SELECT 0 AS seq) AS s',
4843+
$this->quote_sqlite_identifier( $table_name ),
4844+
$this->quote_sqlite_identifier( $table_name )
4845+
);
4846+
4847+
$expanded_list[] = sprintf( '%s AS %s', $auto_increment_subquery, $quoted_column );
47864848
} else {
47874849
$expanded_list[] = $quoted_column;
47884850
}
@@ -4899,6 +4961,83 @@ private function recreate_table_from_information_schema(
48994961
// @TODO: Triggers and views.
49004962
}
49014963

4964+
/**
4965+
* Apply the AUTO_INCREMENT table option from a CREATE TABLE or ALTER TABLE
4966+
* statement by adjusting the row in SQLite's "sqlite_sequence" table.
4967+
*
4968+
* @param bool $table_is_temporary Whether the table is temporary.
4969+
* @param string $table_name The table name.
4970+
* @param WP_Parser_Node $node The "createStatement" or "alterStatement" AST node.
4971+
*/
4972+
private function apply_auto_increment_table_option(
4973+
bool $table_is_temporary,
4974+
string $table_name,
4975+
WP_Parser_Node $node
4976+
): void {
4977+
// Find the last AUTO_INCREMENT = N option (MySQL uses the last one).
4978+
$value = null;
4979+
foreach ( $node->get_descendant_nodes( 'createTableOption' ) as $option ) {
4980+
if ( ! $option->has_child_token( WP_MySQL_Lexer::AUTO_INCREMENT_SYMBOL ) ) {
4981+
continue;
4982+
}
4983+
$number_node = $option->get_first_child_node( 'ulonglong_number' );
4984+
if ( null === $number_node ) {
4985+
continue;
4986+
}
4987+
$value = (int) $number_node->get_first_descendant_token()->get_value();
4988+
}
4989+
if ( null === $value ) {
4990+
return;
4991+
}
4992+
4993+
// Find the AUTO_INCREMENT column.
4994+
$columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' );
4995+
$auto_column = $this->execute_sqlite_query(
4996+
sprintf(
4997+
"SELECT column_name FROM %s
4998+
WHERE table_schema = ?
4999+
AND table_name = ?
5000+
AND extra = 'auto_increment'",
5001+
$this->quote_sqlite_identifier( $columns_table )
5002+
),
5003+
array( $this->get_saved_db_name(), $table_name )
5004+
)->fetchColumn();
5005+
if ( false === $auto_column ) {
5006+
return;
5007+
}
5008+
5009+
/*
5010+
* Prepare an expression for the sequence value.
5011+
* 1. Use N - 1. MySQL stores the next value, SQLite the last one.
5012+
* 2. Clamp to MAX(col) like MySQL (we can't go below existing values).
5013+
*
5014+
* The value is inlined as an integer literal because PDO binds PHP ints
5015+
* as TEXT, and SQLite's type affinity ranks TEXT above INTEGER in MAX().
5016+
*/
5017+
$schema = $table_is_temporary ? 'temp' : 'main';
5018+
$seq_expr = sprintf(
5019+
'MAX(%d, COALESCE((SELECT MAX(%s) FROM %s), 0))',
5020+
$value - 1,
5021+
$this->quote_sqlite_identifier( $auto_column ),
5022+
$this->quote_sqlite_identifier( $table_name )
5023+
);
5024+
5025+
// Update the value in the "sqlite_sequence" table.
5026+
$updated = $this->execute_sqlite_query(
5027+
sprintf( 'UPDATE %s.sqlite_sequence SET seq = %s WHERE name = ?', $schema, $seq_expr ),
5028+
array( $table_name )
5029+
)->rowCount();
5030+
5031+
// If the sequence value does not exist yet, insert a new row.
5032+
// SQLite reports matched (not affected) rows, so the 0 check is safe.
5033+
if ( 0 === $updated ) {
5034+
$this->execute_sqlite_query(
5035+
sprintf( 'INSERT INTO %s.sqlite_sequence (name, seq) VALUES (?, %s)', $schema, $seq_expr ),
5036+
array( $table_name )
5037+
);
5038+
}
5039+
}
5040+
49025041
/**
49035042
* Translate a MySQL SHOW LIKE ... or SHOW WHERE ... condition to SQLite.
49045043
*
@@ -6335,7 +6474,8 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str
63356474
)->fetchAll( PDO::FETCH_ASSOC );
63366475

63376476
// 6. Generate CREATE TABLE statement columns.
6338-
$rows = array();
6477+
$rows = array();
6478+
$has_auto_increment = false;
63396479
foreach ( $column_info as $column ) {
63406480
$sql = ' ';
63416481
$sql .= $this->quote_mysql_identifier( $column['COLUMN_NAME'] );
@@ -6347,7 +6487,8 @@ private function get_mysql_create_table_statement( bool $table_is_temporary, str
63476487
$sql .= ' NULL';
63486488
}
63496489
if ( 'auto_increment' === $column['EXTRA'] ) {
6350-
$sql .= ' AUTO_INCREMENT';
6490+
$has_auto_increment = true;
6491+
$sql .= ' AUTO_INCREMENT';
63516492
}
63526493

63536494
// Handle DEFAULT CURRENT_TIMESTAMP. This works only with timestamp
@@ -6486,6 +6627,28 @@ function ( $column ) {
64866627
$sql .= implode( ",\n", $rows );
64876628
$sql .= "\n)";
64886629
$sql .= sprintf( ' ENGINE=%s', $table_info['ENGINE'] );
6630+
6631+
// Add "AUTO_INCREMENT=N" if a sequence exists and has been advanced.
6632+
if ( $has_auto_increment ) {
6633+
try {
6634+
$seq = (int) $this->execute_sqlite_query(
6635+
sprintf(
6636+
'SELECT seq FROM %s.sqlite_sequence WHERE name = ?',
6637+
$table_is_temporary ? 'temp' : 'main'
6638+
),
6639+
array( $table_name )
6640+
)->fetchColumn();
6641+
} catch ( PDOException $e ) {
6642+
if ( ! str_contains( $e->getMessage(), 'no such table' ) ) {
6643+
throw $e;
6644+
}
6645+
$seq = 0;
6646+
}
6647+
if ( $seq > 0 ) {
6648+
$sql .= sprintf( ' AUTO_INCREMENT=%d', $seq + 1 );
6649+
}
6650+
}
6651+
64896652
$sql .= sprintf( ' DEFAULT CHARSET=%s', $charset );
64906653
$sql .= sprintf( ' COLLATE=%s', $collation );
64916654
if ( '' !== $table_info['TABLE_COMMENT'] ) {

0 commit comments

Comments
 (0)