truequery 0.1.0__py3-none-any.whl
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.
- truequery/__init__.py +3 -0
- truequery/analyzers/__init__.py +0 -0
- truequery/analyzers/project_checks.py +220 -0
- truequery/analyzers/sql.py +529 -0
- truequery/checks/__init__.py +0 -0
- truequery/checks/sql.py +1278 -0
- truequery/cli.py +710 -0
- truequery/engine.py +346 -0
- truequery/hook.py +860 -0
- truequery/keylookup.py +135 -0
- truequery/models.py +32 -0
- truequery/project.py +561 -0
- truequery/registry_impl.json +127 -0
- truequery/report.py +343 -0
- truequery/schema.py +206 -0
- truequery/settings.py +239 -0
- truequery/standard/consume/README.md +111 -0
- truequery/standard/consume/adhoc-limit-required.yaml +50 -0
- truequery/standard/consume/aggregate-before-export.yaml +39 -0
- truequery/standard/consume/consumer-cost-guard.yaml +44 -0
- truequery/standard/consume/consumer-reads-marts.yaml +38 -0
- truequery/standard/consume/dashboard-query-budget.yaml +40 -0
- truequery/standard/consume/dashboard-refresh-schedule.yaml +47 -0
- truequery/standard/consume/deterministic-export-order.yaml +44 -0
- truequery/standard/consume/explicit-timezone-consumers.yaml +44 -0
- truequery/standard/consume/explore-answer-carries-reproducible-query.yaml +56 -0
- truequery/standard/consume/explore-caveats-and-unchecked-stated.yaml +52 -0
- truequery/standard/consume/explore-day-boundaries-and-partial-periods.yaml +51 -0
- truequery/standard/consume/explore-experiment-readout-follows-plan.yaml +63 -0
- truequery/standard/consume/explore-freshness-and-coverage-checked.yaml +54 -0
- truequery/standard/consume/explore-inspect-distributions-and-examples.yaml +47 -0
- truequery/standard/consume/explore-join-row-count-checks.yaml +53 -0
- truequery/standard/consume/explore-null-aware-negative-filters.yaml +50 -0
- truequery/standard/consume/explore-question-metric-grain-stated.yaml +55 -0
- truequery/standard/consume/explore-rates-not-counts-across-segments.yaml +51 -0
- truequery/standard/consume/explore-reconcile-to-known-total.yaml +51 -0
- truequery/standard/consume/explore-reuse-governed-metric.yaml +52 -0
- truequery/standard/consume/export-row-cap.yaml +45 -0
- truequery/standard/consume/freshness-displayed.yaml +44 -0
- truequery/standard/consume/no-deep-offset.yaml +38 -0
- truequery/standard/consume/parameterized-dashboard-filters.yaml +38 -0
- truequery/standard/consume/reverse-etl-idempotent-keys.yaml +45 -0
- truequery/standard/consume/reverse-etl-pii-consent.yaml +50 -0
- truequery/standard/consume/reverse-etl-schema-contract.yaml +47 -0
- truequery/standard/dq/README.md +117 -0
- truequery/standard/dq/accuracy-proxy-or-unmeasurable.yaml +45 -0
- truequery/standard/dq/audits-blocking-every-run.yaml +47 -0
- truequery/standard/dq/checks-versioned-with-code.yaml +52 -0
- truequery/standard/dq/completeness-declare-missing-sentinels.yaml +44 -0
- truequery/standard/dq/completeness-fill-rate-thresholds.yaml +45 -0
- truequery/standard/dq/completeness-not-null-required-columns.yaml +60 -0
- truequery/standard/dq/completeness-table-not-empty.yaml +48 -0
- truequery/standard/dq/consistency-cross-column-row-rules.yaml +49 -0
- truequery/standard/dq/consistency-no-orphan-keys.yaml +49 -0
- truequery/standard/dq/consistency-paired-aggregate-reconciliation.yaml +48 -0
- truequery/standard/dq/schema-conformance-checked-on-read.yaml +48 -0
- truequery/standard/dq/unenforced-constraints-are-tested.yaml +51 -0
- truequery/standard/dq/uniqueness-composite-grain.yaml +52 -0
- truequery/standard/dq/uniqueness-key-no-duplicates.yaml +53 -0
- truequery/standard/dq/unit-tests-with-fixtures.yaml +52 -0
- truequery/standard/dq/validity-allowed-value-sets.yaml +46 -0
- truequery/standard/dq/validity-enforce-model-contracts.yaml +51 -0
- truequery/standard/dq/validity-range-format-type-rules.yaml +50 -0
- truequery/standard/gov/README.md +94 -0
- truequery/standard/gov/audit-columns-or-snapshots.yaml +45 -0
- truequery/standard/gov/column-grants-for-pii.yaml +41 -0
- truequery/standard/gov/declared-model-owner.yaml +43 -0
- truequery/standard/gov/description-coverage-for-shared-models.yaml +40 -0
- truequery/standard/gov/erasure-path-documented.yaml +43 -0
- truequery/standard/gov/exposures-registered.yaml +73 -0
- truequery/standard/gov/group-private-access.yaml +41 -0
- truequery/standard/gov/least-privilege-grants.yaml +80 -0
- truequery/standard/gov/lineage-from-declared-sources.yaml +41 -0
- truequery/standard/gov/mesh-public-contracts-only.yaml +41 -0
- truequery/standard/gov/no-bypassrls-roles.yaml +39 -0
- truequery/standard/gov/no-public-privileges.yaml +39 -0
- truequery/standard/gov/no-raw-pii-in-seeds.yaml +41 -0
- truequery/standard/gov/pii-classification-declared.yaml +42 -0
- truequery/standard/gov/production-pii-not-in-dev.yaml +43 -0
- truequery/standard/gov/redacting-views-for-pii.yaml +41 -0
- truequery/standard/gov/retention-declared.yaml +42 -0
- truequery/standard/gov/row-policy-for-sensitive-tables.yaml +38 -0
- truequery/standard/ingest/README.md +100 -0
- truequery/standard/ingest/cap-intake-cost-and-volume.yaml +46 -0
- truequery/standard/ingest/cdc-replica-identity-required.yaml +47 -0
- truequery/standard/ingest/checkpoint-streaming-intake.yaml +42 -0
- truequery/standard/ingest/choose-batch-vs-streaming-deliberately.yaml +45 -0
- truequery/standard/ingest/declare-source-freshness.yaml +80 -0
- truequery/standard/ingest/dedupe-on-natural-key-before-append.yaml +45 -0
- truequery/standard/ingest/handle-schema-drift-explicitly.yaml +45 -0
- truequery/standard/ingest/idempotent-batch-loads.yaml +49 -0
- truequery/standard/ingest/isolate-source-failures.yaml +50 -0
- truequery/standard/ingest/landing-rebuildable-from-source.yaml +43 -0
- truequery/standard/ingest/monitor-cdc-lag-and-slots.yaml +47 -0
- truequery/standard/ingest/pin-connector-and-schema-versions.yaml +42 -0
- truequery/standard/ingest/pin-file-format-options.yaml +42 -0
- truequery/standard/ingest/propagate-deletes-explicitly.yaml +48 -0
- truequery/standard/ingest/quarantine-invalid-rows-at-intake.yaml +56 -0
- truequery/standard/ingest/raw-landing-append-only.yaml +48 -0
- truequery/standard/ingest/reconcile-row-counts-per-load.yaml +43 -0
- truequery/standard/ingest/record-file-lineage-at-intake.yaml +48 -0
- truequery/standard/ingest/reference-sources-not-raw-tables.yaml +42 -0
- truequery/standard/ingest/require-completeness-marker.yaml +50 -0
- truequery/standard/ingest/scan-pii-at-landing.yaml +45 -0
- truequery/standard/ingest/seeds-small-static-only.yaml +43 -0
- truequery/standard/ingest/test-intake-against-fixtures.yaml +46 -0
- truequery/standard/ingest/watermark-and-late-data-policy.yaml +49 -0
- truequery/standard/model/README.md +91 -0
- truequery/standard/model/dbt-contract-enforced.yaml +50 -0
- truequery/standard/model/delete-handling-incremental.yaml +51 -0
- truequery/standard/model/explicit-column-lists.yaml +83 -0
- truequery/standard/model/full-refresh-parity.yaml +49 -0
- truequery/standard/model/grain-preserved-joins.yaml +52 -0
- truequery/standard/model/idempotent-reexec.yaml +48 -0
- truequery/standard/model/incremental-lookback-window.yaml +53 -0
- truequery/standard/model/incremental-watermark-filter.yaml +48 -0
- truequery/standard/model/jinja-macro-determinism.yaml +46 -0
- truequery/standard/model/jinja-no-nested-curlies.yaml +43 -0
- truequery/standard/model/materialization-choice.yaml +51 -0
- truequery/standard/model/merge-source-deduped.yaml +57 -0
- truequery/standard/model/microbatch-event-time.yaml +39 -0
- truequery/standard/model/null-join-keys-excluded.yaml +45 -0
- truequery/standard/model/snapshot-scd2-validity.yaml +45 -0
- truequery/standard/model/snapshot-strategy-choice.yaml +41 -0
- truequery/standard/model/staging-no-business-logic.yaml +44 -0
- truequery/standard/model/surrogate-keys-stable.yaml +37 -0
- truequery/standard/model/unique-key-merge-key.yaml +55 -0
- truequery/standard/ops/README.md +98 -0
- truequery/standard/ops/breaking-change-categorization.yaml +35 -0
- truequery/standard/ops/ci-pr-checks-required.yaml +44 -0
- truequery/standard/ops/ci-slim-builds-only-changed.yaml +42 -0
- truequery/standard/ops/cost-estimate-before-run.yaml +41 -0
- truequery/standard/ops/cost-guardrail-confidence-levels.yaml +39 -0
- truequery/standard/ops/dev-environment-expiry.yaml +42 -0
- truequery/standard/ops/disposable-dev-environments.yaml +42 -0
- truequery/standard/ops/drift-detection-history-first.yaml +42 -0
- truequery/standard/ops/full-refresh-approval.yaml +76 -0
- truequery/standard/ops/model-versioning-breaking-changes.yaml +83 -0
- truequery/standard/ops/plan-preview-before-apply.yaml +39 -0
- truequery/standard/ops/post-deploy-data-validation.yaml +42 -0
- truequery/standard/ops/pre-destructive-snapshot.yaml +35 -0
- truequery/standard/ops/prod-credentials-isolation.yaml +96 -0
- truequery/standard/ops/run-artifact-retention.yaml +41 -0
- truequery/standard/ops/staged-upgrades-with-backup.yaml +39 -0
- truequery/standard/orch/README.md +74 -0
- truequery/standard/orch/concurrency-pools-and-limits.yaml +63 -0
- truequery/standard/orch/dag-ci-parse-and-unit-tests.yaml +70 -0
- truequery/standard/orch/data-aware-scheduling-over-polling.yaml +62 -0
- truequery/standard/orch/declared-asset-lineage.yaml +66 -0
- truequery/standard/orch/deferrable-sensors-with-timeout.yaml +75 -0
- truequery/standard/orch/explicit-catchup-policy.yaml +53 -0
- truequery/standard/orch/explicit-schedule-timezone.yaml +68 -0
- truequery/standard/orch/failure-alert-routing.yaml +100 -0
- truequery/standard/orch/idempotent-backfill-windowing.yaml +68 -0
- truequery/standard/orch/idempotent-retries-with-limits.yaml +79 -0
- truequery/standard/orch/light-dag-parse-no-heavy-top-level.yaml +75 -0
- truequery/standard/orch/no-circular-dependencies.yaml +67 -0
- truequery/standard/orch/partitioned-assets-windowing.yaml +66 -0
- truequery/standard/orch/sla-deadline-alerts.yaml +76 -0
- truequery/standard/orch/task-atomicity-no-partial-writes.yaml +62 -0
- truequery/standard/orch/task-timeout-execution-limit.yaml +70 -0
- truequery/standard/py/README.md +63 -0
- truequery/standard/py/airflow-isolate-task-dependencies.yaml +40 -0
- truequery/standard/py/dagster-asset-declared-dependencies.yaml +47 -0
- truequery/standard/py/dagster-io-manager-not-manual-io.yaml +47 -0
- truequery/standard/py/dagster-resources-for-external-clients.yaml +53 -0
- truequery/standard/py/dbt-python-model-contract.yaml +48 -0
- truequery/standard/py/deterministic-model-outputs.yaml +83 -0
- truequery/standard/py/hook-adapter-connection-lifecycle.yaml +59 -0
- truequery/standard/py/idempotent-task-logic.yaml +55 -0
- truequery/standard/py/no-n-plus-one-queries-in-loops.yaml +46 -0
- truequery/standard/py/pandas-filter-before-collect.yaml +44 -0
- truequery/standard/py/pandas-no-chained-assignment.yaml +41 -0
- truequery/standard/py/pandas-no-row-loop-mutations.yaml +44 -0
- truequery/standard/py/pin-pipeline-dependencies.yaml +82 -0
- truequery/standard/py/sqlmesh-audit-python-outputs.yaml +49 -0
- truequery/standard/py/sqlmesh-python-model-execution-context.yaml +52 -0
- truequery/standard/sem/README.md +97 -0
- truequery/standard/sem/aggregation-type-matches-additivity.yaml +61 -0
- truequery/standard/sem/conversion-metrics-window-bounds.yaml +54 -0
- truequery/standard/sem/count-distinct-uses-entity-key.yaml +52 -0
- truequery/standard/sem/cumulative-metrics-time-spine.yaml +53 -0
- truequery/standard/sem/derived-metrics-compose-from-governed.yaml +52 -0
- truequery/standard/sem/description-and-ownership-per-metric.yaml +50 -0
- truequery/standard/sem/dimension-naming-conventions.yaml +52 -0
- truequery/standard/sem/entity-primary-key-declared.yaml +54 -0
- truequery/standard/sem/join-paths-declared-via-entities.yaml +47 -0
- truequery/standard/sem/metric-filters-declared-not-hidden.yaml +57 -0
- truequery/standard/sem/metric-grain-declaration.yaml +52 -0
- truequery/standard/sem/metric-smoke-test-on-change.yaml +43 -0
- truequery/standard/sem/no-fanout-double-counting.yaml +52 -0
- truequery/standard/sem/no-metric-defined-off-staging.yaml +47 -0
- truequery/standard/sem/null-handling-in-aggregations.yaml +57 -0
- truequery/standard/sem/percentile-metrics-use-approximate.yaml +51 -0
- truequery/standard/sem/ratio-metrics-divide-after-aggregation.yaml +49 -0
- truequery/standard/sem/saved-query-canonical-slices.yaml +56 -0
- truequery/standard/sem/semantic-model-validates.yaml +50 -0
- truequery/standard/sem/single-governed-metric-definition.yaml +68 -0
- truequery/standard/sem/time-dimension-standardized.yaml +51 -0
- truequery/standard/sem/unit-consistency.yaml +54 -0
- truequery/standard/sql/README.md +126 -0
- truequery/standard/sql/accidental-cross-join.yaml +58 -0
- truequery/standard/sql/average-of-ratios.yaml +63 -0
- truequery/standard/sql/between-date-on-timestamp.yaml +74 -0
- truequery/standard/sql/case-without-else-in-aggregate.yaml +67 -0
- truequery/standard/sql/count-star-after-left-join.yaml +67 -0
- truequery/standard/sql/distinct-dedup-bandaid.yaml +59 -0
- truequery/standard/sql/explore-limit-is-not-a-sample.yaml +56 -0
- truequery/standard/sql/explore-profile-filter-columns.yaml +48 -0
- truequery/standard/sql/fanout-join-pre-aggregation.yaml +64 -0
- truequery/standard/sql/group-by-bare-column.yaml +75 -0
- truequery/standard/sql/insert-explicit-column-list.yaml +60 -0
- truequery/standard/sql/integer-division-truncation.yaml +94 -0
- truequery/standard/sql/keyset-pagination.yaml +70 -0
- truequery/standard/sql/left-join-null-rejecting-where.yaml +58 -0
- truequery/standard/sql/limit-requires-order-by.yaml +67 -0
- truequery/standard/sql/natural-join-explicit-keys.yaml +56 -0
- truequery/standard/sql/negative-filter-drops-nulls.yaml +70 -0
- truequery/standard/sql/nondeterministic-merge-keys.yaml +69 -0
- truequery/standard/sql/not-in-nullable-subquery.yaml +57 -0
- truequery/standard/sql/null-equality-comparison.yaml +65 -0
- truequery/standard/sql/random-sampling-no-sort.yaml +48 -0
- truequery/standard/sql/row-number-dedupe-tiebreaker.yaml +67 -0
- truequery/standard/sql/sargable-filter-bare-column.yaml +53 -0
- truequery/standard/sql/set-operation-column-position.yaml +59 -0
- truequery/standard/sql/unguarded-update-delete.yaml +63 -0
- truequery/standard/sql/union-all-default.yaml +70 -0
- truequery/standard/store/README.md +88 -0
- truequery/standard/store/cluster-on-frequent-filters-joins.yaml +44 -0
- truequery/standard/store/compact-small-files-optimize.yaml +44 -0
- truequery/standard/store/declare-not-null-check-constraints.yaml +43 -0
- truequery/standard/store/declare-sort-order-for-range-queries.yaml +44 -0
- truequery/standard/store/enable-change-feed-for-incremental-consumers.yaml +44 -0
- truequery/standard/store/evolve-partitions-by-metadata-not-rewrite.yaml +45 -0
- truequery/standard/store/explicit-physical-types-no-implicit-casts.yaml +43 -0
- truequery/standard/store/full-refresh-by-replace-not-delete-insert.yaml +44 -0
- truequery/standard/store/incremental-watermark-partition-alignment.yaml +40 -0
- truequery/standard/store/no-high-cardinality-partition-column.yaml +47 -0
- truequery/standard/store/partition-expiration-retention-policy.yaml +43 -0
- truequery/standard/store/partition-on-filter-column.yaml +43 -0
- truequery/standard/store/partition-predicate-required.yaml +41 -0
- truequery/standard/store/prefer-auto-liquid-clustering-over-manual-tune.yaml +37 -0
- truequery/standard/store/rewrite-manifests-and-stats-periodically.yaml +37 -0
- truequery/standard/store/single-open-format-per-medallion-layer.yaml +37 -0
- truequery/standard/store/time-travel-retention-explicit.yaml +46 -0
- truequery/standard/store/use-transient-temporary-for-ephemeral.yaml +35 -0
- truequery/standard/store/vacuum-expire-snapshots-with-retention-floor.yaml +42 -0
- truequery/std_registry.py +3310 -0
- truequery-0.1.0.dist-info/METADATA +52 -0
- truequery-0.1.0.dist-info/RECORD +254 -0
- truequery-0.1.0.dist-info/WHEEL +4 -0
- truequery-0.1.0.dist-info/entry_points.txt +3 -0
- truequery-0.1.0.dist-info/licenses/LICENSE +201 -0
- truequery-0.1.0.dist-info/licenses/NOTICE +4 -0
truequery/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Preview non-SQL checks (v1, Stop tier, never immediate).
|
|
2
|
+
|
|
3
|
+
Each check is backed by an existing standard practice (cited in its
|
|
4
|
+
``why``); practices with no detector-shaped record yet (Airflow dynamic
|
|
5
|
+
``start_date``, pandas ``merge(validate=)``) are NOT implemented here —
|
|
6
|
+
they are listed as gaps in docs. Deterministic, offline, file/project
|
|
7
|
+
evidence only.
|
|
8
|
+
"""
|
|
9
|
+
import ast
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
INCREMENTAL = re.compile(r"materialized\s*=\s*['\"]incremental['\"]")
|
|
16
|
+
UNIQUE_KEY = re.compile(r"unique_key\s*=")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _finding(rule, registry, settings, mode, path, line, why, fix,
|
|
20
|
+
snippet=""):
|
|
21
|
+
record = registry.RECORDS.get(rule) if registry else None
|
|
22
|
+
sev = None
|
|
23
|
+
if settings:
|
|
24
|
+
sev = settings.get(rule, {}).get("severity")
|
|
25
|
+
if not sev and record:
|
|
26
|
+
sev = record["severity"].get(mode, "warning")
|
|
27
|
+
return {"rule": rule, "severity": sev or "warning", "mode": mode,
|
|
28
|
+
"evidence": "project", "file": path, "line": line, "col": 1,
|
|
29
|
+
"snippet": snippet[:120], "why": why, "fix": fix}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
STRATEGY = re.compile(r"incremental_strategy\s*=\s*['\"]([\w+]+)['\"]")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _yaml_unique_key(path):
|
|
36
|
+
"""True when a properties YAML next to the model sets unique_key in
|
|
37
|
+
the model's config block (dbt allows config there as well as in
|
|
38
|
+
the SQL)."""
|
|
39
|
+
model = os.path.splitext(os.path.basename(path))[0]
|
|
40
|
+
directory = os.path.dirname(os.path.abspath(path))
|
|
41
|
+
try:
|
|
42
|
+
names = sorted(os.listdir(directory))
|
|
43
|
+
except OSError:
|
|
44
|
+
return False
|
|
45
|
+
for name in names:
|
|
46
|
+
if not name.endswith((".yml", ".yaml")):
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
with open(os.path.join(directory, name), encoding="utf-8") as handle:
|
|
50
|
+
doc = yaml.safe_load(handle)
|
|
51
|
+
except (OSError, ValueError, yaml.YAMLError):
|
|
52
|
+
continue
|
|
53
|
+
specs = doc.get("models") if isinstance(doc, dict) else None
|
|
54
|
+
for spec in specs or []:
|
|
55
|
+
if (isinstance(spec, dict) and spec.get("name") == model
|
|
56
|
+
and isinstance(spec.get("config"), dict)
|
|
57
|
+
and spec["config"].get("unique_key")):
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_dbt_model(text, path, mode="build", registry=None,
|
|
63
|
+
settings=None):
|
|
64
|
+
"""model/unique-key-merge-key: merge/delete+insert incremental
|
|
65
|
+
model with no unique_key (in the SQL config or the model's YAML).
|
|
66
|
+
Microbatch/insert_overwrite strategies key on event_time/partitions
|
|
67
|
+
instead — no unique_key expected."""
|
|
68
|
+
out = []
|
|
69
|
+
m = INCREMENTAL.search(text)
|
|
70
|
+
strat = STRATEGY.search(text)
|
|
71
|
+
merge_like = strat is None or strat.group(1) in (
|
|
72
|
+
"merge", "delete+insert", "delete_insert")
|
|
73
|
+
if (m and merge_like and not UNIQUE_KEY.search(text)
|
|
74
|
+
and not _yaml_unique_key(path)):
|
|
75
|
+
line = text[:m.start()].count("\n") + 1
|
|
76
|
+
out.append(_finding(
|
|
77
|
+
"model/unique-key-merge-key", registry, settings, mode, path,
|
|
78
|
+
line,
|
|
79
|
+
"incremental model declares no unique_key; a merge degrades "
|
|
80
|
+
"to append and duplicates rows on every re-run "
|
|
81
|
+
"(model/unique-key-merge-key)",
|
|
82
|
+
"add unique_key='<key>' to the model config and a "
|
|
83
|
+
"unique + not_null test on the key"))
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _tests_of(entry):
|
|
88
|
+
tests = set()
|
|
89
|
+
for t in entry.get("tests") or []:
|
|
90
|
+
tests.add(t if isinstance(t, str) else next(iter(t), ""))
|
|
91
|
+
for t in entry.get("data_tests") or []:
|
|
92
|
+
tests.add(t if isinstance(t, str) else next(iter(t), ""))
|
|
93
|
+
return tests
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_schema_yaml(text, path, mode="build", registry=None,
|
|
97
|
+
settings=None):
|
|
98
|
+
"""dq/uniqueness-key-no-duplicates + ingest/declare-source-freshness
|
|
99
|
+
over a dbt schema YAML (models:/sources:)."""
|
|
100
|
+
out = []
|
|
101
|
+
try:
|
|
102
|
+
doc = yaml.safe_load(text)
|
|
103
|
+
except Exception:
|
|
104
|
+
return out
|
|
105
|
+
if not isinstance(doc, dict):
|
|
106
|
+
return out
|
|
107
|
+
lines = text.splitlines()
|
|
108
|
+
|
|
109
|
+
def line_of(needle, after=0):
|
|
110
|
+
for i, row in enumerate(lines[after:], start=after + 1):
|
|
111
|
+
if needle in row:
|
|
112
|
+
return i
|
|
113
|
+
return 1
|
|
114
|
+
|
|
115
|
+
for model in doc.get("models") or []:
|
|
116
|
+
if not isinstance(model, dict):
|
|
117
|
+
continue
|
|
118
|
+
name = model.get("name", "")
|
|
119
|
+
cols = model.get("columns") or []
|
|
120
|
+
# A key candidate needs a declared signal: a MetricFlow primary
|
|
121
|
+
# entity, or a unique test (someone claimed uniqueness). A bare
|
|
122
|
+
# *_id column is as often a foreign key, and a column with an
|
|
123
|
+
# accepted_values test is not a key — stay silent there.
|
|
124
|
+
key_cols = []
|
|
125
|
+
for c in cols:
|
|
126
|
+
if not isinstance(c, dict):
|
|
127
|
+
continue
|
|
128
|
+
entity = c.get("entity") or {}
|
|
129
|
+
if entity.get("type") == "primary" or "unique" in _tests_of(c):
|
|
130
|
+
key_cols.append(c)
|
|
131
|
+
if not key_cols:
|
|
132
|
+
continue # no declared key candidate: cannot infer, stay silent
|
|
133
|
+
col = key_cols[0]
|
|
134
|
+
tests = _tests_of(col)
|
|
135
|
+
combo = any("unique_combination_of_columns" in t
|
|
136
|
+
for t in _tests_of(model))
|
|
137
|
+
if combo or {"unique", "not_null"} <= tests:
|
|
138
|
+
continue
|
|
139
|
+
out.append(_finding(
|
|
140
|
+
"dq/uniqueness-key-no-duplicates", registry, settings, mode,
|
|
141
|
+
path, line_of(str(col.get("name"))),
|
|
142
|
+
f"model {name}: key column {col.get('name')} lacks a "
|
|
143
|
+
"unique + not_null test pair, so duplicate or null keys "
|
|
144
|
+
"pass silently (dq/uniqueness-key-no-duplicates)",
|
|
145
|
+
f"add unique and not_null tests on {col.get('name')}, or a "
|
|
146
|
+
"unique_combination_of_columns test on the model"))
|
|
147
|
+
for source in doc.get("sources") or []:
|
|
148
|
+
if not isinstance(source, dict):
|
|
149
|
+
continue
|
|
150
|
+
for table in source.get("tables") or []:
|
|
151
|
+
if not isinstance(table, dict):
|
|
152
|
+
continue
|
|
153
|
+
freshness = source.get("freshness") or table.get("freshness")
|
|
154
|
+
loaded = (source.get("loaded_at_field")
|
|
155
|
+
or table.get("loaded_at_field"))
|
|
156
|
+
# Fire only on intent: the source tracks its load time but
|
|
157
|
+
# never checks it. A source with no loaded_at_field at all
|
|
158
|
+
# has not opted into freshness — stay silent.
|
|
159
|
+
if freshness or not loaded:
|
|
160
|
+
continue
|
|
161
|
+
out.append(_finding(
|
|
162
|
+
"ingest/declare-source-freshness", registry, settings,
|
|
163
|
+
mode, path, line_of(str(table.get("name", ""))),
|
|
164
|
+
f"source table {table.get('name')} tracks loaded_at_field "
|
|
165
|
+
"but declares no freshness check, so a stalled source is "
|
|
166
|
+
"invisible (ingest/declare-source-freshness)",
|
|
167
|
+
"add freshness: {warn_after/error_after} to the source"))
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_python(text, path, mode="build", registry=None, settings=None):
|
|
172
|
+
"""orch/explicit-catchup-policy: Airflow DAG without catchup."""
|
|
173
|
+
out = []
|
|
174
|
+
try:
|
|
175
|
+
tree = ast.parse(text)
|
|
176
|
+
except SyntaxError:
|
|
177
|
+
return out
|
|
178
|
+
for node in ast.walk(tree):
|
|
179
|
+
if not isinstance(node, ast.Call):
|
|
180
|
+
continue
|
|
181
|
+
func = node.func
|
|
182
|
+
name = (func.id if isinstance(func, ast.Name)
|
|
183
|
+
else func.attr if isinstance(func, ast.Attribute) else "")
|
|
184
|
+
if name not in ("DAG", "dag"): # DAG(...), models.DAG, @dag(...)
|
|
185
|
+
continue
|
|
186
|
+
kws = {k.arg for k in node.keywords if k.arg}
|
|
187
|
+
if "catchup" in kws:
|
|
188
|
+
continue
|
|
189
|
+
out.append(_finding(
|
|
190
|
+
"orch/explicit-catchup-policy", registry, settings, mode,
|
|
191
|
+
path, node.lineno,
|
|
192
|
+
"Airflow DAG declares no explicit catchup; the default "
|
|
193
|
+
"backfills every missed interval on first deploy "
|
|
194
|
+
"(orch/explicit-catchup-policy)",
|
|
195
|
+
"set catchup=False (or True deliberately) on the DAG"))
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def check_aux_file(text, path, mode="build", registry=None, settings=None):
|
|
200
|
+
"""Dispatch by file kind. Returns finding dicts (engine shape)."""
|
|
201
|
+
if path.endswith(".py"):
|
|
202
|
+
return check_python(text, path, mode, registry, settings)
|
|
203
|
+
if path.endswith((".yml", ".yaml")):
|
|
204
|
+
return check_schema_yaml(text, path, mode, registry, settings)
|
|
205
|
+
return []
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def check_model_sql(text, path, mode="build", registry=None, settings=None):
|
|
209
|
+
if path.endswith(".sql"):
|
|
210
|
+
return check_dbt_model(text, path, mode, registry, settings)
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# slug -> implementing function, for the standard<->checks CI test.
|
|
215
|
+
PREVIEW_CHECKS = {
|
|
216
|
+
"model/unique-key-merge-key": check_dbt_model,
|
|
217
|
+
"dq/uniqueness-key-no-duplicates": check_schema_yaml,
|
|
218
|
+
"ingest/declare-source-freshness": check_schema_yaml,
|
|
219
|
+
"orch/explicit-catchup-policy": check_python,
|
|
220
|
+
}
|