Breaking changes:
- Write statements executed with
db.execute()are now committed automatically, unless a transaction is already open in which case they join it. Previously they opened an implicit transaction that stayed open until something committed it - writes appeared to work when read on the same connection but were silently rolled back when the connection closed. Code that relied on rolling back uncommitteddb.execute()writes should use the newdb.begin()method to open an explicit transaction first. The transaction model is documented in full at Transactions and saving your changes. db.query()now executes its SQL as soon as it is called, rather than waiting until the returned generator is first iterated. Rows are still fetched lazily during iteration. SQL errors are now raised at the call site, statements such asINSERT ... RETURNINGare executed and committed immediately without needing to iterate over their results, and passing a statement that returns no rows - previously a silent no-op - now raises aValueErrorrecommendingdb.execute()instead. A statement rejected this way is rolled back before the error is raised, so it has no effect on the database.- Python API validation errors now raise
ValueErrorinstead ofAssertionError. Previously invalid arguments - such ascreate_table()with no columns,transform()on a table that does not exist, or passing bothignore=Trueandreplace=True- were rejected using bareassertstatements, which are silently skipped when Python runs with the-Oflag. Code that caughtAssertionErrorfor these cases should catchValueErrorinstead. table.upsert()andtable.upsert_all()now raisePrimaryKeyRequiredif a record is missing a value for any primary key column, or has a value ofNonefor one. Previously such records - which can never match an existing row - were quietly inserted as brand new rows, or triggered a confusingKeyErrorafter the insert had already taken place.db.enable_wal()anddb.disable_wal()now raise asqlite_utils.db.TransactionErrorif called while a transaction is open. Previously they would silently commit the open transaction as a side effect of changing the journal mode, breaking the rollback guarantee ofdb.atomic()and of user-managed transactions.- The
Viewclass no longer has anenable_fts()method. It existed only to raiseNotImplementedError, since full-text search is not supported for views - calling it now raisesAttributeErrorinstead, and the method no longer appears in the API reference. Thesqlite-utilsenable-ftscommand shows a clean error when pointed at a view. - The no-op
-d/--detect-typesflag has been removed from theinsertandupsertcommands. Type detection has been the default for CSV/TSV data since 4.0a1, so the flag did nothing - invocations using it should simply drop it.--no-detect-typesremains available to disable detection. Database()now raises asqlite_utils.db.TransactionErrorif passed a connection created with the Python 3.12+sqlite3.connect(..., autocommit=True)orautocommit=Falseoptions.commit()androllback()behave differently on those connections, which previously caused every write made by the library to be silently discarded when the connection closed.
Everything else:
- Fixed a bug where
table.delete_where(),table.optimize()andtable.rebuild_fts()did not commit their changes, leaving the connection inside an open transaction. Their work - and any subsequent writes - could then be silently rolled back when the connection was closed. All three now usedb.atomic(), consistent with the other write methods. - The
sqlite-utils drop-tablecommand now refuses to drop a view, anddrop-viewrefuses to drop a table. Previously each would silently drop the wrong type of object if the name matched. Both now exit with an error suggesting the correct command to use. - Migrations applied by the new migrations system now run inside a transaction, together with the record of the migration having been applied. If a migration raises an exception its changes are rolled back and it stays pending, so it can be safely re-applied after the error is fixed. Migrations that cannot run inside a transaction, such as those executing
VACUUM, can opt out using@migrations(transactional=False)- see Migrations and transactions. table.upsert()andtable.upsert_all()now detect the primary key or compound primary key of an existing table, so thepk=argument is no longer required when upserting into a table that already has a primary key.db.table(table_name).insert({})can now be used to insert a row consisting entirely of default values into an existing table, usingINSERT INTO ... DEFAULT VALUES. (#759)- Improvements to the
sqlite-utils migratecommand:--stop-beforevalues that do not match any known migration are now an error instead of being silently ignored,--stop-beforenow works correctly with migration files that still use the oldersqlite_migrate.Migrationsclass, and--listis now a read-only operation that no longer creates the database file or the migrations tracking table.migrations.applied()now returns migrations in the order they were applied. - New
db.begin(),db.commit()anddb.rollback()methods for taking manual control of transactions, as an alternative to thedb.atomic()context manager. - New documentation: Transactions and saving your changes describes how transactions work and when changes are committed, and a new Upgrading page details the changes needed to move between major versions.