Skip to content

Commit 3d368c8

Browse files
authored
Feat: add suport for @EACH dynamic blueprints, improve docs (#4317)
1 parent 3117d76 commit 3d368c8

File tree

5 files changed

+137
-10
lines changed

5 files changed

+137
-10
lines changed

docs/concepts/models/python_models.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,32 @@ def entrypoint(
367367
)
368368
```
369369

370-
!!! note
370+
Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints="@gen_blueprints()"`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files.
371+
372+
For example, the definition of the `gen_blueprints` may look like this:
373+
374+
```python linenums="1"
375+
from sqlmesh import macro
376+
377+
@macro()
378+
def gen_blueprints(evaluator):
379+
return (
380+
"((customer := customer1, field_a := x, field_b := y),"
381+
" (customer := customer2, field_a := z, field_b := w))"
382+
)
383+
```
371384

372-
Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files.
385+
It's also possible to use the `@EACH` macro, combined with a global list variable (`@values`):
386+
387+
```python linenums="1"
388+
389+
@model(
390+
"@{customer}.some_table",
391+
blueprints="@EACH(@values, x -> (customer := schema_@x))",
392+
...
393+
)
394+
...
395+
```
373396

374397
## Examples
375398
### Basic

docs/concepts/models/sql_models.md

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,33 @@ SELECT
175175
FROM customer2.some_source
176176
```
177177

178-
!!! note
178+
Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints @gen_blueprints()`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files.
179+
180+
For example, the definition of the `gen_blueprints` may look like this:
181+
182+
```python linenums="1"
183+
from sqlmesh import macro
184+
185+
@macro()
186+
def gen_blueprints(evaluator):
187+
return (
188+
"((customer := customer1, field_a := x, field_b := y),"
189+
" (customer := customer2, field_a := z, field_b := w))"
190+
)
191+
```
192+
193+
It's also possible to use the `@EACH` macro, combined with a global list variable (`@values`):
179194

180-
Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints @gen_blueprints()`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files.
195+
```sql linenums="1"
196+
MODEL (
197+
name @customer.some_table,
198+
kind FULL,
199+
blueprints @EACH(@values, x -> (customer := schema_@x)),
200+
);
201+
202+
SELECT
203+
1 AS c
204+
```
181205

182206
## Python-based definition
183207

@@ -262,9 +286,33 @@ def entrypoint(evaluator: MacroEvaluator) -> str | exp.Expression:
262286

263287
The two models produced from this template are the same as in the [example](#SQL-model-blueprinting) for SQL-based blueprinting.
264288

265-
!!! note
289+
Blueprint variable mappings can also be constructed dynamically, e.g., by using a macro: `blueprints="@gen_blueprints()"`. This is useful in cases where the `blueprints` list needs to be sourced from external sources, such as CSV files.
290+
291+
For example, the definition of the `gen_blueprints` may look like this:
292+
293+
```python linenums="1"
294+
from sqlmesh import macro
295+
296+
@macro()
297+
def gen_blueprints(evaluator):
298+
return (
299+
"((customer := customer1, field_a := x, field_b := y),"
300+
" (customer := customer2, field_a := z, field_b := w))"
301+
)
302+
```
266303

267-
Blueprint variable mappings can also be evaluated dynamically, by using a macro (i.e. `blueprints="@gen_blueprints()"`). This is useful in cases where the `blueprints` list needs to be sourced from external sources, e.g. CSV files.
304+
It's also possible to use the `@EACH` macro, combined with a global list variable (`@values`):
305+
306+
```python linenums="1"
307+
308+
@model(
309+
"@{customer}.some_table",
310+
is_sql=True,
311+
blueprints="@EACH(@values, x -> (customer := schema_@x))",
312+
...
313+
)
314+
...
315+
```
268316

269317
## Automatic dependencies
270318

sqlmesh/core/model/decorator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ def models(
117117
if not blueprints:
118118
raise_config_error("Failed to render blueprints property", path)
119119

120+
if len(blueprints) > 1:
121+
blueprints = [exp.Tuple(expressions=blueprints)]
122+
120123
blueprints = blueprints[0]
121124

122125
return create_models_from_blueprints(

sqlmesh/core/model/definition.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1966,7 +1966,13 @@ def load_sql_based_models(
19661966
if not rendered_blueprints:
19671967
raise_config_error("Failed to render blueprints property", path)
19681968

1969-
blueprints = t.cast(t.List, rendered_blueprints)[0]
1969+
# Help mypy see that rendered_blueprints can't be None
1970+
assert rendered_blueprints
1971+
1972+
if len(rendered_blueprints) > 1:
1973+
rendered_blueprints = [exp.Tuple(expressions=rendered_blueprints)]
1974+
1975+
blueprints = rendered_blueprints[0]
19701976

19711977
return create_models_from_blueprints(
19721978
gateway=gateway,

tests/core/test_model.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8493,10 +8493,10 @@ def entrypoint(evaluator):
84938493
)
84948494

84958495

8496-
def test_dynamic_blueprinting(tmp_path: Path) -> None:
8496+
def test_dynamic_blueprinting_using_custom_macro(tmp_path: Path) -> None:
84978497
init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY)
84988498

8499-
dynamic_template_sql = tmp_path / "models/dynamic_template.sql"
8499+
dynamic_template_sql = tmp_path / "models/dynamic_template_custom_macro.sql"
85008500
dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True)
85018501
dynamic_template_sql.write_text(
85028502
"""
@@ -8514,7 +8514,7 @@ def test_dynamic_blueprinting(tmp_path: Path) -> None:
85148514
"""
85158515
)
85168516

8517-
dynamic_template_py = tmp_path / "models/dynamic_template.py"
8517+
dynamic_template_py = tmp_path / "models/dynamic_template_custom_macro.py"
85188518
dynamic_template_py.parent.mkdir(parents=True, exist_ok=True)
85198519
dynamic_template_py.write_text(
85208520
"""
@@ -8556,6 +8556,53 @@ def gen_blueprints(evaluator):
85568556
assert '"memory"."customer2"."some_other_table"' in ctx.models
85578557

85588558

8559+
def test_dynamic_blueprinting_using_each(tmp_path: Path) -> None:
8560+
init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY)
8561+
8562+
dynamic_template_sql = tmp_path / "models/dynamic_template_each.sql"
8563+
dynamic_template_sql.parent.mkdir(parents=True, exist_ok=True)
8564+
dynamic_template_sql.write_text(
8565+
"""
8566+
MODEL (
8567+
name @customer.some_table,
8568+
kind FULL,
8569+
blueprints @EACH(@values, x -> (customer := schema_@x)),
8570+
);
8571+
8572+
SELECT
8573+
1 AS c
8574+
"""
8575+
)
8576+
8577+
dynamic_template_py = tmp_path / "models/dynamic_template_each.py"
8578+
dynamic_template_py.parent.mkdir(parents=True, exist_ok=True)
8579+
dynamic_template_py.write_text(
8580+
"""
8581+
from sqlmesh import model
8582+
8583+
@model(
8584+
"@{customer}.some_other_table",
8585+
kind="FULL",
8586+
blueprints="@EACH(@values, x -> (customer := schema_@x))",
8587+
is_sql=True,
8588+
)
8589+
def entrypoint(evaluator):
8590+
return "SELECT 1 AS c"
8591+
"""
8592+
)
8593+
8594+
model_defaults = ModelDefaultsConfig(dialect="duckdb")
8595+
variables = {"values": ["customer1", "customer2"]}
8596+
config = Config(model_defaults=model_defaults, variables=variables)
8597+
ctx = Context(config=config, paths=tmp_path)
8598+
8599+
assert len(ctx.models) == 4
8600+
assert '"memory"."schema_customer1"."some_table"' in ctx.models
8601+
assert '"memory"."schema_customer2"."some_table"' in ctx.models
8602+
assert '"memory"."schema_customer1"."some_other_table"' in ctx.models
8603+
assert '"memory"."schema_customer2"."some_other_table"' in ctx.models
8604+
8605+
85598606
def test_single_blueprint(tmp_path: Path) -> None:
85608607
init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY)
85618608

0 commit comments

Comments
 (0)