pola-rs/polars · error · RuntimeError

cannot return a frame before executing a query

Error message

cannot return a frame before executing a query

What it means

Raised by ConnectionExecutor.to_polars() when self.result is None, meaning no result set exists to convert. The executor requires that .execute(query) has run and produced a fetchable result before a DataFrame can be materialized. Statements that return no rows (DDL/DML like CREATE/INSERT/UPDATE) or calling to_polars() before execute() both leave result unset.

Source

Thrown at py-polars/src/polars/io/database/_executor.py:596

        return self

    def to_polars(
        self,
        *,
        iter_batches: bool = False,
        batch_size: int | None = None,
        schema_overrides: SchemaDict | None = None,
        infer_schema_length: int | None = N_INFER_DEFAULT,
    ) -> DataFrame | Iterator[DataFrame]:
        """
        Convert the result set to a DataFrame.

        Wherever possible we try to return arrow-native data directly; only
        fall back to initialising with row-level data if no other option.
        """
        if self.result is None:
            msg = "cannot return a frame before executing a query"
            raise RuntimeError(msg)

        can_close = self.can_close_cursor

        if defer_cursor_close := (iter_batches and can_close):
            self.can_close_cursor = False

        for frame_init in (
            self._from_arrow,  # init from arrow-native data (where support exists)
            self._from_rows,  # row-wise fallback (sqlalchemy, dbapi2, pyodbc, etc)
        ):
            frame = frame_init(
                batch_size=batch_size,
                iter_batches=iter_batches,
                schema_overrides=schema_overrides,
                infer_schema_length=infer_schema_length,
            )
            if frame is not None:
                if defer_cursor_close:

View on GitHub (pinned to df599052da)

Solutions

  1. Only pass queries that return a result set (SELECT / SHOW / EXPLAIN) to read_database; run DDL and DML through the connection's own cursor or engine.execute
  2. If using ConnectionExecutor directly, always call cx.execute(query) and check cx.result is not None before cx.to_polars(...)
  3. For row-affecting statements, wrap them in a SELECT that returns rows, or read with 'RETURNING'-style clauses where the backend supports them

Example fix

# before
df = pl.read_database("INSERT INTO t VALUES (1)", connection)  # no result set

# after
with connection.cursor() as cur:
    cur.execute("INSERT INTO t VALUES (1)")
df = pl.read_database("SELECT * FROM t", connection)
Defensive patterns

Strategy: try-catch

Validate before calling

from polars.io.database._executor import ConnectionExecutor

with ConnectionExecutor(conn) as cx:
    cx.execute(query=query)
    if cx.result is None:
        # statement produced no result set; nothing to frame
        ...

Try / catch

try:
    df = pl.read_database(sql, conn)
except RuntimeError as e:
    if "cannot return a frame" in str(e):
        # sql was DDL/DML with no result set — run it via raw cursor instead
        with conn.cursor() as cur:
            cur.execute(sql)
        df = None
    else:
        raise

Prevention

When it happens

Trigger: Calling pl.read_database() with a non-SELECT statement (e.g. 'CREATE TABLE ...' or an INSERT) on a DBAPI/SQLAlchemy connection; using ConnectionExecutor directly and calling .to_polars() without a prior successful .execute(); executing a query on a cursor whose .fetch*() yields nothing the executor can consume.

Common situations: Scripts that run setup DDL and read-back in one loop through read_database; migrating code from a driver's raw cursor API where execute() alone was harmless; calling read_database on a connection whose dialect/driver returns None from cursor.description.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/3c5a9bde6bd2ed46. Report an issue: GitHub.