tursodatabase/turso · error · ProgrammingError

batch statement {index} must be a SQL string or a (sql, para

Error message

batch statement {index} must be a SQL string or a (sql, parameters) pair

What it means

Each entry passed to Connection.batch() must be either a plain SQL string or a 2-item (sql, parameters) tuple/list whose first element is a string. _normalize_batch_statements raises this ProgrammingError for any item that doesn't fit those shapes. It fails fast client-side, before any network or native call.

Source

Thrown at bindings/python/turso/lib.py:249

def _normalize_batch_statements(
    statements: Iterable[Any],
) -> list[tuple[str, Sequence[Any] | Mapping[str, Any]]]:
    """Normalize batch input to (sql, parameters) pairs. Accepts SQL
    strings and (sql, parameters) pairs."""
    normalized = []
    for index, statement in enumerate(statements):
        if isinstance(statement, str):
            normalized.append((statement, ()))
            continue
        if (
            isinstance(statement, (tuple, list))
            and len(statement) == 2
            and isinstance(statement[0], str)
        ):
            normalized.append((statement[0], statement[1]))
            continue
        raise ProgrammingError(
            f"batch statement {index} must be a SQL string or a (sql, parameters) pair"
        )
    return normalized


def _reject_transaction_control_statements(
    statements: list[tuple[str, Sequence[Any] | Mapping[str, Any]]],
) -> None:
    for index, (sql, _parameters) in enumerate(statements):
        if _first_keyword(sql) in _TRANSACTION_CONTROL_KEYWORDS:
            error = ProgrammingError(
                "transaction-control SQL is not allowed in a batch with a transaction mode"
            )
            raise _batch_statement_error(index, error) from error


def _batch_statement_error(
    index: int,

View on GitHub (pinned to c1e5928725)

Solutions

  1. Ensure every item is either a str or a 2-tuple/list (sql_string, parameters).
  2. Convert dict-style statements: {"sql": s, "params": p} -> (s, p).
  3. Trim extra elements from tuples; parameters must be the single second element.
  4. Print/type-check the offending item at the given index in the message; the error names the exact index.

Example fix

# before
conn.batch([{"sql": "SELECT 1"}, ("SELECT ?", (1,), "extra")])
# after
conn.batch(["SELECT 1", ("SELECT ?", (1,))])
Defensive patterns

Strategy: validation

Validate before calling

def normalize(items):
    out = []
    for i, s in enumerate(items):
        if isinstance(s, str):
            out.append(s)
        elif isinstance(s, (tuple, list)) and len(s) == 2 and isinstance(s[0], str):
            out.append((s[0], s[1]))
        else:
            raise TypeError(f"statement {i} must be str or (sql, params)")
    return out

Type guard

def is_batch_statement(s: object) -> bool:
    return isinstance(s, str) or (
        isinstance(s, (tuple, list)) and len(s) == 2 and isinstance(s[0], str)
    )

Try / catch

try:
    conn.batch(stmts)
except ProgrammingError as e:
    log.error("malformed batch input: %s", e)
    conn.batch(normalize(stmts))

Prevention

When it happens

Trigger: batch([...]) with items like a dict ({'sql': ...}), a 3-element tuple, a tuple whose first element is not a string (e.g. (123, params)), a bare int/None, or a (sql, params, extra) triple.

Common situations: Migrating from drivers that accept dicts for statements, accidentally passing Cursor.execute-style kwargs, building statements programmatically and appending an extra element, or passing parameter objects instead of strings.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of tursodatabase/turso@c1e5928725 (2026-08-31). Data as JSON: /api/errors/0fd4f04aa3eddbe4. Report an issue: GitHub.