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>
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
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
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