Fixes for final review in issue #769#779
Merged
Merged
Conversation
db.query() promises that a statement rejected with ValueError is rolled back and has no effect. PRAGMA statements are the exception: some of them refuse to run inside a transaction, so they execute outside the savepoint guard - a row-less PRAGMA such as "PRAGMA user_version = 5" therefore takes effect despite the ValueError. Documenting this as a known limitation rather than fixing it, since a fix would need a hardcoded list of row-returning PRAGMAs. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PRAGMA table_info sets is_pk to the 1-based position of each column within the PRIMARY KEY, which can differ from table column order. table.pks previously returned table column order, so an implicit compound FOREIGN KEY ... REFERENCES other was introspected with its referenced columns inverted, and transform() baked that inverted order into the rewritten schema - failing with IntegrityError on valid data, or silently reversing the constraint with foreign keys off. table.pks, compound foreign key guessing (create, add_foreign_key) and transform() now all use declaration order, and transform() no longer reorders a compound PRIMARY KEY (b, a) into table column order. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The namedtuple-to-dataclass change made ForeignKey unhashable, breaking set(table.foreign_keys) and dict-key usage that worked in 3.x. frozen=True restores immutability and hashability. Equality and hashing cover all compared fields including on_delete/on_update - two foreign keys differing only in their actions are different constraints. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolve_foreign_keys() used all-or-nothing isinstance checks, so mixing ForeignKey objects with tuples raised "foreign_keys= should be a list of tuples" - a regression from 3.x, where ForeignKey was a namedtuple and passed the tuple check. Each item is now normalized individually, which also allows bare column-name strings in a mixed list. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous compound-pk-ordering commit switched pks_and_rows_where() to Table-only properties, but the method is defined on Queryable and views exposed it too - calling it on a View raised AttributeError. Restored Queryable-safe logic, and stopped double-quoting the synthesized rowid column: SQLite turns a double-quoted identifier that does not resolve into a string literal, so on a view the generated select "rowid" silently produced the string 'rowid' and a confusing KeyError, where 3.x's [rowid] quoting raised OperationalError cleanly. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Type detection is the 4.0 default for CSV/TSV data, and the detected-type
transform ran even when the target table already existed - inserting a
CSV into a table with a TEXT zip column converted the column to INTEGER,
corrupting values with leading zeros ('01234' became 1234) with no
warning. Detected types now only apply to tables the command created.
Refs #769 (comment)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The InvalidColumns check added for #732 fired before alter=True had a chance to add the missing pk column - a regression from 3.x, where insert({"id": 5, "a": 2}, pk="id", alter=True) against a table without an id column worked. With alter=True the check is now deferred until the first batch of record keys is known: a pk column supplied by the records passes, one found in neither the table nor the records still raises InvalidColumns before anything is inserted. An empty record iterator with alter=True returns without error, matching the 3.x no-op. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CLI validated --stop-before names against both pending and applied migrations, but Migrations.apply() only looked for the stop name among pending ones - naming an applied migration passed validation and then silently applied every migration after it, the exact outcome the option exists to prevent. apply() now raises ValueError before applying anything if a stop_before name matches an applied migration in its set; names not in the set are still ignored, since unqualified CLI values are offered to every set. The migrate command reports it as a clean error. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assigning conn.isolation_level commits any pending transaction as a side effect, so entering the context manager with a transaction open silently committed the caller's work and made a later rollback() a no-op. All internal callers already ensure no transaction is open; the public API now enforces it, matching enable_wal() and disable_wal(). Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The existing-foreign-key dedup compared only columns and other_table, so
requesting an FK that already exists with different ON DELETE/ON UPDATE
actions was a silent no-op - and with add_foreign_key() raising "already
exists", there was no signal that the requested actions were dropped.
An exact match (including actions) is still skipped for idempotency;
a mismatch now raises AlterError pointing at table.transform().
Also validates that compound foreign keys passed as 4-tuples have the
same number of columns on both sides - extra other-columns were being
silently discarded, e.g. ("t", ("a",), "other", ("a", "b")) created a
single-column key referencing just "a".
Refs #769 (comment)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lookup-table insert relied on INSERT OR IGNORE and the unique index to dedupe against existing rows, but SQLite unique indexes treat NULLs as distinct - extracting a second table into the same lookup table re-inserted every NULL-containing value, growing orphan rows on each extract. The insert now also has an IS-based NOT EXISTS guard, matching how the foreign keys themselves are resolved. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sqlite-utils insert db t - --pk badcol and sqlite-utils extract db t nosuchcol dumped raw InvalidColumns tracebacks - the insert error handling caught NoTable and OperationalError but not the InvalidColumns introduced for #732, and the extract command had no handling at all (including for NoTable when pointed at a view). Both now exit with click-style Error: messages. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docs promise --list will not create the database file or the _sqlite_migrations table, but legacy sqlite_migrate.Migrations classes create the table (in the legacy schema) from their pending()/applied() methods. The listing now runs inside a transaction that is rolled back, keeping --list read-only regardless of what the migration class does. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SQLite 3.23.1 rejects a UTF-8 byte order mark before the first token, so the BOM variant of the execute()-prefixed-BEGIN test now skips when the SQLite version does not accept a leading BOM. And versions before 3.36 allowed selecting rowid from a view, returning NULL, rather than raising an error - the pks_and_rows_where() view test now accepts either behavior. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A RAISE(ROLLBACK) trigger or INSERT OR ROLLBACK conflict rolls back the
entire transaction and destroys every savepoint. The cleanup paths in
atomic() and query() then raised OperationalError ("no such savepoint" /
"cannot rollback - no transaction is active"), masking the original
IntegrityError - breaking user code that catches sqlite3.IntegrityError.
Cleanup now checks conn.in_transaction first: if the error already
destroyed the transaction there is nothing left to undo, and the
original exception propagates.
Refs #769 (comment)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #779 +/- ##
==========================================
+ Coverage 95.11% 95.15% +0.03%
==========================================
Files 9 9
Lines 3682 3712 +30
==========================================
+ Hits 3502 3532 +30
Misses 180 180 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The query() exception-masking test uses INSERT ... RETURNING, which is a syntax error on SQLite 3.23.1 - skip it there like the other RETURNING tests. Refs #769 (comment) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simonw
marked this pull request as ready for review
July 7, 2026 05:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See:
📚 Documentation preview 📚: https://sqlite-utils--779.org.readthedocs.build/en/779/