The version numbers on Simon Willison’s sqlite-utils are moving fast — 4.0 landed, then 4.1 “a few days ago,” and 4.1.1 the very next day. Individually these are unremarkable maintenance drops. Read together, they tell a quieter story about how small, sharp tools get built now: with an LLM riding shotgun as an unpredictable QA tester.
The feature: code as a first-class input
The headline of 4.1 is a new --code option on insert and upsert. Instead of importing rows from a file, you can hand the CLI a block of Python (inline or as a .py path) that defines a rows() function or a rows iterable. That’s a subtle but meaningful shift in posture: the tool stops assuming your data lives in a static file and starts treating generation logic as a valid source. For anyone scripting ETL glue, it collapses a two-step “write a script that emits JSON, then pipe it in” dance into one command.
It fits the sqlite-utils philosophy — meet data wherever it already is — and it’s the kind of ergonomics improvement that only shows up once you’ve felt the friction enough times.
The fix: a transaction guardrail
4.1.1 is where it gets interesting. table.transform() — the workhorse that rebuilds a table to change its schema — now raises a TransactionError if you call it mid-transaction while PRAGMA foreign_keys is on and the table is referenced by foreign keys carrying destructive ON DELETE actions: CASCADE, SET NULL, or SET DEFAULT.
That is a precise, nasty edge case. SQLite’s transform() works by creating a new table, copying data, dropping the old one, and renaming. If foreign keys with cascading deletes are live inside an open transaction, that drop-and-swap can silently trigger data loss in other tables. Refusing to run — loudly — is exactly right. A footgun that fails closed beats one that fails silently.
The tester: “regular Claude chat”
The detail worth pausing on is provenance. Per the release notes, the bug was surfaced by “regular Claude chat” while someone was experimenting with the 4.1 release to answer a question about ON DELETE behavior. Nobody filed it from a production incident. An LLM, asked to reason about foreign-key semantics, wandered into a corner the maintainer hadn’t hardened.
This is a genuinely new development loop. Not “AI writes the code,” but AI-as-curious-user — generating the weird combinations of PRAGMAs and cascade rules that a human might never think to try. The value isn’t automation; it’s coverage of the improbable.
Why it matters
None of this is a landmark release. But it’s a clean snapshot of where tooling is heading: fast iteration, defensive guardrails around genuinely dangerous operations, and an emerging feedback channel where chatting with a model about your own library shakes out edge cases. The 24-hour turnaround from feature to fix suggests the loop is already tight.