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/engine.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Deterministic offline engine facade.
|
|
2
|
+
|
|
3
|
+
One code path for the CLI and the hook: discover files, resolve
|
|
4
|
+
mode + dialect per file, run the registry-driven analyzer, return
|
|
5
|
+
results in a stable order. Zero network, zero LLM in this path.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import fnmatch
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from truequery import __version__
|
|
18
|
+
from truequery import project as P
|
|
19
|
+
from truequery import settings as S
|
|
20
|
+
from truequery import std_registry
|
|
21
|
+
from truequery.analyzers import project_checks
|
|
22
|
+
from truequery.analyzers import sql as analyzer
|
|
23
|
+
from truequery.models import Diagnostic, Finding
|
|
24
|
+
|
|
25
|
+
ENGINE_VERSION = __version__
|
|
26
|
+
|
|
27
|
+
IGNORES_NAME = "ignores.json"
|
|
28
|
+
|
|
29
|
+
MAX_FILE_BYTES = 10 * 1024 * 1024 # ~10 MB; bigger files are not checked.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def active_overrides(config: dict | None) -> dict:
|
|
33
|
+
"""tier_overrides from DATA.md, restricted to known slugs and tiers.
|
|
34
|
+
|
|
35
|
+
Dogfooding only: init never writes this, defaults never ship it.
|
|
36
|
+
Unknown slugs/tiers are dropped (typo safety)."""
|
|
37
|
+
raw = (config or {}).get("tier_overrides") or {}
|
|
38
|
+
if not isinstance(raw, dict):
|
|
39
|
+
return {}
|
|
40
|
+
return {slug: tier for slug, tier in raw.items()
|
|
41
|
+
if slug in std_registry.RECORDS
|
|
42
|
+
and tier in ("immediate", "stop")}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class FileResult:
|
|
47
|
+
path: str
|
|
48
|
+
rel: str
|
|
49
|
+
mode: str
|
|
50
|
+
findings: list = field(default_factory=list)
|
|
51
|
+
diagnostics: list = field(default_factory=list)
|
|
52
|
+
suppressed: list = field(default_factory=list)
|
|
53
|
+
key_lookups: list = field(default_factory=list)
|
|
54
|
+
# "sql"; "project" for .py/.yml/.yaml files; "macro" for a file of
|
|
55
|
+
# dbt macro definitions, which is listed as not checked.
|
|
56
|
+
kind: str = "sql"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def verified(self) -> bool:
|
|
60
|
+
return not self.diagnostics
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_ignores(root: str) -> list[dict]:
|
|
64
|
+
"""Entries from .truequery/ignores.json: {rule, path, line?, reason?}.
|
|
65
|
+
|
|
66
|
+
Corrupt or absent files mean no ignores (the `ignore add` verb
|
|
67
|
+
validates on write; the hook never breaks on a bad file).
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
with open(os.path.join(root, ".truequery", IGNORES_NAME),
|
|
71
|
+
encoding="utf-8") as handle:
|
|
72
|
+
data = json.load(handle)
|
|
73
|
+
except (OSError, ValueError):
|
|
74
|
+
return []
|
|
75
|
+
entries = data.get("ignores", []) if isinstance(data, dict) else []
|
|
76
|
+
out = []
|
|
77
|
+
for entry in entries:
|
|
78
|
+
if not isinstance(entry, dict):
|
|
79
|
+
continue
|
|
80
|
+
rule, path = entry.get("rule"), entry.get("path")
|
|
81
|
+
if not isinstance(rule, str) or not isinstance(path, str):
|
|
82
|
+
continue
|
|
83
|
+
line = entry.get("line")
|
|
84
|
+
stmt = entry.get("stmt")
|
|
85
|
+
offset = entry.get("offset")
|
|
86
|
+
out.append({"rule": rule, "path": path,
|
|
87
|
+
"line": line if isinstance(line, int) else None,
|
|
88
|
+
"stmt": stmt if isinstance(stmt, str) else None,
|
|
89
|
+
"offset": offset if isinstance(offset, int) else None,
|
|
90
|
+
"reason": entry.get("reason") or "",
|
|
91
|
+
"broad": not isinstance(line, int) and not isinstance(
|
|
92
|
+
stmt, str)})
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _suppressed_by(finding, rel: str, ignores: list[dict],
|
|
97
|
+
spans=()) -> dict | None:
|
|
98
|
+
"""Match by rule + file + normalized-statement hash + the line's
|
|
99
|
+
offset inside that statement: an ignore covers one line, and can't
|
|
100
|
+
drift onto another statement when lines shift. Entries without a
|
|
101
|
+
hash fall back to rule + file + line."""
|
|
102
|
+
for entry in ignores:
|
|
103
|
+
if not fnmatch.fnmatchcase(finding.rule, entry["rule"]):
|
|
104
|
+
continue
|
|
105
|
+
if not fnmatch.fnmatchcase(rel, entry["path"]):
|
|
106
|
+
continue
|
|
107
|
+
if entry["stmt"] is not None:
|
|
108
|
+
if getattr(finding, "_stmt", None) != entry["stmt"]:
|
|
109
|
+
continue
|
|
110
|
+
if (entry["offset"] is not None
|
|
111
|
+
and stmt_offset(spans, finding.line) != entry["offset"]):
|
|
112
|
+
continue
|
|
113
|
+
elif entry["line"] is not None and finding.line != entry["line"]:
|
|
114
|
+
continue
|
|
115
|
+
return entry
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _chunk_hashes(text: str, dialect: str | None) -> list[tuple]:
|
|
120
|
+
"""(start_line, end_line, sha1-12) per statement chunk, on the same
|
|
121
|
+
cleaning the analyzer uses, so ignore entries and git baselines
|
|
122
|
+
identify statements independent of line shifts."""
|
|
123
|
+
cleaned = analyzer.strip_comments(analyzer.strip_jinja(text),
|
|
124
|
+
dialect or "generic")
|
|
125
|
+
chunks = analyzer.split_statements(cleaned)
|
|
126
|
+
spans = []
|
|
127
|
+
for chunk, start in chunks:
|
|
128
|
+
# First and last content lines of the statement: the
|
|
129
|
+
# splitter's start trails the previous `;`, so blank/comment
|
|
130
|
+
# gap lines between statements belong to no statement.
|
|
131
|
+
rows = chunk.split("\n")
|
|
132
|
+
content = [i for i, row in enumerate(rows) if row.strip()]
|
|
133
|
+
if not content:
|
|
134
|
+
continue
|
|
135
|
+
first, last = start + content[0], start + content[-1]
|
|
136
|
+
norm = re.sub(r"\s+", " ", chunk).strip().rstrip(";")
|
|
137
|
+
digest = hashlib.sha1(norm.encode("utf-8")).hexdigest()[:12]
|
|
138
|
+
spans.append((first, last, digest))
|
|
139
|
+
return spans
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def stmt_hash_at(spans: list[tuple], line: int) -> str | None:
|
|
143
|
+
for start, end, digest in spans:
|
|
144
|
+
if start <= line <= end:
|
|
145
|
+
return digest
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def stmt_offset(spans: list[tuple], line: int) -> int | None:
|
|
150
|
+
"""line's offset from the first line of its statement."""
|
|
151
|
+
for start, end, _ in spans:
|
|
152
|
+
if start <= line <= end:
|
|
153
|
+
return line - start
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def discover_aux(paths: list[str]) -> list[str]:
|
|
158
|
+
"""Non-SQL files the preview project checks read (.py, .yml/.yaml)."""
|
|
159
|
+
out: list[str] = []
|
|
160
|
+
for path in paths:
|
|
161
|
+
if os.path.isdir(path):
|
|
162
|
+
for dirpath, dirnames, filenames in os.walk(path):
|
|
163
|
+
dirnames[:] = sorted(d for d in dirnames
|
|
164
|
+
if d not in P.SKIP_DIRS
|
|
165
|
+
and not d.startswith("."))
|
|
166
|
+
for name in sorted(filenames):
|
|
167
|
+
if name.endswith((".py", ".yml", ".yaml")):
|
|
168
|
+
out.append(os.path.join(dirpath, name))
|
|
169
|
+
elif os.path.isfile(path) and path.endswith((".py", ".yml", ".yaml")):
|
|
170
|
+
out.append(os.path.abspath(path))
|
|
171
|
+
return sorted(set(out))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _aux_findings(text, path, mode, tiers, settings):
|
|
175
|
+
"""Preview non-SQL checks, honoring enabled/tier like SQL checks."""
|
|
176
|
+
raw = (project_checks.check_model_sql(text, path, mode, std_registry,
|
|
177
|
+
settings)
|
|
178
|
+
+ project_checks.check_aux_file(text, path, mode, std_registry,
|
|
179
|
+
settings))
|
|
180
|
+
kept = []
|
|
181
|
+
for f in raw:
|
|
182
|
+
record = std_registry.RECORDS.get(f["rule"])
|
|
183
|
+
if record is None:
|
|
184
|
+
continue
|
|
185
|
+
setting = (settings or {}).get(f["rule"], {})
|
|
186
|
+
enabled = setting.get("enabled", record["enabled"])
|
|
187
|
+
tier = setting.get("tier", record["tier"])
|
|
188
|
+
if not enabled:
|
|
189
|
+
continue
|
|
190
|
+
if tiers is not None and tier not in tiers:
|
|
191
|
+
continue
|
|
192
|
+
kept.append(Finding(rule=f["rule"], severity=f["severity"],
|
|
193
|
+
mode=f["mode"], evidence=f["evidence"],
|
|
194
|
+
file=f["file"], line=f["line"], col=f["col"],
|
|
195
|
+
snippet=f["snippet"], why=f["why"],
|
|
196
|
+
fix=f["fix"]))
|
|
197
|
+
kept.sort(key=Finding.sort_key)
|
|
198
|
+
return kept
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def check_file(path: str, dialect: str | None = None,
|
|
202
|
+
mode: str = "build", tiers=None,
|
|
203
|
+
settings=None, text: str | None = None
|
|
204
|
+
) -> tuple[list[Finding], list[Diagnostic], list[str]]:
|
|
205
|
+
"""Check one file from disk. The CLI and hook use run()."""
|
|
206
|
+
if text is None:
|
|
207
|
+
with open(path, encoding="utf-8") as handle:
|
|
208
|
+
text = handle.read()
|
|
209
|
+
res = analyzer.check_text(text, dialect=dialect, path=path, mode=mode,
|
|
210
|
+
tiers=tiers,
|
|
211
|
+
settings=settings)
|
|
212
|
+
findings = [Finding(rule=f["rule"], severity=f["severity"],
|
|
213
|
+
mode=f["mode"], evidence=f["evidence"],
|
|
214
|
+
file=f["file"], line=f["line"], col=f["col"],
|
|
215
|
+
snippet=f["snippet"], why=f["why"], fix=f["fix"])
|
|
216
|
+
for f in res["findings"]]
|
|
217
|
+
diagnostics = [Diagnostic(file=path, message=d["message"],
|
|
218
|
+
detail=d.get("detail", ""))
|
|
219
|
+
for d in res["diagnostics"]]
|
|
220
|
+
findings.sort(key=Finding.sort_key)
|
|
221
|
+
diagnostics.sort(key=Diagnostic.sort_key)
|
|
222
|
+
return findings, diagnostics, sorted(set(res.get("key_lookups", [])))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
NOT_UTF8 = "not UTF-8 text (not checked)"
|
|
226
|
+
|
|
227
|
+
# dbt definition blocks: templates that render into other files' SQL.
|
|
228
|
+
_DEFINITION_BLOCK = re.compile(
|
|
229
|
+
r"{%-?\s*(macro|test|materialization|docs)\b.*?{%-?\s*end\1\s*-?%}",
|
|
230
|
+
re.DOTALL)
|
|
231
|
+
_JINJA_COMMENT = re.compile(r"{#.*?#}", re.DOTALL)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def is_macro_file(text: str) -> bool:
|
|
235
|
+
"""True when a file holds only dbt macro/test/materialization/docs
|
|
236
|
+
definitions (plus comments): templates, not queries. A file that also
|
|
237
|
+
has a query outside those blocks is checked as usual."""
|
|
238
|
+
if "{%" not in text:
|
|
239
|
+
return False
|
|
240
|
+
rest, blocks = _DEFINITION_BLOCK.subn("", _JINJA_COMMENT.sub("", text))
|
|
241
|
+
if not blocks:
|
|
242
|
+
return False
|
|
243
|
+
return not analyzer.strip_comments(rest, "generic").strip()
|
|
244
|
+
|
|
245
|
+
TOO_LARGE = "file over the 10 MB size limit (not checked)"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _read(path: str) -> str | None:
|
|
249
|
+
"""File text; None when unreadable. NOT_UTF8 and TOO_LARGE are
|
|
250
|
+
sentinels handled by run(): the file is reported as not checked."""
|
|
251
|
+
try:
|
|
252
|
+
if os.path.getsize(path) > MAX_FILE_BYTES:
|
|
253
|
+
return TOO_LARGE
|
|
254
|
+
with open(path, encoding="utf-8") as handle:
|
|
255
|
+
return handle.read()
|
|
256
|
+
except UnicodeDecodeError:
|
|
257
|
+
return NOT_UTF8
|
|
258
|
+
except OSError:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _apply_ignores(result: FileResult, findings: list, spans, ignores) -> None:
|
|
263
|
+
"""Tag each finding with its statement hash, then split kept from
|
|
264
|
+
suppressed by .truequery/ignores.json."""
|
|
265
|
+
for finding in findings:
|
|
266
|
+
finding.file = result.rel
|
|
267
|
+
finding._stmt = stmt_hash_at(spans, finding.line)
|
|
268
|
+
entry = _suppressed_by(finding, result.rel, ignores, spans)
|
|
269
|
+
if entry is None:
|
|
270
|
+
result.findings.append(finding)
|
|
271
|
+
else:
|
|
272
|
+
result.suppressed.append({"rule": finding.rule,
|
|
273
|
+
"line": finding.line,
|
|
274
|
+
"broad": entry["broad"],
|
|
275
|
+
"reason": entry["reason"]})
|
|
276
|
+
result.findings.sort(key=Finding.sort_key)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def run(paths: list[str], root: str, dialect: str | None = None,
|
|
280
|
+
mode: str | None = None, tiers=None, settings=None,
|
|
281
|
+
config: dict | None = None) -> list[FileResult]:
|
|
282
|
+
"""Run enabled checks over discovered files. Deterministic order.
|
|
283
|
+
|
|
284
|
+
SQL files get the SQL checks plus the preview dbt-model check;
|
|
285
|
+
.py/.yml/.yaml files under the given paths get the preview project
|
|
286
|
+
checks. ``config`` is the parsed DATA.md tq-config (read from root
|
|
287
|
+
when None). ``settings`` is an Effective rules map; without
|
|
288
|
+
one it is resolved from that config plus the user's config.
|
|
289
|
+
|
|
290
|
+
Applies .truequery/ignores.json to every finding; suppressed ones
|
|
291
|
+
land on FileResult.suppressed (with a broad/narrow flag) so
|
|
292
|
+
reporters can say what an ignore hid.
|
|
293
|
+
"""
|
|
294
|
+
sql_files = P.discover(paths)
|
|
295
|
+
if config is None:
|
|
296
|
+
config = P.load_config(root)
|
|
297
|
+
base_dialect = dialect or (config or {}).get("dialect")
|
|
298
|
+
if settings is None:
|
|
299
|
+
settings = S.effective(project=config).check_map()
|
|
300
|
+
ignores = load_ignores(root)
|
|
301
|
+
results: list[FileResult] = []
|
|
302
|
+
for path in sql_files:
|
|
303
|
+
text = _read(path)
|
|
304
|
+
if text is None:
|
|
305
|
+
continue
|
|
306
|
+
rel = P._rel(path, root)
|
|
307
|
+
file_mode = P.resolve_mode(rel, config, mode)
|
|
308
|
+
if text is NOT_UTF8 or text is TOO_LARGE:
|
|
309
|
+
# Never silent: an undecodable or oversize file is listed
|
|
310
|
+
# as not checked, with the reason.
|
|
311
|
+
results.append(FileResult(path=path, rel=rel, mode=file_mode,
|
|
312
|
+
diagnostics=[Diagnostic(rel, text)]))
|
|
313
|
+
continue
|
|
314
|
+
if is_macro_file(text):
|
|
315
|
+
# Macro definitions aren't queries: listed as not checked
|
|
316
|
+
# (kind "macro"), never as a parse failure, never as clean.
|
|
317
|
+
results.append(FileResult(path=path, rel=rel, mode=file_mode,
|
|
318
|
+
kind="macro"))
|
|
319
|
+
continue
|
|
320
|
+
findings, diagnostics, key_lookups = check_file(
|
|
321
|
+
path, dialect=base_dialect, mode=file_mode, tiers=tiers,
|
|
322
|
+
settings=settings, text=text)
|
|
323
|
+
# Preview dbt-model check (incremental without unique_key).
|
|
324
|
+
findings += _aux_findings(text, path, file_mode, tiers, settings)
|
|
325
|
+
for diagnostic in diagnostics:
|
|
326
|
+
diagnostic.file = rel
|
|
327
|
+
result = FileResult(path=path, rel=rel, mode=file_mode,
|
|
328
|
+
diagnostics=diagnostics, key_lookups=key_lookups)
|
|
329
|
+
_apply_ignores(result, findings, _chunk_hashes(text, base_dialect),
|
|
330
|
+
ignores)
|
|
331
|
+
results.append(result)
|
|
332
|
+
for path in discover_aux(paths):
|
|
333
|
+
text = _read(path)
|
|
334
|
+
if text is None or text is NOT_UTF8 or text is TOO_LARGE:
|
|
335
|
+
continue
|
|
336
|
+
rel = P._rel(path, root)
|
|
337
|
+
file_mode = P.resolve_mode(rel, config, mode)
|
|
338
|
+
result = FileResult(path=path, rel=rel, mode=file_mode,
|
|
339
|
+
kind="project")
|
|
340
|
+
# Project files have no statements: ignores match on rule + file
|
|
341
|
+
# + line.
|
|
342
|
+
_apply_ignores(result, _aux_findings(text, path, file_mode, tiers,
|
|
343
|
+
settings), [], ignores)
|
|
344
|
+
results.append(result)
|
|
345
|
+
results.sort(key=lambda r: r.rel)
|
|
346
|
+
return results
|