semolina.cursor¶
DBAPI 2.0 cursor wrapper with Row convenience methods.
SemolinaCursor delegates to any DBAPI 2.0-compatible cursor and adds fetchall_rows(), fetchmany_rows(), and fetchone_row() that convert raw tuples into Row objects using cursor.description column names.
Classes¶
ADBC cursor wrapper with Row convenience methods. |
Module Contents¶
- class semolina.cursor.SemolinaCursor(cursor: Any, conn: Any)[source]¶
ADBC cursor wrapper with Row convenience methods.
Wraps the ADBC cursor
execute()opens, and keeps its DBAPI 2.0 fetch methods. Adds fetchall_rows(), fetchmany_rows(), and fetchone_row(), which return Row objects, and the Arrow and DataFrame readers ADBC provides.A result is read one way. The first read picks it: the DBAPI fetches (
fetchone,fetchmany,fetchalland their_rowsvariants, which interleave freely),for row in cursor,fetch_record_batch(),iter_into(), or one of the whole-result methods. Reading it a second way raisesSemolinaResultConsumedErrornaming both calls, because the second would start after whatever the first had buffered and silently miss those rows.Every way of reading rows needs pyarrow, because ADBC reads through a pyarrow reader; each raises
SemolinaMissingDependencyErrorwithout it. The one exception isfetch_polars().Context manager support releases the reader, cursor and connection on exit.
- fetchall_rows() list[semolina.results.Row][source]¶
Fetch all remaining rows as Row objects.
- Returns:
List of Row objects with attribute and dict access.
- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed.
SemolinaResultConsumedError – If the result is already being read another way.
ValueError – If two result columns share a name, which a Row cannot represent.
- fetchone_row() semolina.results.Row | None[source]¶
Fetch next row as a Row, or None if exhausted.
- Returns:
Row object, or None if no rows remain.
- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed.
SemolinaResultConsumedError – If the result is already being read another way.
ValueError – If two result columns share a name, which a Row cannot represent.
- fetchmany_rows(size: int = 1) list[semolina.results.Row][source]¶
Fetch up to size rows as Row objects.
- Parameters:
size – Maximum number of rows to fetch. Defaults to 1.
- Returns:
List of Row objects (may be shorter than size).
- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed.
SemolinaResultConsumedError – If the result is already being read another way.
ValueError – If two result columns share a name, which a Row cannot represent.
- fetchall() list[tuple[Any, Ellipsis]][source]¶
Fetch all remaining rows as raw tuples (DBAPI passthrough).
- Returns:
List of tuple rows.
- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed.
SemolinaResultConsumedError – If the result is already being read another way.
- fetchone() tuple[Any, Ellipsis] | None[source]¶
Fetch next row as raw tuple (DBAPI passthrough).
- Returns:
Tuple row, or None if exhausted.
- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed.
SemolinaResultConsumedError – If the result is already being read another way.
- fetchmany(size: int = 1) list[tuple[Any, Ellipsis]][source]¶
Fetch up to size rows as raw tuples (DBAPI passthrough).
- Parameters:
size – Maximum number of rows to fetch.
- Returns:
List of tuple rows.
- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed.
SemolinaResultConsumedError – If the result is already being read another way.
- fetch_arrow_table() pyarrow.Table[source]¶
Fetch all remaining rows as a PyArrow Table (ADBC passthrough).
Delegates to the underlying ADBC cursor’s
fetch_arrow_table()method for zero-copy Arrow data transfer.Requires an ADBC-capable cursor (Snowflake, Databricks, or DuckDB pool connections). Not supported by non-ADBC cursors.
- Returns:
pyarrow.Tablewith the query results.- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed. ADBC builds this table through a
pyarrow.RecordBatchReader; without pyarrow it raises its ownProgrammingError("This API requires PyArrow to be installed"), which names neither Semolina nor the extra that fixes it.SemolinaResultConsumedError – If the result is already being read another way.
AttributeError – If the underlying cursor does not support
fetch_arrow_table()(e.g. a non-ADBC cursor).
Example
cursor = Sales.query().metrics(Sales.revenue).execute() table = cursor.fetch_arrow_table() df = table.to_pandas()
- fetch_record_batch() pyarrow.RecordBatchReader[source]¶
Fetch the result as a PyArrow
RecordBatchReader(ADBC passthrough).Delegates to the underlying ADBC cursor’s
fetch_record_batch()method for lazy, memory-bounded streaming consumption of Arrow data.Requires an ADBC-capable cursor (Snowflake, Databricks, or DuckDB pool connections). Not supported by non-ADBC cursors.
The reader is this cursor’s way of reading its result: once it is taken,
fetchone(),fetch_arrow_table(), iterating the cursor and the rest raiseSemolinaResultConsumedError. A repeat call returns the same reader, which the cursor keeps soclose()can close it.The cursor must outlive the reader: consume the reader inside the context manager (or before
.close()). See arrow-adbc issue #1893.- Returns:
pyarrow.RecordBatchReaderover the query result.- Raises:
SemolinaMissingDependencyError – If pyarrow is not installed. The reader is a pyarrow object; ADBC calls its own
_requires_pyarrow()here and raises aProgrammingErrorthat names neither Semolina nor the extra.SemolinaResultConsumedError – If the result is already being read another way.
AttributeError – If the underlying cursor does not support
fetch_record_batch()(e.g. a non-ADBC cursor).
Example
with Sales.query().metrics(Sales.revenue).execute() as cursor: reader = cursor.fetch_record_batch() for batch in reader: process(batch)
- fetch_df() pandas.DataFrame[source]¶
Fetch all remaining rows as a pandas
DataFrame(ADBC passthrough).Delegates to the underlying ADBC cursor’s
fetch_df(), which reads the result through a pyarrow reader —self.reader.read_pandas()— so this path needs both pyarrow and pandas. Semolina converts nothing itself: ADBC’s implementation is already cancellation-aware, so a long fetch stays interruptible.Requires an ADBC-capable cursor (Snowflake, Databricks, or DuckDB pool connections). Not supported by non-ADBC cursors.
Reads the whole result, like
fetch_arrow_table(), so any other way of reading the same cursor afterwards raisesSemolinaResultConsumedError.A
DECIMALmetric arrives as anobjectcolumn holdingdecimal.Decimalvalues — pandas has no native decimal dtype, so precision survives but the column is untyped.fetch_polars()does better; see its note.- Returns:
pandas.DataFramewith the query results.- Raises:
SemolinaMissingDependencyError – If pyarrow or pandas is not installed. pyarrow is checked first, because ADBC reaches its pyarrow reader before anything imports pandas.
SemolinaResultConsumedError – If the result is already being read another way.
AttributeError – If the underlying cursor does not support
fetch_df()(e.g. a non-ADBC cursor).
Example
with Sales.query().metrics(Sales.revenue).execute() as cursor: df = cursor.fetch_df() df.head()
- fetch_polars() polars.DataFrame[source]¶
Fetch all remaining rows as a polars
DataFrame(ADBC passthrough).Delegates to the underlying ADBC cursor’s
fetch_polars(), which hands polars the result’s raw Arrow PyCapsule stream —polars.from_arrow(self.fetch_arrow())— and never touches pyarrow. This method is therefore guarded on polars only: requiring pyarrow here would refuse a call that works on a polars-and-no-pyarrow install.Requires an ADBC-capable cursor (Snowflake, Databricks, or DuckDB pool connections). Not supported by non-ADBC cursors.
This must be the first read on the cursor. After any other way of reading it — iterating,
fetch_record_batch(),fetch_arrow_table(),into()— this raisesSemolinaResultConsumedError, naming the earlier call. ADBC’s implementation takes the cursor’s Arrow stream handle and leavesNonebehind, so callingfetch_polars()twice reaches the driver’s ownProgrammingError("Result set has been closed or consumed"). Readingdescriptionfirst is safe; it does not import the stream.A
DECIMALmetric keeps its precision and its type: polars 1.43.2 gives a warehousedecimal128(38, 2)column a nativeDecimal(precision=38, scale=2)dtype holdingdecimal.Decimalvalues, measured on this project’s own type-fidelity probe. That is better thanfetch_df(), where the same column falls back to an untypedobjectdtype. One condition, recorded because it is reachable in principle and not in practice: polars was measured raising a RustPanicExceptionon adecimal256column, and no backend Semolina supports has been observed producing one — a SnowflakeNUMBERstops at precision 38, and Databricks and DuckDB decimals stop there too.- Returns:
polars.DataFramewith the query results.- Raises:
SemolinaMissingDependencyError – If polars is not installed. ADBC does a bare
import polarsinside the fetch, so without this guard the caller gets aModuleNotFoundErrorraised several frames deep in someone else’s module.SemolinaResultConsumedError – If the result is already being read another way.
AttributeError – If the underlying cursor does not support
fetch_polars()(e.g. a non-ADBC cursor).
Example
with Sales.query().metrics(Sales.revenue).execute() as cursor: df = cursor.fetch_polars() df.head()
- into(model: type[_M], *, validate: bool = False) list[_M][source]¶
Convert the whole result into a list of Pydantic model instances.
Columns are matched to fields by name, resolved through arrowmodel’s own key rule —
validation_alias, thenalias, then the field name. Result columns the model does not declare are ignored, so one DTO can serve several queries. A declared field with no matching column is an error unless it carries a default.Any Pydantic
BaseModelsubclass works; inheriting fromarrowmodel.ArrowModelis not required and buys nothing here.validateselects between two coherent behaviours, and the schema check follows it (seesemolina.dto.check_result_schema()):validate=False(default) — types must match. Instances are built withmodel_construct, which converts nothing, so an annotation that disagreed with its column would leave a wrong-typed value sitting in the field. The structural check runs before any row moves and raisesSemolinaSchemaMismatchErrorinstead, naming every offending field at once. Nothing is coerced; nothing is per-value; the cost is one schema comparison.validate=True— types are coerced. Pydantic’s full pipeline runs per row at roughly 2-5x the cost, converting where it legally can (decimal128into afloatfield,int64intofloat) and raisingValidationErrorwhere it cannot (decimal128intoint). The structural type comparison is skipped, because Pydantic owns the decision and refusing first would block narrowings that work.
Column presence is checked on both settings — a required field with no matching column is an error either way, since no amount of coercion invents a column.
Reads the whole result, like
fetch_arrow_table(), so any other way of reading the same cursor afterwards raisesSemolinaResultConsumedError.- Parameters:
model – The Pydantic model to build. Any
type[BaseModel].validate – Coerce values to the declared annotations through Pydantic’s per-row pipeline, instead of the fast path’s exact-type contract. Passed straight through to arrowmodel. Defaults to False.
- Returns:
A list of
modelinstances, one per result row. Empty for a zero-row result.- Raises:
SemolinaMissingDependencyError – If pyarrow or arrowmodel is not installed.
SemolinaSchemaMismatchError – If the model’s annotations do not describe the result schema.
SemolinaResultConsumedError – If the result is already being read another way.
pydantic.ValidationError – Under
validate=Trueonly, for a value Pydantic cannot convert to its declared annotation. A NULL in a non-optional field is the common one, since the structural check says nothing about nullability.TypeError – From arrowmodel, for a field annotated
dictorMappingagainst an Arrowmapcolumn, which it materializes as a list of pairs instead. The structural check records no verdict on a parameterized generic, so this one stays arrowmodel’s to raise.ValueError – From arrowmodel, naming a required column it could not find. The check above pre-empts this with a better message whenever it has a schema to read, so it survives only where
descriptionwasNone.
Example
import decimal import pydantic class SalesDTO(pydantic.BaseModel): region: str total_order_value: decimal.Decimal with Sales.query().metrics(Sales.total_order_value).execute() as cursor: rows = cursor.into(SalesDTO) # [SalesDTO(region='US', total_order_value=Decimal('43.25')), ...]
- iter_into(model: type[_M], *, validate: bool = False) collections.abc.Iterator[_M][source]¶
Stream the result as Pydantic model instances, one at a time.
The streaming sibling of
into(). Instances are produced individually, but conversion happens a whole Arrow batch at a time: consuming one instance pulls exactly one batch from the underlying reader, so a result larger than memory is never materialized. Column matching, alias resolution and the treatment of extra or defaulted fields are identical tointo()— only the delivery differs.The schema check happens at the call, not on first iteration.
iter_intois a plain method that validates and then returns a generator, rather than a generator function whose body would not run until the firstnext(). That distinction is the whole point: arrowmodel’s ownModel.iter(batch)is a generator function, so a bad DTO handed to it returns quietly and blows up several frames away, inside whatever loop or comprehension eventually consumed it. Here a mismatched DTO raises on theiter_into(...)expression itself, with the traceback pointing at the line that named the wrong type, and before any reader is created or any batch moves.The returned iterator is this cursor’s way of reading its result: any other way,
fetch_record_batch()included, then raisesSemolinaResultConsumedError. The cursor must also outlive the iterator: consume it inside the context manager (or beforeclose()). See arrow-adbc issue #1893.- Parameters:
model – The Pydantic model to build. Any
type[BaseModel].validate – Run Pydantic validation per row instead of the fast path. Set on the converter once and reused for every batch. Defaults to False. Read
into()’s note first —validate=Truecoerces instead of requiring exact types.
- Returns:
An
Iteratoryielding onemodelinstance per result row. Empty for a zero-row result; zero-row batches mid-stream are skipped rather than ending it.- Raises:
SemolinaMissingDependencyError – If pyarrow or arrowmodel is not installed. Raised at the call.
SemolinaSchemaMismatchError – If the model’s annotations do not describe the result schema. Raised at the call.
SemolinaResultConsumedError – If the result is already being read another way.
pydantic.ValidationError – Under
validate=Trueonly, for a value Pydantic cannot convert to its declared annotation — a NULL in a non-optional field being the common one. Raised from the batch that carries the value, not at the call, since no value has been read yet when the call returns.TypeError – From arrowmodel, for a field annotated
dictorMappingagainst an Arrowmapcolumn. Raised on the firstnext().ValueError – From arrowmodel, naming a required column it could not find. The check above pre-empts this whenever it has a schema to read, so it survives only where
descriptionwasNone. Raised on the firstnext().
Example
import decimal import pydantic class SalesDTO(pydantic.BaseModel): region: str total_order_value: decimal.Decimal with Sales.query().metrics(Sales.total_order_value).execute() as cursor: for dto in cursor.iter_into(SalesDTO): process(dto)
- property description: list[tuple[Any, Ellipsis]] | None[source]¶
Cursor description passthrough.
- Returns:
List of 7-element tuples describing columns, or None before execute.
- property rowcount: int[source]¶
Row count passthrough.
- Returns:
Number of rows affected by the last operation.
- close() None[source]¶
Close the reader, then the cursor, then return the connection to its pool.
The same order and the same failure handling as the async twin’s
aclose(). Each step is suppressed narrowly —Exception, notBaseException— so a close that fails cannot keep the connection out of the pool, and cannot replace the exception awithbody raised. A connection that fails to close is a pool slot that never comes back, so that one is reported as aResourceWarning. Idempotent: the closed flag is set first, so a second call is a no-op.