@@ -422,8 +422,8 @@ def test_format_model_expressions_meta_render_policy(dialect: str, audit_type: s
422422@pytest .mark .parametrize (
423423 "header" ,
424424 [
425- "columns (ts DATETIME2(6))" ,
426425 "audits (my_audit(t := CAST('2024-01-01' AS DATETIME2)))" ,
426+ "audits (my_audit(flag := true))" ,
427427 "kind SCD_TYPE_2_BY_COLUMN(unique_key id, columns (a, b), time_data_type DATETIME2(6))" ,
428428 "physical_properties (labels = (('env', 'prod')))" ,
429429 "allow_partials true, description 'my description'" ,
@@ -447,6 +447,28 @@ def test_format_model_expressions_is_idempotent(header: str):
447447 assert once == twice
448448
449449
450+ @pytest .mark .parametrize (
451+ "dialect,column_type" ,
452+ [("bigquery" , "DATETIME" ), ("tsql" , "DATETIME2(6)" )],
453+ )
454+ def test_format_model_expressions_preserves_column_types (dialect : str , column_type : str ):
455+ """Repeated `sqlmesh format` runs must not change a model's declared column types.
456+
457+ Rendering `columns` with the generic generator rewrote them: BigQuery `DATETIME`
458+ became `TIMESTAMP` and then `TIMESTAMPTZ`, tsql `DATETIME2` became `TIMESTAMP` and
459+ then `VARBINARY`.
460+ """
461+ expected = exp .DataType .build (column_type , dialect = dialect )
462+ formatted = f"MODEL (name a.b, dialect { dialect } , columns (ts { column_type } ));\n SELECT 1 AS ts"
463+
464+ for _ in range (2 ):
465+ formatted = format_model_expressions (
466+ parse (formatted , default_dialect = dialect ), dialect = dialect
467+ )
468+ model = load_sql_based_model (parse (formatted , default_dialect = dialect ), dialect = dialect )
469+ assert model .columns_to_types == {"ts" : expected }
470+
471+
450472def test_format_audit_expressions_meta_render_policy ():
451473 """AUDIT headers have their own meta model, and get the same split: `blocking` is
452474 SQLMesh's own boolean and must not become tsql's `(1 = 0)`, while `defaults` holds
@@ -473,60 +495,21 @@ def test_format_audit_expressions_meta_render_policy():
473495 assert "cutoff := '2024-01-01'::DATETIME2" in formatted
474496
475497
476- def test_format_model_expressions_time_column_dialect ():
477- """`time_column` is a nested Pydantic model (`TimeColumn`) wrapping an expression, not
498+ def test_format_model_expressions_kind_time_column_dialect ():
499+ """Expression-bearing properties nested inside `kind` render with the model dialect,
500+ while their scalar siblings stay dialect-agnostic.
501+
502+ `time_column` is a nested Pydantic model (`TimeColumn`) wrapping an expression, not
478503 an `exp.Expr` annotation itself, so the render-policy reflection must recurse into
479504 nested Pydantic models to classify it as warehouse SQL. Otherwise it falls back to
480505 generic rendering and loses dialect-specific identifier quoting: tsql's `[end]`
481506 becomes ANSI `"end"`, even though the same identifier in the query body is correctly
482507 kept as `[end]`.
483- """
484- formatted = format_model_expressions (
485- parse (
486- """
487- MODEL (
488- name a.b,
489- dialect tsql,
490- kind INCREMENTAL_BY_TIME_RANGE (
491- time_column [end]
492- )
493- );
494-
495- SELECT 1 AS x, [end] FROM t
496- """ ,
497- default_dialect = "tsql" ,
498- ),
499- dialect = "tsql" ,
500- )
501-
502- assert (
503- formatted
504- == """MODEL (
505- name a.b,
506- dialect tsql,
507- kind INCREMENTAL_BY_TIME_RANGE (
508- time_column [end]
509- )
510- );
511-
512- SELECT
513- 1 AS x,
514- [end]
515- FROM t"""
516- )
517-
518-
519- def test_format_model_expressions_kind_scalar_sibling_dialect ():
520- """A scalar sibling property of an expression-bearing property inside `kind` (e.g.
521- `forward_only` next to `time_column`) must stay dialect-agnostic even though the
522- render policy correctly marks `kind` as containing an expression-holding field
523- somewhere in the `ModelKind` union.
524508
525- Regression: recursing into nested Pydantic models to fix `time_column` (see
526- `test_format_model_expressions_time_column_dialect`) made `_holds_expression` also
527- match on `kind` itself, since *some* member of the `ModelKind` union
528- (`IncrementalByTimeRangeKind.time_column`) holds an expression. That routed the
529- entire `kind (...)` subtree through a dialect-specific generator, so tsql's
509+ Regression: recursing into nested Pydantic models to fix `time_column` made
510+ `_holds_expression` also match on `kind` itself, since *some* member of the
511+ `ModelKind` union (`IncrementalByTimeRangeKind.time_column`) holds an expression.
512+ That routed the entire `kind (...)` subtree through a dialect-specific generator, so tsql's
530513 boolean-literal preprocessing rewrote `forward_only TRUE` into `forward_only (1 = 1)`.
531514 That reparses without error, but `str_to_bool` on `Paren(EQ(1, 1)).name` (`""`)
532515 evaluates to `False`, so the value silently flips on reload.
0 commit comments