From a65df4cc69cd7d994505818b6bb6444e7a0b9193 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 21 Jun 2026 10:18:25 -0700 Subject: [PATCH] Split out _iter_complete_sql_statements To make it clearer why we are avoiding executescript() --- sqlite_utils/db.py | 26 ++++++++++++++++---------- tests/test_atomic.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index e021a97..677a1f8 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -196,6 +196,19 @@ DEFAULT = Default() Tracer = Callable[[str, Optional[Union[Sequence[Any], Dict[str, Any]]]], None] +def _iter_complete_sql_statements(sql: str) -> Generator[str, None, None]: + statement = [] + for char in sql: + statement.append(char) + statement_sql = "".join(statement).strip() + if statement_sql and sqlite3.complete_statement(statement_sql): + yield statement_sql + statement = [] + statement_sql = "".join(statement).strip() + if statement_sql: + yield statement_sql + + COLUMN_TYPE_MAPPING: Dict[Any, str] = { float: "REAL", int: "INTEGER", @@ -618,16 +631,9 @@ class Database: def _executescript(self, sql: str) -> sqlite3.Cursor: if self.conn.in_transaction: cursor = self.conn.cursor() - statement = [] - for char in sql: - statement.append(char) - statement_sql = "".join(statement).strip() - if statement_sql and sqlite3.complete_statement(statement_sql): - cursor.execute(statement_sql) - statement = [] - statement_sql = "".join(statement).strip() - if statement_sql: - cursor.execute(statement_sql) + # avoid sqlite3.executescript()'s implicit commit: + for statement in _iter_complete_sql_statements(sql): + cursor.execute(statement) return cursor return self.conn.executescript(sql) diff --git a/tests/test_atomic.py b/tests/test_atomic.py index c51e542..acd5474 100644 --- a/tests/test_atomic.py +++ b/tests/test_atomic.py @@ -1,8 +1,46 @@ import pytest +from sqlite_utils.db import _iter_complete_sql_statements from sqlite_utils.utils import sqlite3 +@pytest.mark.parametrize( + "sql,expected", + ( + ( + "CREATE TABLE t(id); INSERT INTO t VALUES (1)", + ["CREATE TABLE t(id);", "INSERT INTO t VALUES (1)"], + ), + ( + "INSERT INTO t VALUES ('a;b');", + ["INSERT INTO t VALUES ('a;b');"], + ), + ( + "-- comment;\nCREATE TABLE t(id);", + ["-- comment;\nCREATE TABLE t(id);"], + ), + ( + """ + CREATE TRIGGER t_ai AFTER INSERT ON t + BEGIN + UPDATE t SET value = 'a;b' WHERE id = new.id; + INSERT INTO log VALUES ('x;y'); + END; + """, + [ + "CREATE TRIGGER t_ai AFTER INSERT ON t\n" + " BEGIN\n" + " UPDATE t SET value = 'a;b' WHERE id = new.id;\n" + " INSERT INTO log VALUES ('x;y');\n" + " END;" + ], + ), + ), +) +def test_iter_complete_sql_statements(sql, expected): + assert list(_iter_complete_sql_statements(sql)) == expected + + def test_atomic_commits(fresh_db): with fresh_db.atomic(): fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id") @@ -54,9 +92,10 @@ def test_executescript_does_not_commit_open_atomic_block(fresh_db): CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT); CREATE TRIGGER dogs_ai AFTER INSERT ON dogs BEGIN - UPDATE dogs SET name = upper(new.name) WHERE id = new.id; + UPDATE dogs SET name = upper(new.name) || '; updated' WHERE id = new.id; END; - INSERT INTO dogs VALUES (1, 'Cleo'); + -- This comment has a semicolon; + INSERT INTO dogs VALUES (1, 'Cleo; the first'); """) raise RuntimeError("boom")