Commit graph

1,105 commits

Author SHA1 Message Date
Simon Willison
0ec0180405 add_foreign_key() on_delete= and on_update= parameters, closes #530
table.add_foreign_key() now accepts on_delete= and on_update= to
create foreign keys with ON DELETE/ON UPDATE actions:

    table.add_foreign_key("author_id", "authors", "id", on_delete="CASCADE")

Works for compound foreign keys too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:49:20 -07:00
Simon Willison
658185d297 Fix compound foreign key test failure against sqlean
test_create_table_compound_foreign_key_enforced caught the stdlib
sqlite3.IntegrityError, but sqlean's IntegrityError is not a subclass
of it. Import sqlite3 from sqlite_utils.utils like the other test
modules, so the right exception is used whichever backend is installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:46:16 -07:00
Simon Willison
5b61530965 db.add_foreign_keys() no longer drops ON DELETE/ON UPDATE actions
ForeignKey objects passed to db.add_foreign_keys() were flattened to
plain (table, column, other_table, other_column) tuples before being
handed to transform(), silently discarding their on_delete and
on_update actions. The method now carries ForeignKey objects through
to transform() intact - tuple inputs are converted to ForeignKey
objects up front, which also simplifies the validation loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:42:41 -07:00
Simon Willison
2f599fc7c6 Run ty in just lint, matching CI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:38:05 -07:00
Simon Willison
8dfbfa80b8 Fix ty errors in transform_sql foreign key closures
The fk_should_be_dropped and fk_with_renamed_columns closures captured
the drop and rename parameters, but type checkers do not narrow
captured variables inside nested functions so ty saw their Optional
declared types. Bind the already-normalized values to fresh names for
the closures to use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:36:46 -07:00
Simon Willison
d100264e9c Normalize compound foreign key columns to tuples, promote tuples in docs
ForeignKey.columns and .other_columns are now tuples rather than lists,
both when introspected and when constructed directly (lists are
normalized in __post_init__). Tuples are now the documented form for
specifying compound foreign keys everywhere - in create_table()
foreign_keys=, add_foreign_key() and drop_foreign_keys=:

    foreign_keys=[(("campus_name", "dept_code"), "departments")]

Lists continue to work as input but are no longer documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:10:21 -07:00
Simon Willison
c16edb2dc4 Better type signatures for compound foreign key columns
New ForeignKeyColumns type alias - a column name or a list/tuple of
column names - used by add_foreign_key(), add_foreign_keys() (via the
ForeignKeyTuple alias, replacing its Any slots) and the
ForeignKeyIndicator union, which it also simplifies. Tuples now
type-check anywhere lists are accepted.

ForeignKey.__post_init__ normalizes tuple columns/other_columns to
lists so direct construction with tuples compares equal to
introspected foreign keys.

Refs https://github.com/simonw/sqlite-utils/pull/770/changes#r3525703477

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:03:43 -07:00
Simon Willison
42c1dd0d5f Documentation for compound foreign key support, refs #594
- Changelog entries for all the compound foreign key work
- Upgrading guide notes the transform() behavior changes
- ForeignKey added to the API reference
- Updated .foreign_keys introspection example output

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:45:08 -07:00
Simon Willison
8443d7f3ba Resolve implicit primary key references in table.foreign_keys
For foreign keys declared as "REFERENCES other_table" with no explicit
columns, PRAGMA foreign_key_list returns None for the referenced
column. The foreign_keys property now resolves these to the other
table's primary key columns, so other_column=None unambiguously
indicates a compound foreign key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:42:30 -07:00
Simon Willison
be27a96484 Capture and preserve foreign key ON DELETE/ON UPDATE actions
ForeignKey gains on_delete and on_update fields (default "NO ACTION"),
populated from PRAGMA foreign_key_list. create_table_sql() renders the
corresponding clauses for both inline and compound table-level foreign
keys, which means table.transform() now preserves actions such as
ON DELETE CASCADE - previously they were silently stripped whenever a
table was transformed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:40:57 -07:00
Simon Willison
b75edf4b30 add_foreign_key() accepts compound keys, composite FK indexes, refs #594
table.add_foreign_key() and db.add_foreign_keys() now accept lists of
column names to create compound foreign keys:

    table.add_foreign_key(
        ["campus_name", "dept_code"], "departments"
    )

