tursodatabase/turso · error · ProgrammingError
executemany() requires a single DML statement
Error message
executemany() requires a single DML statement
What it means
ProgrammingError raised by Cursor.executemany() (connection.py:252-259) when the SQL text is not classified as DML by _is_dml() (dbapi.py:88-93). The classifier takes the first keyword of the statement, skipping whitespace and -- and /* */ comments, and accepts only INSERT, UPDATE, DELETE, or REPLACE — matching sqlite3, which restricts executemany to a single DML statement. Two non-obvious rejections: multi-statement strings ('INSERT ...; INSERT ...') fail because only the first statement counts, and WITH-prefixed DML ('WITH x AS (...) INSERT ...') is rejected on purpose to avoid false positives.
Source
Thrown at serverless/python/turso_serverless/connection.py:259
self._rowcount = -1
else:
self._description = None
self._rows = []
self._rowcount = result.affected_rows
if result.last_insert_rowid is not None and _is_insert_or_replace(sql):
self._lastrowid = result.last_insert_rowid
return self
def executemany(self, sql: str, seq_of_parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> Cursor:
self._ensure_open()
self._rows = []
self._row_index = 0
self._description = None
if not _is_dml(sql):
raise ProgrammingError("executemany() requires a single DML statement")
self._connection._maybe_implicit_begin(sql)
total = 0
for parameters in seq_of_parameters:
args, named_args = self._convert_params(parameters)
result = self._connection._execute_stmt(
sql, params=args, named_params=named_args, want_rows=False,
)
total += result.affected_rows
self._rowcount = total
return self
def executescript(self, sql_script: str) -> Cursor:
"""Execute multiple statements via the pipeline sequence endpoint."""
self._ensure_open()
self._rows = []View on GitHub (pinned to bad083fafb)
Solutions
- Use execute() per row or executescript() for multi-statement or non-DML SQL
- Pass exactly one INSERT/UPDATE/DELETE/REPLACE statement to executemany
- Rewrite CTE DML as a plain statement (e.g. 'INSERT INTO t (x) VALUES (?)') so the classifier accepts it
Example fix
// before
cur.executemany("INSERT INTO t VALUES (?); SELECT changes()", rows)
// after
cur.executemany("INSERT INTO t VALUES (?)", rows) Defensive patterns
Strategy: validation
Validate before calling
import re
_DML_RE = re.compile(r"^(?:--[^\n]*\n|/\*.*?\*/|\s)*(INSERT|UPDATE|DELETE|REPLACE)\b", re.IGNORECASE | re.DOTALL)
def is_executemany_safe(sql: str) -> bool:
"""Mirror of the driver's first-keyword DML check (WITH is rejected)."""
return bool(_DML_RE.match(sql))
if not is_executemany_safe(sql):
raise ValueError(f"executemany needs a single INSERT/UPDATE/DELETE/REPLACE: {sql[:40]!r}") Try / catch
from turso_serverless.dbapi import ProgrammingError
try:
cur.executemany(sql, rows)
except ProgrammingError as e:
if "requires a single DML statement" not in str(e):
raise
if ";" in sql.strip().rstrip(";"):
conn.executescript(sql) # multi-statement script
else:
for row in rows: # non-DML: run per row
cur.execute(sql, row) Prevention
- Reserve executemany for exactly one INSERT/UPDATE/DELETE/REPLACE statement
- Keep multi-statement strings for executescript(), never executemany()
- Remember WITH-prefixed DML is rejected by design — rewrite as a plain statement
When it happens
Trigger: cur.executemany("SELECT ...", rows); executemany with DDL (CREATE/ALTER); a batch string containing two statements separated by ';'; an upsert written as 'WITH ... INSERT ... SELECT'.
Common situations: Porting code that concatenates statements into one batch string; trying to seed schema with executemany instead of executescript(); CTE-based bulk upserts moved from execute() to executemany() for speed.
Related errors
- executemany() requires a single DML (INSERT/UPDATE/DELETE/RE
- no SQL statements to execute
- You can only execute one statement at a time
- Cannot operate on a closed cursor
- Named parameters are not supported; use positional parameter
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/5bb40346ba8f73c1.
Report an issue: GitHub.