- Breaking change:
table.extract()- and thesqlite-utils extractcommand - no longer extract rows where every extracted column isnull. Those rows now keep anullvalue in the new foreign key column instead of pointing at an all-nullrecord in the lookup table. When extracting multiple columns, rows are still extracted if at least one of the columns has a value. (#186) - The
extracts=option totable.insert()and friends no longer creates a lookup table record forNonevalues - the column value staysnull. Previously every batch of inserted rows containing aNonevalue would add a duplicatenullrecord to the lookup table. - Fixed a bug where
table.lookup()inserted a duplicate row on every call if any of the lookup values wereNone. Lookup values are now compared usingISso thatNonevalues match existing rows correctly. - JSON output from the command-line tool no longer escapes non-ASCII characters, so
sqlite-utils data.db "select '日本語' as text"now outputs[{"text": "日本語"}]. This matches how values were already stored byinsertand how CSV/TSV output already behaved. A new--asciioption restores the previous behavior of escaping non-ASCII characters, for output destinations that cannot handle UTF-8 - see Unicode characters in JSON. The option is available on thequery,rows,search,tables,views,triggers,indexesandmemorycommands. Theconvert --multi --dry-runpreview andpluginsoutput also no longer escape non-ASCII characters. (#625) --no-headersnow omits the header row from--fmtand--tableoutput, not just CSV and TSV output. (#566)table.insert_all(..., pk=...)now raisesInvalidColumnsifpk=names columns that do not exist in an existing table. Previously this behaved inconsistently, with single-row inserts raising aKeyErrorwhile other row counts succeeded. (#732)- Fixed an
IndexErrorfromtable.insert(..., pk=..., ignore=True)when an ignored insert followed writes to another table on the same connection.last_pkis now populated from the explicit primary key value instead of looking up a stalelastrowid. (#554) - Fixed a bug where a failed write statement executed with
db.execute()left the driver’s implicit transaction open. Every subsequent write then joined that phantom transaction, which nothing committed, so their work was silently rolled back when the connection was closed. The implicit transaction opened by a failed statement is now rolled back before the exception is raised. A failed write inside a transaction opened withdb.begin()ordb.atomic()leaves that transaction open and untouched, as before. - Fixed a bug where transaction-control statements prefixed with an empty statement -
db.query("; COMMIT")- or a UTF-8 byte order mark slipped past the check that rejects them, committing the caller’s open transaction before raising a confusingOperationalError. The keyword scanner used bydb.query()anddb.execute()now skips leading;and byte order marks, matching what thesqlite3driver tolerates before the first token, so these statements are rejected with aValueErrorwithout being executed. The same fix meansdb.execute("; BEGIN")no longer auto-commits the transaction it just opened. - Documented a limitation of
db.query(): aPRAGMAstatement that returns no rows raises aValueErrorbut still takes effect, because PRAGMA statements run outside the savepoint guard used to roll back other rejected statements. Usedb.execute()for row-less PRAGMA statements. - Fixed exception masking when a statement destroys the enclosing transaction. An error such as a
RAISE(ROLLBACK)trigger orINSERT OR ROLLBACKconflict rolls back the whole transaction, destroying every savepoint - the cleanup indb.atomic()anddb.query()then failed withOperationalError: no such savepoint(orcannot rollback - no transaction is active), hiding the originalIntegrityErrorfrom code that tried to catch it. Cleanup now checks whether a transaction is still open first, so the original exception propagates. sqlite-utils migrate --listis now read-only even when the migrations file uses the legacysqlite_migrate.Migrationsclass, whose listing methods create the_sqlite_migrationstable as a side effect. The listing now runs inside a transaction that is rolled back.sqlite-utils insert ... --pk <missing column>andsqlite-utils extract <missing column>now show a cleanError:message instead of a raw Python traceback. Theextractcommand also shows a clean error when pointed at a view.- Fixed a bug where running
table.extract()more than once against the same lookup table inserted duplicate rows for values containingnull- SQLite unique indexes treatNULLvalues as distinct, soINSERT OR IGNOREalone could not dedupe them. Each repeat extract added another copy that nothing referenced. The insert now uses anIS-basedNOT EXISTSguard sonull-containing rows match existing lookup rows. db.add_foreign_keys()no longer silently ignores requestedON DELETE/ON UPDATEactions when a foreign key with the same columns already exists - it raisesAlterErrorsuggestingtable.transform(), since the actions of an existing foreign key cannot be changed in place. Exact duplicates, including actions, are still skipped so repeated calls stay idempotent. The method also now validates that compound foreign keys have the same number of columns on both sides, instead of silently discarding the extra columns.db.ensure_autocommit_on()now raisesTransactionErrorif called while a transaction is open. Assigningisolation_levelcommits any pending transaction as a side effect, so entering the block silently committed the caller’s open transaction and made a laterrollback()a no-op.sqlite-utils migrate --stop-beforenow exits with an error if the named migration has already been applied. Previously the name passed validation but was only checked against pending migrations, so every migration after it was silently applied - the exact outcome--stop-beforeexists to prevent.Migrations.apply(db, stop_before=...)raisesValueErrorin the same situation, before applying anything.- Fixed a regression where
table.insert(..., pk=..., alter=True)raisedInvalidColumnsif the primary key column did not exist in the table yet. Withalter=Truethe check now waits until the record keys are known, so a pk column supplied by the records is added by the alter as it was in 3.x. A pk column found in neither the table nor the records still raisesInvalidColumns. - Fixed a bug where inserting CSV or TSV data into an existing table rewrote that table’s column types to match the incoming file. Type detection is the default in 4.0, so
sqlite-utils insert data.db places places.csv --csvagainst a table with aTEXTzip code column would convert the column toINTEGERand corrupt values with leading zeros -"01234"became1234. Detected types are now only applied when theinsertorupsertcommand creates the table. - Fixed
pks_and_rows_where()raisingAttributeErrorwhen called on a view, and no longer double-quotes the synthesizedrowidcolumn in its generated SQL - SQLite turns a double-quoted identifier that does not resolve into a string literal, which on a view produced a confusingKeyErrorinstead of theOperationalErrorraised in 3.x. Compound primary keys returned by this method now followPRIMARY KEYdeclaration order. - The
foreign_keys=argument tocreate()andinsert()accepts a mixed list ofForeignKeyobjects, tuples and column name strings again. In 4.0 pre-releases mixingForeignKeyobjects with tuples raised aValueError- a regression from 3.x, whereForeignKeywas anamedtupleand passed the tuple checks. ForeignKeyobjects are hashable again. The 4.0 change fromnamedtupleto dataclass accidentally made them unhashable, breaking patterns likeset(table.foreign_keys)that worked in 3.x.ForeignKeyis now a frozen dataclass - immutable and hashable, like the namedtuple was.- Fixed a bug where compound primary key columns were returned in table column order instead of
PRIMARY KEYdeclaration order. For a table declared asCREATE TABLE other (b TEXT, a TEXT, PRIMARY KEY (a, b))an implicitFOREIGN KEY (x, y) REFERENCES otherwas introspected as referencing(b, a)when SQLite resolves it as(a, b)- runningtransform()on such a table then rewrote the schema with the inverted column order, silently reversing the meaning of the constraint and causing foreign key errors on valid data.table.pks, compound foreign key guessing andtransform()now all use the primary key declaration order, andtransform()no longer reorders a compoundPRIMARY KEY (b, a)into table column order.