Omitting the other columns uses the compound primary key of the other
table. Duplicate detection compares the full column lists.

db.index_foreign_keys() now creates a single composite index across the
columns of a compound foreign key, rather than an index per column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:37:32 -07:00
Simon Willison
7d43fd50e1 transform() now round-trips compound foreign keys, refs #594
Compound foreign keys are passed through table.transform() intact
instead of being degraded to per-column single foreign keys:

- rename= applies to each member column of a compound key
- drop= of any member column drops the whole constraint, matching
  the existing single-column behavior
- drop_foreign_keys= accepts a bare column name (drops any foreign
  key that column participates in) or a tuple of columns (drops the
  compound key with exactly those columns)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:33:15 -07:00
Simon Willison
577f3011e5 Create tables with compound foreign keys, refs #594
create_table() and friends now accept compound foreign keys, specified
as lists of column names in the existing tuple forms:

    foreign_keys=[
        (["campus_name", "dept_code"], "departments"),
        (["campus_name", "dept_code"], "departments", ["campus_name", "dept_code"]),
    ]

The two-item form guesses the compound primary key of the other table.
Compound keys are rendered as table-level FOREIGN KEY constraints,
while single-column keys keep their inline REFERENCES clauses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:30:41 -07:00
Simon Willison
d5bf51df35 Fix TypeError when sorting mixed compound/single foreign keys
sorted() on a foreign_keys list containing both compound (column=None)
and single-column ForeignKeys raised TypeError because dataclass
ordering compared None against str. column/other_column are now
excluded from comparison - ordering and equality use the always
populated columns/other_columns lists instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:26:56 -07:00
Simon Willison
f10459cffb table.foreign_keys now handles compound foreign keys, refs #594
This is a breaking change, since the return type is now a dataclass
and not a namedtuple.
2026-07-05 13:59:25 -07:00
Simon Willison
623331b3f4 Fix for test failure against sqlean 2026-07-05 12:16:11 -07:00
Simon Willison
61619498fa Give db.query() SQL restrictions top billing in upgrading docs
Rejecting statements that return no rows is the change most likely
to require code edits when upgrading to 4.0, so it now leads the
Python API changes section - before db.table() - with the full list
of rejected statement types and a before/after example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:50:10 -07:00
Simon Willison
87cf1c5a00 Replace SQL prefix matching with a first-token scanner
query() and execute() classified statements using
sql.lstrip().upper().startswith(...), which a leading SQL comment
defeated: db.query('/* c */ COMMIT') inside db.atomic() committed
the caller's transaction and masked the ValueError with a 'no such
savepoint' error, comment-prefixed PRAGMAs ran inside the savepoint
guard where journal mode changes are refused, and a comment-prefixed
BEGIN passed to db.execute() was instantly auto-committed.

The new _first_keyword() helper skips leading whitespace and -- or
/* */ comments - the only things SQLite's tokenizer allows before
the first token - and returns that token for exact comparison, so
keyword detection now matches what SQLite itself will see.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:49:59 -07:00
Simon Willison
a8e3a643f2 Release 4.0rc2 4.0rc2
Refs #759, #629, #761, #767, #768
2026-07-04 17:45:42 -07:00
Simon Willison
0566a9f128
Improve db.query() error handling and transaction safety
db.query() rejected statements roll back, RETURNING commits immediately.

Fixes two transaction bugs in db.query():

- A non-row-returning statement such as an UPDATE was executed and
  auto-committed by execute() before the ValueError was raised, so the
  write took effect despite being rejected. query() now runs the
  statement inside a savepoint and rolls it back before raising, so a
  rejected statement has no effect on the database - in every
  connection mode, including autocommit=True.

