Skip to content

bug: CREATE TABLE IF NOT EXISTS ignores its guard — duplicate sqlite_master row + leaked page, corrupts the file on second run #697

Description

@dpsiderius

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:

  1. 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.
  2. 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

  • CREATE TABLE IF NOT EXISTS run twice leaves exactly one
    sqlite_master row, no error, and data from between the two runs intact
  • PRAGMA integrity_check under the pinned 3.53.4 oracle reports ok
    afterwards — including the composite-PK case
  • No leaked pages: page count unchanged by the no-op second create
  • Without IF NOT EXISTS, a duplicate CREATE TABLE still fails,
    with the oracle's message (table t already exists)
  • Same coverage for CREATE INDEX IF NOT EXISTS and CREATE VIEW IF NOT EXISTS, and for DROP ... IF EXISTS on a missing object
  • Corpus test in the SQE consumer family: the catalog DDL run twice, then
    the full statement list, diffed against the oracle
  • Full suite green, make lint both clippy passes, make assurance no
    dead 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 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions