dataform-sqlx-lint 0.1.2__tar.gz → 0.2.0__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.
- {dataform_sqlx_lint-0.1.2/src/dataform_sqlx_lint.egg-info → dataform_sqlx_lint-0.2.0}/PKG-INFO +5 -2
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/README.md +4 -1
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/pyproject.toml +1 -1
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint/__init__.py +1 -1
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint/config.py +13 -1
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint/linter.py +17 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0/src/dataform_sqlx_lint.egg-info}/PKG-INFO +5 -2
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/tests/test_config.py +4 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/tests/test_lint.py +71 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/LICENSE +0 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/setup.cfg +0 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint/cli.py +0 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint.egg-info/SOURCES.txt +0 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint.egg-info/dependency_links.txt +0 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint.egg-info/entry_points.txt +0 -0
- {dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint.egg-info/top_level.txt +0 -0
{dataform_sqlx_lint-0.1.2/src/dataform_sqlx_lint.egg-info → dataform_sqlx_lint-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataform-sqlx-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Convention linter for Dataform .sqlx files — checks the config-block and project conventions that SQL linters cannot see
|
|
5
5
|
Author-email: Ivan Histand <ihistand@rotoplas.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -42,6 +42,7 @@ pip install dataform-sqlx-lint
|
|
|
42
42
|
| E007 | on* | configurable per-directory naming/type policies (*no-op until policies are configured) |
|
|
43
43
|
| W008 | opt-in | `post_operations {}` placed before the main SELECT (style preference; Dataform accepts either) |
|
|
44
44
|
| E010 | on | every determinable output column appears in `columns: {}` — parses the main SELECT conservatively (unparseable expressions are skipped, never false-flagged) and follows `select *` through a single plain `${ref()}` into the upstream file |
|
|
45
|
+
| E011 | on* | `FOREIGN KEY` written by hand in `post_operations` under configured paths — for projects that declare keys once in a map and generate both the DDL and the referential assertions from it (*no-op until `foreign_key_paths` is configured) |
|
|
45
46
|
|
|
46
47
|
Why E010 matters: `columns: {}` is what Dataform writes to BigQuery column
|
|
47
48
|
descriptions — the metadata data catalogs, BI tools, and AI/conversational
|
|
@@ -78,6 +79,8 @@ documented_types = ["table", "view", "incremental", "declaration"] # E002
|
|
|
78
79
|
coverage_paths = ["definitions/output/"] # E010 scope; empty = everywhere
|
|
79
80
|
enable = ["E005", "W008"] # switch on opt-in rules
|
|
80
81
|
disable = ["E004"] # switch off default rules
|
|
82
|
+
foreign_key_paths = ["definitions/output/"] # E011 scope; empty = rule off
|
|
83
|
+
foreign_key_hint = "includes/keys.js" # named in E011's message
|
|
81
84
|
|
|
82
85
|
[[dir_policies]] # E007 (repeatable)
|
|
83
86
|
path_contains = "definitions/output/looker/"
|
|
@@ -127,7 +130,7 @@ and let the agent install itself.
|
|
|
127
130
|
|
|
128
131
|
```bash
|
|
129
132
|
python3 -m venv .venv && .venv/bin/pip install -e . pytest
|
|
130
|
-
.venv/bin/pytest #
|
|
133
|
+
.venv/bin/pytest # 59 tests
|
|
131
134
|
```
|
|
132
135
|
|
|
133
136
|
## License
|
|
@@ -25,6 +25,7 @@ pip install dataform-sqlx-lint
|
|
|
25
25
|
| E007 | on* | configurable per-directory naming/type policies (*no-op until policies are configured) |
|
|
26
26
|
| W008 | opt-in | `post_operations {}` placed before the main SELECT (style preference; Dataform accepts either) |
|
|
27
27
|
| E010 | on | every determinable output column appears in `columns: {}` — parses the main SELECT conservatively (unparseable expressions are skipped, never false-flagged) and follows `select *` through a single plain `${ref()}` into the upstream file |
|
|
28
|
+
| E011 | on* | `FOREIGN KEY` written by hand in `post_operations` under configured paths — for projects that declare keys once in a map and generate both the DDL and the referential assertions from it (*no-op until `foreign_key_paths` is configured) |
|
|
28
29
|
|
|
29
30
|
Why E010 matters: `columns: {}` is what Dataform writes to BigQuery column
|
|
30
31
|
descriptions — the metadata data catalogs, BI tools, and AI/conversational
|
|
@@ -61,6 +62,8 @@ documented_types = ["table", "view", "incremental", "declaration"] # E002
|
|
|
61
62
|
coverage_paths = ["definitions/output/"] # E010 scope; empty = everywhere
|
|
62
63
|
enable = ["E005", "W008"] # switch on opt-in rules
|
|
63
64
|
disable = ["E004"] # switch off default rules
|
|
65
|
+
foreign_key_paths = ["definitions/output/"] # E011 scope; empty = rule off
|
|
66
|
+
foreign_key_hint = "includes/keys.js" # named in E011's message
|
|
64
67
|
|
|
65
68
|
[[dir_policies]] # E007 (repeatable)
|
|
66
69
|
path_contains = "definitions/output/looker/"
|
|
@@ -110,7 +113,7 @@ and let the agent install itself.
|
|
|
110
113
|
|
|
111
114
|
```bash
|
|
112
115
|
python3 -m venv .venv && .venv/bin/pip install -e . pytest
|
|
113
|
-
.venv/bin/pytest #
|
|
116
|
+
.venv/bin/pytest # 59 tests
|
|
114
117
|
```
|
|
115
118
|
|
|
116
119
|
## License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dataform-sqlx-lint"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Convention linter for Dataform .sqlx files — checks the config-block and project conventions that SQL linters cannot see"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -11,7 +11,7 @@ from dataclasses import dataclass, field
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
#: Rules that run unless disabled.
|
|
14
|
-
DEFAULT_ENABLED = {"E001", "E002", "E003", "E004", "E006", "E007", "E010"}
|
|
14
|
+
DEFAULT_ENABLED = {"E001", "E002", "E003", "E004", "E006", "E007", "E010", "E011"}
|
|
15
15
|
#: Opt-in rules (house-style checks): enable via `enable = [...]`.
|
|
16
16
|
OPT_IN = {"E005", "W008"}
|
|
17
17
|
|
|
@@ -35,6 +35,10 @@ class Config:
|
|
|
35
35
|
#: E010 applies only to files whose path contains one of these; empty = all.
|
|
36
36
|
coverage_paths: list[str] = field(default_factory=list)
|
|
37
37
|
dir_policies: list[DirPolicy] = field(default_factory=list)
|
|
38
|
+
#: E011 applies only to files whose path contains one of these; empty = rule is a no-op.
|
|
39
|
+
foreign_key_paths: list[str] = field(default_factory=list)
|
|
40
|
+
#: Where E011 tells the author to declare the key instead (named in the message).
|
|
41
|
+
foreign_key_hint: str | None = None
|
|
38
42
|
enabled_extra: set[str] = field(default_factory=set)
|
|
39
43
|
disabled: set[str] = field(default_factory=set)
|
|
40
44
|
|
|
@@ -51,6 +55,8 @@ class Config:
|
|
|
51
55
|
and self.documented_types == other.documented_types
|
|
52
56
|
and self.coverage_paths == other.coverage_paths
|
|
53
57
|
and self.dir_policies == other.dir_policies
|
|
58
|
+
and self.foreign_key_paths == other.foreign_key_paths
|
|
59
|
+
and self.foreign_key_hint == other.foreign_key_hint
|
|
54
60
|
and self.enabled_extra == other.enabled_extra
|
|
55
61
|
and self.disabled == other.disabled
|
|
56
62
|
)
|
|
@@ -61,6 +67,8 @@ _KNOWN_KEYS = {
|
|
|
61
67
|
"documented_types",
|
|
62
68
|
"coverage_paths",
|
|
63
69
|
"dir_policies",
|
|
70
|
+
"foreign_key_paths",
|
|
71
|
+
"foreign_key_hint",
|
|
64
72
|
"enable",
|
|
65
73
|
"disable",
|
|
66
74
|
}
|
|
@@ -88,6 +96,10 @@ def _from_dict(raw: dict) -> Config:
|
|
|
88
96
|
kwargs["documented_types"] = set(raw["documented_types"])
|
|
89
97
|
if "coverage_paths" in raw:
|
|
90
98
|
kwargs["coverage_paths"] = list(raw["coverage_paths"])
|
|
99
|
+
if "foreign_key_paths" in raw:
|
|
100
|
+
kwargs["foreign_key_paths"] = list(raw["foreign_key_paths"])
|
|
101
|
+
if "foreign_key_hint" in raw:
|
|
102
|
+
kwargs["foreign_key_hint"] = str(raw["foreign_key_hint"])
|
|
91
103
|
return Config(
|
|
92
104
|
dir_policies=policies,
|
|
93
105
|
enabled_extra=set(raw.get("enable", [])),
|
|
@@ -12,6 +12,7 @@ offending line or `-- sqlx-lint: disable-file=E006` anywhere in the file):
|
|
|
12
12
|
E007 directory policy violation (configured prefix/type per path)
|
|
13
13
|
W008 post_operations block appears before the main SELECT [opt-in]
|
|
14
14
|
E010 columns:{} does not cover every determinable output column
|
|
15
|
+
E011 FOREIGN KEY written by hand in post_operations (configured paths) [no-op until configured]
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
from __future__ import annotations
|
|
@@ -358,6 +359,22 @@ def lint_text(text, path, config: Config | None = None, resolver=None):
|
|
|
358
359
|
f"column(s): {shown}{more}",
|
|
359
360
|
)
|
|
360
361
|
|
|
362
|
+
# --- E011: hand-written foreign keys in post_operations (configured paths) ---
|
|
363
|
+
# Projects that generate FK DDL from one declared map (so the constraint and its
|
|
364
|
+
# referential test cannot drift apart) flag any ADD CONSTRAINT ... FOREIGN KEY that
|
|
365
|
+
# bypasses the map. Comments are stripped first, so a note like
|
|
366
|
+
# "-- No FK to looker_product" never trips it.
|
|
367
|
+
if cfg.foreign_key_paths and any(p in path for p in cfg.foreign_key_paths):
|
|
368
|
+
where = cfg.foreign_key_hint or "the project's foreign-key map"
|
|
369
|
+
for fm in re.finditer(r"\bFOREIGN\s+KEY\b", clean_body, re.I):
|
|
370
|
+
add(
|
|
371
|
+
"E011",
|
|
372
|
+
"error",
|
|
373
|
+
_line_of(text, body_offset + fm.start()),
|
|
374
|
+
f"foreign key written by hand in post_operations; declare it in "
|
|
375
|
+
f"{where} and let the map's helper emit the DDL",
|
|
376
|
+
)
|
|
377
|
+
|
|
361
378
|
# --- W008 (opt-in): post_operations placement ---
|
|
362
379
|
pm = re.search(r"\bpost_operations\s*{", body)
|
|
363
380
|
if pm:
|
{dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0/src/dataform_sqlx_lint.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataform-sqlx-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Convention linter for Dataform .sqlx files — checks the config-block and project conventions that SQL linters cannot see
|
|
5
5
|
Author-email: Ivan Histand <ihistand@rotoplas.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -42,6 +42,7 @@ pip install dataform-sqlx-lint
|
|
|
42
42
|
| E007 | on* | configurable per-directory naming/type policies (*no-op until policies are configured) |
|
|
43
43
|
| W008 | opt-in | `post_operations {}` placed before the main SELECT (style preference; Dataform accepts either) |
|
|
44
44
|
| E010 | on | every determinable output column appears in `columns: {}` — parses the main SELECT conservatively (unparseable expressions are skipped, never false-flagged) and follows `select *` through a single plain `${ref()}` into the upstream file |
|
|
45
|
+
| E011 | on* | `FOREIGN KEY` written by hand in `post_operations` under configured paths — for projects that declare keys once in a map and generate both the DDL and the referential assertions from it (*no-op until `foreign_key_paths` is configured) |
|
|
45
46
|
|
|
46
47
|
Why E010 matters: `columns: {}` is what Dataform writes to BigQuery column
|
|
47
48
|
descriptions — the metadata data catalogs, BI tools, and AI/conversational
|
|
@@ -78,6 +79,8 @@ documented_types = ["table", "view", "incremental", "declaration"] # E002
|
|
|
78
79
|
coverage_paths = ["definitions/output/"] # E010 scope; empty = everywhere
|
|
79
80
|
enable = ["E005", "W008"] # switch on opt-in rules
|
|
80
81
|
disable = ["E004"] # switch off default rules
|
|
82
|
+
foreign_key_paths = ["definitions/output/"] # E011 scope; empty = rule off
|
|
83
|
+
foreign_key_hint = "includes/keys.js" # named in E011's message
|
|
81
84
|
|
|
82
85
|
[[dir_policies]] # E007 (repeatable)
|
|
83
86
|
path_contains = "definitions/output/looker/"
|
|
@@ -127,7 +130,7 @@ and let the agent install itself.
|
|
|
127
130
|
|
|
128
131
|
```bash
|
|
129
132
|
python3 -m venv .venv && .venv/bin/pip install -e . pytest
|
|
130
|
-
.venv/bin/pytest #
|
|
133
|
+
.venv/bin/pytest # 59 tests
|
|
131
134
|
```
|
|
132
135
|
|
|
133
136
|
## License
|
|
@@ -7,6 +7,8 @@ schema_suffixes = ["_prod", "_dev", "_staging"]
|
|
|
7
7
|
coverage_paths = ["definitions/output/looker/"]
|
|
8
8
|
enable = ["E005", "W008"]
|
|
9
9
|
disable = ["E004"]
|
|
10
|
+
foreign_key_paths = ["definitions/output/looker/"]
|
|
11
|
+
foreign_key_hint = "includes/keys.js"
|
|
10
12
|
|
|
11
13
|
[[dir_policies]]
|
|
12
14
|
path_contains = "definitions/output/looker/"
|
|
@@ -27,6 +29,8 @@ def test_load_standalone_toml(tmp_path):
|
|
|
27
29
|
assert cfg.coverage_paths == ["definitions/output/looker/"]
|
|
28
30
|
assert "E005" in cfg.enabled_extra and "W008" in cfg.enabled_extra
|
|
29
31
|
assert "E004" in cfg.disabled
|
|
32
|
+
assert cfg.foreign_key_paths == ["definitions/output/looker/"]
|
|
33
|
+
assert cfg.foreign_key_hint == "includes/keys.js"
|
|
30
34
|
assert len(cfg.dir_policies) == 2
|
|
31
35
|
assert cfg.dir_policies[0].require_prefix == "looker_"
|
|
32
36
|
assert cfg.dir_policies[1].severity == "warning"
|
|
@@ -369,3 +369,74 @@ class TestE010ColumnCoverage:
|
|
|
369
369
|
cfg = Config(disabled={"E010"})
|
|
370
370
|
text = self._table('a: "A."', 'select a, b from ${ref("t")}')
|
|
371
371
|
assert "E010" not in codes(lint_text(text, PATH, config=cfg))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class TestE011HandWrittenForeignKeys:
|
|
375
|
+
FK_PATH = "definitions/output/looker/looker_orders.sqlx"
|
|
376
|
+
CFG = Config(
|
|
377
|
+
foreign_key_paths=["definitions/output/looker/"],
|
|
378
|
+
foreign_key_hint="includes/keys.js",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def _table(self, post_ops):
|
|
382
|
+
return (
|
|
383
|
+
'config {\n type: "table",\n schema: "looker",\n'
|
|
384
|
+
' columns: { order_id: "Order." }\n}\n\n'
|
|
385
|
+
'select order_id from ${ref("vw_orders")}\n\n'
|
|
386
|
+
f"post_operations {{\n ALTER TABLE ${{self()}}\n{post_ops}\n}}\n"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
HAND_WRITTEN = (
|
|
390
|
+
" ADD PRIMARY KEY (order_id) NOT ENFORCED,\n"
|
|
391
|
+
" ADD CONSTRAINT o_to_c FOREIGN KEY (customer_id) "
|
|
392
|
+
'REFERENCES ${ref("looker_customer")}(customer_id) NOT ENFORCED;'
|
|
393
|
+
)
|
|
394
|
+
GENERATED = (
|
|
395
|
+
" ADD PRIMARY KEY (order_id) NOT ENFORCED,\n"
|
|
396
|
+
" -- No FK to looker_product: kept ids of deleted products.\n"
|
|
397
|
+
" ${keys.fkAdd(self, ref)};"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def test_hand_written_fk_flagged_in_scope(self):
|
|
401
|
+
found = [
|
|
402
|
+
f
|
|
403
|
+
for f in lint_text(self._table(self.HAND_WRITTEN), self.FK_PATH, config=self.CFG)
|
|
404
|
+
if f.code == "E011"
|
|
405
|
+
]
|
|
406
|
+
assert len(found) == 1
|
|
407
|
+
assert "includes/keys.js" in found[0].message
|
|
408
|
+
assert found[0].line == 12
|
|
409
|
+
|
|
410
|
+
def test_generated_fk_and_comments_pass(self):
|
|
411
|
+
assert "E011" not in codes(
|
|
412
|
+
lint_text(self._table(self.GENERATED), self.FK_PATH, config=self.CFG)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def test_out_of_scope_path_not_flagged(self):
|
|
416
|
+
assert "E011" not in codes(
|
|
417
|
+
lint_text(
|
|
418
|
+
self._table(self.HAND_WRITTEN),
|
|
419
|
+
"definitions/output/reports/orders.sqlx",
|
|
420
|
+
config=self.CFG,
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def test_no_op_without_configured_paths(self):
|
|
425
|
+
assert "E011" not in codes(lint_text(self._table(self.HAND_WRITTEN), self.FK_PATH))
|
|
426
|
+
|
|
427
|
+
def test_default_hint_when_unconfigured(self):
|
|
428
|
+
cfg = Config(foreign_key_paths=["definitions/output/looker/"])
|
|
429
|
+
found = [
|
|
430
|
+
f
|
|
431
|
+
for f in lint_text(self._table(self.HAND_WRITTEN), self.FK_PATH, config=cfg)
|
|
432
|
+
if f.code == "E011"
|
|
433
|
+
]
|
|
434
|
+
assert found and "foreign-key map" in found[0].message
|
|
435
|
+
|
|
436
|
+
def test_line_suppression(self):
|
|
437
|
+
text = self._table(
|
|
438
|
+
self.HAND_WRITTEN.replace(
|
|
439
|
+
"NOT ENFORCED;", "NOT ENFORCED; -- sqlx-lint: disable=E011 (legacy)"
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
assert "E011" not in codes(lint_text(text, self.FK_PATH, config=self.CFG))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataform_sqlx_lint-0.1.2 → dataform_sqlx_lint-0.2.0}/src/dataform_sqlx_lint.egg-info/top_level.txt
RENAMED
|
File without changes
|