Define models

A Model transforms permitted project sources into a project-owned analytical output. The goal is not merely to make SQL run; it is to establish a stable grain and field contract that semantic metrics can safely reuse.

Before you begin

Complete Connect a data source, stage a representative source revision, and profile its keys, nulls, and types. Write the intended output grain in one sentence before creating YAML.

Work in this order:

  1. Declare named identity entities, the selected grain entity, permitted sources, and output fields.
  2. Implement source cleanup and grain-preserving SQL.
  3. Discover and validate the model resource.
  4. Refresh it with representative data.
  5. Verify keys, types, row counts, and repeatability.

Design and create the Model

Start from the grain

Write one sentence before writing SQL: “One row represents one order,” “one customer,” or “one rating by one user for one movie.” Declare a primary entity (and any composite or foreign entities), select it as grain.entity, and determine which source joins preserve that identity.

For an order-grain Model, joining raw order items directly would duplicate orders. Aggregate item-level values to order_id first, then join the one-row-per-order result.

Create the resource

Create dashboards/models/orders.yaml:

apiVersion: leapview.dev/v1
kind: Model
metadata:
  id: model:orders
  name: orders
  displayName: Sales orders
  description: One row per order with normalized purchase date and revenue.
spec:
  definition:
    type: sql
    sql: |
      SELECT order_id, customer_id,
        try_cast(purchased_at AS DATE) AS purchase_date,
        coalesce(try_cast(amount AS DECIMAL(38, 2)), CAST(0 AS DECIMAL(38, 2))) AS revenue
      FROM source."commerce.orders"
      WHERE order_id IS NOT NULL
  entities:
    order:
      type: primary
      fields: [order_id]
    customer:
      type: foreign
      fields: [customer_id]
  grain:
    entity: order
  fields:
    order_id: {datatype: String, label: Order ID, description: Stable order identifier.}
    customer_id: {datatype: String, label: Customer ID}
    purchase_date: {datatype: Date, label: Purchase date}
    revenue: {datatype: Decimal, label: Revenue}

The quoted source name is important because logical source IDs can contain dots. The compiler derives lineage from the governed SQL definition. spec.fields documents the output; it is not a substitute for selecting those columns in SQL.

Normalize deliberately

Use the Model boundary for source-specific cleanup:

  • cast weak source types to stable analytical types;
  • normalize empty strings and sentinel values;
  • choose how malformed values become null or are rejected;
  • deduplicate with an explicit ordering rule;
  • aggregate child rows before joining them to a parent grain;
  • assign readable output field names.

Avoid silent lossy conversions. try_cast can keep a refresh running, but unexpected nulls must still be measured and reviewed. If malformed input should block activation, encode or validate that invariant explicitly.

Validate the Model

Discover and validate

Place the model file beneath the source root's conventional models/ directory:

dashboards/models/*.yaml

Then validate the project:

leapview validate --source-root dashboards

Validation checks configuration shape and references. Data-level correctness requires a materialization or preview against actual source data.

Verify the result

After deploying and refreshing in development, verify:

  1. The output row count is plausible relative to the input.
  2. order_id is non-null and unique.
  3. purchase_date and revenue have stable types.
  4. Failed casts and null rates are understood.
  5. Joins have not multiplied the declared grain.
  6. A repeated refresh over the same revision produces equivalent output.

Use the project resource browser and refresh history to inspect the Model and its lineage. When several Models are related, validate each Model independently before declaring semantic relationships between them.

Choose the materialization boundary

Keep reusable source cleanup and expensive cross-source shaping here. Put aggregations such as total revenue and average order value in semantic metrics so they remain filter-aware. A Model should only be pre-aggregated when its declared row grain is intentionally aggregated.

Troubleshooting

If refresh multiplies rows, inspect each join against the declared grain and aggregate child records before joining. If casts silently create nulls, query the rejected source values and decide whether the resource should normalize or fail them. If validation succeeds but SQL fails, remember that structural validation cannot prove runtime column names or data types; test the transform against the staged revision.

Next steps

Continue with Build a semantic model. The generated Model configuration remains the exact syntax reference.