Description
CREATE TABLE IF NOT EXISTS ignores its guard. Running it a second time
against an existing table does not error and does not skip — it creates a
second sqlite_master row for the same table and leaves the file corrupt.
Spec 013 lists IF NOT EXISTS being "ignored after parsing" as a prerequisite
owned by V3/V7 (if_not_exists appears only in src/parser/grammar.rs and
src/parser/printer.rs). That description undersells it: the consequence is
not a missing feature, it is silent corruption of a valid database.
Measured on origin/main at 7701d18 against the pinned 3.53.4 oracle. Not
introduced by #685/#689 — verified from a clean worktree of main itself.
$ DDL="CREATE TABLE IF NOT EXISTS t (a TEXT, b TEXT, PRIMARY KEY (a,b))"
$ sqlite-rs exec m.db "$DDL"
$ sqlite-rs exec m.db "INSERT INTO t VALUES('x','y')"
$ sqlite-rs exec m.db "$DDL" # rc=0, no error
$ sqlite3 m.db "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='t';"
2 # want 1
$ sqlite3 m.db "PRAGMA integrity_check;"
*** in database main ***
Page 3: never used
wrong # of entries in index sqlite_autoindex_t_1
$ sqlite3 m.db "SELECT count(*) FROM t;"
Error: database disk image is malformed
The oracle, given the same three statements, reports no error and one
sqlite_master row.
Two independent symptoms
Isolated by re-running the test without a composite primary key:
| DDL |
duplicate master row |
integrity_check |
CREATE TABLE IF NOT EXISTS n (a TEXT) ×2 |
2 rows |
Page 3: never used |
... (a,b,c) PRIMARY KEY ×2 |
2 rows |
Page 3: never used + wrong # of entries in index sqlite_autoindex_t_1 |
So there are two things wrong, and the second is not the autoindex work's:
- The guard is not honoured. Codegen emits the
CreateTable path
regardless, appending a second schema row for a name that already exists.
Every subsequent schema read sees the table twice.
- The second create leaks its root page.
Page 3: never used — a root
page is allocated for the duplicate table and then referenced by nothing,
which is what makes the oracle call the file malformed rather than merely
odd.
The composite-PK case adds a duplicate autoindex on top, which is why it
reports wrong # of entries as well.
Why this is urgent rather than a V3/V7 nicety
CREATE TABLE IF NOT EXISTS on startup is the entire idiom the statement
exists for, and it is the first statement in SQE's catalog list (spec 013
Req 6). SQE runs it every time it opens the catalog. So:
- first startup: fine
- second startup: the catalog is corrupt
That makes this worse than the missing embedding API (#695) and worse than the
reserved-keyword divergence (#696), because it corrupts a database that this
crate itself created, on the second run of a normal consumer. #685 fixed
writing into a stock-created file; this is the same class of failure in the
direction we control.
Scope
src/codegen/ddl/create_table.rs (and the CREATE INDEX/CREATE VIEW
equivalents — check whether they share the defect; IF NOT EXISTS is valid
on all three) must consult the catalog and emit nothing when the name
already exists.
- Confirm the matching
DROP TABLE IF EXISTS direction, which has the mirror
guard and may have the mirror bug.
- The leaked page needs accounting for: skipping the create avoids allocating
it at all, so fixing symptom 1 should remove symptom 2 — but assert it
rather than assume, since a partially-emitted program could still allocate
before bailing.
Acceptance Criteria
Complexity
Estimate: medium
Reasoning: The fix itself is a catalog lookup before emission and is
small. What makes it medium is the blast radius: IF NOT EXISTS/IF EXISTS
appears on three create paths and the drops, the leaked-page symptom needs
proving gone rather than assumed, and the "still fails without the guard"
direction has to keep working — so the test matrix is wider than the change.
Found while running SQE's actual statement list end to end (spec 013 Req 6's
own acceptance mechanism) rather than trusting exit codes: all ten statements
returned rc=0, and the corruption only showed up on comparing state against
the oracle.
Refs: 013/Req-6, #678, #695
Description
CREATE TABLE IF NOT EXISTSignores its guard. Running it a second timeagainst an existing table does not error and does not skip — it creates a
second
sqlite_masterrow for the same table and leaves the file corrupt.Spec 013 lists
IF NOT EXISTSbeing "ignored after parsing" as a prerequisiteowned by V3/V7 (
if_not_existsappears only insrc/parser/grammar.rsandsrc/parser/printer.rs). That description undersells it: the consequence isnot a missing feature, it is silent corruption of a valid database.
Measured on
origin/mainat 7701d18 against the pinned 3.53.4 oracle. Notintroduced by #685/#689 — verified from a clean worktree of
mainitself.The oracle, given the same three statements, reports no error and one
sqlite_masterrow.Two independent symptoms
Isolated by re-running the test without a composite primary key:
integrity_checkCREATE TABLE IF NOT EXISTS n (a TEXT)×2Page 3: never used... (a,b,c) PRIMARY KEY×2Page 3: never used+wrong # of entries in index sqlite_autoindex_t_1So there are two things wrong, and the second is not the autoindex work's:
CreateTablepathregardless, appending a second schema row for a name that already exists.
Every subsequent schema read sees the table twice.
Page 3: never used— a rootpage is allocated for the duplicate table and then referenced by nothing,
which is what makes the oracle call the file malformed rather than merely
odd.
The composite-PK case adds a duplicate autoindex on top, which is why it
reports
wrong # of entriesas well.Why this is urgent rather than a V3/V7 nicety
CREATE TABLE IF NOT EXISTSon startup is the entire idiom the statementexists for, and it is the first statement in SQE's catalog list (spec 013
Req 6). SQE runs it every time it opens the catalog. So:
That makes this worse than the missing embedding API (#695) and worse than the
reserved-keyword divergence (#696), because it corrupts a database that this
crate itself created, on the second run of a normal consumer. #685 fixed
writing into a stock-created file; this is the same class of failure in the
direction we control.
Scope
src/codegen/ddl/create_table.rs(and theCREATE INDEX/CREATE VIEWequivalents — check whether they share the defect;
IF NOT EXISTSis validon all three) must consult the catalog and emit nothing when the name
already exists.
DROP TABLE IF EXISTSdirection, which has the mirrorguard and may have the mirror bug.
it at all, so fixing symptom 1 should remove symptom 2 — but assert it
rather than assume, since a partially-emitted program could still allocate
before bailing.
Acceptance Criteria
CREATE TABLE IF NOT EXISTSrun twice leaves exactly onesqlite_masterrow, no error, and data from between the two runs intactPRAGMA integrity_checkunder the pinned 3.53.4 oracle reportsokafterwards — including the composite-PK case
IF NOT EXISTS, a duplicateCREATE TABLEstill fails,with the oracle's message (
table t already exists)CREATE INDEX IF NOT EXISTSandCREATE VIEW IF NOT EXISTS, and forDROP ... IF EXISTSon a missing objectthe full statement list, diffed against the oracle
make lintboth clippy passes,make assurancenodead links
Complexity
Estimate: medium
Reasoning: The fix itself is a catalog lookup before emission and is
small. What makes it medium is the blast radius:
IF NOT EXISTS/IF EXISTSappears on three create paths and the drops, the leaked-page symptom needs
proving gone rather than assumed, and the "still fails without the guard"
direction has to keep working — so the test matrix is wider than the change.
Found while running SQE's actual statement list end to end (spec 013 Req 6's
own acceptance mechanism) rather than trusting exit codes: all ten statements
returned rc=0, and the corruption only showed up on comparing state against
the oracle.
Refs: 013/Req-6, #678, #695