Skip to content

Commit f7de34b

Browse files
committed
test(format): cover sqlmesh format on BigQuery header lists
Add a Context-level test that formats a BigQuery model with `tags`, `ignored_rules`, `grain` and `partitioned_by`, then reloads it. Trim the header-list unit tests and drop the postgres/databricks cases, which passed with or without the fix. Signed-off-by: mday-io <mdaytn@gmail.com>
1 parent 11d2e62 commit f7de34b

2 files changed

Lines changed: 47 additions & 66 deletions

File tree

‎tests/core/test_dialect.py‎

Lines changed: 13 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -620,20 +620,8 @@ def test_format_model_expressions_macro_property_comments_preserved_with_dialect
620620
@pytest.mark.parametrize("dialect", ["bigquery", "duckdb", "snowflake"])
621621
@pytest.mark.parametrize("prop_name", ["tags", "ignored_rules"])
622622
def test_format_model_expressions_list_property_array_literal(dialect: str, prop_name: str):
623-
"""Dialect-agnostic header properties that hold a list (`tags`, `ignored_rules`) must
624-
render as a bracketed list literal (`[a, b]`) on dialects that spell arrays that way,
625-
not the base generator's `ARRAY(a, b)`.
626-
627-
On BigQuery, `ARRAY(` is parsed as a subquery constructor, so a multi-element
628-
`ARRAY('C1', 'c2')` fails to reparse with `Required keyword: 'value' missing for
629-
Property`. This previously affected any dialect using this generator, since these
630-
properties render generically (the `dialect=None` path) regardless of the model's
631-
own dialect. Only bigquery/duckdb/snowflake-like dialects are covered here;
632-
dialects whose own array syntax is not brackets (postgres' `ARRAY[...]`,
633-
databricks' `ARRAY(...)`) or that reuse `[`/`]` for identifier quoting (tsql,
634-
sqlite, ...) are covered by
635-
`test_format_model_expressions_list_property_dialects_without_bracket_arrays`.
636-
"""
623+
"""List-valued header properties render as `[a, b]`, not `ARRAY(a, b)`, which
624+
BigQuery parses as a subquery and fails to load."""
637625
source = f"""MODEL (
638626
name a.b,
639627
dialect {dialect},
@@ -642,14 +630,9 @@ def test_format_model_expressions_list_property_array_literal(dialect: str, prop
642630
SELECT 1 AS x"""
643631

644632
formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect)
645-
646633
assert f"{prop_name} ['C1', 'c2']" in formatted
647634

648-
# Reparses cleanly with the model's own dialect.
649-
reparsed = parse(formatted, default_dialect=dialect)
650-
651-
# Idempotent: formatting an already-formatted model is a no-op.
652-
twice = format_model_expressions(reparsed, dialect=dialect)
635+
twice = format_model_expressions(parse(formatted, default_dialect=dialect), dialect=dialect)
653636
assert formatted == twice
654637