- INSERT ... RETURNING only committed once the returned generator was
  fully exhausted, so calling query() without iterating - or partially
  iterating with next() - left the transaction open and the write could
  be rolled back on close. The write is now completed and committed at
  call time, as the documentation already promised. Plain SELECTs are
  still fetched lazily.

Transaction control statements, VACUUM, ATTACH and DETACH never return
rows, so query() now rejects them without executing them. PRAGMA
statements skip the savepoint guard because some of them - such as
pragma journal_mode=wal - refuse to run inside a transaction.

Claude-Session: https://claude.ai/code/session_012U3iRfJoTZ5vd22cBSF2nJ
2026-07-04 17:40:48 -07:00
Simon Willison
04f8971546
Merge changes based on a Fable review of 4.0rc1
Review of what it found: 0c369a447e/fable-review-4.0rc1.md

Full details here in the PR: https://github.com/simonw/sqlite-utils/pull/767
2026-07-04 16:36:13 -07:00
Claude
7a015cff52
Unwrap hard-wrapped prose in the documentation
The documentation style for this project is one line per paragraph,
not text-wrapped RST. Unwraps three recently added paragraphs in
python-api.rst and two older ones in plugins.rst.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 23:33:48 +00:00
Simon Willison
89af511f79
Also run "pytest --sqlite-autocommit" for one matrix item in CI
To test that our code works correctly with the new autocommit option introduced in Python 3.13+

https://github.com/simonw/sqlite-utils/pull/767#discussion_r3523983636
2026-07-04 16:24:07 -07:00
Claude
b49af65a6d
Fix PytestRemovedIn10Warning in test_sniff
parametrize received a generator from Path.glob(), which pytest 10
will reject. sorted() materializes the list and makes the parameter
order deterministic as a bonus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 23:20:31 +00:00
Claude
2510745de4
Add --sqlite-autocommit pytest option
Runs the entire test suite against connections created with the
Python 3.12+ sqlite3.connect(autocommit=True) mode, by patching
connect() to default autocommit=True for internally-created
connections. Errors on older Python versions. The full suite
passes in both modes with no test changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 23:19:51 +00:00
Claude
f7ff3e2027
db.execute() write statements now commit automatically
Raw write statements previously opened an implicit transaction that
stayed open until something committed it - the write was visible on
the same connection, making it look saved, but was silently rolled
back when the connection closed. execute() now commits any implicit
transaction it opens, so raw writes behave like every other write
in the library: committed as soon as they run, unless an explicit
transaction (db.begin() or db.atomic()) is open, in which case they
join it.

Row-returning writes such as INSERT ... RETURNING via db.query()
commit once their rows have been iterated. atomic(), commit() and
rollback() now issue literal COMMIT/ROLLBACK statements, which have
identical semantics in every sqlite3 connection mode. The CLI bulk
command uses db.atomic() instead of 'with db.conn:'.

This simplifies the transaction contract to: everything commits
immediately unless you explicitly opened a transaction. Docs
updated throughout; changelog and upgrading guide cover the
breaking change for code that relied on rolling back uncommitted
execute() writes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 23:15:01 +00:00
Claude
bcd9a26560
Reject Python 3.12+ autocommit connections in the Database constructor
Connections created with sqlite3.connect(autocommit=True) make
commit() a documented no-op, and autocommit=False connections are
permanently inside a transaction - in both modes every write made
by this library appeared to work in-process but was silently
discarded when the connection closed. Database() now raises
TransactionError for these connections instead of losing data.

