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

Serverless Python SDK: each batch statement must be a SQL string or a 2-item (sql, parameters) sequence whose first element is a str. _normalize_batch_statements raises ProgrammingError for anything else, before the HTTP pipeline call is issued.

Source

Thrown at serverless/python/turso_serverless/connection.py:96

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, like the JavaScript driver."""
    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. Normalize each item to str or (str, params) before calling batch().
  2. Convert dicts: {"sql": s, "params": p} -> (s, p).
  3. Drop extra tuple elements; only (sql, parameters) pairs are accepted.
  4. The message includes the item index — log the list and inspect that exact element.

Example fix

# before
conn.batch([Query("SELECT 1"), ("SELECT ?", (1,), 2)])
# 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:
    await conn.batch(stmts)
except ProgrammingError as e:
    log.error("malformed batch input: %s", e)
    await conn.batch(normalize(stmts))

Prevention

When it happens

Trigger: batch([...]) with a dict entry, a 3-tuple, an entry whose first element isn't a string, None entries, or objects like parsed Statement dataclasses not converted to the expected shape.

Common situations: Reusing statement-building code written for other SDKs (dict-based steps), passing ORM/query-builder objects directly, and code that appends metadata to statement tuples.

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/d99bc08278050475. Report an issue: GitHub.