Skip to content

fix: table-level PRIMARY KEY(col) rowid alias reads back NULL when the table has other columns #686

Description

@dpsiderius

Description

A rowid-alias column declared as a table-level PRIMARY KEY (col)
reads back as NULL whenever the table has more than one column. This
is a silent wrong answer on a plain SELECT against a valid SQLite
file — no error, no warning.

Measured against stock sqlite3 3.51.0 at 0.18.10:

$ sqlite3 m1.db "CREATE TABLE m1 (a INTEGER, b TEXT, PRIMARY KEY (a));
                 INSERT INTO m1 VALUES (1,'x');"

$ sqlite3 m1.db "SELECT a, b FROM m1"
1|x
$ sqlite-rs query m1.db "SELECT a, b FROM m1"
|x                                   # <-- a is NULL

Control, the column-level form, is correct:

$ sqlite3 m2.db "CREATE TABLE m2 (a INTEGER PRIMARY KEY, b TEXT);
                 INSERT INTO m2 VALUES (1,'x');"
$ sqlite-rs query m2.db "SELECT a, b FROM m2"
1|x                                  # <-- correct

Cause

src/schema/ddl_reader.rs::rowid_alias_from_sql handles the table-level
PRIMARY KEY (col) form only when the primary key names the table's one
and only column:

if let [only] = columns.as_slice() {
    if is_integer_column(only) { ... }
}

Its comment states the rule it is implementing:

SQLite only treats this as a rowid alias when it names the table's one
and only column, and that column is INTEGER-typed (a composite key, or
a second column, rules it out).

That is not SQLite's rule. Measured: a second column does not rule
it out. (a INTEGER, b TEXT, PRIMARY KEY (a)) creates no autoindex and
accepts INSERT INTO m1 VALUES (NULL, 'x') with an auto-assigned rowid
— both proof that a is the rowid. Only a composite key rules it out.

Because the reader returns None, a is treated as an ordinary column.
The value lives in the rowid, the record's column slot holds NULL, and
that NULL is what the caller gets.

Correct rule, oracle-derived

A table-level PRIMARY KEY (col) is a rowid alias when it names exactly
one column, that column's declared type is INTEGER, and the table is
not WITHOUT ROWID. The number of other columns is irrelevant.

DDL rowid alias?
(a INTEGER, PRIMARY KEY (a)) yes
(a INTEGER, b TEXT, PRIMARY KEY (a)) yes — currently missed
(a TEXT, b TEXT, PRIMARY KEY (a)) no — not INTEGER, gets an autoindex
(a INTEGER, b TEXT, PRIMARY KEY (a, b)) no — composite
(a INTEGER, b TEXT, PRIMARY KEY (a)) WITHOUT ROWID no

Severity

Tier 0 read-correctness (spec 001/Req 4). The file is valid, the query
is ordinary, the answer is wrong, and nothing reports it. The affected
DDL shape is common — it is the form most schema generators emit when
the primary key is declared separately from the column.

Found while deriving the autoindex-numbering rule for #685, which needs
this same rowid-alias question answered to know which constraints get an
autoindex. #685 implements the correct rule locally for its own
resolver rather than changing rowid behaviour inside a corruption fix;
this ticket owns the shared fix.

Scope

  • Relax rowid_alias_from_sql's table-level branch: drop the
    "table's only column" condition, keep the single-column and
    INTEGER-type conditions.
  • Corpus test against oracle-created files for each row of the table
    above, asserting the column value round-trips rather than just that
    no error occurs.
  • Check PRAGMA table_info output for the affected shape, which reports
    pk and may share the assumption.

Complexity

Estimate: small
Reasoning: The corrected rule is derived and tabulated above, and
the change is to one branch of one function. The care is in the test
matrix, not the logic — and in checking whether anything else in the
reader encoded the same wrong assumption.

Acceptance Criteria

  • Every row of the rule table above round-trips against the oracle
  • SELECT a, b returns the alias value, not NULL
  • Composite and non-INTEGER cases still get their autoindex
  • WITHOUT ROWID unaffected
  • Full suite shows no regression
  • The misleading comment is corrected, not just the code

Refs: 001/Req-4, #685

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