diff --git a/docs/CLI_Guide.md b/docs/CLI_Guide.md index 3c6f327..12117d9 100644 --- a/docs/CLI_Guide.md +++ b/docs/CLI_Guide.md @@ -11,10 +11,29 @@ FastFlowTransform’s CLI is the entry point for seeding data, running DAGs, gen | `fft dag --html` | Render the DAG graph/site for quick inspection. | | `fft docgen [--out site/docs] [--emit-json path] [--open-source]` | Generate the full documentation bundle (graph + model pages + optional JSON). Default output is `/site/docs`. | | `fft test [--env dev]` | Run schema/data-quality tests defined in `project.yml` or schema YAML files. | +| `fft source-freshness [--env dev] [--select …]` | Evaluate freshness rules for sources and emit a summary (e.g. in the DQ demo). | | `fft utest ` | Execute unit tests defined under `tests/unit/*.yml`. | | `fft sync-db-comments ` | Push model/column descriptions into Postgres or Snowflake comments. | -Use `--select` to scope `run`, `dag`, or `test` commands (e.g. `state:modified`, `tag:finance`, `result:error`). Environment overrides rely on the selected profile in `profiles.yml` or the `FF_*` variables. +Use `--select` to scope `run`, `dag`, `test`, or `source-freshness` commands (e.g. `state:modified`, `tag:finance`, `result:error`). Environment overrides rely on the selected profile in `profiles.yml` or the `FF_*` variables. + +## Source Freshness Checks + +`fft source-freshness --env ` evaluates **freshness rules for sources** (typically configured alongside your `sources.yml` / project metadata). + +Typical usage: + +```bash +fft source-freshness examples/dq_demo --env dev_duckdb +fft source-freshness . --env dev --select tag:critical_source +```` + +Key points: + +* Uses the active profile (`--env`) to connect to the warehouse. +* Honors `--select`/`--exclude` so you can restrict checks to specific tags or source groups. +* Prints a per-source summary (status, max loaded timestamp, configured thresholds) plus an overall status code suitable for CI. +* Integrates naturally with the DQ demo: the same environment and seeds are reused, but checks focus on **source recency** rather than row-level tests in marts. ## HTTP/API Helpers @@ -22,16 +41,16 @@ Python models can make HTTP calls via `fastflowtransform.api.http`. When you nee ## DAG & Documentation -- Narrow the graph with `fft dag ... --select ` (for example `state:modified` or `tag:finance`). Combined with `--html` this produces a focused mini-site under `/docs/index.html`. -- Control schema introspection via `--with-schema/--no-schema`. Use `--no-schema` when the executor should avoid fetching column metadata (for example, BigQuery without sufficient permissions). -- `fft docgen` renders the DAG, model pages, and an optional JSON manifest in one command. Append `--open-source` to open `index.html` in your default browser after rendering. +* Narrow the graph with `fft dag ... --select ` (for example `state:modified` or `tag:finance`). Combined with `--html` this produces a focused mini-site under `/docs/index.html`. +* Control schema introspection via `--with-schema/--no-schema`. Use `--no-schema` when the executor should avoid fetching column metadata (for example, BigQuery without sufficient permissions). +* `fft docgen` renders the DAG, model pages, and an optional JSON manifest in one command. Append `--open-source` to open `index.html` in your default browser after rendering. ## Sync Database Comments `fft sync-db-comments --env ` pushes model and column descriptions from project YAML or Markdown into database comments. The command currently supports Postgres and Snowflake Snowpark: -- Start with `--dry-run` to review the generated `COMMENT` statements. -- Postgres honors `profiles.yml -> postgres.db_schema` (and any `FF_PG_SCHEMA` override). -- Snowflake reuses the session or connection exposed by the executor. +* Start with `--dry-run` to review the generated `COMMENT` statements. +* Postgres honors `profiles.yml -> postgres.db_schema` (and any `FF_PG_SCHEMA` override). +* Snowflake reuses the session or connection exposed by the executor. If no descriptions are found, the command exits without making changes. diff --git a/docs/Source_Freshness.md b/docs/Source_Freshness.md new file mode 100644 index 0000000..f441edc --- /dev/null +++ b/docs/Source_Freshness.md @@ -0,0 +1,162 @@ +# Source Freshness + +Source freshness checks answer a simple question: + +> “How old is the latest data in this source, and is that acceptable?” + +They complement table-level DQ tests by validating **recency of inputs** (seeds, raw tables, +landing zones) *before* you build marts. + +- Configuration lives alongside your `sources.yml` metadata. +- Evaluation is done via the `fft source-freshness` CLI command. +- Output is CI-friendly (non-zero exit when critical freshness rules fail). + +--- + +## When to use source freshness + +Use source freshness when: + +- you rely on upstream ingestion jobs (ETL, CDC, streaming) and need a guard-rail like + “`crm.orders` must be < 60 minutes old”; +- you have critical feeds (payments, auth logs, PII) where stale data is dangerous; +- you want a cheap pre-flight check in CI before running a heavier `fft run` + `fft test`. + +It is *not* a replacement for table-level `freshness` tests on marts – they work nicely together. + +--- + +## Configuration + +Freshness rules are attached to source tables in your metadata (conceptually alongside `sources.yml`). + +A minimal example: + +```yaml +version: 2 +sources: + - name: crm + schema: raw + tables: + - name: orders + identifier: seed_orders + freshness: + loaded_at_field: "_ff_loaded_at" + max_delay_minutes: 1440 # 1 day + warn_after_minutes: 720 # optional: warning threshold + error_after_minutes: 1440 # optional: hard error threshold + tags: ["example:dq_demo", "critical_source"] +```` + +Key fields: + +* `loaded_at_field`: timestamp column used to compute the **max** loaded time. When seeds are + materialized via `fft seed`, every table automatically includes `_ff_loaded_at` (UTC timestamp + captured during the seed run). Pointing freshness rules at this metadata column keeps demo seeds + “fresh” even if the CSV contains static business timestamps. +* `max_delay_minutes` / `warn_after_minutes` / `error_after_minutes`: + + * if only `max_delay_minutes` is set, it is treated as an error threshold; + * `warn_after_minutes` and `error_after_minutes` allow a 3-state result: + + * ✅ **on-time** (age ≤ `warn_after_minutes`) + * ❕ **late (warning)** (`warn_after_minutes` < age ≤ `error_after_minutes`) + * ❌ **stale (error)** (age > `error_after_minutes`) + +The exact field names should mirror whatever you wired into `run_source_freshness`; adjust the snippet if your structure differs. + +--- + +## Running checks + +Basic usage: + +```bash +fft source freshness --env +``` + +Examples: + +```bash +# Check all sources in the DQ demo (DuckDB) +fft source freshness examples/dq_demo --env dev_duckdb + +# Only check sources tagged "critical_source" +fft source freshness . \ + --env dev \ + --select tag:critical_source + +# Combine with other selectors (depends on your implementation) +fft source freshness . \ + --env dev \ + --select source:crm --exclude tag:experimental +``` + +The command: + +* connects using the selected profile (`--env`); +* loads source + freshness metadata; +* executes a `max(loaded_at_column)` query per configured source; +* compares the result to your thresholds and produces: + + * per-source rows (age, thresholds, status), + * an overall exit status (`0` if all within thresholds, non-zero on error). + +--- + +## CI / automation + +Typical pattern in CI: + +```bash +# 1) Check source recency +fft source freshness . --env ci + +# 2) Only if sources are fresh, run the pipeline and DQ tests +fft run . --env ci +fft test . --env ci --select tag:ci +``` + +Because `fft source freshness` exits non-zero on stale inputs, you can simply let the +CI job fail early rather than running a full DAG on obviously outdated data. + +--- + +## Troubleshooting + +**“No freshness rules found”** + +* You called `fft source-freshness` but nothing was evaluated. +* Check that: + + * at least one source table has a `freshness:` block; + * your `--select` / `--exclude` patterns aren’t filtering everything out. + +**“Column not found”** + +* The `loaded_at_column` doesn’t exist in the physical source. +* Verify the column name and that your `identifier` / schema overrides for that source are correct. + +**Unexpectedly large ages** + +* Make sure your warehouse and timestamps are in the expected timezone. +* Confirm that the ingestion job actually updates `loaded_at_column` (and not some other field). + +--- + +## Relationship to table-level freshness tests + +Table-level `freshness` tests in `project.yml`: + +* operate on **models** (e.g. `mart_orders_agg.last_order_ts`); +* run via `fft test`. + +Source freshness: + +* operates on **sources** (e.g. `crm.orders.order_ts`); +* runs via `fft source freshness`. + +Using both lets you catch: + +1. Stale upstream ingestion (source is old), +2. And downstream pipeline lag or bugs (mart not refreshed even though source is fresh). diff --git a/docs/Sources.md b/docs/Sources.md index a2477cf..c67d827 100644 --- a/docs/Sources.md +++ b/docs/Sources.md @@ -6,12 +6,12 @@ Place `sources.yml` at your project root (same level as `models/`). Example: -``` +```text project/ ├── models/ ├── sources.yml └── seeds/ -``` +```` ## YAML Schema (Version 2) @@ -39,18 +39,18 @@ sources: ### Fields -| Level | Field | Description | -|----------|-------------|-------------| -| source | `name` | Logical group identifier referenced by `source('name', ...)`. | -| | `schema` | Default target schema/database for the group. | -| | `database`/`catalog` | Optional qualifiers per engine (BigQuery, Snowflake). | -| | `overrides` | Map of engine → config snippet (schema overrides, formats, locations). | -| table | `name` | Logical table name (second argument in `source()`). | -| | `identifier`| Physical name; defaults to `name` if omitted. | -| | `location` | File/path location (used with `format`). | -| | `format` | Ingestion format for engines supporting path-based sources (`delta`, `parquet`, …). | -| | `options` | Dict of format options (Spark/Databricks). | -| | `overrides` | Additional engine-specific settings merged with source-level overrides. | +| Level | Field | Description | +| ------ | -------------------- | ----------------------------------------------------------------------------------- | +| source | `name` | Logical group identifier referenced by `source('name', ...)`. | +| | `schema` | Default target schema/database for the group. | +| | `database`/`catalog` | Optional qualifiers per engine (BigQuery, Snowflake). | +| | `overrides` | Map of engine → config snippet (schema overrides, formats, locations). | +| table | `name` | Logical table name (second argument in `source()`). | +| | `identifier` | Physical name; defaults to `name` if omitted. | +| | `location` | File/path location (used with `format`). | +| | `format` | Ingestion format for engines supporting path-based sources (`delta`, `parquet`, …). | +| | `options` | Dict of format options (Spark/Databricks). | +| | `overrides` | Additional engine-specific settings merged with source-level overrides. | Engine-specific overrides follow this merge order: @@ -60,8 +60,8 @@ Engine-specific overrides follow this merge order: ### Engine Behavior -- **DuckDB / Postgres / BigQuery / Snowflake**: expect `identifier` (plus `schema`/`database` where relevant). Path-based sources raise errors. -- **Databricks Spark**: supports `format` + `location`. The executor registers a temp view with optional `options` (e.g. `compression`). +* **DuckDB / Postgres / BigQuery / Snowflake**: expect `identifier` (plus `schema`/`database` where relevant). Path-based sources raise errors. +* **Databricks Spark**: supports `format` + `location`. The executor registers a temp view with optional `options` (e.g. `compression`). ### Path-Based Sources Example @@ -77,6 +77,70 @@ Engine-specific overrides follow this merge order: multiline: true ``` +### Example: Typical Project Sources + +A typical analytics project mixes **seeded reference data**, **database tables**, and **lakehouse paths**. A single `sources.yml` might look like this: + +```yaml +version: 2 +sources: + # Seeded reference data (CSV → tables) + - name: ref + schema: ref + tables: + - name: countries + identifier: seed_countries + - name: currencies + identifier: seed_currencies + + # Core application database (OLTP / CDC) + - name: crm + schema: crm + overrides: + postgres: + schema: public + bigquery: + dataset: crm_raw + tables: + - name: customers + identifier: customers + - name: orders + identifier: orders + + # Lakehouse-style raw events (Spark-only) + - name: events + tables: + - name: clickstream + overrides: + databricks_spark: + format: parquet + location: "abfss://raw@storage.dfs.core.windows.net/clickstream/*.parquet" + - name: pageviews + overrides: + databricks_spark: + format: delta + location: "abfss://delta@storage.dfs.core.windows.net/pageviews" +``` + +Models then reference sources in a uniform way: + +```sql +-- Seeded lookup +select * from {{ source('ref', 'countries') }}; + +-- OLTP / warehouse tables +select * from {{ source('crm', 'customers') }}; + +-- Lakehouse paths (on Spark) +select * from {{ source('events', 'clickstream') }}; +``` + +The executor resolves each reference to the correct physical object for the active engine: + +* Postgres: `"public"."customers"` +* BigQuery: `crm_raw.customers` +* Databricks: `delta.` or `parquet.` tables / paths behind the scenes. + ## Referencing Sources in Models ```sql @@ -99,10 +163,26 @@ targets: postgres: staging ``` +### Seed metadata columns + +The `fft seed` command automatically appends a small set of metadata columns to every materialized +seed table: + +| Column | Description | +|-------------------|----------------------------------------------------------------| +| `_ff_loaded_at` | UTC timestamp captured when the seed was written. | +| `_ff_seed_id` | Stable identifier derived from the path inside `seeds/`. | +| `_ff_seed_file` | Absolute path of the source file (CSV/Parquet) used to load it.| + +These columns live alongside your business fields, so downstream models (and freshness checks) +can reference them directly. For example, point a source freshness rule to `_ff_loaded_at` to +assert “seed data was loaded within the last N minutes” irrespective of the timestamps stored in +the raw file. + ## Validation & Errors -- Missing `identifier` *and* `location` produce `KeyError` during rendering. -- Unknown source/table names raise `KeyError` with suggestions. -- Unsupported path-based sources on an engine (`location` provided but no `format`) raise descriptive `NotImplementedError`. +* Missing `identifier` *and* `location` produce `KeyError` during rendering. +* Unknown source/table names raise `KeyError` with suggestions. +* Unsupported path-based sources on an engine (`location` provided but no `format`) raise descriptive `NotImplementedError`. Keep `sources.yml` declarative, use engine overrides for schema differences, and lean on `.env` files where credentials or URIs vary per environment. diff --git a/docs/examples/DQ_Demo.md b/docs/examples/DQ_Demo.md index d3cecbf..28fcbe3 100644 --- a/docs/examples/DQ_Demo.md +++ b/docs/examples/DQ_Demo.md @@ -82,10 +82,14 @@ examples/dq_demo/ ### Seeds * `seeds/customers.csv` - Simple customer dimension (e.g. `customer_id`, `name`, `status`). + Simple customer dimension with a creation timestamp: + `customer_id`, `name`, `status`, `created_at` (ISO-8601, e.g. `2025-01-01T10:00:00`). + The demo ships with three rows (Alice, Bob, Carol) so it’s easy to reason about failures. * `seeds/orders.csv` - Order fact data (e.g. `order_id`, `customer_id`, `amount`, `order_ts` as a string). + Order fact data with per-order timestamps: + `order_id`, `customer_id`, `amount`, `order_ts` (ISO-8601, e.g. `2025-01-10T09:00:00`). + One order has `amount = 0.00` so the custom `min_positive_share` test has something to complain about. ### Models @@ -95,6 +99,28 @@ examples/dq_demo/ * Casts IDs and other fields into proper types. * Used as the “clean” customer dimension for downstream checks. + ```sql + {{ config( + materialized='table', + tags=[ + 'example:dq_demo', + 'scope:staging', + 'engine:duckdb', + 'engine:postgres', + 'engine:databricks_spark', + 'engine:bigquery', + 'engine:snowflake_snowpark' + ], + ) }} + + select + cast(customer_id as int) as customer_id, + name, + status, + cast(created_at as timestamp) as created_at + from {{ source('crm', 'customers') }}; + ``` + **2. Staging: `orders.ff.sql`** * Materialized as a table. @@ -212,11 +238,11 @@ tests: column: amount tags: [example:dq_demo, batch] - # 4) Customer status values must be within a known set + # 4) Customer status values must be within a known set - type: accepted_values table: mart_orders_agg column: status - values: ["active", "churned", "prospect"] + values: ["active", "inactive", "prospect"] severity: warn # show as warning, not hard failure tags: [example:dq_demo, batch] @@ -571,3 +597,16 @@ The Data Quality Demo is designed to be: * reconciling sums and row counts across tables. Once you’re comfortable with this example, you can copy the patterns into your real project: start with staging-level checks, then layer in reconciliations and freshness on your most important marts. + +> **Tip – Source vs. table freshness** +> +> The demo uses the `freshness` test type on the mart (`mart_orders_agg.last_order_ts`). +> For *source-level freshness* (e.g. “when was `crm.orders` last loaded?”), define +> freshness rules on your sources and run: +> +> ```bash +> fft source freshness examples/dq_demo --env dev_duckdb +> ``` +> +> This complements table-level DQ tests by checking whether your inputs are recent enough +> *before* you even build marts. diff --git a/docs/index.md b/docs/index.md index e0c2003..9473cbf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,6 +59,7 @@ Use this page as the front door into the docs: start with the orientation sectio - **Schema-bound YAML tests:** [YAML Tests](YAML_Tests.md) details how to define and run column-level constraints declared in `.yml`. - **Reusable data-quality suites:** [Data Quality Tests](Data_Quality_Tests.md) catalogs reconciliation, freshness, and anomaly rules that can attach to models or sources. +- **Source freshness guard-rails:** [Source Freshness](Source_Freshness.md) covers `fft source freshness`, metadata in `sources.yml`, and interpreting warn/error thresholds in the docs UI. - **Fast model unit tests:** [Unit Tests](Unit_Tests.md) shows how to author `.sql` / `.py` assertions, seed fixtures, and run them via `fft utest`. --- diff --git a/examples/dq_demo/Makefile b/examples/dq_demo/Makefile index 599f2c4..c84854e 100644 --- a/examples/dq_demo/Makefile +++ b/examples/dq_demo/Makefile @@ -85,6 +85,9 @@ test: dag: env $(RUN_ENV) $(UV) run fft dag "$(PROJECT)" --env $(PROFILE_ENV) $(SELECT_FLAGS) --html +freshness: + env $(RUN_ENV) $(UV) run fft source freshness "$(PROJECT)" --env $(PROFILE_ENV) + artifacts: @echo @echo "== 📦 Artifacts ==" @@ -105,6 +108,7 @@ demo: clean @echo "== 🧪 dq_demo (Data Quality) ==" @echo "Profile=$(PROFILE_ENV) PROJECT=$(PROJECT)" +$(MAKE) seed + +$(MAKE) freshness +$(MAKE) run +$(MAKE) dag +$(MAKE) test diff --git a/examples/dq_demo/models/staging/customers.ff.sql b/examples/dq_demo/models/staging/customers.ff.sql index 74697ed..30e9a4f 100644 --- a/examples/dq_demo/models/staging/customers.ff.sql +++ b/examples/dq_demo/models/staging/customers.ff.sql @@ -13,8 +13,8 @@ -- Staging table for customers select - customer_id, + cast(customer_id as int) as customer_id, name, status, - created_at + cast(created_at as timestamp) as created_at from {{ source('crm', 'customers') }}; diff --git a/examples/dq_demo/seeds/schema.yml b/examples/dq_demo/seeds/schema.yml new file mode 100644 index 0000000..47c2fa0 --- /dev/null +++ b/examples/dq_demo/seeds/schema.yml @@ -0,0 +1,20 @@ +# Target placement for demo seeds +targets: + seed_customers: + schema: dq_demo + seed_orders: + schema: dq_demo + +columns: + seed_customers: + customer_id: integer + name: string + status: string + created_at: + type: timestamp + seed_orders: + order_id: integer + customer_id: integer + amount: double + order_ts: + type: timestamp diff --git a/examples/dq_demo/sources.yml b/examples/dq_demo/sources.yml index 0260a5a..57bc26a 100644 --- a/examples/dq_demo/sources.yml +++ b/examples/dq_demo/sources.yml @@ -2,8 +2,27 @@ version: 2 sources: - name: crm + schema: dq_demo tables: - name: customers - identifier: seed_customers # materialized via `fft seed` + identifier: seed_customers + description: "Seeded customers table" + freshness: + loaded_at_field: _ff_loaded_at + warn_after: + count: 60 + period: minute # or "hour"/"day" + error_after: + count: 240 + period: minute - name: orders - identifier: seed_orders # materialized via `fft seed` + identifier: seed_orders + description: "Seeded orders table" + freshness: + loaded_at_field: _ff_loaded_at + warn_after: + count: 60 + period: minute + error_after: + count: 240 + period: minute diff --git a/examples/incremental_demo/site/dag/events_base.ff.html b/examples/incremental_demo/site/dag/events_base.ff.html index 87c952c..f3a9097 100644 --- a/examples/incremental_demo/site/dag/events_base.ff.html +++ b/examples/incremental_demo/site/dag/events_base.ff.html @@ -101,6 +101,17 @@

Metadata

+
Sources
+
+ + + +
+
Referenced by
diff --git a/examples/incremental_demo/site/dag/fct_events_py_incremental.html b/examples/incremental_demo/site/dag/fct_events_py_incremental.html index bb33bf9..3653808 100644 --- a/examples/incremental_demo/site/dag/fct_events_py_incremental.html +++ b/examples/incremental_demo/site/dag/fct_events_py_incremental.html @@ -105,6 +105,13 @@

Metadata

+
Sources
+
+ + No source() refs + +
+ diff --git a/examples/incremental_demo/site/dag/fct_events_sql_inline.ff.html b/examples/incremental_demo/site/dag/fct_events_sql_inline.ff.html index f65aa52..92dc088 100644 --- a/examples/incremental_demo/site/dag/fct_events_sql_inline.ff.html +++ b/examples/incremental_demo/site/dag/fct_events_sql_inline.ff.html @@ -105,6 +105,13 @@

Metadata

+
Sources
+
+ + No source() refs + +
+ diff --git a/examples/incremental_demo/site/dag/fct_events_sql_yaml.ff.html b/examples/incremental_demo/site/dag/fct_events_sql_yaml.ff.html index 0f17ffa..4846cb1 100644 --- a/examples/incremental_demo/site/dag/fct_events_sql_yaml.ff.html +++ b/examples/incremental_demo/site/dag/fct_events_sql_yaml.ff.html @@ -105,6 +105,13 @@

Metadata

+
Sources
+
+ + No source() refs + +
+ diff --git a/examples/incremental_demo/site/dag/index.html b/examples/incremental_demo/site/dag/index.html index 6fc455c..e189cee 100644 --- a/examples/incremental_demo/site/dag/index.html +++ b/examples/incremental_demo/site/dag/index.html @@ -68,6 +68,7 @@ .chip { font-size:12px; padding:2px 10px; border-radius:999px; border:1px solid var(--border); } .chip.sql { background: var(--chip-sql-bg); color: var(--chip-sql-fg); } .chip.py { background: var(--chip-py-bg); color: var(--chip-py-fg); } + .pill { display:inline-block; padding:2px 8px; border-radius:999px; border:1px solid var(--border); font-size:12px; margin-right:4px; } .badge { font: 12px/1.6 system-ui, sans-serif; padding: 2px 8px; border-radius: 999px; border: 1px solid transparent; } .badge-table { background:#eef7ff; color:#0a3a77; border-color:#bcd8fb; } @@ -127,15 +128,24 @@

DAG

flowchart TD classDef sql fill:#e8f1ff,stroke:#5b8def,color:#0a1f44; classDef py fill:#e9fbf1,stroke:#2bb673,color:#0b2e1f; + classDef source fill:#fef3c7,stroke:#f59e0b,color:#78350f; events_base_ff["events_base.ff
(events_base)"] class events_base_ff sql; + click events_base_ff "events_base.ff.html" "View model" fct_events_py_incremental("fct_events_py_incremental
(fct_events_py_incremental)") class fct_events_py_incremental py; + click fct_events_py_incremental "fct_events_py_incremental.html" "View model" fct_events_sql_inline_ff["fct_events_sql_inline.ff
(fct_events_sql_inline)"] class fct_events_sql_inline_ff sql; + click fct_events_sql_inline_ff "fct_events_sql_inline.ff.html" "View model" fct_events_sql_yaml_ff["fct_events_sql_yaml.ff
(fct_events_sql_yaml)"] class fct_events_sql_yaml_ff sql; + click fct_events_sql_yaml_ff "fct_events_sql_yaml.ff.html" "View model" + src_raw_events[["raw.events"]] + class src_raw_events source; + click src_raw_events "source_raw.events.html" "View source" events_base_ff --> fct_events_sql_inline_ff + src_raw_events --> events_base_ff events_base_ff --> fct_events_sql_yaml_ff events_base_ff --> fct_events_py_incremental
@@ -241,6 +251,49 @@

Macros

No macros found.

+ +
+

Sources

+ + + + + + + + + + + + + + + + + + + + +
NameRelationFreshnessConsumers
+ raw.events + + seed_events + +
+ + no warn + + + no error + +
+
+ + 1 model + +
+ +