Skip to content

Commit 4cf5aee

Browse files
feat(snowflake_workspace): import_tables broadened + table/view dual-mode emission (v0.10.14)
import_tables now covers every table-shaped TABLE_TYPE not handled by another import_* flag: - BASE TABLE (permanent / transient / temporary; CTAS-created tables show as BASE TABLE too — Snowflake doesn't preserve the SELECT) - ICEBERG TABLE - HYBRID TABLE (Unistore) - EVENT TABLE (External / materialized / dynamic tables still on their own flags.) Dual-mode emission, mirroring dt_modeling shape: table_modeling: "observable" (default) → @observable_source_asset, polls row_count + last_altered, stable data_version, no click-to-materialize. table_modeling: "asset" → @asset that runs `CREATE OR REPLACE TABLE <name> AS <materialize_sql>`. Each imported table MUST carry a `materialize_sql:` under `assets_by_name.<name>:` — ValueError at component build time if missing. Snowflake's GET_DDL('TABLE',...) returns schema only (no SELECT — even for CTAS-created tables), so the customer supplies the SELECT, same as a dbt model file. MaterializeResult emits the same row_count + last_altered metadata + the executed query_id. view_modeling: "asset" (default) → @asset that runs `CREATE OR REPLACE VIEW <name> AS <VIEW_DEFINITION>`. Zero customer input — Snowflake stores the SELECT in INFORMATION_SCHEMA.VIEWS.VIEW_DEFINITION. Click materialize = recompile against current upstream schema, catches schema drift. Per-view `materialize_sql:` override pins a known-good SELECT if you'd rather not trust the stored copy. view_modeling: "observable" → falls back to observation-only. Fallback safety: if view_modeling=asset but no VIEW_DEFINITION available AND no override, that specific view falls back to observable mode with a warning log entry — no hard failure for secure views / privilege gaps. Shared helpers (_fetch_table_state, _build_metadata) factor out the INFORMATION_SCHEMA round-trip so observable and asset paths emit identical metadata + data_version signatures. Downstream eager only cascades when the table actually moves regardless of which mode emitted the event. README: - Features list: tables + views entries updated to reflect dual-mode. - New "Tables and views" section between assets_by_name and the stored procedure section. Covers what TABLE_TYPEs each flag pulls in, table_modeling vs view_modeling semantics, the materialize_sql: pattern, and the dbt-parallel framing for why tables need the SELECT supplied (CTAS doesn't preserve it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent de5fff9 commit 4cf5aee

3 files changed

Lines changed: 374 additions & 79 deletions

File tree

integrations/snowflake_workspace/README.md

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ uv run dg dev # auto-loads .env + .env.secrets
3636

3737
## Features
3838

39-
- **Tables**: Observe base tables (row_count + last_altered, stable data_version)
40-
- **Views**: Observe non-materialized views (same shape as tables)
39+
- **Tables**: Observe (default) or materialize regular / Iceberg / Hybrid / Event tables (row_count + last_altered, stable data_version). Materialize via customer-supplied `materialize_sql:` (see [Tables and views](#tables-and-views))
40+
- **Views**: Materialize (default — re-runs `CREATE OR REPLACE VIEW`) or observe (toggle via `view_modeling`)
4141
- **Tasks**: Execute Snowflake tasks on demand from Dagster
4242
- **Stored Procedures**: Call stored procedures from Dagster
4343
- **Dynamic Tables**: Trigger manual refreshes for dynamic tables
@@ -101,6 +101,7 @@ uv run dg dev # auto-loads .env + .env.secrets
101101

102102
| Field | Type | Default | Description |
103103
|---|---|---|---|
104+
| `table_modeling` | `str` | `"observable"` | How imported tables are emitted: 'observable' (default; @observable_source_asset, polls row_count + last_altered via INFORMATION_SCHEMA, no click-to-materialize) or 'asset' (@asset that runs CREATE OR REPLACE TABLE <name> AS <materialize_sql>). 'asset' requires per-table `materialize_sql:` in `assets_by_name`; ValueError at component build time if any imported table is missing the SQL. Snowflake doesn't store the SELECT used at CTAS time — so the customer must supply it, same as a dbt model file. |
104105
| `filter_by_name_pattern` | `str` || Regex pattern to filter entities by name |
105106

106107
### Other
@@ -119,8 +120,9 @@ uv run dg dev # auto-loads .env + .env.secrets
119120
| `import_external_tables` | `bool` | `false` | Import external tables as materializable assets (trigger refresh) |
120121
| `import_alerts` | `bool` | `false` | Import Snowflake alerts as observable assets (monitor alert status) |
121122
| `import_openflow_flows` | `bool` | `false` | Import OpenFlow data integration flows as observable assets (monitor via telemetry) |
122-
| `import_tables` | `bool` | `false` | Import base tables (TABLE_TYPE='BASE TABLE') as observable source assets. Each table becomes an @observable_source_asset that polls row_count + last_altered via INFORMATION_SCHEMA.TABLES and emits a stable data_version signature. Useful for lineage display + freshness checks against tables the team owns out-of-band (manually loaded, ETL'd by external tools, etc.). Iceberg + Hybrid tables are not included here — use dedicated components for those. |
123-
| `import_views` | `bool` | `false` | Import non-materialized views (TABLE_TYPE='VIEW') as observable source assets. Same shape as import_tables — row_count + last_altered observation, stable data_version. Materialized views are a separate concept; use import_materialized_views. |
123+
| `import_tables` | `bool` | `false` | Import tables as Dagster assets. Covers every table-shaped object that doesn't have its own dedicated import_* flag: regular base tables (BASE TABLE — permanent / transient / temporary), Iceberg tables, Hybrid tables (Unistore), and Event tables. Default behavior is observation (row_count + last_altered, stable data_version) — opt into materialization via `table_modeling: asset` + a per-table `materialize_sql:` in `assets_by_name`. External / materialized / dynamic tables have their own flags and are not included here. |
124+
| `import_views` | `bool` | `false` | Import non-materialized views (TABLE_TYPE='VIEW') as Dagster assets. Default behavior is materializable — re-runs the view's stored definition via CREATE OR REPLACE VIEW (recompiles against current upstream schema, catches schema drift). Opt back into observation-only via `view_modeling: observable`. Materialized views are a separate concept; use `import_materialized_views`. |
125+
| `view_modeling` | `str` | `"asset"` | How imported views are emitted: 'asset' (default; @asset that runs CREATE OR REPLACE VIEW <name> AS <VIEW_DEFINITION pulled from INFORMATION_SCHEMA.VIEWS> — zero config, Snowflake already stores the SELECT) or 'observable' (@observable_source_asset, lineage-display-only). |
124126
| `exclude_name_pattern` | `str` || Regex pattern to exclude entities by name |
125127
| `task_filter_by_state` | `str` || Filter tasks by state (STARTED, SUSPENDED). If not specified, imports all tasks. |
126128
| `generate_sensor` | `bool` | `true` | Create a sensor to observe task runs and dynamic table refreshes |
@@ -265,6 +267,84 @@ Supported field types: `int`, `str`, `float`, `bool`. Anything else raises `Valu
265267

266268
This works for `instances[].config_schema:` too — useful when one Snowflake task underlies N intentionally-distinct assets (e.g. by-region), each with its own overridable knobs.
267269

270+
## Tables and views
271+
272+
`import_tables` and `import_views` surface table-shaped objects from `INFORMATION_SCHEMA.TABLES` as Dagster assets. Both support dual-mode emission — pure observation (default for tables) or materializable @asset (default for views).
273+
274+
### What each flag covers
275+
276+
| `TABLE_TYPE` (Snowflake) | Imported by | Notes |
277+
|---|---|---|
278+
| `BASE TABLE` (permanent / transient / temporary) | `import_tables` | Regular tables. CTAS-created tables show as `BASE TABLE` too — Snowflake doesn't store the SELECT used at create time. |
279+
| `ICEBERG TABLE` | `import_tables` | Iceberg tables on external volumes. |
280+
| `HYBRID TABLE` (Unistore) | `import_tables` | OLTP + analytics on the same row store. |
281+
| `EVENT TABLE` | `import_tables` | Telemetry capture tables. |
282+
| `VIEW` | `import_views` | Non-materialized views. |
283+
| `MATERIALIZED VIEW` | `import_materialized_views` | Separate flag. |
284+
| `DYNAMIC TABLE` | `import_dynamic_tables` | Separate flag. |
285+
| `EXTERNAL TABLE` | `import_external_tables` | Separate flag. |
286+
287+
### Table modeling
288+
289+
`table_modeling:` controls how every imported table is emitted:
290+
291+
| Value | What you get | When to use |
292+
|---|---|---|
293+
| `observable` (default) | `@observable_source_asset`. Polls `INFORMATION_SCHEMA.TABLES.{ROW_COUNT, LAST_ALTERED}` on each tick. Emits `ObserveResult` with `data_version = f"{row_count}:{last_altered}"` so downstream eager only fires on real change. No click-to-materialize. | Catalog discoverability, lineage display, freshness checks against tables an external system writes. |
294+
| `asset` | `@asset` that runs `CREATE OR REPLACE TABLE <name> AS <materialize_sql>` on materialize. Emits `MaterializeResult` with the same row_count + last_altered metadata plus the executed query_id. | "Rebuild this table from SQL" — the customer-supplied SELECT plays the same role as a dbt model's `.sql` file. |
295+
296+
When `table_modeling: asset` is set, each imported table **must** carry a `materialize_sql:` in `assets_by_name`:
297+
298+
```yaml
299+
import_tables: true
300+
table_modeling: asset
301+
assets_by_name:
302+
DAILY_REVENUE:
303+
materialize_sql: |
304+
SELECT REGION,
305+
COUNT(*) AS ORDER_COUNT,
306+
SUM(TOTAL) AS REVENUE
307+
FROM RAW.ORDERS
308+
WHERE STATUS = 'paid'
309+
GROUP BY REGION
310+
CUSTOMER_LIFETIME_VALUE:
311+
materialize_sql: |
312+
SELECT CUSTOMER_ID,
313+
SUM(TOTAL) AS LIFETIME_VALUE,
314+
MAX(ORDER_DATE) AS LAST_ORDER
315+
FROM RAW.ORDERS
316+
WHERE STATUS IN ('paid', 'delivered')
317+
GROUP BY CUSTOMER_ID
318+
```
319+
320+
The component raises `ValueError` at build time if any imported table is missing `materialize_sql:` while `table_modeling: asset` is on — fail loud, no surprises at materialize time. Why is the SELECT a customer input? Snowflake's `GET_DDL('TABLE', …)` returns schema only (no SELECT), so the workspace can't auto-supply it. Same constraint dbt's `materialized='table'` works under.
321+
322+
### View modeling
323+
324+
`view_modeling:` controls how every imported view is emitted:
325+
326+
| Value | What you get | When to use |
327+
|---|---|---|
328+
| `asset` (default) | `@asset` that runs `CREATE OR REPLACE VIEW <name> AS <VIEW_DEFINITION>`. The SELECT is auto-loaded from `INFORMATION_SCHEMA.VIEWS.VIEW_DEFINITION` — zero customer input. Click materialize = recompile against current upstream schema, catches schema drift. | Default. Views are pure DDL; re-running is safe and useful. |
329+
| `observable` | `@observable_source_asset`. Lineage-display only, no click-to-materialize. | When you don't want Dagster touching the view DDL. |
330+
331+
To override the stored view definition (e.g. pin a known-good SELECT from source control rather than trusting Snowflake's stored copy), set `materialize_sql:` on the view's `assets_by_name` entry — same field as tables.
332+
333+
```yaml
334+
import_views: true
335+
view_modeling: asset # default — shown for clarity
336+
assets_by_name:
337+
V_PAID_ORDERS:
338+
# Optional: pin the SELECT explicitly. Without this, the workspace
339+
# uses whatever VIEW_DEFINITION Snowflake currently has stored.
340+
materialize_sql: |
341+
SELECT ORDER_ID, CUSTOMER_ID, TOTAL, ORDER_DATE
342+
FROM RAW.ORDERS
343+
WHERE STATUS = 'paid'
344+
```
345+
346+
If `view_modeling: asset` is set but Snowflake doesn't return a `VIEW_DEFINITION` for a given view (privilege issue, secure view) and no override is supplied, that view falls back to observable mode with a warning log entry.
347+
268348
## Stored procedure `args:` and `config_schema:`
269349

270350
Snowflake stored procedures are called positionally — `CALL <proc>(arg1, arg2, …)`. `assets_by_name` exposes the same dual shape as tasks for wiring those args: hardcoded via `args:`, or launchpad-overridable via `config_schema:`. They're mutually exclusive on the same asset (setting both raises `ValueError` at component build time).

0 commit comments

Comments
 (0)