Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [1.14.0] - 2026-10-07

### Added
- **Lightdash metrics in the generated dbt project.** `generate --lightdash` writes the model's
metrics, dimensions and joins where [Lightdash](https://docs.lightdash.com/references/metrics)
reads them: under `config.meta` of each staging model's own `models/staging/stg_<table>.yml`,
next to its tests (dbt reads a model's properties from one file only, so the meta is merged in
rather than written beside it). It takes the `--metrics` file's metrics, or without one the
metrics the model implies. Each table is joined many-to-one to every table it reaches along
exactly one path; a dimension gets its label and a column that is not one is hidden; a simple
metric sits on its column, a row count is a `count_distinct` of the one-column primary key, and
a ratio or expression is a `type: number` metric over its inputs; `ai_context` becomes
`ai_hint`, `percent` and `currency` the formats `'0.00%'` and `'#,##0.00'`. A filter becomes
the metric's `filters` wherever Lightdash's filter grammar means exactly the same (with
`'!null'` beside a not-equal, since Lightdash's lets nulls through), and a `CASE WHEN` in its
`sql` where it does not (an `any` group, a date comparison), so the metric counts what its
known value counts. What Lightdash has no place for (an enum's members, a table's role and
grain, a currency, a metric dated by another table's column, a ratio whose inputs are on two
tables, which is left out) is listed in the run's summary. Every example's output validates
against Lightdash's published JSON Schema for dbt YAML, parses with dbt, and compiles with
`lightdash compile --no-partial-compilation`.
- **`model2data metrics export --to lightdash`** writes the same meta as one dbt properties file
for the staging models, without generating, the losses listed at its top.
- **`model2data.metrics.to_lightdash`** and **`write_lightdash`** in the Python API, beside
`to_ossie`.

Without `--lightdash`, a run writes byte for byte what 1.13.0 wrote, with or without
`--metrics`.

## [1.13.0] - 2026-10-06

### Added
Expand Down
4 changes: 3 additions & 1 deletion LLMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,13 @@ model2data --file SCHEMA.dbml [OPTIONS]
--metrics PATH The model's metrics file: adds metric_values.json (known values),
osi/NAME.yml (Apache Ossie 0.1.1) and data-tests/metrics/ (a dbt test
per metric). Changes no other file
--lightdash Add Lightdash metrics and dimensions (config.meta) to the staging
models' YAML, from --metrics or the inferred metrics

model2data validate FILE... [--model MODEL] a metrics file (*.metrics.yml) is checked against
--model, or the model beside it with its stem
model2data metrics list -f MODEL [--metrics FILE] every metric, inferred too
model2data metrics export -f MODEL [--metrics FILE] --to ossie [-o FILE] without generating
model2data metrics export -f MODEL [--metrics FILE] --to ossie|lightdash [-o FILE] without generating
```

For anything not covered here, see [README.md](README.md) and [docs/](docs/README.md) — this file exists to make a schema
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ orphans, nulls, invalid values, messy text, late-arriving rows. Every run with d
orders, average order value. The run writes each metric's value over the generated data to
`metric_values.json`, overall and per month, a dbt test per metric that must reproduce it on the
warehouse, and an [Apache Ossie](https://github.com/apache/ossie) semantic model under `osi/`.
New in 1.14.0: `--lightdash` writes the same metrics into the staging models' YAML as
[Lightdash](https://www.lightdash.com/) reads them, filters, joins and ratios included, so the
dashboard's revenue can be checked against the number the run already knows.
→ [Metrics with known values](docs/metrics.md)

**Validate in CI.** `model2data validate` checks models and metrics files and prints GitHub
Expand Down Expand Up @@ -225,7 +228,7 @@ definition goes in; nothing but synthetic data comes out.
`accepted_values` fail for the right reason.
- **A team defining metrics once.** Write revenue in one metrics file, check every number the
semantic layer or dashboard shows against `metric_values.json`, let `dbt build` prove the SQL,
and hand the definitions on as Apache Ossie.
and hand the definitions on as Apache Ossie or straight to Lightdash.

## How it works

Expand Down Expand Up @@ -281,7 +284,8 @@ flowchart LR
model's hints, table and column `description:` fields from the model, and a profile for
DuckDB (zero-config, file-based) or Postgres.
4. **Compute metrics.** With `--metrics`, computes each metric over the generated data and writes
its known value, a dbt test that checks it, and the Apache Ossie file.
its known value, a dbt test that checks it, and the Apache Ossie file; with `--lightdash`,
Lightdash metrics and dimensions in the staging models' YAML.

---

Expand Down
8 changes: 6 additions & 2 deletions README_PYPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ orphans, nulls, invalid values, messy text, late-arriving rows. Every run with d
orders, average order value. The run writes each metric's value over the generated data to
`metric_values.json`, overall and per month, a dbt test per metric that must reproduce it on the
warehouse, and an [Apache Ossie](https://github.com/apache/ossie) semantic model under `osi/`.
New in 1.14.0: `--lightdash` writes the same metrics into the staging models' YAML as
[Lightdash](https://www.lightdash.com/) reads them, filters, joins and ratios included, so the
dashboard's revenue can be checked against the number the run already knows.
→ [Metrics with known values](https://github.com/JB-Analytica/model2data/blob/main/docs/metrics.md)

**Validate in CI.** `model2data validate` checks models and metrics files and prints GitHub
Expand Down Expand Up @@ -225,7 +228,7 @@ definition goes in; nothing but synthetic data comes out.
`accepted_values` fail for the right reason.
- **A team defining metrics once.** Write revenue in one metrics file, check every number the
semantic layer or dashboard shows against `metric_values.json`, let `dbt build` prove the SQL,
and hand the definitions on as Apache Ossie.
and hand the definitions on as Apache Ossie or straight to Lightdash.

## How it works

Expand All @@ -251,7 +254,8 @@ model (.model2data.yml or DBML) + metrics file (optional)
model's hints, table and column `description:` fields from the model, and a profile for
DuckDB (zero-config, file-based) or Postgres.
4. **Compute metrics.** With `--metrics`, computes each metric over the generated data and writes
its known value, a dbt test that checks it, and the Apache Ossie file.
its known value, a dbt test that checks it, and the Apache Ossie file; with `--lightdash`,
Lightdash metrics and dimensions in the staging models' YAML.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The [README](../README.md) shows what model2data does; these pages say how each
- [Defects on purpose](defects.md) — `--defects messy|training`, defects per table, the
expected-failures report.
- [Metrics with known values](metrics.md) — the metrics file, `metric_values.json`, a dbt test
per metric, the Apache Ossie export.
per metric, the Apache Ossie export, Lightdash metrics in the dbt YAML.
- [Validate models in CI](ci.md) — the GitHub Action, pre-commit, GitLab CI.
- [Using model2data from a coding agent or an LLM](agents.md) — `model2data guide`, LLMS.md.

Expand Down
2 changes: 1 addition & 1 deletion docs/dbt-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ dbt_{project_name}/
├── models/
│ └── staging/
│ ├── stg_table1.sql
│ ├── stg_table1.yml
│ ├── stg_table1.yml # with --lightdash, also its Lightdash meta
│ ├── ut_stg_table1.yml # only with --unit-tests
│ └── ...
├── data-tests/
Expand Down
79 changes: 77 additions & 2 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,88 @@ With `--metrics`, the run also writes, and changes nothing else:
as ANSI SQL expressions. What Ossie 0.1.1 has no field for (a metric's label, format and time,
enum members) is carried in `custom_extensions` and listed at the top of the file.

## Lightdash

`--lightdash` puts the metrics where [Lightdash](https://docs.lightdash.com/references/metrics)
reads them: in the dbt project's own YAML, under `config.meta` of each staging model
(`models/staging/stg_<table>.yml`), next to the tests already there. dbt reads a model's
properties from one file only, so the meta is merged into those files rather than written beside
them; with or without `--metrics`, and without `--lightdash` they are byte for byte what they were.

```bash
model2data --file coffee_webshop.model2data.yml --metrics coffee_webshop.metrics.yml --lightdash
cd dbt_coffee_webshop && dbt parse
lightdash compile --no-warehouse-credentials --no-partial-compilation # every explore compiles
```

Lightdash queries a warehouse it can reach, Postgres for one (`--adapter postgres`), or
MotherDuck; it does not open a local DuckDB file, so `lightdash deploy` needs one of those.

```yaml
- name: stg_orders
config:
meta:
label: Orders
primary_key: id
joins:
- join: stg_customers
sql_on: ${stg_orders.customer_id} = ${stg_customers.id}
relationship: many-to-one
metrics:
average_order_value:
type: number
sql: ${revenue} / NULLIF(${orders}, 0)
label: Average order value
format: '#,##0.00'
columns:
- name: total_amount
config:
meta:
dimension:
hidden: true
metrics:
revenue:
type: sum
label: Revenue
ai_hint: What customers paid for. ...
format: '#,##0.00'
filters:
- status: [paid, shipped, delivered]
default_time_dimension: {field: order_date, interval: MONTH}
```

Each table becomes a Lightdash table, joined many-to-one to every table it reaches along exactly
one path, as a metric's filter reaches them. A dimension gets its label; a column that is not one
(a key, an amount) is hidden, as `dimensions: {customers.email: false}` asks. A simple metric sits
on its column with its aggregation as its `type`; a row count is a `count_distinct` of the
one-column primary key (the same number, and still right where another table joins this one); a
ratio or an expression is a `type: number` metric over its inputs. `ai_context` becomes
Lightdash's `ai_hint`, `percent` and `currency` its `'0.00%'` and `'#,##0.00'`.

A filter becomes the metric's `filters` where Lightdash's filter grammar says exactly the same:
values and lists, `ne` and `not_in` (with `'!null'`, since Lightdash's not-equal lets nulls
through and model2data's does not), `is_null`, and comparisons and `between` on numbers.
Anything else, an `any` group or a date comparison, goes into the metric's `sql` as a
`CASE WHEN`, so the metric still counts exactly the rows its known value counts.

What Lightdash has no place for is listed in the summary and at the top of the exported file: an
enum's members, a table's role and grain, a currency, a metric dated by a column of another table
(Lightdash's default time dimension is a field of the metric's own table), and a ratio whose
inputs are on two tables, which is left out: a Lightdash metric belongs to one table and sees
another only through a join from it, which would drop or repeat rows.

## Without generating

`model2data metrics list -f MODEL [--metrics FILE]` shows every metric, and `model2data metrics
export -f MODEL [--metrics FILE] --to ossie [-o FILE]` writes the Ossie file without generating.
export -f MODEL [--metrics FILE] --to ossie|lightdash [-o FILE]` writes the Ossie file, or the
Lightdash meta as one dbt properties file for the staging models, without generating. In a
project `generate` wrote, use `--lightdash` instead: a second properties file for the same models
would not parse.
Without `--metrics`, both use the model's inferred metrics. A sibling metrics file is not picked up
by `generate` on its own: a file appearing beside a model must not change what an unchanged
command writes. From Python, `model2data.metrics` loads and checks a file (`load`, `validate`),
resolves it with the model into one semantic representation (`resolve`), and computes
(`known_values`) and exports (`to_ossie`) from it.
(`known_values`) and exports (`to_ossie`, `to_lightdash`, `write_lightdash`) from it.

The e-commerce example has a metrics file too, [`examples/ecommerce.metrics.yml`](../examples/ecommerce.metrics.yml)
(revenue, orders, average order value, units sold); the [README](../README.md#before-and-after)
Expand Down
63 changes: 56 additions & 7 deletions model2data/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@
SPEC_VERSION as METRICS_SPEC_VERSION,
)
from model2data.metrics import (
LightdashExport,
Metrics,
MetricsError,
is_metrics_file,
known_values,
metrics_stem,
sibling_model,
to_lightdash,
to_ossie,
write_lightdash,
write_metric_tests,
)
from model2data.metrics import load as load_metrics
Expand Down Expand Up @@ -471,6 +474,16 @@ def main(
"without it. Not picked up from beside the model: pass it."
),
),
lightdash: bool = typer.Option(
False,
"--lightdash",
help=(
"Add Lightdash metrics and dimensions to the staging models' YAML\n"
"(models/staging/stg_*.yml, under config.meta): the --metrics file's metrics,\n"
"or without one the metrics the model implies. Without it those files are\n"
"unchanged."
),
),
):
"""
Generate synthetic data and a dbt project from a model.
Expand Down Expand Up @@ -726,6 +739,13 @@ def pick(option: Any, from_run: Any, default: Any) -> Any:
# Metrics: only added files, computed from what the seeds hold
# -------------------------
metrics_count: Optional[int] = None
lightdash_export = None
if _given(lightdash):
typer.echo("🔦 Adding Lightdash metrics and dimensions to the staging models...")
lightdash_export = to_lightdash(
resolve_metrics(model, metrics_doc, model_name=_model_stem(file))
)
write_lightdash(dest, lightdash_export)
if metrics_doc is not None and metrics_file is not None:
typer.echo("📏 Computing each metric's known value and its dbt test...")
semantic = resolve_metrics(model, metrics_doc, model_name=_model_stem(file))
Expand Down Expand Up @@ -797,6 +817,13 @@ def pick(option: Any, from_run: Any, default: Any) -> Any:
f"osi/{project_name}.yml, data-tests/metrics/)"
)

if lightdash_export is not None:
typer.echo(
f" Lightdash: {len(lightdash_export.metrics)} metrics "
"(models/staging/stg_*.yml, config.meta)"
)
_print_losses("Not expressible in Lightdash, left out", lightdash_export.lossiness)

if report is not None:
_print_defects(report)

Expand Down Expand Up @@ -1074,7 +1101,14 @@ def guide_command(
)
app.add_typer(metrics_app, name="metrics")

_METRICS_EXPORTS = ("ossie",)
_METRICS_EXPORTS = ("ossie", "lightdash")


def _print_losses(heading: str, losses: list) -> None:
if losses:
typer.echo(f"ℹ️ {heading}:")
for loss in losses:
typer.echo(f" - {loss}")


def _metrics_inputs(file: Path, metrics_file: Optional[Path]) -> Any:
Expand Down Expand Up @@ -1138,7 +1172,14 @@ def metrics_list_command(
def metrics_export_command(
file: Path = _MODEL_OPTION,
metrics_file: Optional[Path] = _METRICS_OPTION,
to: str = typer.Option("ossie", "--to", help="The format: 'ossie' (Apache Ossie 0.1.1, YAML)."),
to: str = typer.Option(
"ossie",
"--to",
help=(
"The format: 'ossie' (Apache Ossie 0.1.1, YAML) or 'lightdash' (dbt model "
"properties with Lightdash's config.meta, YAML)."
),
),
output: Optional[Path] = typer.Option( # noqa: B008
None,
"--output",
Expand All @@ -1153,14 +1194,19 @@ def metrics_export_command(
Ossie datasets read the staging models of the dbt project `generate` writes
(staging.stg_<table>). What Ossie 0.1.1 cannot say is listed at the top of the file,
and carried in custom_extensions.

Lightdash's file describes those staging models (stg_<table>) as a dbt properties
file. dbt reads a model's properties from one file only, so in a project `generate`
wrote, use `generate --lightdash` instead: it merges the same meta into the staging
models' own YAML. What Lightdash cannot say is listed at the top of the file.
"""
to = (_given(to) or "ossie").lower()
if to not in _METRICS_EXPORTS:
raise typer.BadParameter(
f"Choose one of: {', '.join(_METRICS_EXPORTS)}.", param_hint="--to"
)
semantic = _metrics_inputs(file, _given(metrics_file))
export = to_ossie(semantic)
export = to_ossie(semantic) if to == "ossie" else to_lightdash(semantic)
text = export.to_yaml()
output = _given(output)
if output is None:
Expand All @@ -1170,8 +1216,11 @@ def metrics_export_command(
typer.echo(f"❌ {output} already exists. Use --force to overwrite it.")
raise typer.Exit(1)
output.write_text(text, encoding="utf-8")
if isinstance(export, LightdashExport):
typer.echo(f"✅ Wrote {output} (Lightdash, dbt YAML, {len(export.metrics)} metrics)")
_print_losses("Not expressible in Lightdash, left out", export.lossiness)
return
typer.echo(f"✅ Wrote {output} (Apache Ossie 0.1.1, {len(semantic.metrics)} metrics)")
if export.lossiness:
typer.echo("ℹ️ Not expressible in Ossie 0.1.1 itself, carried in custom_extensions:")
for loss in export.lossiness:
typer.echo(f" - {loss}")
_print_losses(
"Not expressible in Ossie 0.1.1 itself, carried in custom_extensions", export.lossiness
)
6 changes: 4 additions & 2 deletions model2data/guide/tune.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
| `model2data validate M...` | check models against spec 0.4.0, and `*.metrics.yml` files against the metrics spec and their model (`--model M`, else the model beside it with the same stem) | 0 conforms (warnings allowed), 1 any error |
| `model2data convert M` | print M as `.model2data.yml`; `-o FILE` writes it, `--force` overwrites | 0 ok, 1 unreadable model or output exists |
| `model2data metrics list -f M [--metrics F]` | every metric: the file's, then the inferred ones | 0, 1 on a model or metrics error |
| `model2data metrics export -f M [--metrics F] --to ossie` | the model and its metrics as Apache Ossie 0.1.1; `-o FILE` writes it, `--force` overwrites | 0, 1 on an error or an existing output, 2 on another `--to` |
| `model2data metrics export -f M [--metrics F] --to ossie\|lightdash` | the model and its metrics as Apache Ossie 0.1.1, or as Lightdash meta in one dbt properties file for the staging models; `-o FILE` writes it, `--force` overwrites | 0, 1 on an error or an existing output, 2 on another `--to` |
| `model2data guide [TOPIC]` | this page; topics `setup`, `triage`, `tune` | 0 |

`generate` exits 0 on success; 1 on a model error, an existing `dbt_<name>/` without
Expand Down Expand Up @@ -142,7 +142,9 @@ Only tables with `incremental` change after the first day.
With `--metrics FILE`, and nothing else changed: `metric_values.json` (each metric's known
value over the seeds, overall and by month), `data-tests/metrics/metric_<name>.sql` (a dbt
test per metric that fails unless the warehouse gets that value) and `osi/<name>.yml`
(Apache Ossie 0.1.1).
(Apache Ossie 0.1.1). With `--lightdash` (with or without `--metrics`), the staging models'
`stg_<table>.yml` also carry Lightdash metrics, dimensions and joins under `config.meta`; what
Lightdash cannot say is listed in the summary.

## Metrics file

Expand Down
Loading
Loading