Skip to content

Commit 03c705a

Browse files
committed
Patch driver AUTO_INCREMENT lookup to a JOIN form under Turso
Turso mis-evaluates correlated scalar subqueries in derived-table SELECT lists: the outer alias binding is not refreshed per row, so WHERE filters on the aliased column mismatch and the previous outer row's value leaks into subsequent rows. The driver computes the AUTO_INCREMENT column for information_schema.tables and SHOW TABLE STATUS via such a correlated subquery, breaking the four metadata tests testInformationSchemaTablesFilterByAutoIncrement, testShowTableStatusFilterByAutoIncrement, testCreateTableSetAutoIncrement, and testAlterTableSetAutoIncrement. Replace it with an equivalent JOIN form: LEFT JOIN against a derived table that marks rows with auto_increment columns (aliased ai_schema/ai_name to avoid colliding with the outer projection), plus sqlite_sequence, and a CASE expression at the projection that returns NULL when no auto_increment column exists. Verified locally on real SQLite — all 667 mysql-on-sqlite tests pass with the patch applied to the driver source.
1 parent 67718c6 commit 03c705a

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

.github/workflows/phpunit-tests-turso.yml

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,234 @@ jobs:
14711471
assert n > 0, 'no Translation_Tests UPDATE expectations matched'
14721472
open(path, 'w').write(new_src)
14731473
print(f'patched {n} Translation_Tests UPDATE expectations to EXISTS form')
1474+
1475+
# 17. Turso evaluates correlated scalar subqueries in derived-table
1476+
# SELECT lists incorrectly: the outer alias binding is not
1477+
# refreshed per row, so a WHERE filter on the aliased column
1478+
# mismatches and the prior outer row's value leaks into the
1479+
# next row. The driver computes AUTO_INCREMENT for
1480+
# information_schema.tables / SHOW TABLE STATUS via such a
1481+
# correlated subquery, breaking 4 metadata tests. Replace it
1482+
# with an equivalent JOIN form (LEFT JOIN against a derived
1483+
# table marking auto_increment rows + sqlite_sequence) and a
1484+
# CASE expression at the projection.
1485+
path = 'src/sqlite/class-wp-pdo-mysql-on-sqlite.php'
1486+
src = open(path).read()
1487+
1488+
# 17a. SHOW TABLE STATUS path.
1489+
old_a = (
1490+
"\t\t// Compose a subquery to compute auto-increment values.\n"
1491+
"\t\t$has_sequence_table = (bool) $this->execute_sqlite_query(\n"
1492+
"\t\t\t\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'\"\n"
1493+
"\t\t)->fetchColumn();\n"
1494+
"\n"
1495+
"\t\t$auto_increment_subquery = sprintf(\n"
1496+
"\t\t\t\"(\n"
1497+
"\t\t\t\tSELECT COALESCE(s.seq + 1, 1)\n"
1498+
"\t\t\t\tFROM %s AS c\n"
1499+
"\t\t\t\t%s\n"
1500+
"\t\t\t\tWHERE c.extra = 'auto_increment'\n"
1501+
"\t\t\t\tAND c.table_schema = t.table_schema\n"
1502+
"\t\t\t\tAND c.table_name = t.table_name\n"
1503+
"\t\t\t)\",\n"
1504+
"\t\t\t$this->quote_sqlite_identifier( $columns_table ),\n"
1505+
"\t\t\t$has_sequence_table\n"
1506+
"\t\t\t\t? 'LEFT JOIN main.sqlite_sequence AS s ON s.name = c.table_name'\n"
1507+
"\t\t\t\t: 'LEFT JOIN (SELECT 0 AS seq) AS s'\n"
1508+
"\t\t);\n"
1509+
"\n"
1510+
"\t\t$query = sprintf(\n"
1511+
"\t\t\t'SELECT * FROM (\n"
1512+
"\t\t\t\tSELECT\n"
1513+
"\t\t\t\t\ttable_name AS `Name`,\n"
1514+
"\t\t\t\t\tengine AS `Engine`,\n"
1515+
"\t\t\t\t\tversion AS `Version`,\n"
1516+
"\t\t\t\t\trow_format AS `Row_format`,\n"
1517+
"\t\t\t\t\ttable_rows AS `Rows`,\n"
1518+
"\t\t\t\t\tavg_row_length AS `Avg_row_length`,\n"
1519+
"\t\t\t\t\tdata_length AS `Data_length`,\n"
1520+
"\t\t\t\t\tmax_data_length AS `Max_data_length`,\n"
1521+
"\t\t\t\t\tindex_length AS `Index_length`,\n"
1522+
"\t\t\t\t\tdata_free AS `Data_free`,\n"
1523+
"\t\t\t\t\t%s AS `Auto_increment`,\n"
1524+
"\t\t\t\t\tcreate_time AS `Create_time`,\n"
1525+
"\t\t\t\t\tupdate_time AS `Update_time`,\n"
1526+
"\t\t\t\t\tcheck_time AS `Check_time`,\n"
1527+
"\t\t\t\t\ttable_collation AS `Collation`,\n"
1528+
"\t\t\t\t\tchecksum AS `Checksum`,\n"
1529+
"\t\t\t\t\tcreate_options AS `Create_options`,\n"
1530+
"\t\t\t\t\ttable_comment AS `Comment`\n"
1531+
"\t\t\t\tFROM %s AS t\n"
1532+
"\t\t\t\tWHERE table_schema = ?\n"
1533+
"\t\t\t)\n"
1534+
"\t\t\tWHERE 1 %s\n"
1535+
"\t\t\tORDER BY `Name`',\n"
1536+
"\t\t\t$auto_increment_subquery,\n"
1537+
"\t\t\t$this->quote_sqlite_identifier( $tables_table ),\n"
1538+
"\t\t\t$condition ?? ''\n"
1539+
"\t\t);\n"
1540+
)
1541+
new_a = (
1542+
"\t\t// JOIN-based AUTO_INCREMENT computation (Turso mis-evaluates\n"
1543+
"\t\t// correlated scalar subqueries in derived-table SELECT lists).\n"
1544+
"\t\t$has_sequence_table = (bool) $this->execute_sqlite_query(\n"
1545+
"\t\t\t\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'\"\n"
1546+
"\t\t)->fetchColumn();\n"
1547+
"\n"
1548+
"\t\t$auto_increment_join = sprintf(\n"
1549+
"\t\t\t\" LEFT JOIN (\n"
1550+
"\t\t\t\tSELECT table_schema AS ai_schema, table_name AS ai_name, 1 AS has_ai\n"
1551+
"\t\t\t\tFROM %s\n"
1552+
"\t\t\t\tWHERE extra = 'auto_increment'\n"
1553+
"\t\t\t\tGROUP BY table_schema, table_name\n"
1554+
"\t\t\t) AS ai ON ai.ai_schema = t.table_schema AND ai.ai_name = t.table_name\",\n"
1555+
"\t\t\t$this->quote_sqlite_identifier( $columns_table )\n"
1556+
"\t\t);\n"
1557+
"\t\tif ( $has_sequence_table ) {\n"
1558+
"\t\t\t$auto_increment_join .= ' LEFT JOIN main.sqlite_sequence AS s ON s.name = t.table_name';\n"
1559+
"\t\t\t$auto_increment_expr = 'CASE WHEN ai.has_ai = 1 THEN COALESCE(s.seq + 1, 1) ELSE NULL END';\n"
1560+
"\t\t} else {\n"
1561+
"\t\t\t$auto_increment_expr = 'CASE WHEN ai.has_ai = 1 THEN 1 ELSE NULL END';\n"
1562+
"\t\t}\n"
1563+
"\n"
1564+
"\t\t$query = sprintf(\n"
1565+
"\t\t\t'SELECT * FROM (\n"
1566+
"\t\t\t\tSELECT\n"
1567+
"\t\t\t\t\ttable_name AS `Name`,\n"
1568+
"\t\t\t\t\tengine AS `Engine`,\n"
1569+
"\t\t\t\t\tversion AS `Version`,\n"
1570+
"\t\t\t\t\trow_format AS `Row_format`,\n"
1571+
"\t\t\t\t\ttable_rows AS `Rows`,\n"
1572+
"\t\t\t\t\tavg_row_length AS `Avg_row_length`,\n"
1573+
"\t\t\t\t\tdata_length AS `Data_length`,\n"
1574+
"\t\t\t\t\tmax_data_length AS `Max_data_length`,\n"
1575+
"\t\t\t\t\tindex_length AS `Index_length`,\n"
1576+
"\t\t\t\t\tdata_free AS `Data_free`,\n"
1577+
"\t\t\t\t\t%s AS `Auto_increment`,\n"
1578+
"\t\t\t\t\tcreate_time AS `Create_time`,\n"
1579+
"\t\t\t\t\tupdate_time AS `Update_time`,\n"
1580+
"\t\t\t\t\tcheck_time AS `Check_time`,\n"
1581+
"\t\t\t\t\ttable_collation AS `Collation`,\n"
1582+
"\t\t\t\t\tchecksum AS `Checksum`,\n"
1583+
"\t\t\t\t\tcreate_options AS `Create_options`,\n"
1584+
"\t\t\t\t\ttable_comment AS `Comment`\n"
1585+
"\t\t\t\tFROM %s AS t%s\n"
1586+
"\t\t\t\tWHERE table_schema = ?\n"
1587+
"\t\t\t)\n"
1588+
"\t\t\tWHERE 1 %s\n"
1589+
"\t\t\tORDER BY `Name`',\n"
1590+
"\t\t\t$auto_increment_expr,\n"
1591+
"\t\t\t$this->quote_sqlite_identifier( $tables_table ),\n"
1592+
"\t\t\t$auto_increment_join,\n"
1593+
"\t\t\t$condition ?? ''\n"
1594+
"\t\t);\n"
1595+
)
1596+
assert old_a in src, 'SHOW TABLE STATUS auto_increment block not found'
1597+
src = src.replace(old_a, new_a, 1)
1598+
1599+
# 17b. translate_table_ref information_schema.tables path.
1600+
# Initialize $auto_increment_join before the per-column loop so
1601+
# the return-time sprintf can append it to the FROM clause.
1602+
old_b1 = (
1603+
"\t\t\t$expanded_list = array();\n"
1604+
"\t\t\tforeach ( $columns as $column ) {\n"
1605+
)
1606+
new_b1 = (
1607+
"\t\t\t$expanded_list = array();\n"
1608+
"\t\t\t$auto_increment_join = '';\n"
1609+
"\t\t\tforeach ( $columns as $column ) {\n"
1610+
)
1611+
assert old_b1 in src, 'translate_table_ref expanded_list init not found'
1612+
src = src.replace(old_b1, new_b1, 1)
1613+
1614+
# Replace the elseif body that builds the auto_increment subquery.
1615+
old_b2 = (
1616+
"\t\t\t\t} elseif ( 'tables' === $table_name && 'AUTO_INCREMENT' === $column ) {\n"
1617+
"\t\t\t\t\t// Inject the auto-increment values.\n"
1618+
"\t\t\t\t\t$columns_table = $this->information_schema_builder->get_table_name( false, 'columns' );\n"
1619+
"\t\t\t\t\t$has_sequence_table = (bool) $this->execute_sqlite_query(\n"
1620+
"\t\t\t\t\t\t\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'\"\n"
1621+
"\t\t\t\t\t)->fetchColumn();\n"
1622+
"\n"
1623+
"\t\t\t\t\t$auto_increment_subquery = sprintf(\n"
1624+
"\t\t\t\t\t\t\"(\n"
1625+
"\t\t\t\t\t\t\tSELECT COALESCE(s.seq + 1, 1)\n"
1626+
"\t\t\t\t\t\t\tFROM %s AS c\n"
1627+
"\t\t\t\t\t\t\t%s\n"
1628+
"\t\t\t\t\t\t\tWHERE c.extra = 'auto_increment'\n"
1629+
"\t\t\t\t\t\t\tAND c.table_schema = %s.table_schema\n"
1630+
"\t\t\t\t\t\t\tAND c.table_name = %s.table_name\n"
1631+
"\t\t\t\t\t\t)\",\n"
1632+
"\t\t\t\t\t\t$this->quote_sqlite_identifier( $columns_table ),\n"
1633+
"\t\t\t\t\t\t$has_sequence_table\n"
1634+
"\t\t\t\t\t\t\t? 'LEFT JOIN main.sqlite_sequence AS s ON s.name = c.table_name'\n"
1635+
"\t\t\t\t\t\t\t: 'LEFT JOIN (SELECT 0 AS seq) AS s',\n"
1636+
"\t\t\t\t\t\t$this->quote_sqlite_identifier( $table_name ),\n"
1637+
"\t\t\t\t\t\t$this->quote_sqlite_identifier( $table_name )\n"
1638+
"\t\t\t\t\t);\n"
1639+
"\n"
1640+
"\t\t\t\t\t$expanded_list[] = sprintf( '%s AS %s', $auto_increment_subquery, $quoted_column );\n"
1641+
)
1642+
new_b2 = (
1643+
"\t\t\t\t} elseif ( 'tables' === $table_name && 'AUTO_INCREMENT' === $column ) {\n"
1644+
"\t\t\t\t\t// JOIN-based AUTO_INCREMENT (Turso mis-evaluates correlated form).\n"
1645+
"\t\t\t\t\t$columns_table = $this->information_schema_builder->get_table_name( false, 'columns' );\n"
1646+
"\t\t\t\t\t$has_sequence_table = (bool) $this->execute_sqlite_query(\n"
1647+
"\t\t\t\t\t\t\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'\"\n"
1648+
"\t\t\t\t\t)->fetchColumn();\n"
1649+
"\n"
1650+
"\t\t\t\t\t$tables_alias = $this->quote_sqlite_identifier( $table_name );\n"
1651+
"\t\t\t\t\t$auto_increment_join = sprintf(\n"
1652+
"\t\t\t\t\t\t\" LEFT JOIN (\n"
1653+
"\t\t\t\t\t\t\tSELECT table_schema AS ai_schema, table_name AS ai_name, 1 AS has_ai\n"
1654+
"\t\t\t\t\t\t\tFROM %s\n"
1655+
"\t\t\t\t\t\t\tWHERE extra = 'auto_increment'\n"
1656+
"\t\t\t\t\t\t\tGROUP BY table_schema, table_name\n"
1657+
"\t\t\t\t\t\t) AS ai ON ai.ai_schema = %s.table_schema AND ai.ai_name = %s.table_name\",\n"
1658+
"\t\t\t\t\t\t$this->quote_sqlite_identifier( $columns_table ),\n"
1659+
"\t\t\t\t\t\t$tables_alias,\n"
1660+
"\t\t\t\t\t\t$tables_alias\n"
1661+
"\t\t\t\t\t);\n"
1662+
"\t\t\t\t\tif ( $has_sequence_table ) {\n"
1663+
"\t\t\t\t\t\t$auto_increment_join .= sprintf(\n"
1664+
"\t\t\t\t\t\t\t' LEFT JOIN main.sqlite_sequence AS s ON s.name = %s.table_name',\n"
1665+
"\t\t\t\t\t\t\t$tables_alias\n"
1666+
"\t\t\t\t\t\t);\n"
1667+
"\t\t\t\t\t\t$auto_increment_expr = 'CASE WHEN ai.has_ai = 1 THEN COALESCE(s.seq + 1, 1) ELSE NULL END';\n"
1668+
"\t\t\t\t\t} else {\n"
1669+
"\t\t\t\t\t\t$auto_increment_expr = 'CASE WHEN ai.has_ai = 1 THEN 1 ELSE NULL END';\n"
1670+
"\t\t\t\t\t}\n"
1671+
"\n"
1672+
"\t\t\t\t\t$expanded_list[] = sprintf( '%s AS %s', $auto_increment_expr, $quoted_column );\n"
1673+
)
1674+
assert old_b2 in src, 'translate_table_ref AUTO_INCREMENT elseif not found'
1675+
src = src.replace(old_b2, new_b2, 1)
1676+
1677+
# Append the JOIN clause to the outer FROM in the return sprintf.
1678+
old_b3 = (
1679+
"\t\t\t// Compose information schema subquery.\n"
1680+
"\t\t\treturn sprintf(\n"
1681+
"\t\t\t\t'(SELECT %s FROM %s AS %s)',\n"
1682+
"\t\t\t\t$column_list,\n"
1683+
"\t\t\t\t$this->quote_sqlite_identifier( $sqlite_table_name ),\n"
1684+
"\t\t\t\t$this->quote_sqlite_identifier( $table_name )\n"
1685+
"\t\t\t);\n"
1686+
)
1687+
new_b3 = (
1688+
"\t\t\t// Compose information schema subquery.\n"
1689+
"\t\t\treturn sprintf(\n"
1690+
"\t\t\t\t'(SELECT %s FROM %s AS %s%s)',\n"
1691+
"\t\t\t\t$column_list,\n"
1692+
"\t\t\t\t$this->quote_sqlite_identifier( $sqlite_table_name ),\n"
1693+
"\t\t\t\t$this->quote_sqlite_identifier( $table_name ),\n"
1694+
"\t\t\t\t$auto_increment_join\n"
1695+
"\t\t\t);\n"
1696+
)
1697+
assert old_b3 in src, 'translate_table_ref return sprintf not found'
1698+
src = src.replace(old_b3, new_b3, 1)
1699+
1700+
open(path, 'w').write(src)
1701+
print('patched AUTO_INCREMENT correlated-subquery -> JOIN form (SHOW TABLE STATUS + information_schema.tables)')
14741702
PY
14751703
14761704
- name: Run PHPUnit tests against Turso DB

0 commit comments

Comments
 (0)