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
Refs: 001/Req-4, #685
Description
A rowid-alias column declared as a table-level
PRIMARY KEY (col)reads back as
NULLwhenever the table has more than one column. Thisis a silent wrong answer on a plain
SELECTagainst a valid SQLitefile — no error, no warning.
Measured against stock
sqlite33.51.0 at 0.18.10:Control, the column-level form, is correct:
Cause
src/schema/ddl_reader.rs::rowid_alias_from_sqlhandles the table-levelPRIMARY KEY (col)form only when the primary key names the table's oneand only column:
Its comment states the rule it is implementing:
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 andaccepts
INSERT INTO m1 VALUES (NULL, 'x')with an auto-assigned rowid— both proof that
ais the rowid. Only a composite key rules it out.Because the reader returns
None,ais treated as an ordinary column.The value lives in the rowid, the record's column slot holds
NULL, andthat
NULLis what the caller gets.Correct rule, oracle-derived
A table-level
PRIMARY KEY (col)is a rowid alias when it names exactlyone column, that column's declared type is
INTEGER, and the table isnot
WITHOUT ROWID. The number of other columns is irrelevant.(a INTEGER, PRIMARY KEY (a))(a INTEGER, b TEXT, PRIMARY KEY (a))(a TEXT, b TEXT, PRIMARY KEY (a))(a INTEGER, b TEXT, PRIMARY KEY (a, b))(a INTEGER, b TEXT, PRIMARY KEY (a)) WITHOUT ROWIDSeverity
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
rowid_alias_from_sql's table-level branch: drop the"table's only column" condition, keep the single-column and
INTEGER-type conditions.above, asserting the column value round-trips rather than just that
no error occurs.
PRAGMA table_infooutput for the affected shape, which reportspkand 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
SELECT a, breturns the alias value, notNULLWITHOUT ROWIDunaffectedRefs: 001/Req-4, #685