{"record":{"id":"b42a39809543239d","repo":"tursodatabase/turso","slug":"executemany-requires-a-single-dml-insert-update","errorCode":null,"errorMessage":"executemany() requires a single DML (INSERT/UPDATE/DELETE/REPLACE) statement","messagePattern":"executemany\\(\\) requires a single DML \\(INSERT/UPDATE/DELETE/REPLACE\\) statement","errorType":"exception","errorClass":"ProgrammingError","httpStatus":null,"severity":"error","filePath":"bindings/python/turso/lib.py","lineNumber":761,"sourceCode":"                    return value\n                try:\n                    return int(value)\n                except Exception:\n                    return self._lastrowid\n            # Finalize anyway\n            q.finalize()\n        except Exception:\n            # Ignore errors; lastrowid remains unchanged on failure\n            pass\n        return self._lastrowid\n\n    def executemany(self, sql: str, seq_of_parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> \"Cursor\":\n        self._ensure_open()\n        self._reset_last_result()\n\n        # executemany only accepts DML; enforce this to match sqlite3 semantics\n        if not _is_dml(sql):\n            raise ProgrammingError(\"executemany() requires a single DML (INSERT/UPDATE/DELETE/REPLACE) statement\")\n\n        # Implement legacy implicit transaction: same as execute()\n        self._maybe_implicit_begin(sql)\n\n        prepared = self._prepare_single_statement(sql)\n        stmt = prepared.stmt\n        try:\n            # For executemany, discard any rows produced (even if RETURNING was used)\n            # Therefore we ALWAYS use execute() path per-iteration.\n            for parameters in seq_of_parameters:\n                # Reset previous bindings and program memory before reusing\n                stmt.reset()\n                self._bind_params(stmt, parameters)\n                result = _run_execute_with_io(stmt, self._connection.extra_io)\n                # rowcount is \"the number of modified rows\" for the LAST executed statement only\n                self._rowcount = int(result.rows_changed) + (self._rowcount if self._rowcount != -1 else 0)\n            # After loop, finalize statement\n            stmt.finalize()","sourceCodeStart":743,"sourceCodeEnd":779,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/bindings/python/turso/lib.py#L743-L779","documentation":"executemany() runs an _is_dml check before preparing: it only accepts a single INSERT, UPDATE, DELETE, or REPLACE statement, matching sqlite3 semantics. SELECT, CREATE/ALTER/DROP, PRAGMA, or multi-statement SQL raise ProgrammingError immediately. sqlite3 also discards rows for DML with RETURNING under executemany, so DML-with-RETURNING is allowed but rows are thrown away.","triggerScenarios":"`cur.executemany(\"SELECT ...\", [...])`, `cur.executemany(\"CREATE TABLE ...\", [])`, executemany of a script containing multiple statements, or passing an empty/None SQL string (not DML).","commonSituations":"Generic batch helpers that route any SQL through executemany; migration code that batches DDL; passing a SELECT with a parameter list expecting per-row results.","solutions":["Use execute() (optionally in a loop) for SELECT/DDL/PRAGMA — only batch INSERT/UPDATE/DELETE/REPLACE via executemany","Split mixed scripts: run DDL with execute/executescript, then batch the DML with executemany","If you need rows back per iteration, loop execute() and consume results; executemany discards RETURNING rows by design"],"exampleFix":"# before\ncur.executemany(\"SELECT * FROM t WHERE id = ?\", [(1,), (2,)])  # ProgrammingError\n\n# after\nfor id_ in (1, 2):\n    for row in cur.execute(\"SELECT * FROM t WHERE id = ?\", (id_,)):\n        process(row)","handlingStrategy":"validation","validationCode":"import re\n\ndef is_dml(sql: str) -> bool:\n    first = re.search(r\"\\S\", sql)\n    head = sql[first.start():].lstrip(\"(\").split(None, 1)[0].upper() if first else \"\"\n    return head in {\"INSERT\", \"UPDATE\", \"DELETE\", \"REPLACE\"}\n\nif is_dml(sql):\n    cur.executemany(sql, params_seq)\nelse:\n    raise ValueError(f\"executemany needs DML, got: {sql[:40]!r}\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Batch only INSERT/UPDATE/DELETE/REPLACE with executemany — everything else goes through execute/executescript","Remember executemany discards RETURNING rows by design; loop execute() when you need results","Guard generic batch helpers with an _is_dml-style check so misuse fails with your own message"],"tags":["python","executemany","dml","db-api","batch"],"backgroundTag":"executemany-non-dml","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}