Summary
When ANALYZE runs on a table that has indexes, Turso creates a spurious (table_name, NULL, rowcount) entry in sqlite_stat1 alongside the correct per-index entries. SQLite only creates the NULL-idx entry for tables that have no indexes (as a fallback row count estimate).
Reproduction
CREATE TABLE t1(id INTEGER PRIMARY KEY, a TEXT);
INSERT INTO t1 VALUES(1, 'x');
CREATE INDEX i1 ON t1(a);
ANALYZE t1;
SELECT tbl,
CASE WHEN idx IS NULL THEN '<NULL>' ELSE idx END,
stat
FROM sqlite_stat1 WHERE tbl='t1' ORDER BY idx;
Turso (wrong — extra row):
SQLite (correct):
Table without indexes — both correct (control)
CREATE TABLE t2(a TEXT, b INTEGER);
INSERT INTO t2 VALUES('x', 1);
ANALYZE t2;
Both produce t2|<NULL>|1 — correct. The NULL-idx entry is appropriate when there are no indexes.
Multi-column index — same bug
CREATE TABLE t3(id INTEGER PRIMARY KEY, a TEXT, b INTEGER);
INSERT INTO t3 VALUES(1, 'x', 10);
INSERT INTO t3 VALUES(2, 'x', 20);
CREATE INDEX i3 ON t3(a, b);
ANALYZE t3;
Turso: t3|<NULL>|2 + t3|i3|2 2 1 (extra row)
SQLite: t3|i3|2 2 1 only
200-row table with two indexes
Turso produces 3 rows (NULL + idx_cat + idx_val). SQLite produces 2 (idx_cat + idx_val). Per-index statistics match — only the extra NULL-idx row differs.
Controls
| Scenario |
SQLite |
Turso |
Bug? |
| Table with index(es) |
idx entries only |
idx entries + NULL entry |
YES |
| Table without indexes |
NULL entry |
NULL entry |
No |
| Per-index stat values |
correct |
correct |
No |
| Query results after ANALYZE |
correct |
correct |
No |
Root Cause
The stat1 insertion code in core/translate/analyze.rs unconditionally creates a (table_name, NULL, rowcount) entry for every analyzed table, then creates per-index entries. SQLite only creates the NULL-idx entry when the table has no indexes.
Impact
- Metadata incompatibility: Applications reading sqlite_stat1 directly (ORMs, schema tools) see unexpected rows. Files written by Turso contain entries SQLite wouldn't generate.
- Potential optimizer divergence: The extra NULL-idx entry provides a table scan cost estimate that wouldn't normally exist, potentially causing different (but still correct) query plan choices vs SQLite.
Version
v0.5.3 (commit 09c149a)
Summary
When
ANALYZEruns on a table that has indexes, Turso creates a spurious(table_name, NULL, rowcount)entry insqlite_stat1alongside the correct per-index entries. SQLite only creates the NULL-idx entry for tables that have no indexes (as a fallback row count estimate).Reproduction
Turso (wrong — extra row):
SQLite (correct):
Table without indexes — both correct (control)
Both produce
t2|<NULL>|1— correct. The NULL-idx entry is appropriate when there are no indexes.Multi-column index — same bug
Turso:
t3|<NULL>|2+t3|i3|2 2 1(extra row)SQLite:
t3|i3|2 2 1only200-row table with two indexes
Turso produces 3 rows (NULL + idx_cat + idx_val). SQLite produces 2 (idx_cat + idx_val). Per-index statistics match — only the extra NULL-idx row differs.
Controls
Root Cause
The stat1 insertion code in
core/translate/analyze.rsunconditionally creates a(table_name, NULL, rowcount)entry for every analyzed table, then creates per-index entries. SQLite only creates the NULL-idx entry when the table has no indexes.Impact
Version
v0.5.3 (commit 09c149a)