{"record":{"id":"5bb40346ba8f73c1","repo":"tursodatabase/turso","slug":"executemany-requires-a-single-dml-statement","errorCode":null,"errorMessage":"executemany() requires a single DML statement","messagePattern":"executemany\\(\\) requires a single DML statement","errorType":"exception","errorClass":"ProgrammingError","httpStatus":null,"severity":"error","filePath":"serverless/python/turso_serverless/connection.py","lineNumber":259,"sourceCode":"            self._rowcount = -1\n        else:\n            self._description = None\n            self._rows = []\n            self._rowcount = result.affected_rows\n\n        if result.last_insert_rowid is not None and _is_insert_or_replace(sql):\n            self._lastrowid = result.last_insert_rowid\n\n        return self\n\n    def executemany(self, sql: str, seq_of_parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> Cursor:\n        self._ensure_open()\n        self._rows = []\n        self._row_index = 0\n        self._description = None\n\n        if not _is_dml(sql):\n            raise ProgrammingError(\"executemany() requires a single DML statement\")\n\n        self._connection._maybe_implicit_begin(sql)\n\n        total = 0\n        for parameters in seq_of_parameters:\n            args, named_args = self._convert_params(parameters)\n            result = self._connection._execute_stmt(\n                sql, params=args, named_params=named_args, want_rows=False,\n            )\n            total += result.affected_rows\n\n        self._rowcount = total\n        return self\n\n    def executescript(self, sql_script: str) -> Cursor:\n        \"\"\"Execute multiple statements via the pipeline sequence endpoint.\"\"\"\n        self._ensure_open()\n        self._rows = []","sourceCodeStart":241,"sourceCodeEnd":277,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/serverless/python/turso_serverless/connection.py#L241-L277","documentation":"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.","triggerScenarios":"cur.executemany(\"SELECT ...\", rows); executemany with DDL (CREATE/ALTER); a batch string containing two statements separated by ';'; an upsert written as 'WITH ... INSERT ... SELECT'.","commonSituations":"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.","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"],"exampleFix":"// before\ncur.executemany(\"INSERT INTO t VALUES (?); SELECT changes()\", rows)\n\n// after\ncur.executemany(\"INSERT INTO t VALUES (?)\", rows)","handlingStrategy":"validation","validationCode":"import re\n\n_DML_RE = re.compile(r\"^(?:--[^\\n]*\\n|/\\*.*?\\*/|\\s)*(INSERT|UPDATE|DELETE|REPLACE)\\b\", re.IGNORECASE | re.DOTALL)\n\n\ndef is_executemany_safe(sql: str) -> bool:\n    \"\"\"Mirror of the driver's first-keyword DML check (WITH is rejected).\"\"\"\n    return bool(_DML_RE.match(sql))\n\n\nif not is_executemany_safe(sql):\n    raise ValueError(f\"executemany needs a single INSERT/UPDATE/DELETE/REPLACE: {sql[:40]!r}\")","typeGuard":null,"tryCatchPattern":"from turso_serverless.dbapi import ProgrammingError\n\ntry:\n    cur.executemany(sql, rows)\nexcept ProgrammingError as e:\n    if \"requires a single DML statement\" not in str(e):\n        raise\n    if \";\" in sql.strip().rstrip(\";\"):\n        conn.executescript(sql)          # multi-statement script\n    else:\n        for row in rows:                 # non-DML: run per row\n            cur.execute(sql, row)","preventionTips":["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"],"tags":["python","db-api","executemany","dml","sql"],"backgroundTag":"invalid-sql-statement-type","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}