How to generate Semolina model classes from warehouse views¶
Already have a Snowflake semantic view or Databricks metric view set up? semolina codegen
introspects it and prints a Python model class to stdout. You can drop that output straight
into your codebase.
Run codegen¶
semolina codegen my_schema.sales_view --backend snowflake
That connects to your warehouse, reads the view’s column metadata, and prints a ready-to-use
SemanticView subclass.
Introspect multiple views at once¶
Pass multiple view names in a single call:
semolina codegen schema.sales_view schema.orders_view --backend databricks
All classes appear in one output block with a single shared imports section.
Pipe output to a file¶
semolina codegen my_schema.sales_view --backend snowflake > models.py
There is no --output flag; redirect stdout as you would with any CLI tool.
Format the generated output¶
By default semolina codegen prints valid but unformatted Python. Install the
optional codegen-lint extra and codegen runs the generated source through ruff
– formatting it and sorting imports – before printing:
pip install semolina[codegen-lint]
# or
uv add "semolina[codegen-lint]"
Without the extra, codegen still prints the model source to stdout and adds a short
reminder on stderr. The reminder stays out of stdout, so redirecting to a file
(> models.py) captures only the Python.
Choose a backend¶
Use --backend (or -b):
Value |
Warehouse |
Introspects via |
|---|---|---|
|
Snowflake semantic views |
|
|
Databricks metric views |
|
|
DuckDB semantic views |
|
Credentials come from environment variables
(for example, SNOWFLAKE_ACCOUNT for Snowflake).
For DuckDB, pass the database path with --database (or set DUCKDB_DATABASE).
See How to configure codegen credentials for the full list of
environment variables, .env file setup, and config
file fallback.
Point DuckDB codegen at a database file¶
The DuckDB backend reads a database file on disk, so --backend duckdb needs a
--database path. You can write that path three ways:
semolina codegen sales_view --backend duckdb --database /data/sales.duckdb
semolina codegen sales_view --backend duckdb --database ./sales.duckdb
semolina codegen sales_view --backend duckdb --database ~/data/sales.duckdb
A relative path resolves against your current working directory, and a leading
~ expands to your home directory. Setting DUCKDB_DATABASE accepts the same
forms, so you can keep the path out of the command line:
export DUCKDB_DATABASE=~/data/sales.duckdb
semolina codegen sales_view --backend duckdb
Codegen has no in-memory default for DuckDB. If you supply neither --database
nor DUCKDB_DATABASE, the command stops and asks you for a path.
The first run installs the semantic_views community extension onto the codegen
connection, which needs one-time network access to community.duckdb.org. DuckDB
caches the extension under ~/.duckdb/extensions/, so later runs work offline.
Understand the generated output¶
Given this semantic view in your warehouse:
CREATE OR REPLACE SEMANTIC VIEW analytics.sales_view
TABLES (
s AS source_table PRIMARY KEY (id)
)
DIMENSIONS (
s.country AS country,
s.unit_price AS unit_price
)
METRICS (
s.revenue AS SUM(s.revenue)
)
;
Running:
semolina codegen analytics.sales_view --backend snowflake
Produces:
from semolina import SemanticView, Metric, Dimension, Fact
class SalesView(SemanticView, view="analytics.sales_view"):
revenue = Metric[int]()
country = Dimension[str]()
unit_price = Fact[float]()
Given this metric view in your warehouse:
CREATE OR REPLACE VIEW main.analytics.orders_view
WITH METRICS
LANGUAGE YAML
AS $$
version: 1.1
source: source_table
dimensions:
- name: region
expr: region
measures:
- name: total_orders
expr: COUNT(*)
$$;
Running:
semolina codegen main.analytics.orders_view --backend databricks
Produces:
from semolina import SemanticView, Metric, Dimension, Fact
class OrdersView(
SemanticView, view="main.analytics.orders_view"
):
total_orders = Metric[int]()
region = Dimension[str]()
Given this semantic view in your DuckDB database:
CREATE SEMANTIC VIEW sales_view AS
TABLES (s AS sales_data PRIMARY KEY (id))
FACTS (
s.unit_price AS unit_price
)
DIMENSIONS (
s.country AS country,
s.region AS region
)
METRICS (
s.revenue AS SUM(s.revenue),
s.cost AS SUM(s.cost)
);
Running:
semolina codegen sales_view --backend duckdb --database ./sales.duckdb
Produces:
from semolina import Dimension, Fact, Metric, SemanticView
class SalesView(SemanticView, view="sales_view"):
unit_price = Fact[int]()
country = Dimension[str]()
region = Dimension[str]()
revenue = Metric[int]()
cost = Metric[int]()
Every column gets a concrete field type. Codegen reads the role each backend
records for the column and emits the matching Metric, Dimension, or
Fact. None of the backends leave a column unclassified, so you never get a
bare Field() placeholder for a known role.
Note
Databricks metric views model only two roles: measures and dimensions. There
is no Fact concept, so every non-measure column maps to Dimension(). This
is intentional, not a missing feature. Snowflake and DuckDB semantic views
support all three roles (METRIC, DIMENSION, FACT), and codegen
maps each one directly.
Understand field type mapping¶
Codegen resolves each backend’s native role string to a field type:
Warehouse classification |
Generated field type |
|---|---|
Metric / Measure |
|
Dimension |
|
Fact (Snowflake and DuckDB) |
|
If a backend ever hands back a role string that codegen doesn’t recognize,
generation stops with a ValueError instead of guessing. A new warehouse
version or a schema change could introduce a role the mapping above doesn’t
cover, and silently labelling that column a Dimension would hide the drift
in your generated model. Failing loudly keeps the generated code honest: you
find out at codegen time, not when a query returns the wrong shape.
Handle TODO comments¶
When a field’s SQL type has no clean Python equivalent (GEOGRAPHY, VARIANT, ARRAY, MAP,
STRUCT), codegen types the field as Any and drops the raw warehouse type into a
TODO comment rather than guessing:
# TODO: {"type": "GEOGRAPHY"}
territory = Dimension[Any]()
The comment carries the warehouse’s own type descriptor verbatim, so you have the
detail you need to pick a concrete type. Any keeps the generated module valid in
the meantime; codegen adds from typing import Any for you whenever a field needs it.
Review these fields after generation and replace Any with the type you want.
Exit codes¶
semolina codegen uses distinct exit codes so scripts can handle each failure mode separately:
Exit code |
Meaning |
|---|---|
|
Success – model class written to stdout |
|
Unexpected error (see stderr for details) |
|
Invalid |
|
View not found – the warehouse has no semantic view with that name |
|
Connection failure – credentials missing or authentication rejected |
Tip
Exit code 2 is also emitted by the CLI argument parser when --backend is
omitted entirely. Both cases mean “the backend could not be resolved.”
Override the SQL column name with source=¶
By default, Semolina maps Python field names to SQL column names using each dialect’s
identifier casing rules (Snowflake uppercases unquoted identifiers; Databricks lowercases them).
For a field order_id, Snowflake resolves ORDER_ID automatically.
If your warehouse stores a column with non-default casing, for example a quoted
lowercase column "order_id" in Snowflake, you can override the SQL column name
with source=:
class Orders(SemanticView, view="orders"):
order_id = Metric[int](
source="order_id"
) # maps to quoted "order_id", not "ORDER_ID"
semolina codegen emits source= automatically when introspection detects that a column
uses non-default casing.
See also¶
How to configure codegen credentials – environment variables, .env files, and config file fallback
How to define models – model class structure and field types
How to connect to Snowflake – Snowflake pool configuration
How to connect to Databricks – Databricks pool configuration
How to connect to DuckDB – DuckDB pool configuration