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

Snowflake semantic views

SHOW COLUMNS IN VIEW

databricks

Databricks metric views

DESCRIBE TABLE EXTENDED AS JSON

duckdb

DuckDB semantic views

DESCRIBE SEMANTIC VIEW

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

Metric[T]()

Dimension

Dimension[T]()

Fact (Snowflake and DuckDB)

Fact[T]()

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

0

Success – model class written to stdout

1

Unexpected error (see stderr for details)

2

Invalid --backend specifier – value provided but not recognised

3

View not found – the warehouse has no semantic view with that name

4

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