655638
model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect)
@@ -660,37 +643,20 @@ def test_format_model_expressions_list_property_array_literal(dialect: str, prop
660643

661644

662645
def test_format_model_expressions_array_property_no_dialect_unchanged():
663-
"""Regression guard: with no model dialect, list-valued header properties must keep
664-
rendering through the base generator (`ARRAY(...)`), exactly as pinned by
665-
`test_format_model_expressions`. The `[...]` rewrite only applies once a model
666-
dialect is present (gated on `meta_dialect`)."""
667646
formatted = format_model_expressions(
668647
parse("MODEL (name a.b, tags ['C1', 'c2']); SELECT 1 AS x")
669648
)
670649

671650
assert "tags ARRAY('C1', 'c2')" in formatted
672651

673652

674-
@pytest.mark.parametrize("dialect", ["tsql", "sqlite", "postgres", "databricks"])
653+
@pytest.mark.parametrize("dialect", ["tsql", "sqlite"])
675654
@pytest.mark.parametrize("prop_name", ["tags", "ignored_rules"])
676-
def test_format_model_expressions_list_property_dialects_without_bracket_arrays(
655+
def test_format_model_expressions_list_property_bracket_identifier_dialects(
677656
dialect: str, prop_name: str
678657
):
679-
"""Regression: dialects whose own generator does not spell an `exp.Array` as
680-
`[a, b]` must NOT get the bracket-list rewrite from
681-
`test_format_model_expressions_list_property_array_literal`, and must keep the
682-
generic `ARRAY(...)` form.
683-
684-
This matters most for tsql and sqlite (also true of tableau, exasol, fabric), which
685-
reuse `[`/`]` for identifier quoting: `['a', 'b']` is not an array literal in their
686-
grammar at all, so rewriting `tags` or `ignored_rules` to that form reparses as a
687-
single bracket-quoted identifier, silently collapsing two values into one and
688-
corrupting the tag/rule names -- even though this exact source formatted correctly
689-
on `main` before bracket rendering was introduced. postgres (`ARRAY[...]`) and
690-
databricks (`ARRAY(...)`) are not corrupted by the bracket form, but should still
691-
keep rendering with their own generator's spelling rather than a generic bracket
692-
literal that is not how either dialect writes arrays.
693-
"""
658+
"""These dialects quote identifiers with `[...]`, so a bracketed list would reload
659+
as a single identifier. List properties must keep the `ARRAY(...)` form."""
694660
source = f"""MODEL (
695661
name a.b,
696662
dialect {dialect},
@@ -699,13 +665,9 @@ def test_format_model_expressions_list_property_dialects_without_bracket_arrays(
699665
SELECT 1 AS x"""
700666

701667
formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect)
668+
assert f"{prop_name} ARRAY('C1', 'c2')" in formatted
702669

703-
prop_line = formatted.split(f"{prop_name} ")[1].split("\n")[0]
704-
assert prop_line.startswith("ARRAY")
705-
assert "[" not in prop_line
706-
707-
reparsed = parse(formatted, default_dialect=dialect)
708-
twice = format_model_expressions(reparsed, dialect=dialect)
670+
twice = format_model_expressions(parse(formatted, default_dialect=dialect), dialect=dialect)
709671
assert formatted == twice
710672

711673
model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect)
@@ -717,12 +679,8 @@ def test_format_model_expressions_list_property_dialects_without_bracket_arrays(
717679

718680
@pytest.mark.parametrize("dialect", ["bigquery", "duckdb", "snowflake", "postgres"])
719681
def test_format_model_expressions_grain_alias_render_policy(dialect: str):
720-
"""`grain` is renamed to `grains` in `ModelMeta._pre_root_validator`, not via a
721-
Pydantic alias, so `_meta_render_policy` must special-case it to inherit `grains`'
722-
render policy (warehouse SQL). Otherwise a multi-column `grain [id, id2]` falls back
723-
to the generic, dialect-agnostic path and (via the base generator) becomes
724-
`ARRAY(id, id2)`, which fails to reparse on BigQuery.
725-
"""
682+
"""`grain` is renamed to `grains` before validation, so it must share the
683+
`grains` render policy instead of falling back to `ARRAY(...)`."""
726684
source = f"""MODEL (
727685
name a.b,
728686
dialect {dialect},
@@ -731,29 +689,18 @@ def test_format_model_expressions_grain_alias_render_policy(dialect: str):
731689
SELECT 1 AS x, 2 AS id, 3 AS id2"""
732690

733691
formatted = format_model_expressions(parse(source, default_dialect=dialect), dialect=dialect)
734-
735-
# Rendered with the model's own dialect (e.g. postgres' native `ARRAY[...]`), never
736-
# the base generator's `ARRAY(id, id2)`, which fails to reparse on BigQuery.
737692
assert "ARRAY(id, id2)" not in formatted
738693

739694
twice = format_model_expressions(parse(formatted, default_dialect=dialect), dialect=dialect)
740695
assert formatted == twice
741696

742-
# `grain [id, id2]` parses to a single composite grain wrapping both columns
743-
# (independent of this fix); what matters here is that it survives a dialect-
744-
# specific round trip rather than being flattened to the generic `ARRAY(...)`.
745697
model = load_sql_based_model(parse(formatted, default_dialect=dialect), dialect=dialect)
746-
assert len(model.grains) == 1
747698
assert {c.name for c in model.grains[0].find_all(exp.Column)} == {"id", "id2"}
748699

749700

750701
def test_format_model_expressions_table_properties_alias_render_policy():
751-
"""`table_properties` is the deprecated alias for `physical_properties`, renamed in
752-
`ModelMeta._pre_root_validator`, not via a Pydantic alias. It must inherit
753-
`physical_properties`' render policy (warehouse SQL) so dialect-specific values
754-
inside it, such as tsql's `DATETIME2`, are not flattened to the generic generator's
755-
`TIMESTAMP` spelling.
756-
"""
702+
"""`table_properties` is renamed to `physical_properties` before validation, so it
703+
must keep dialect-specific types such as tsql's `DATETIME2`."""
757704
formatted = format_model_expressions(
758705
parse(
759706
"""

‎tests/core/test_format.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,37 @@ def test_format_without_state_load(tmp_path: pathlib.Path, mocker: MockerFixture
161161
context = Context(paths=tmp_path, config=Config(project="local_only"), load_state=False)
162162
context.format(check=True)
163163
mock.assert_not_called()
164+
165+
166+
def test_format_bigquery_header_list_properties(tmp_path: pathlib.Path):
167+
# A BigQuery model must survive `sqlmesh format` and still load: list-valued header
168+
# properties were rewritten to `ARRAY(...)`, which BigQuery parses as a subquery.
169+
model_file = create_temp_file(
170+
tmp_path,
171+
pathlib.Path("models/model.sql"),
172+
"""MODEL (
173+
name test.model,
174+
kind INCREMENTAL_BY_TIME_RANGE (time_column ds),
175+
tags ['C1', 'c2'],
176+
ignored_rules ['noselectstar', 'ambiguousorinvalidcolumn'],
177+
grain [id],
178+
partitioned_by DATE_TRUNC(ds, MONTH)
179+
);
180+
SELECT 1 AS id, CURRENT_DATE() AS ds""",
181+
)
182+
config = Config(model_defaults=ModelDefaultsConfig(dialect="bigquery"))
183+
184+
Context(paths=tmp_path, config=config).format()
185+
186+
formatted = model_file.read_text(encoding="utf-8")
187+
assert "tags ['C1', 'c2']" in formatted
188+
assert "ignored_rules ['noselectstar', 'ambiguousorinvalidcolumn']" in formatted
189+
assert "grain [id]" in formatted
190+
assert "partitioned_by DATE_TRUNC(ds, MONTH)" in formatted
191+
192+
context = Context(paths=tmp_path, config=config)
193+
assert context.format(check=True)
194+
model = context.get_model("test.model")
195+
assert model.tags == ["C1", "c2"]
196+
assert model.ignored_rules == {"noselectstar", "ambiguousorinvalidcolumn"}
197+
assert [p.sql("bigquery") for p in model.partitioned_by] == ["DATE_TRUNC(`ds`, MONTH)"]

0 commit comments

Comments
 (0)