interlaced 2.3.0__tar.gz → 2.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {interlaced-2.3.0/src/interlaced.egg-info → interlaced-2.4.1}/PKG-INFO +47 -11
  2. {interlaced-2.3.0 → interlaced-2.4.1}/README.md +43 -9
  3. {interlaced-2.3.0 → interlaced-2.4.1}/pyproject.toml +7 -2
  4. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/cli/main.py +3 -1
  5. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/config/config.py +11 -7
  6. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/dsl/decorators.py +12 -0
  7. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/dsl/discovery.py +69 -10
  8. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/duckdb.py +54 -3
  9. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/graph/project.py +10 -3
  10. interlaced-2.4.1/src/interlace/ir/macros.py +154 -0
  11. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/plan/apply.py +126 -30
  12. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/project.py +12 -3
  13. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/scaffold.py +19 -2
  14. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/append.py +12 -4
  15. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/base.py +13 -0
  16. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/hash_merge.py +40 -18
  17. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/incremental.py +6 -0
  18. interlaced-2.4.1/src/interlace/strategies/merge.py +188 -0
  19. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/scd.py +9 -1
  20. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/interlace.yaml +2 -1
  21. interlaced-2.4.1/src/interlace/templates/quickstart/interlace.yaml +6 -0
  22. {interlaced-2.3.0 → interlaced-2.4.1/src/interlaced.egg-info}/PKG-INFO +47 -11
  23. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlaced.egg-info/SOURCES.txt +1 -0
  24. interlaced-2.3.0/src/interlace/strategies/merge.py +0 -115
  25. interlaced-2.3.0/src/interlace/templates/quickstart/interlace.yaml +0 -6
  26. {interlaced-2.3.0 → interlaced-2.4.1}/LICENSE +0 -0
  27. {interlaced-2.3.0 → interlaced-2.4.1}/MANIFEST.in +0 -0
  28. {interlaced-2.3.0 → interlaced-2.4.1}/setup.cfg +0 -0
  29. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/__init__.py +0 -0
  30. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/checks/__init__.py +0 -0
  31. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/checks/builtin.py +0 -0
  32. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/checks/runner.py +0 -0
  33. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/checks/spec.py +0 -0
  34. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/cli/__init__.py +0 -0
  35. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/config/__init__.py +0 -0
  36. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/contracts.py +0 -0
  37. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/dsl/__init__.py +0 -0
  38. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/dsl/sql_config.py +0 -0
  39. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/__init__.py +0 -0
  40. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/adbc.py +0 -0
  41. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/base.py +0 -0
  42. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/bigquery.py +0 -0
  43. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/postgres.py +0 -0
  44. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/quack.py +0 -0
  45. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/redshift.py +0 -0
  46. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/registry.py +0 -0
  47. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/snowflake.py +0 -0
  48. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/engines/spark.py +0 -0
  49. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/exceptions.py +0 -0
  50. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/graph/__init__.py +0 -0
  51. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/graph/column_lineage.py +0 -0
  52. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/graph/dag.py +0 -0
  53. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/graph/selectors.py +0 -0
  54. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/ir/__init__.py +0 -0
  55. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/ir/canonicalize.py +0 -0
  56. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/ir/fingerprint.py +0 -0
  57. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/ir/relation.py +0 -0
  58. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/plan/__init__.py +0 -0
  59. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/plan/differ.py +0 -0
  60. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/plan/plan.py +0 -0
  61. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/plan/resolve.py +0 -0
  62. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/plan/run.py +0 -0
  63. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/py.typed +0 -0
  64. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/query.py +0 -0
  65. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/runtime/__init__.py +0 -0
  66. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/runtime/handles.py +0 -0
  67. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/runtime/python_model.py +0 -0
  68. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/scheduler/__init__.py +0 -0
  69. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/scheduler/engine.py +0 -0
  70. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/scheduler/triggers.py +0 -0
  71. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/scheduler/worker.py +0 -0
  72. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/__init__.py +0 -0
  73. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/app.py +0 -0
  74. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/auth.py +0 -0
  75. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/app.css +0 -0
  76. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/favicon.svg +0 -0
  77. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/index.html +0 -0
  78. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/api.js +0 -0
  79. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/app.js +0 -0
  80. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/dag.js +0 -0
  81. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/timeline.js +0 -0
  82. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/ui.js +0 -0
  83. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/checks.js +0 -0
  84. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/environments.js +0 -0
  85. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/lineage.js +0 -0
  86. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/models.js +0 -0
  87. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/overview.js +0 -0
  88. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/plan.js +0 -0
  89. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/query.js +0 -0
  90. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/runs.js +0 -0
  91. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/streams.js +0 -0
  92. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/service/ui/js/views/system.js +0 -0
  93. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/sinks.py +0 -0
  94. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/sources/__init__.py +0 -0
  95. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/sources/auth.py +0 -0
  96. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/sources/rest.py +0 -0
  97. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/state/__init__.py +0 -0
  98. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/state/interval.py +0 -0
  99. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/state/janitor.py +0 -0
  100. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/state/snapshot.py +0 -0
  101. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/state/store.py +0 -0
  102. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/__init__.py +0 -0
  103. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/full_merge.py +0 -0
  104. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/replace.py +0 -0
  105. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/replace_in_place.py +0 -0
  106. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/strategies/view.py +0 -0
  107. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/streaming/__init__.py +0 -0
  108. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/streaming/log.py +0 -0
  109. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/streaming/materializer.py +0 -0
  110. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/streaming/schema.py +0 -0
  111. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/README.md +0 -0
  112. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/generate.py +0 -0
  113. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/models/events.py +0 -0
  114. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
  115. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/models/events_by_type.sql +0 -0
  116. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/models/top_users.sql +0 -0
  117. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/models/user_spend.sql +0 -0
  118. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/events/template.yaml +0 -0
  119. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/github/README.md +0 -0
  120. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/github/interlace.yaml +0 -0
  121. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/github/models/github_issues.py +0 -0
  122. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
  123. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/github/template.yaml +0 -0
  124. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/postgres/README.md +0 -0
  125. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/postgres/docker-compose.yml +0 -0
  126. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/postgres/init/seed.sql +0 -0
  127. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/postgres/interlace.yaml +0 -0
  128. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/postgres/models/orders.py +0 -0
  129. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
  130. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/postgres/template.yaml +0 -0
  131. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/quickstart/README.md +0 -0
  132. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
  133. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
  134. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
  135. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlace/templates/quickstart/template.yaml +0 -0
  136. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlaced.egg-info/dependency_links.txt +0 -0
  137. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlaced.egg-info/entry_points.txt +0 -0
  138. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlaced.egg-info/requires.txt +0 -0
  139. {interlaced-2.3.0 → interlaced-2.4.1}/src/interlaced.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlaced
