Changelog¶
What changed in each Semolina release. The full history of earlier releases is on the GitHub releases page.
0.7.0 (2026-10-16)¶
0.7.0 adds an async query surface and typed results. It also fixes several 0.6 queries that ran without complaint and returned the wrong rows. Read the first two sections before you upgrade from 0.6.
Changes that need action¶
The semolina command needs the [cli] extra. typer, rich and jinja2 are
no longer base dependencies, so pip install semolina no longer installs what
semolina codegen and semolina codegen-dto need. griffelib, which is new in 0.7 and
reads committed files for --check, is in the extra too. Install the command with:
pip install "semolina[cli]"
Without the extra, semolina prints that line and exits with code 2. A service that
only runs queries skips the command’s packages, and import semolina defers
pydantic-settings and SQLAlchemy until you call create_engine.
The SQL-generation base class is SQLDialect. Only one thing in Semolina is now
called Dialect: the enum you pass to to_sql(). If you subclassed the base class
through semolina.engines.sql.Dialect or semolina.engines.DialectABC, rename it.
Both old names still work in 0.7 with a DeprecationWarning.
Result columns are named after your fields on every warehouse. Semolina now aliases
each selected column to its Python field name, so row.revenue works on Snowflake,
Databricks and DuckDB alike. In 0.6 the same query returned AGG("REVENUE") on
Snowflake and measure(revenue) on Databricks. If your code read those keys, read
the field names instead.
A filter given None raises TypeError. In 0.6 a filter such as
Sales.revenue >= None compiled to a comparison with NULL, which matches no row,
so the query quietly came back empty. Now every comparison and lookup refuses None
when you build it, and so does a None inside in_(). == and != are the
exception (see below). If your code passes an optional parameter straight into a filter,
leave the filter out when the value is None, or use .isnull() if you meant to
match missing values.
A connection failure raises SemolinaConnectionError. In 0.6 a connection that
could not be opened, in create_engine() or when execute() checked one out of the
pool, raised the driver’s own error. Now it raises SemolinaConnectionError, with the
driver’s error as __cause__, the same as introspect() already did. If you catch
adbc_driver_manager.Error around those calls to answer a warehouse that is down, catch
SemolinaConnectionError too. A statement the warehouse rejects still raises the
driver’s own error. On DuckDB, a connection that cannot install or load the
semantic_views extension raises SemolinaExtensionLoadError, a subclass of
SemolinaConnectionError whose message names the extension.
Fixed¶
Each of these ran without an error in 0.6 and returned the wrong result. 0.7 either returns the right rows or refuses the query with an exception.
in_()given a string, such asSales.country.in_("US"), treated each character as a separate value. It now raisesTypeError. It also accepts a generator, and the SQL always has as many placeholders as there are values.field == Noneandfield != Nonecompiled to= NULLand<> NULL, which match nothing. They now compile toIS NULLandIS NOT NULL. Every other comparison or lookup givenNone,between()and aNoneinsidein_()among them, raisesTypeErrorand points you at.isnull().startswith,istartswith,endswith,iendswithandiexacttreated%and_in your value as wildcards. They now match them literally on every dialect.likeandilikestill pass your pattern through as written.A result with two columns of the same name, which selecting one field twice produces, silently kept the last value. Selecting a field twice now raises
ValueErrorin the builder, and aRowrefuses duplicate column names.On DuckDB, a query that selects facts and no metric returned one row per source row, where Snowflake and Databricks group the same query. DuckDB now returns each distinct row once too.
Two more raised an error where they should have worked:
On DuckDB, a DBAPI fetch after the result was drained, such as the last call of a
while batch := cursor.fetchmany_rows(100)loop, raisedInternalError. Every DBAPI fetch now returns[]orNoneonce the result is drained.semolina codegen --backend my_module.MyEnginecould not import a module in the working directory unless it was installed. Both codegen commands now add the working directory to the end ofsys.pathfirst.
Changed¶
to_sql()with no argument previews the dialect of the engine.using()names, when that engine is registered. Without.using(), it still previews Snowflake.Every exception class Semolina defines derives from
SemolinaError, so oneexceptclause can catch them all. Each keeps its 0.6 base class, so existingexcept RuntimeErrorandexcept ValueErrorclauses still match. Looking up an engine that was never registered raisesSemolinaEngineNotRegisteredError, which is still aValueError. A result with two columns of one name, which only a custom engine can produce, raisesSemolinaDuplicateColumnsError, still aValueError. Both new errors are importable fromsemolina.introspect(), and sosemolina codegen, reports a view the warehouse reached but could not describe, such as one whose definition no longer binds, as the newSemolinaIntrospectionErrorrather thanSemolinaConnectionError, andsemolina codegenexits 6 for it rather than 4. Connection and authentication failures still raiseSemolinaConnectionError.Queryis public, andField,Engine,AsyncEngine,And,Or,NotandLookupare importable fromsemolina. The query builder’s methods are typed, so a type checker flags.metrics(Sales.country)or.limit("10"). A query reports its state through read-only properties:selected_metrics,selected_dimensions,filters,ordering,limit_value,engine_nameandmodel.IntrospectedViewandIntrospectedField, the typesintrospect()returns, live insemolina.introspection. Importing them fromsemolina.codegen.introspectorstill works.
Added¶
aexecute()and async row streaming, under asyncio or Trio, with the[async]extra. Cancelling a query stops it inside the driver..into(Model)turns a result into Pydantic models, andfetch_df()andfetch_polars()return a pandas or polars DataFrame.semolina codegen-dtowrites the Pydantic model for a query’s result, typed from the warehouse’s own result schema.semolina codegen --checkreports where your models no longer match the warehouse, and--outputwrites the generated module to a file.to_sql()takes a dialect object as well as a name, so you can preview a custom dialect’s SQL without registering an engine. Each built-in dialect names its backend inkind.
See also¶
Installation covers the extras,
[cli]among them.How to run Semolina inside FastAPI shows how to catch
SemolinaErrorin a request handler.How to filter queries covers filters, including
Noneand the string lookups.