Tests are skipped on Python versions before 3.12, where the
autocommit parameter does not exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 22:38:31 +00:00
Claude
96793668ed
Raise TransactionError instead of RuntimeError from enable_wal/disable_wal
A dedicated sqlite_utils.db.TransactionError exception is clearer
to catch than a generic RuntimeError, and gives future
transaction-state guards a home.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 22:32:36 +00:00
Claude
788c393a30
Add db.begin(), db.commit() and db.rollback() methods
Manual transaction control previously required mixing raw
db.execute("begin") strings with methods on db.conn. These thin
wrappers give the documentation and callers a single consistent
idiom. Docs updated to use them throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 22:31:22 +00:00
Claude
6c88067ab7
Explain why the Database context manager rolls back instead of committing
Expands the closing-a-database docs: exit is equivalent to close(),
open transactions can only come from manual transaction control,
auto-committing could silently persist half-finished work, and the
behavior deliberately differs from sqlite3.Connection's own context
manager.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 22:25:28 +00:00
Claude
ff76fb7740
Clarify that db.atomic() is fine inside transactional migrations
The manual-transaction warning could be read as covering
db.atomic() too. Nested atomic() blocks are safe - they become
savepoints inside the migration's transaction, preserving the
all-or-nothing rollback guarantee.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 22:20:37 +00:00
Claude
0fca5908c1
Group the Unreleased changelog into breaking changes and everything else
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 22:19:47 +00:00
Claude
adea475a61
Remove View.enable_fts()
The method existed only to raise NotImplementedError, since
full-text search is not supported for views, and it showed up in
the generated API reference as a documented View method. Calling
enable_fts() on a View now raises AttributeError like any other
missing method. The sqlite-utils enable-fts command uses db.table()
and shows a clean error when pointed at a view instead of a
traceback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 20:44:04 +00:00
Claude
70055d7432
Document the transaction model as a first-class concept
New top-level 'Transactions and saving your changes' section in the
Python API documentation, stating the fundamental contract up
front: every write method commits its own changes before returning,
no commit() call is ever needed. The db.atomic() documentation
moves under it, alongside new subsections covering the two cases
where the user owns the commit - raw db.execute() writes and
manually managed transactions - and the supported connection modes.

Cross-referenced from Getting started (the first insert example now
says the rows are saved immediately), a warning on db.execute(),
the closing-a-database section and the upgrading guide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 20:16:15 +00:00
Claude
509d87b292
Remove review notes files
These were working documents for the 4.0 pre-release review - the
actionable findings have all been fixed on this branch and the
changelog and upgrading guide carry the user-facing record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:56:42 +00:00
Claude
910a90970d
Add upgrading guide to the documentation
New docs page describing the changes needed to upgrade between
major versions, with a detailed 3.x to 4.0 section covering CLI,
Python API and packaging changes, plus brief sections for the
2.x to 3.0 and 1.x to 2.0 upgrades.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:21:34 +00:00
Claude
a2e6ea2eca
Raise ValueError instead of AssertionError for invalid arguments
User-facing argument validation in db.py previously used bare
assert statements, which vanish entirely under python -O and raise
AssertionError - an exception type callers should not have to
catch for input validation. Fifteen validation sites now raise
ValueError with the same messages. Internal invariants (an
unreachable branch and a post-update rowcount check) remain
asserts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:19:24 +00:00
Claude
f1cdceaca9
Remove the no-op -d/--detect-types flag from insert and upsert
Type detection has been the default for CSV/TSV data since 4.0a1,
making this flag a no-op kept only for backwards compatibility.
4.0 is the release where it can be removed - the flag now errors,
prompting scripts to drop it. --no-detect-types is unchanged. The
dead detect_types parameter has been removed from
insert_upsert_implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:15:31 +00:00
Claude
397cdcc491
Polish: clearer errors, corrected docstrings, setuptools pin
- insert/upsert into a name that is actually a view now shows a
  clean error instead of a traceback
- db.view() on a name that is a table now says so in its error
- Database.__getitem__ docstring documents that views are returned
  for view names; hash_id docstring corrected (it is a column name,
  not a boolean)
- Registering two migrations with the same name in one set now
  raises ValueError at registration time instead of executing both
  and failing with IntegrityError at apply time
- build-system requires setuptools>=77, needed for the PEP 639
  license expression

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:13:15 +00:00
Claude
30cc95c0a6
migrate --list is now read-only
pending() and applied() no longer create the _sqlite_migrations
table or perform the one-way legacy schema upgrade - that now only
happens in apply(). The CLI --list path no longer creates the
database file when it does not exist. applied() results are now
explicitly ordered by insertion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:10:56 +00:00
Claude
0bad21280f
migrate --stop-before: validate names and fix legacy compatibility
An unknown --stop-before value previously matched nothing and every
migration was silently applied, including the one the user meant to
stop before. The CLI now errors unless each value matches a known
migration.

The CLI also always passed stop_before as a list, but older
duck-typed sqlite-migrate Migrations objects compare it against a
single string name - so the flag was silently ignored for exactly
the migration files the compatibility shim exists to support. Legacy
sets now receive a single string, with an error if multiple values
target one legacy set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:08:53 +00:00
Claude
fd867282b3
Document atomic() deferred BEGIN, savepoint behavior and connection modes
Documents that atomic() opens a deferred BEGIN, that calling it
while the connection is already inside a transaction produces a
savepoint whose changes only persist when the outer transaction
commits, and that Python 3.12+ autocommit=True/False connections
are not supported. Adds a test pinning the savepoint-inside-manual-
BEGIN behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:06:44 +00:00
Claude
a6fcb5af62
Pin the Database context manager contract
with Database(...) closes the connection on exit without
committing, so uncommitted changes are rolled back. This was the
existing behavior but was undocumented and untested - it is now
stated in the docs and pinned by a test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 19:05:59 +00:00
Claude
70b48a3b16
Use the sqlite3/sqlean shim in test_query.py
test_query_executes_eagerly failed on the sqlean CI matrix jobs
because sqlean.dbapi2.OperationalError is not a subclass of the
stdlib sqlite3.OperationalError. Import sqlite3 via
sqlite_utils.utils like the other test modules do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 18:51:55 +00:00
Claude
c76aad50ae
Correct applied_at type annotation to str
_AppliedMigration.applied_at was annotated datetime.datetime but
the value is the TEXT timestamp read straight from the
_sqlite_migrations table - always a string, matching the format
written by both this module and the older sqlite-migrate package.
Added a test pinning the runtime type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 18:47:08 +00:00
Claude
ffec11cfb7
enable_wal() and disable_wal() refuse to run inside a transaction
Changing the journal mode assigns conn.isolation_level, which
commits any open transaction as a side effect - silently breaking
the rollback guarantee of atomic() blocks and of user-managed
transactions. Both methods now raise RuntimeError if a transaction
is open. Calling them when the database is already in the requested
mode remains a no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 18:46:09 +00:00
Claude
d6753a9f57
upsert() now requires a value for every primary key column
A record missing a primary key value - or with None for one - can
never match an existing row, because a NULL primary key never
satisfies ON CONFLICT. Such records were previously inserted as
brand new rows (silently, for multi-record upsert_all calls) or
triggered a KeyError after the insert had already happened. Both
cases, including upserting an empty record, now raise
PrimaryKeyRequired before any SQL is executed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 18:43:37 +00:00
Claude
286a87dba4
Add Unreleased changelog section covering changes since 4.0rc1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 18:37:20 +00:00
Claude
e762656d06
optimize() and rebuild_fts() now commit their changes
Both ran their INSERT INTO fts(fts) statements via a bare execute()
with no commit, leaving the connection inside an open implicit
transaction - the FTS operation and all subsequent writes were then
silently rolled back when the connection closed. Both are now
wrapped in db.atomic(), matching delete_where() and the other
write operations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 18:35:45 +00:00
Claude
45af93d463
db.query() now executes its SQL immediately
Previously query() was a generator function, so nothing - including
the SQL itself - ran until the result was first iterated. A write
statement passed to query() silently did nothing, and SQL errors
surfaced far from the call site. The SQL now executes as soon as
query() is called, while rows are still fetched lazily during
iteration.

Statements that return no rows now raise a ValueError directing
callers to execute() instead. As a side effect, statements like
INSERT ... RETURNING now work naturally with query().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
2026-07-04 18:24:52 +00:00