3
- Version: 2.3.0
4
- Summary: Python/SQL-first data platform: transformation, built-in orchestration, and durable streaming ingestion
3
+ Version: 2.4.1
4
+ Summary: Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres.
5
5
  Author-email: Mark <mark@interlace.sh>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://interlace.sh
@@ -13,8 +13,10 @@ Keywords: data,pipeline,orchestration,transformation,etl,streaming,dbt,sqlmesh,d
13
13
  Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Topic :: Software Development :: Build Tools
16
+ Classifier: Topic :: Database
16
17
  Classifier: Programming Language :: Python :: 3.12
17
18
  Classifier: Programming Language :: SQL
19
+ Classifier: Operating System :: POSIX :: Linux
18
20
  Requires-Python: >=3.12
19
21
  Description-Content-Type: text/markdown
20
22
  License-File: LICENSE
@@ -64,15 +66,41 @@ Dynamic: license-file
64
66
 
65
67
  # interlace
66
68
 
67
- **Python/SQL-first data platform: transformation, orchestration, and durable streaming — one process.**
69
+ **Python and SQL models are the same kind of node in one DAG.**
68
70
 
69
- interlace is an independent, MIT-licensed alternative to dbt/SQLMesh that also replaces the
70
- orchestrator (no Airflow) and the ingestion layer (Cloudflare-Pipelines-style durable streams).
71
- Models are `.sql` files or Python functions; state is versioned snapshots with virtual
71
+ A `.py` model sits mid-graph with SQL either side, in both directions, with no bridge and no
72
+ separate runtime — running in-process on DuckDB and Postgres, not only on a cloud warehouse.
73
+ The Python model stays a plain function: call it in a test with no warehouse and no session.
74
+
75
+ How that compares with dbt and SQLMesh, including where they are ahead, is on
76
+ [interlace.sh/why](https://interlace.sh/why).
77
+
78
+ ```sql
79
+ -- models/raw_events.sql SQL
80
+ SELECT event_id, user_id, kind, amount, country, ts FROM read_parquet('events/*.parquet')
81
+ ```
82
+ ```python
83
+ # models/enriched_events.py Python, mid-DAG
84
+ @model() # the parameter name IS the dependency — no depends_on
85
+ def enriched_events(raw_events):
86
+ for batch in raw_events.reader(): # Arrow in, Arrow out, bounded memory
87
+ yield add_revenue(batch)
88
+ ```
89
+ ```sql
90
+ -- models/event_summary.sql SQL again, straight over the Python
91
+ SELECT country, count(*) FILTER (WHERE is_conversion) AS conversions
92
+ FROM enriched_events GROUP BY country
93
+ ```
94
+
95
+ `interlace init` scaffolds exactly this shape, runnable, with no external source.
96
+
97
+ That is the wedge. The rest is the reveal: interlace is an independent, MIT-licensed alternative
98
+ to dbt/SQLMesh that also replaces the orchestrator (no Airflow) and the ingestion layer
99
+ (Cloudflare-Pipelines-style durable streams). State is versioned snapshots with virtual
72
100
  environments and a terraform-style plan/apply; everything runs in a single daemon on
73
- DuckDB + DuckLake by default.
101
+ DuckDB by default (DuckLake one config line away).
74
102
 
75
- > **Status: 2.0.** Requires Python 3.12+.
103
+ > **2.x — see [releases](https://github.com/interlace-sh/interlace/releases).** Requires Python 3.12+.
76
104
  > The package is published to PyPI as **`interlaced`**; the import name and CLI are `interlace`.
77
105
 
78
106
  ```bash
@@ -80,6 +108,13 @@ pip install 'interlaced[service]' # the CLI + daemon; core CLI only: pip insta
80
108
  # more extras: [adbc] postgres/redshift · [spark] · [polars] · [all]
81
109
  ```
82
110
 
111
+ > **Platforms.** Developed on Linux; CI runs Linux only. Nothing in the codebase is
112
+ > platform-specific — no `fork`, no signal handling, no POSIX-only calls, no shelling out — and
113
+ > every dependency ships macOS and Windows wheels, so both are expected to work. But
114
+ > **neither is tested**, so treat them as unverified rather than supported. If you run interlace
115
+ > on macOS or Windows, please open an issue either way; that is the fastest route to changing
116
+ > this paragraph.
117
+
83
118
  ## Sixty seconds
84
119
 
85
120
  ```bash
@@ -210,7 +245,7 @@ prod, so a dev apply never writes to a live external table (opt in with
210
245
 
211
246
  ## Multi-engine
212
247
 
213
- Models run on **named engines**: DuckDB/DuckLake by default, Postgres natively over ADBC
248
+ Models run on **named engines**: DuckDB by default (DuckLake opt-in), Postgres natively over ADBC
214
249
  (`pip install 'interlaced[adbc]'`), Spark (beta, `[spark]` extra), plus alpha adapters for
215
250
  MotherDuck, Redshift, Snowflake and BigQuery (wired and dialect-correct, not yet run against a
216
251
  live account), with per-model pinning:
@@ -252,7 +287,8 @@ other processes (CLI runs, ad-hoc DuckDB clients) then share it concurrently by
252
287
 
253
288
  - The IR is a **sqlglot AST**; the wire format is an **Arrow RecordBatchReader**; strategies
254
289
  are AST builders and dialect appears only at `transpile()`.
255
- - Storage defaults to **DuckLake** (Parquet + SQL catalog) opened as DuckDB's primary database.
290
+ - Storage defaults to a plain **DuckDB** file; **DuckLake** (Parquet + SQL catalog, and
291
+ concurrent writers) is one `database: ducklake:…` line away.
256
292
  - Control plane (snapshots, intervals, queue, events, keys) is **SQLite WAL**; Postgres is the
257
293
  scale-out swap.
258
294
  - Streams live in their own durable log; the materializer commits data + watermark in one
@@ -269,7 +305,7 @@ Toolchain is pinned with [proto](https://moonrepo.dev/proto), tasks run via
269
305
  ```bash
270
306
  proto install
271
307
  moon run interlace:sync # install deps
272
- moon run interlace:test # 350+ tests
308
+ moon run interlace:test # 500+ tests
273
309
  moon run interlace:check # black + ruff (CI equivalent)
274
310
  moon run interlace:typecheck # mypy
275
311
  ```
@@ -1,14 +1,40 @@
1
1
  # interlace
2
2
 
3
- **Python/SQL-first data platform: transformation, orchestration, and durable streaming — one process.**
3
+ **Python and SQL models are the same kind of node in one DAG.**
4
4
 
5
- interlace is an independent, MIT-licensed alternative to dbt/SQLMesh that also replaces the
6
- orchestrator (no Airflow) and the ingestion layer (Cloudflare-Pipelines-style durable streams).
7
- Models are `.sql` files or Python functions; state is versioned snapshots with virtual
5
+ A `.py` model sits mid-graph with SQL either side, in both directions, with no bridge and no
6
+ separate runtime — running in-process on DuckDB and Postgres, not only on a cloud warehouse.
7
+ The Python model stays a plain function: call it in a test with no warehouse and no session.
8
+
9
+ How that compares with dbt and SQLMesh, including where they are ahead, is on
10
+ [interlace.sh/why](https://interlace.sh/why).
11
+
12
+ ```sql
13
+ -- models/raw_events.sql SQL
14
+ SELECT event_id, user_id, kind, amount, country, ts FROM read_parquet('events/*.parquet')
15
+ ```
16
+ ```python
17
+ # models/enriched_events.py Python, mid-DAG
18
+ @model() # the parameter name IS the dependency — no depends_on
19
+ def enriched_events(raw_events):
20
+ for batch in raw_events.reader(): # Arrow in, Arrow out, bounded memory
21
+ yield add_revenue(batch)
22
+ ```
23
+ ```sql
24
+ -- models/event_summary.sql SQL again, straight over the Python
25
+ SELECT country, count(*) FILTER (WHERE is_conversion) AS conversions
26
+ FROM enriched_events GROUP BY country
27
+ ```
28
+
29
+ `interlace init` scaffolds exactly this shape, runnable, with no external source.
30
+
31
+ That is the wedge. The rest is the reveal: interlace is an independent, MIT-licensed alternative
32
+ to dbt/SQLMesh that also replaces the orchestrator (no Airflow) and the ingestion layer
33
+ (Cloudflare-Pipelines-style durable streams). State is versioned snapshots with virtual
8
34
  environments and a terraform-style plan/apply; everything runs in a single daemon on
9
- DuckDB + DuckLake by default.
35
+ DuckDB by default (DuckLake one config line away).
10
36
 
11
- > **Status: 2.0.** Requires Python 3.12+.
37
+ > **2.x — see [releases](https://github.com/interlace-sh/interlace/releases).** Requires Python 3.12+.
12
38
  > The package is published to PyPI as **`interlaced`**; the import name and CLI are `interlace`.
13
39
 
14
40
  ```bash
@@ -16,6 +42,13 @@ pip install 'interlaced[service]' # the CLI + daemon; core CLI only: pip insta
16
42
  # more extras: [adbc] postgres/redshift · [spark] · [polars] · [all]
17
43
  ```
18
44
 
45
+ > **Platforms.** Developed on Linux; CI runs Linux only. Nothing in the codebase is
46
+ > platform-specific — no `fork`, no signal handling, no POSIX-only calls, no shelling out — and
47
+ > every dependency ships macOS and Windows wheels, so both are expected to work. But
48
+ > **neither is tested**, so treat them as unverified rather than supported. If you run interlace
49
+ > on macOS or Windows, please open an issue either way; that is the fastest route to changing
50
+ > this paragraph.
51
+
19
52
  ## Sixty seconds
20
53
 
21
54
  ```bash
@@ -146,7 +179,7 @@ prod, so a dev apply never writes to a live external table (opt in with
146
179
 
147
180
  ## Multi-engine
148
181
 
149
- Models run on **named engines**: DuckDB/DuckLake by default, Postgres natively over ADBC
182
+ Models run on **named engines**: DuckDB by default (DuckLake opt-in), Postgres natively over ADBC
150
183
  (`pip install 'interlaced[adbc]'`), Spark (beta, `[spark]` extra), plus alpha adapters for
151
184
  MotherDuck, Redshift, Snowflake and BigQuery (wired and dialect-correct, not yet run against a
152
185
  live account), with per-model pinning:
@@ -188,7 +221,8 @@ other processes (CLI runs, ad-hoc DuckDB clients) then share it concurrently by
188
221
 
189
222
  - The IR is a **sqlglot AST**; the wire format is an **Arrow RecordBatchReader**; strategies
190
223
  are AST builders and dialect appears only at `transpile()`.
191
- - Storage defaults to **DuckLake** (Parquet + SQL catalog) opened as DuckDB's primary database.
224
+ - Storage defaults to a plain **DuckDB** file; **DuckLake** (Parquet + SQL catalog, and
225
+ concurrent writers) is one `database: ducklake:…` line away.
192
226
  - Control plane (snapshots, intervals, queue, events, keys) is **SQLite WAL**; Postgres is the
193
227
  scale-out swap.
194
228
  - Streams live in their own durable log; the materializer commits data + watermark in one
@@ -205,7 +239,7 @@ Toolchain is pinned with [proto](https://moonrepo.dev/proto), tasks run via
205
239
  ```bash
206
240
  proto install
207
241
  moon run interlace:sync # install deps
208
- moon run interlace:test # 350+ tests
242
+ moon run interlace:test # 500+ tests
209
243
  moon run interlace:check # black + ruff (CI equivalent)
210
244
  moon run interlace:typecheck # mypy
211
245
  ```
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "interlaced"
7
- version = "2.3.0"
8
- description = "Python/SQL-first data platform: transformation, built-in orchestration, and durable streaming ingestion"
7
+ version = "2.4.1"
8
+ description = "Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
11
11
  license = "MIT"
@@ -17,8 +17,12 @@ classifiers = [
17
17
  "Development Status :: 5 - Production/Stable",
18
18
  "Intended Audience :: Developers",
19
19
  "Topic :: Software Development :: Build Tools",
20
+ "Topic :: Database",
20
21
  "Programming Language :: Python :: 3.12",
21
22
  "Programming Language :: SQL",
23
+ # Linux is what CI runs and what this is developed on. Nothing in the codebase
24
+ # is platform-specific, but macOS and Windows are untested — see the README.
25
+ "Operating System :: POSIX :: Linux",
22
26
  ]
23
27
 
24
28
  # Core: the IR + engine + state + plan spine (Phase 1). Everything is Arrow- and
@@ -176,6 +180,7 @@ markers = [
176
180
  "integration: Integration tests (slower, require database)",
177
181
  "slow: Slow tests (may take a long time)",
178
182
  "requires_db: Needs a reachable external database (e.g. Postgres)",
183
+ "requires_network: Needs the public internet (e.g. an example reading a remote CSV)",
179
184
  ]
180
185
  asyncio_mode = "auto"
181
186
  asyncio_default_fixture_loop_scope = "function"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import contextlib
7
+ import time
7
8
  from datetime import datetime
8
9
  from pathlib import Path
9
10
  from typing import Any
@@ -467,6 +468,7 @@ async def _execute(
467
468
  restate=restate,
468
469
  )
469
470
  progress = _build_progress(plan_result)
471
+ started = time.perf_counter()
470
472
  try:
471
473
  with progress.progress if progress else contextlib.nullcontext():
472
474
  result = await apply_plan(
@@ -487,7 +489,7 @@ async def _execute(
487
489
  await _render_empty_incrementals(result, compiled, engines)
488
490
  verb = "Restated" if restate else "Ran"
489
491
  console.print(
490
- f"[green]{verb} {len(set(result.built))} model(s) ({len(result.built)} task(s)); "
492
+ f"[green]{verb} {len(set(result.built))} model(s) in {time.perf_counter() - started:.2f}s; "
491
493
  f"promoted {result.promoted} to '{environment}'.[/green]"
492
494
  )
493
495
  finally:
@@ -84,7 +84,7 @@ class EngineConfig(BaseModel):
84
84
  declaring them fails at open until an adapter ships.
85
85
  """
86
86
 
87
- type: str = "ducklake"
87
+ type: str = "duckdb"
88
88
  # Path / URI for DuckDB-family engines. Also accepted on the project top level
89
89
  # as ``database:`` (synthesised into the ``default`` engine).
90
90
  database: str | None = None
@@ -117,13 +117,14 @@ class ProjectConfig(BaseModel):
117
117
  default_engine: str = "default"
118
118
  engines: dict[str, EngineConfig] = Field(default_factory=dict)
119
119
  state_path: str = ".interlace/state.db" # SQLite control-plane database
120
- # The warehouse. Default is DuckLake (Parquet data + SQL catalog) via DuckDB.
121
- # Also accepted: a DuckLake catalog hosted in a SQL database
122
- # ("ducklake:postgres:dbname=... host=..." — pair with data_path/metadata_schema),
123
- # a plain DuckDB file path, ":memory:", or "quack:<host>:<port>" to connect to a
124
- # warehouse served by `interlace serve --quack`.
120
+ # The warehouse. Default is a plain DuckDB file — simplest, single-process.
121
+ # Also accepted: a DuckLake catalog (``ducklake:.interlace/warehouse.ducklake``, or
122
+ # hosted in a SQL DB: ``ducklake:postgres:dbname=... host=...`` — pair with
123
+ # data_path/metadata_schema) which serialises catalog writes so `interlace serve`
124
+ # and a separate CLI can share the warehouse concurrently; ":memory:"; or
125
+ # "quack:<host>:<port>" to connect to a warehouse served by `interlace serve --quack`.
125
126
  # When ``engines.default`` is not set, these top-level fields synthesise it.
126
- database: str = "ducklake:.interlace/warehouse.ducklake"
127
+ database: str = ".interlace/warehouse.duckdb"
127
128
  # The warehouse catalog's ATTACH alias (defaults to ``name``). Set it when a
128
129
  # schema inside the warehouse shares the project name — see EngineConfig.alias.
129
130
  alias: str | None = None
@@ -144,6 +145,9 @@ class ProjectConfig(BaseModel):
144
145
  # This is T0 federation (all SQL still runs in DuckDB) — see docs/architecture/MULTI_ENGINE.md.
145
146
  attach: dict[str, str] = Field(default_factory=dict)
146
147
  model_paths: list[str] = Field(default_factory=lambda: ["models"])
148
+ # Directories of CREATE MACRO definitions, expanded into every model at compile
149
+ # time (see interlace.ir.macros). Missing directories are ignored.
150
+ macro_paths: list[str] = Field(default_factory=lambda: ["macros"])
147
151
  # How many models build concurrently. Lower it to serialise a build end-to-end
148
152
  # when diagnosing a crash or a warehouse that dislikes parallel writers; `run`
149
153
  # and `apply` also take --parallelism, which wins over this.
@@ -114,6 +114,18 @@ class ModelDef:
114
114
  schedule: dict[str, str] | None = None # {"cron": "0 * * * *"} or {"every": "5m"} for `interlace serve`
115
115
  checks: tuple[CheckSpec, ...] = () # data-quality checks; error severity gates promotion
116
116
 
117
+ def __post_init__(self) -> None:
118
+ # `@model(checks=…)` normalises through parse_checks, but a ModelDef built
119
+ # directly — the dynamic-model path, which is what generated models and dbt
120
+ # migrations use — stored the dicts raw and only failed at compile time with
121
+ # `AttributeError: 'dict' object has no attribute 'type'`. Normalise here too,
122
+ # so one spelling works on both surfaces and a bad check fails at declaration.
123
+ # Passed through as-is, not as list(...): parse_checks already handles a bare
124
+ # CheckSpec and reports a non-list clearly, both of which list() would mangle
125
+ # (TypeError on a CheckSpec; a dict silently degraded to its keys). Always run,
126
+ # so `checks=[]` normalises to the declared tuple rather than staying a list.
127
+ self.checks = parse_checks(self.checks, self.name)
128
+
117
129
  @property
118
130
  def is_terminal(self) -> bool:
119
131
  """A terminal model delivers into an external destination (table/file):
@@ -9,8 +9,10 @@ cleared first for a clean load.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import contextlib
12
13
  import importlib.util
13
14
  import sys
15
+ from collections.abc import Iterator
14
16
  from pathlib import Path
15
17
  from typing import Any
16
18
 
@@ -18,22 +20,61 @@ from interlace.checks.spec import parse_checks
18
20
  from interlace.dsl.decorators import REGISTRY, ModelDef, _as_columns, _as_tuple, validate_materialise
19
21
  from interlace.dsl.sql_config import extract_sql_config
20
22
  from interlace.exceptions import DefinitionError, InterlaceError
23
+ from interlace.ir.macros import Macro, parse_macros
21
24
 
22
25
 
23
26
  def discover_models(root: Path, model_paths: list[str], default_dialect: str) -> list[ModelDef]:
24
27
  REGISTRY.clear()
25
- for relative in model_paths:
28
+ imported = set(sys.modules)
29
+ try:
30
+ for relative in model_paths:
31
+ base = root / relative
32
+ if not base.is_dir():
33
+ continue
34
+ for sql_file in sorted(base.rglob("*.sql")):
35
+ config, sql = extract_sql_config(sql_file.read_text())
36
+ REGISTRY.register_model(_sql_model(_model_name(base, sql_file), sql, config, default_dialect))
37
+ for py_file in sorted(base.rglob("*.py")):
38
+ if py_file.name.startswith("_"):
39
+ continue
40
+ _import_module(base, py_file)
41
+ finally:
42
+ _forget_project_modules(root, imported)
43
+ return list(REGISTRY.models.values())
44
+
45
+
46
+ def _forget_project_modules(root: Path, before: set[str]) -> None:
47
+ """Drop the project's own helper modules from the import cache.
48
+
49
+ A helper is imported under whatever plain name it has (``_macros``), so leaving it
50
+ cached would hand the *next* project — a reload under ``interlace serve``, another
51
+ project in the same process — the first one's version of a file with the same name.
52
+ Only modules loaded from inside this project during this pass are dropped."""
53
+ for name in set(sys.modules) - before:
54
+ module = sys.modules.get(name)
55
+ origin = getattr(module, "__file__", None)
56
+ if origin and Path(origin).is_relative_to(root):
57
+ del sys.modules[name]
58
+
59
+
60
+ def discover_macros(root: Path, macro_paths: list[str], default_dialect: str) -> dict[str, Macro]:
61
+ """Parse every ``CREATE MACRO`` under the macro paths, keyed by casefolded name."""
62
+ macros: dict[str, Macro] = {}
63
+ for relative in macro_paths:
26
64
  base = root / relative
27
65
  if not base.is_dir():
28
66
  continue
29
- for sql_file in sorted(base.rglob("*.sql")):
30
- config, sql = extract_sql_config(sql_file.read_text())
31
- REGISTRY.register_model(_sql_model(_model_name(base, sql_file), sql, config, default_dialect))
32
- for py_file in sorted(base.rglob("*.py")):
33
- if py_file.name.startswith("_"):
34
- continue
35
- _import_module(base, py_file)
36
- return list(REGISTRY.models.values())
67
+ for macro_file in sorted(base.rglob("*.sql")):
68
+ source = str(macro_file.relative_to(root))
69
+ for macro in parse_macros(macro_file.read_text(), default_dialect, source):
70
+ key = macro.name.casefold()
71
+ if key in macros:
72
+ raise DefinitionError(
73
+ f"macro {macro.name!r} is defined twice: {macros[key].source} and {source}",
74
+ details={"macro": macro.name},
75
+ )
76
+ macros[key] = macro
77
+ return macros
37
78
 
38
79
 
39
80
  def _sql_model(default_name: str, sql: str, config: dict[str, Any], default_dialect: str) -> ModelDef:
@@ -89,6 +130,18 @@ def _relative_to_cwd(file: Path) -> str:
89
130
  return str(file)
90
131
 
91
132
 
133
+ @contextlib.contextmanager
134
+ def _importable(directory: Path) -> Iterator[None]:
135
+ """Put ``directory`` at the front of ``sys.path``, and take it back off."""
136
+ entry = str(directory)
137
+ sys.path.insert(0, entry)
138
+ try:
139
+ yield
140
+ finally:
141
+ with contextlib.suppress(ValueError):
142
+ sys.path.remove(entry)
143
+
144
+
92
145
  def _import_module(base: Path, file: Path) -> None:
93
146
  module_name = "interlace_model_" + "_".join(file.relative_to(base).with_suffix("").parts)
94
147
  spec = importlib.util.spec_from_file_location(module_name, file)
@@ -97,7 +150,13 @@ def _import_module(base: Path, file: Path) -> None:
97
150
  module = importlib.util.module_from_spec(spec)
98
151
  sys.modules[module_name] = module
99
152
  try:
100
- spec.loader.exec_module(module)
153
+ # The model's own directory goes on the path for the duration of the import, so a
154
+ # model can `from _shared import ...` a helper sitting next to it — the closest
155
+ # thing to a macro. Files starting with `_` are already skipped as models, which
156
+ # is only useful if they can be imported. Removed again straight after: a project's
157
+ # helpers must not leak into the import path of everything that follows.
158
+ with _importable(file.parent):
159
+ spec.loader.exec_module(module)
101
160
  except InterlaceError:
102
161
  raise # a bad @model config is already a clean, actionable error — don't rewrap it
103
162
  except Exception as exc:
@@ -28,6 +28,7 @@ from __future__ import annotations
28
28
 
29
29
  import asyncio
30
30
  import contextlib
31
+ import re
31
32
  import threading
32
33
  from collections.abc import Iterator, Sequence
33
34
  from uuid import uuid4
@@ -38,6 +39,7 @@ import tenacity
38
39
  from sqlglot import exp
39
40
 
40
41
  from interlace.engines.base import EngineAdapter, EngineCaps, LoadMode
42
+ from interlace.exceptions import ConfigurationError
41
43
  from interlace.ir.relation import TableRef
42
44
 
43
45
  _DUCKDB_CAPS = EngineCaps(
@@ -58,6 +60,39 @@ _commit_retry = tenacity.retry(
58
60
  )
59
61
 
60
62
 
63
+ @contextlib.contextmanager
64
+ def _clean_lock_error(database: str) -> Iterator[None]:
65
+ """Translate DuckDB's file-lock conflict into one actionable line.
66
+
67
+ A DuckDB/DuckLake database is held by a single process. The common way to hit
68
+ that is running a CLI command (`interlace query`, `plan`, `apply`) while
69
+ `interlace serve` is up — an obvious thing to do, since serve is the daemon
70
+ and query is the console's CLI counterpart. Raw, that surfaces as a dozen
71
+ frames ending in `duckdb.IOException`, which names neither the cause nor the
72
+ fix. The fix is `--quack`, and it is already documented; the error just never
73
+ said so.
74
+ """
75
+ try:
76
+ yield
77
+ except duckdb.IOException as exc:
78
+ message = str(exc)
79
+ # "Conflicting lock is held in <exe> (PID n)" is the Linux rendering — DuckDB
80
+ # names the holder from /proc/locks. Elsewhere the message is the bare "Could
81
+ # not set lock on file", and a same-process conflict says "already held", so
82
+ # match all three or macOS/Windows keep the raw traceback.
83
+ if not any(marker in message for marker in ("Conflicting lock", "Could not set lock", "already held")):
84
+ raise
85
+ holder = re.search(r"\(PID (\d+)\)", message)
86
+ held_by = f" (PID {holder.group(1)})" if holder else ""
87
+ raise ConfigurationError(
88
+ f"the warehouse {database!r} is already open in another process{held_by}. "
89
+ "DuckDB allows one process at a time — stop `interlace serve`, or serve the "
90
+ "warehouse over the quack protocol (`interlace serve --quack quack:localhost:4213`) "
91
+ "and point this process at `database: quack:localhost:4213` to share it.",
92
+ details={"database": database},
93
+ ) from None
94
+
95
+
61
96
  def _affected(cur: duckdb.DuckDBPyConnection) -> int:
62
97
  """DML/CTAS/COPY return their affected-row count as a one-cell result; DDL returns
63
98
  nothing. Never raises — row stats are best-effort decoration, not correctness."""
@@ -108,7 +143,9 @@ class DuckDBAdapter(EngineAdapter):
108
143
 
109
144
  @classmethod
110
145
  def connect(cls, path: str) -> DuckDBAdapter:
111
- return cls(duckdb.connect(path), serialise_writes=path.startswith("ducklake:"))
146
+ with _clean_lock_error(path):
147
+ conn = duckdb.connect(path)
148
+ return cls(conn, serialise_writes=path.startswith("ducklake:"))
112
149
 
113
150
  @classmethod
114
151
  def connect_ducklake(
@@ -139,7 +176,8 @@ class DuckDBAdapter(EngineAdapter):
139
176
  options_sql = f" ({', '.join(options)})" if options else ""
140
177
  escaped = catalog.replace("'", "''")
141
178
  alias_sql = exp.to_identifier(alias).sql("duckdb")
142
- conn.execute(f"ATTACH IF NOT EXISTS '{escaped}' AS {alias_sql}{options_sql}")
179
+ with _clean_lock_error(catalog):
180
+ conn.execute(f"ATTACH IF NOT EXISTS '{escaped}' AS {alias_sql}{options_sql}")
143
181
  conn.execute(f"USE {alias_sql}")
144
182
  # LOAD, secrets, and ATTACH are all instance-wide — they carry into every
145
183
  # cursor and must run ONCE (re-running CREATE OR REPLACE SECRET per cursor
@@ -162,10 +200,23 @@ class DuckDBAdapter(EngineAdapter):
162
200
  with contextlib.suppress(Exception):
163
201
  self._conn.interrupt()
164
202
 
203
+ def search_files_from(self, directory: str) -> None:
204
+ """Resolve relative read paths (``read_csv_auto('seeds/x.csv')``) against
205
+ ``directory`` — the project root — as well as the process CWD.
206
+
207
+ Additive: a CWD-relative path still resolves, so this only ever widens what a
208
+ model can find. GLOBAL scope because a plain ``SET`` is session-scoped and would
209
+ not reach the per-task cursors that actually run the queries. Reads only —
210
+ ``COPY`` targets stay CWD-relative, which is why exports resolve their own paths
211
+ against the root (``plan.apply._resolve_export_path``)."""
212
+ escaped = directory.replace("'", "''")
213
+ self._conn.execute(f"SET GLOBAL file_search_path='{escaped}'")
214
+
165
215
  def attach(self, alias: str, uri: str) -> None:
166
216
  """ATTACH another database (duckdb/sqlite/postgres/... URI) under ``alias``."""
167
217
  escaped = uri.replace("'", "''")
168
- self._conn.execute(f"ATTACH IF NOT EXISTS '{escaped}' AS {exp.to_identifier(alias).sql('duckdb')}")
218
+ with _clean_lock_error(uri): # attaching a held duckdb/ducklake file conflicts just like opening one
219
+ self._conn.execute(f"ATTACH IF NOT EXISTS '{escaped}' AS {exp.to_identifier(alias).sql('duckdb')}")
169
220
  self._attached.append(alias)
170
221
  if uri.startswith("ducklake:"): # writes may now reach a DuckLake catalog (e.g. table sinks)
171
222
  if isinstance(self._write_lock, contextlib.nullcontext):
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import inspect
14
14
  import textwrap
15
- from collections.abc import Iterable
15
+ from collections.abc import Iterable, Mapping
16
16
  from dataclasses import dataclass, field
17
17
 
18
18
  from sqlglot import exp
@@ -23,6 +23,7 @@ from interlace.exceptions import CompilationError, DefinitionError
23
23
  from interlace.graph.dag import DependencyGraph
24
24
  from interlace.ir.canonicalize import parse, table_references
25
25
  from interlace.ir.fingerprint import canonical_sql, data_fingerprint, metadata_fingerprint
26
+ from interlace.ir.macros import Macro, expand_macros
26
27
  from interlace.ir.relation import TableRef
27
28
 
28
29
  _PHYSICAL_PREFIX = "interlace__"
@@ -93,7 +94,7 @@ def _physical_table(name: str, fingerprint: str, catalog: str | None) -> TableRe
93
94
 
94
95
 
95
96
  def _resolve_dependencies(
96
- model: ModelDef, names: set[str], default_dialect: str
97
+ model: ModelDef, names: set[str], default_dialect: str, macros: Mapping[str, Macro]
97
98
  ) -> tuple[tuple[str, ...], exp.Expression | None, str]:
98
99
  dialect = model.dialect or default_dialect
99
100
  deps: list[str] = []
@@ -115,6 +116,10 @@ def _resolve_dependencies(
115
116
  raise CompilationError(
116
117
  f"model {model.name!r}: {exc.message}", details={**exc.details, "model": model.name}
117
118
  ) from exc
119
+ # Before anything reads the AST: the fingerprint is canonical SQL, so expanding
120
+ # here is what makes a macro edit rebuild its callers, and what lets a macro body
121
+ # reference a model and have that count as a dependency.
122
+ ast = expand_macros(ast, macros, model.name)
118
123
  for ref in table_references(ast):
119
124
  if ref in names:
120
125
  add(ref)
@@ -149,12 +154,14 @@ def compile_models(
149
154
  known_engines: set[str] | None = None,
150
155
  catalog: str | None = None,
151
156
  checks: Iterable[CheckDef] = (),
157
+ macros: Mapping[str, Macro] | None = None,
152
158
  ) -> CompiledProject:
153
159
  """Compile models into a fingerprinted, topologically-ordered project.
154
160
 
155
161
  ``checks`` are ``@check``-decorated Python functions, attached by model name.
156
162
  ``engine_dialects`` maps engine name → sqlglot dialect (used when a model
157
163
  omits ``dialect``). ``known_engines`` validates model ``engine`` pins.
164
+ ``macros`` are expanded into every model's AST before it is fingerprinted.
158
165
  """
159
166
  definitions = {m.name: m for m in models}
160
167
  names = set(definitions)
@@ -178,7 +185,7 @@ def compile_models(
178
185
  )
179
186
  # Authoring dialect: explicit model dialect, else the engine's, else project default.
180
187
  model_default_dialect = dialects_by_engine.get(engine, default_dialect)
181
- deps, ast, dialect = _resolve_dependencies(definition, names, model_default_dialect)
188
+ deps, ast, dialect = _resolve_dependencies(definition, names, model_default_dialect, macros or {})
182
189
  resolved[name] = (deps, ast, dialect, engine)
183
190
  graph.add_node(name)
184
191
  for dep in deps: