Skip to content

Commit f2f7cde

Browse files
authored
Feat: Introduce format flag for models and audits (#4203)
1 parent d513f24 commit f2f7cde

10 files changed

Lines changed: 105 additions & 2 deletions

File tree

docs/concepts/models/overview.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,9 @@ to `false` causes SQLMesh to disable query canonicalization & simplification. Th
457457
### ignored_rules
458458
: Specifies which linter rules should be ignored/excluded for this model.
459459

460+
### formatting
461+
: Whether the model will be formatted. All models are formatted by default. Setting this to `false` causes SQLMesh to ignore this model during `sqlmesh format`.
462+
460463
## Incremental Model Properties
461464

462465
These properties can be specified in an incremental model's `kind` definition.

docs/reference/model_configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Configuration options for SQLMesh model properties. Supported by all model kinds
4040
| `gateway` | Specifies the gateway to use for the execution of this model. When not specified, the default gateway is used. | str | N |
4141
| `optimize_query` | Whether the model's query should be optimized. This attribute is `true` by default. Setting it to `false` causes SQLMesh to disable query canonicalization & simplification. This should be turned off only if the optimized query leads to errors such as surpassing text limit. | bool | N |
4242
| `ignored_rules` | A list of linter rule names (or "ALL") to be ignored/excluded for this model | str \| array[str] | N |
43-
43+
| `formatting` | Whether the model will be formatted. All models are formatted by default. Setting this to `false` causes SQLMesh to ignore this model during `sqlmesh format`. | bool | N |
4444
### Model defaults
4545

4646
The SQLMesh project-level configuration must contain the `model_defaults` key and must specify a value for its `dialect` key. Other values are set automatically unless explicitly overridden in the model definition. Learn more about project-level configuration in the [configuration guide](../guides/configuration.md).

sqlmesh/core/audit/definition.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class AuditMixin(AuditCommonMetaMixin):
7171
defaults: t.Dict[str, exp.Expression]
7272
expressions_: t.Optional[t.List[exp.Expression]]
7373
jinja_macros: JinjaMacroRegistry
74+
formatting: t.Optional[bool] = Field(default=None, exclude=True)
7475

7576
@property
7677
def expressions(self) -> t.List[exp.Expression]:

sqlmesh/core/config/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class ModelDefaultsConfig(BaseConfig):
6060
allow_partials: t.Optional[t.Union[str, bool]] = None
6161
interval_unit: t.Optional[t.Union[str, IntervalUnit]] = None
6262
enabled: t.Optional[t.Union[str, bool]] = None
63+
formatting: t.Optional[t.Union[str, bool]] = None
6364

6465
_model_kind_validator = model_kind_validator
6566
_on_destructive_change_validator = on_destructive_change_validator

sqlmesh/core/context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,9 +1091,10 @@ def format(
10911091

10921092
for target in filtered_targets:
10931093
if (
1094-
target._path is None
1094+
target._path is None or target.formatting is False
10951095
): # introduced to satisfy type checker as still want to pull filter out as many targets as possible before loop
10961096
continue
1097+
10971098
with open(target._path, "r+", encoding="utf-8") as file:
10981099
before = file.read()
10991100
expressions = parse(before, default_dialect=self.config_for_node(target).dialect)

sqlmesh/core/model/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ def sorted_python_env_payloads(python_env: t.Dict[str, Executable]) -> t.List[st
386386
"allow_partials",
387387
"enabled",
388388
"optimize_query",
389+
"formatting",
389390
mode="before",
390391
check_fields=False,
391392
)(parse_bool)

sqlmesh/core/model/meta.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class ModelMeta(_Node):
8080
ignored_rules_: t.Optional[t.Set[str]] = Field(
8181
default=None, exclude=True, alias="ignored_rules"
8282
)
83+
formatting: t.Optional[bool] = Field(default=None, exclude=True)
8384

8485
_bool_validator = bool_validator
8586
_model_kind_validator = model_kind_validator

tests/core/test_audit.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import pytest
23
from sqlglot import exp, parse_one
34

@@ -959,3 +960,31 @@ def test_multiple_audits_with_same_name():
959960
# Testing that audit arguments are identical for second and third audit
960961
# This establishes that identical audits are preserved
961962
assert model.audits[1][1] == model.audits[2][1]
963+
964+
965+
def test_audit_formatting_flag_serde():
966+
expressions = parse(
967+
"""
968+
AUDIT (
969+
name my_audit,
970+
dialect bigquery,
971+
formatting false,
972+
);
973+
974+
SELECT * FROM db.table WHERE col = @VAR('test_var')
975+
"""
976+
)
977+
978+
audit = load_audit(
979+
expressions,
980+
path="/path/to/audit",
981+
dialect="bigquery",
982+
variables={"test_var": "test_val", "test_var_unused": "unused_val"},
983+
)
984+
985+
audit_json = audit.json()
986+
987+
assert "formatting" not in json.loads(audit_json)
988+
989+
deserialized_audit = ModelAudit.parse_raw(audit_json)
990+
assert deserialized_audit.dict() == audit.dict()

tests/core/test_format.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sqlmesh.core.model import SqlModel, load_sql_based_model
99
from tests.utils.test_filesystem import create_temp_file
1010
from unittest.mock import call
11+
from sqlmesh.core.config import ModelDefaultsConfig
1112

1213

1314
def test_format_files(tmp_path: pathlib.Path, mocker: MockerFixture):
@@ -100,3 +101,46 @@ def test_format_files(tmp_path: pathlib.Path, mocker: MockerFixture):
100101
upd4
101102
== "MODEL (\n name audit.model,\n audits (\n inline_audit\n )\n);\n\nSELECT\n 3 AS item_id;\n\nAUDIT (\n name inline_audit\n);\n\nSELECT\n *\nFROM @this_model\nWHERE\n item_id < 0"
102103
)
104+
105+
106+
def test_ignore_formating_files(tmp_path: pathlib.Path):
107+
models_dir = pathlib.Path("models")
108+
audits_dir = pathlib.Path("audits")
109+
110+
# Case 1: Model and Audit are not formatted if the flag is set to false (overriding defaults)
111+
model1_text = "MODEL(name this.model1, dialect 'duckdb', formatting false); SELECT 1 col"
112+
model1 = create_temp_file(tmp_path, pathlib.Path(models_dir, "model_1.sql"), model1_text)
113+
114+
audit1_text = "AUDIT(name audit1, dialect 'duckdb', formatting false); SELECT col1 col2 FROM @this_model WHERE foo < 0;"
115+
audit1 = create_temp_file(tmp_path, pathlib.Path(audits_dir, "audit_1.sql"), audit1_text)
116+
117+
audit2_text = "AUDIT(name audit2, dialect 'duckdb', standalone true, formatting false); SELECT col1 col2 FROM @this_model WHERE foo < 0;"
118+
audit2 = create_temp_file(tmp_path, pathlib.Path(audits_dir, "audit_2.sql"), audit2_text)
119+
120+
Context(
121+
paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(formatting=True))
122+
).format()
123+
124+
assert model1.read_text(encoding="utf-8") == model1_text
125+
assert audit1.read_text(encoding="utf-8") == audit1_text
126+
assert audit2.read_text(encoding="utf-8") == audit2_text
127+
128+
# Case 2: Model is formatted (or not) based on it's flag and the defaults flag
129+
model2_text = "MODEL(name this.model2, dialect 'duckdb'); SELECT 1 col"
130+
model2 = create_temp_file(tmp_path, pathlib.Path(models_dir, "model_2.sql"), model2_text)
131+
132+
model3_text = "MODEL(name this.model3, dialect 'duckdb', formatting true); SELECT 1 col"
133+
model3 = create_temp_file(tmp_path, pathlib.Path(models_dir, "model_3.sql"), model3_text)
134+
135+
Context(
136+
paths=tmp_path, config=Config(model_defaults=ModelDefaultsConfig(formatting=False))
137+
).format()
138+
139+
# Case 2.1: Model is not formatted if the defaults flag is set to false
140+
assert model2.read_text(encoding="utf-8") == model2_text
141+
142+
# Case 2.2: Model is formatted if it's flag is set to true, overriding defaults
143+
assert (
144+
model3.read_text(encoding="utf-8")
145+
== "MODEL (\n name this.model3,\n dialect 'duckdb',\n formatting TRUE\n);\n\nSELECT\n 1 AS col"
146+
)

tests/core/test_model.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9029,3 +9029,25 @@ def test_var_in_def(assert_exp_eq):
90299029
SELECT '1970-01-01' AS "ds"
90309030
""",
90319031
)
9032+
9033+
9034+
def test_formatting_flag_serde():
9035+
expressions = d.parse(
9036+
"""
9037+
MODEL(
9038+
name test_model,
9039+
formatting False,
9040+
);
9041+
SELECT * FROM tbl;
9042+
""",
9043+
default_dialect="duckdb",
9044+
)
9045+
9046+
model = load_sql_based_model(expressions)
9047+
9048+
model_json = model.json()
9049+
9050+
assert "formatting" not in json.loads(model_json)
9051+
9052+
deserialized_model = SqlModel.parse_raw(model_json)
9053+
assert deserialized_model.dict() == model.dict()

0 commit comments

Comments
 (0)