sqlassert-dbt 0.3.1__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.
Files changed (38) hide show
  1. sqlassert_dbt-0.3.1/LICENSE +21 -0
  2. sqlassert_dbt-0.3.1/PKG-INFO +153 -0
  3. sqlassert_dbt-0.3.1/README.md +129 -0
  4. sqlassert_dbt-0.3.1/pyproject.toml +58 -0
  5. sqlassert_dbt-0.3.1/setup.cfg +4 -0
  6. sqlassert_dbt-0.3.1/sqlassert_dbt/__init__.py +19 -0
  7. sqlassert_dbt-0.3.1/sqlassert_dbt/__main__.py +115 -0
  8. sqlassert_dbt-0.3.1/sqlassert_dbt/expectations.py +336 -0
  9. sqlassert_dbt-0.3.1/sqlassert_dbt/jinja.py +183 -0
  10. sqlassert_dbt-0.3.1/sqlassert_dbt/manifest.py +254 -0
  11. sqlassert_dbt-0.3.1/sqlassert_dbt/program.py +686 -0
  12. sqlassert_dbt-0.3.1/sqlassert_dbt/project.py +340 -0
  13. sqlassert_dbt-0.3.1/sqlassert_dbt/report.py +158 -0
  14. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/PKG-INFO +153 -0
  15. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/SOURCES.txt +36 -0
  16. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/dependency_links.txt +1 -0
  17. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/entry_points.txt +2 -0
  18. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/requires.txt +7 -0
  19. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/scm_file_list.json +32 -0
  20. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/scm_version.json +8 -0
  21. sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/top_level.txt +1 -0
  22. sqlassert_dbt-0.3.1/tests/conftest.py +16 -0
  23. sqlassert_dbt-0.3.1/tests/dbt_project/dbt_project.yml +5 -0
  24. sqlassert_dbt-0.3.1/tests/dbt_project/models/active_customers.sql +4 -0
  25. sqlassert_dbt-0.3.1/tests/dbt_project/models/customer_keys.sql +4 -0
  26. sqlassert_dbt-0.3.1/tests/dbt_project/models/customer_orders.sql +6 -0
  27. sqlassert_dbt-0.3.1/tests/dbt_project/models/customer_signups.sql +2 -0
  28. sqlassert_dbt-0.3.1/tests/dbt_project/models/customers.sql +12 -0
  29. sqlassert_dbt-0.3.1/tests/dbt_project/models/orders_appended.sql +7 -0
  30. sqlassert_dbt-0.3.1/tests/dbt_project/models/orders_incremental.sql +8 -0
  31. sqlassert_dbt-0.3.1/tests/dbt_project/models/schema.yml +85 -0
  32. sqlassert_dbt-0.3.1/tests/dbt_project/models/stg_customers.sql +3 -0
  33. sqlassert_dbt-0.3.1/tests/dbt_project/models/stg_orders.sql +2 -0
  34. sqlassert_dbt-0.3.1/tests/dbt_project/profiles.yml +7 -0
  35. sqlassert_dbt-0.3.1/tests/dbt_project/seeds/raw_payments.csv +2 -0
  36. sqlassert_dbt-0.3.1/tests/dbt_runs.py +26 -0
  37. sqlassert_dbt-0.3.1/tests/test_contracts.py +56 -0
  38. sqlassert_dbt-0.3.1/tests/test_dbt.py +561 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fred Israel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlassert-dbt
3
+ Version: 0.3.1
4
+ Summary: Check a dbt project's tests and sqlassert markers from its compiled manifest.
5
+ Author: Fred Israel
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/polvoazul/sqlassert/tree/main/integrations/dbt
8
+ Project-URL: Issues, https://github.com/polvoazul/sqlassert/issues
9
+ Keywords: dbt,sql,static-analysis,data-quality,sqlassert
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Topic :: Database
13
+ Classifier: Topic :: Software Development :: Quality Assurance
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: sqlassert
18
+ Requires-Dist: sqlglot<31,>=30.30
19
+ Provides-Extra: test
20
+ Requires-Dist: dbt-core>=1.12; extra == "test"
21
+ Requires-Dist: dbt-duckdb>=1.11; extra == "test"
22
+ Requires-Dist: pytest; extra == "test"
23
+ Dynamic: license-file
24
+
25
+ # sqlassert-dbt
26
+
27
+ Prove your dbt tests from the SQL, and skip the ones that can't fail.
28
+
29
+ A `unique` or `not_null` test scans a table in the warehouse on every run, and
30
+ it only tells you about today's data. Many of those tests follow from the SQL
31
+ itself: `customer_id` is unique because the model groups by it, `status` is
32
+ never null because of a `coalesce`. [sqlassert](https://github.com/polvoazul/sqlassert)
33
+ proves them from the compiled model before anything runs, so dbt can leave them
34
+ out: faster CI, a smaller warehouse bill, and the same guarantee for *any*
35
+ data, not just the rows there today.
36
+
37
+ ```bash
38
+ pip install sqlassert-dbt
39
+ ```
40
+
41
+ _Alpha: the markers and dbt test mappings are stable; the CLI output may
42
+ change._
43
+
44
+ ## Skip the tests the SQL proves
45
+
46
+ Run dbt through the plugin:
47
+
48
+ ```bash
49
+ sqlassert-dbt build . -- --target prod # or: sqlassert-dbt test
50
+ ```
51
+
52
+ It runs `dbt compile`, checks every model, then runs `dbt build` (or
53
+ `dbt test`) with each proved test passed as `--exclude`. Arguments after `--`
54
+ go to dbt unchanged. The exit code is dbt's when dbt fails, otherwise the
55
+ check's.
56
+
57
+ The tests of the sources, seeds and models a model reads are trusted as
58
+ facts, since `dbt build` runs them first in the same invocation. If the
59
+ arguments after `--` select nodes (`--select`, `--exclude`, `--selector`, ...),
60
+ an upstream test may not run, so none is trusted.
61
+
62
+ ## Check without dbt
63
+
64
+ `check` reads only `target/manifest.json`, so dbt does not need to be installed
65
+ where it runs (a CI step after `dbt compile`, say):
66
+
67
+ ```bash
68
+ $ dbt compile
69
+ $ sqlassert-dbt check . --trust-upstream-tests
70
+ models/customers.sql:12: UNKNOWN /**ASSERT NOT NULL(first_name)**/ (customers)
71
+
72
+ Proved statically (a test here can be skipped):
73
+ models/schema.yml unique_stg_customers_customer_id (test on stg_customers)
74
+
75
+ Not proved (the warehouse run still checks these):
76
+ models/schema.yml accepted_values_stg_orders_status__placed__shipped__returned (test on stg_orders): its Property is not proved from the SQL
77
+
78
+ Proved but untested (schema.yml additions):
79
+ models/schema.yml customers.customer_id: not_null
80
+ ```
81
+
82
+ Three lists come out:
83
+
84
+ - **Proved statically:** tests the SQL already guarantees. Skip them.
85
+ - **Not proved:** tests that still need the warehouse. Nothing is wrong; the
86
+ SQL alone can't show the property.
87
+ - **Proved but untested:** properties sqlassert proved that no test states.
88
+ Free `schema.yml` additions, which cost nothing to keep.
89
+
90
+ | Option | |
91
+ |---|---|
92
+ | `--trust-upstream-tests` | Treat passing tests of upstream nodes as facts (true under `dbt build`). Without it, a `dbt-untrusted-upstream-tests` warning lists them. |
93
+ | `--require-proved` | Fail when any test is not proved, not only when a marker is `UNKNOWN`. |
94
+ | `--explain` | Print each proof, and each failed proof. |
95
+ | `--dialect NAME` | Analyze under this dialect instead of the adapter's. |
96
+ | `--manifest PATH` | Read another manifest (default `PROJECT/target/manifest.json`). |
97
+
98
+ Exit codes: `0` every marker proved; `1` a marker is `UNKNOWN` or an error was
99
+ reported (unproved tests fail it only with `--require-proved`); `2` the
100
+ manifest cannot be read.
101
+
102
+ ## What it understands
103
+
104
+ | dbt | sqlassert Property |
105
+ |---|---|
106
+ | `unique` | Unique Set |
107
+ | `not_null` | Not Null |
108
+ | `accepted_values` | Enum |
109
+ | `relationships` | Referential Coverage (Foreign Key) |
110
+ | `dbt_utils.unique_combination_of_columns` | Non-Null Unique Set (it counts NULL equal to NULL) |
111
+ | contract `primary_key`, `unique`, `not_null`, `foreign_key` | the same Properties |
112
+
113
+ A model's own tests are what gets proved. Upstream tests are evidence, used
114
+ only when they fail on any failing row: not a test with `severity: warn`, a
115
+ `where:` config, or `error_if`/`fail_calc` thresholds. A constraint counts only
116
+ on a contracted table whose adapter enforces it.
117
+
118
+ ## Markers in models
119
+
120
+ You can also write sqlassert markers straight into a model, and they are checked
121
+ and reported at their line in the model file:
122
+
123
+ ```sql
124
+ -- models/customer_orders.sql
125
+ select c.customer_id, c.first_name, o.order_id
126
+ from {{ ref('stg_orders') }} o
127
+ /**ASSERT MANY TO ONE**/ join {{ ref('stg_customers') }} c
128
+ on o.customer_id = c.customer_id
129
+ ```
130
+
131
+ If someone later loads duplicate customers upstream, or drops the `unique`
132
+ test that made this provable, the marker turns `UNKNOWN` and CI fails before
133
+ the join fans out in production. See the
134
+ [sqlassert README](https://github.com/polvoazul/sqlassert#readme) for every
135
+ marker.
136
+
137
+ ## Details
138
+
139
+ - **Manifest:** only the latest schema (v12, written by dbt 1.8 to 1.12).
140
+ - **Ephemeral models** are analyzed where dbt inlined them.
141
+ - **Incremental models** are analyzed in both `is_incremental()` branches. A
142
+ test holds only if it holds in both, and a key only under a `merge` or
143
+ `delete+insert` strategy on that `unique_key` with no
144
+ `incremental_predicates`. The other branch is rendered without dbt only when
145
+ the model uses `ref`, `source`, `this`, `config` and `is_incremental()`
146
+ alone; otherwise it is reported unanalyzed.
147
+ - **Adapters:** analyzed when sqlassert models their dialect (DuckDB,
148
+ PostgreSQL, MySQL, Snowflake, BigQuery, Databricks / Spark, Redshift, Trino).
149
+ - A model with markers but no compiled code is an error, not skipped.
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,129 @@
1
+ # sqlassert-dbt
2
+
3
+ Prove your dbt tests from the SQL, and skip the ones that can't fail.
4
+
5
+ A `unique` or `not_null` test scans a table in the warehouse on every run, and
6
+ it only tells you about today's data. Many of those tests follow from the SQL
7
+ itself: `customer_id` is unique because the model groups by it, `status` is
8
+ never null because of a `coalesce`. [sqlassert](https://github.com/polvoazul/sqlassert)
9
+ proves them from the compiled model before anything runs, so dbt can leave them
10
+ out: faster CI, a smaller warehouse bill, and the same guarantee for *any*
11
+ data, not just the rows there today.
12
+
13
+ ```bash
14
+ pip install sqlassert-dbt
15
+ ```
16
+
17
+ _Alpha: the markers and dbt test mappings are stable; the CLI output may
18
+ change._
19
+
20
+ ## Skip the tests the SQL proves
21
+
22
+ Run dbt through the plugin:
23
+
24
+ ```bash
25
+ sqlassert-dbt build . -- --target prod # or: sqlassert-dbt test
26
+ ```
27
+
28
+ It runs `dbt compile`, checks every model, then runs `dbt build` (or
29
+ `dbt test`) with each proved test passed as `--exclude`. Arguments after `--`
30
+ go to dbt unchanged. The exit code is dbt's when dbt fails, otherwise the
31
+ check's.
32
+
33
+ The tests of the sources, seeds and models a model reads are trusted as
34
+ facts, since `dbt build` runs them first in the same invocation. If the
35
+ arguments after `--` select nodes (`--select`, `--exclude`, `--selector`, ...),
36
+ an upstream test may not run, so none is trusted.
37
+
38
+ ## Check without dbt
39
+
40
+ `check` reads only `target/manifest.json`, so dbt does not need to be installed
41
+ where it runs (a CI step after `dbt compile`, say):
42
+
43
+ ```bash
44
+ $ dbt compile
45
+ $ sqlassert-dbt check . --trust-upstream-tests
46
+ models/customers.sql:12: UNKNOWN /**ASSERT NOT NULL(first_name)**/ (customers)
47
+
48
+ Proved statically (a test here can be skipped):
49
+ models/schema.yml unique_stg_customers_customer_id (test on stg_customers)
50
+
51
+ Not proved (the warehouse run still checks these):
52
+ models/schema.yml accepted_values_stg_orders_status__placed__shipped__returned (test on stg_orders): its Property is not proved from the SQL
53
+
54
+ Proved but untested (schema.yml additions):
55
+ models/schema.yml customers.customer_id: not_null
56
+ ```
57
+
58
+ Three lists come out:
59
+
60
+ - **Proved statically:** tests the SQL already guarantees. Skip them.
61
+ - **Not proved:** tests that still need the warehouse. Nothing is wrong; the
62
+ SQL alone can't show the property.
63
+ - **Proved but untested:** properties sqlassert proved that no test states.
64
+ Free `schema.yml` additions, which cost nothing to keep.
65
+
66
+ | Option | |
67
+ |---|---|
68
+ | `--trust-upstream-tests` | Treat passing tests of upstream nodes as facts (true under `dbt build`). Without it, a `dbt-untrusted-upstream-tests` warning lists them. |
69
+ | `--require-proved` | Fail when any test is not proved, not only when a marker is `UNKNOWN`. |
70
+ | `--explain` | Print each proof, and each failed proof. |
71
+ | `--dialect NAME` | Analyze under this dialect instead of the adapter's. |
72
+ | `--manifest PATH` | Read another manifest (default `PROJECT/target/manifest.json`). |
73
+
74
+ Exit codes: `0` every marker proved; `1` a marker is `UNKNOWN` or an error was
75
+ reported (unproved tests fail it only with `--require-proved`); `2` the
76
+ manifest cannot be read.
77
+
78
+ ## What it understands
79
+
80
+ | dbt | sqlassert Property |
81
+ |---|---|
82
+ | `unique` | Unique Set |
83
+ | `not_null` | Not Null |
84
+ | `accepted_values` | Enum |
85
+ | `relationships` | Referential Coverage (Foreign Key) |
86
+ | `dbt_utils.unique_combination_of_columns` | Non-Null Unique Set (it counts NULL equal to NULL) |
87
+ | contract `primary_key`, `unique`, `not_null`, `foreign_key` | the same Properties |
88
+
89
+ A model's own tests are what gets proved. Upstream tests are evidence, used
90
+ only when they fail on any failing row: not a test with `severity: warn`, a
91
+ `where:` config, or `error_if`/`fail_calc` thresholds. A constraint counts only
92
+ on a contracted table whose adapter enforces it.
93
+
94
+ ## Markers in models
95
+
96
+ You can also write sqlassert markers straight into a model, and they are checked
97
+ and reported at their line in the model file:
98
+
99
+ ```sql
100
+ -- models/customer_orders.sql
101
+ select c.customer_id, c.first_name, o.order_id
102
+ from {{ ref('stg_orders') }} o
103
+ /**ASSERT MANY TO ONE**/ join {{ ref('stg_customers') }} c
104
+ on o.customer_id = c.customer_id
105
+ ```
106
+
107
+ If someone later loads duplicate customers upstream, or drops the `unique`
108
+ test that made this provable, the marker turns `UNKNOWN` and CI fails before
109
+ the join fans out in production. See the
110
+ [sqlassert README](https://github.com/polvoazul/sqlassert#readme) for every
111
+ marker.
112
+
113
+ ## Details
114
+
115
+ - **Manifest:** only the latest schema (v12, written by dbt 1.8 to 1.12).
116
+ - **Ephemeral models** are analyzed where dbt inlined them.
117
+ - **Incremental models** are analyzed in both `is_incremental()` branches. A
118
+ test holds only if it holds in both, and a key only under a `merge` or
119
+ `delete+insert` strategy on that `unique_key` with no
120
+ `incremental_predicates`. The other branch is rendered without dbt only when
121
+ the model uses `ref`, `source`, `this`, `config` and `is_incremental()`
122
+ alone; otherwise it is reported unanalyzed.
123
+ - **Adapters:** analyzed when sqlassert models their dialect (DuckDB,
124
+ PostgreSQL, MySQL, Snowflake, BigQuery, Databricks / Spark, Redshift, Trino).
125
+ - A model with markers but no compiled code is an error, not skipped.
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "setuptools-scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sqlassert-dbt"
7
+ dynamic = ["version"]
8
+ description = "Check a dbt project's tests and sqlassert markers from its compiled manifest."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Fred Israel" }]
13
+ requires-python = ">=3.13"
14
+ keywords = ["dbt", "sql", "static-analysis", "data-quality", "sqlassert"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Topic :: Database",
19
+ "Topic :: Software Development :: Quality Assurance",
20
+ ]
21
+ # Reads target/manifest.json only: dbt itself is needed to compile the
22
+ # project, not to check it, so it is a test dependency here.
23
+ dependencies = [
24
+ "sqlassert",
25
+ "sqlglot>=30.30,<31",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/polvoazul/sqlassert/tree/main/integrations/dbt"
30
+ Issues = "https://github.com/polvoazul/sqlassert/issues"
31
+
32
+ [project.scripts]
33
+ sqlassert-dbt = "sqlassert_dbt.__main__:main"
34
+
35
+ [project.optional-dependencies]
36
+ test = [
37
+ "dbt-core>=1.12",
38
+ "dbt-duckdb>=1.11",
39
+ "pytest",
40
+ ]
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["sqlassert_dbt", "sqlassert_dbt.*"]
44
+
45
+ [tool.setuptools_scm]
46
+ root = "../.."
47
+ local_scheme = "no-local-version"
48
+
49
+ # In this repository the plugin builds and tests against the sibling sqlassert.
50
+ [tool.uv.sources]
51
+ sqlassert = { path = "../..", editable = true }
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ pythonpath = [".", "../.."]
56
+
57
+ [tool.ty.environment]
58
+ extra-paths = ["../.."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ """dbt integration: check a dbt project's tests and Property Markers from its manifest, without dbt.
2
+
3
+ `analyze_project(project_dir)` reads `target/manifest.json` and returns a
4
+ `DbtReport`: which tests the SQL proves (skippable), which it does not, which
5
+ proved Properties no test states, and the outcome of every marker written in
6
+ a model file, mapped back to that file's lines. `sqlassert-dbt` is its command
7
+ line (`__main__.py`).
8
+ """
9
+
10
+ from sqlassert_dbt.project import SUPPORTED_DIALECTS, ProjectError, analyze_project
11
+ from sqlassert_dbt.report import (
12
+ DbtReport, Location, MarkerOutcome, Problem, Status, TestOutcome, Untested,
13
+ exit_code, format_text,
14
+ )
15
+
16
+ __all__ = [
17
+ "SUPPORTED_DIALECTS", "DbtReport", "Location", "MarkerOutcome", "Problem",
18
+ "ProjectError", "Status", "TestOutcome", "Untested", "analyze_project", "exit_code", "format_text",
19
+ ]
@@ -0,0 +1,115 @@
1
+ """Command line.
2
+
3
+ `sqlassert-dbt check [PROJECT]` checks a compiled dbt project's tests and
4
+ markers from its `target/manifest.json`, with `sqlassert check`'s exit codes.
5
+
6
+ `sqlassert-dbt build|test [PROJECT] [-- DBT_ARGS]` runs dbt with the markers
7
+ checked too: `dbt compile`, then the check, then `dbt build` (or `dbt test`)
8
+ with every test the SQL proves excluded. dbt runs the tests of what a model
9
+ reads in the same invocation, so they are facts, unless DBT_ARGS select nodes
10
+ (a selection may leave an upstream test out).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from sqlassert.check import EXIT_FINDINGS, EXIT_OK, EXIT_USAGE
21
+ from sqlassert_dbt.project import ProjectError, analyze_project
22
+ from sqlassert_dbt.report import DbtReport, exit_code, format_text
23
+
24
+ # The dbt flags that narrow which nodes run: under any of them, the tests of
25
+ # what a model reads may not run, so they are no facts.
26
+ SELECTION_FLAGS = frozenset({"-s", "--select", "-m", "--models", "--model", "--exclude", "--selector", "--resource-type", "--resource-types"})
27
+
28
+
29
+ def run_dbt(arguments: list[str]) -> int:
30
+ """Run dbt from PATH with `arguments`; its exit status."""
31
+ try:
32
+ return subprocess.run(["dbt", *arguments], check=False).returncode
33
+ except FileNotFoundError:
34
+ print("sqlassert-dbt: dbt is not on PATH", file=sys.stderr)
35
+ return EXIT_USAGE
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ parser = argparse.ArgumentParser(prog="sqlassert-dbt")
40
+ commands = parser.add_subparsers(dest="command", required=True)
41
+ check = commands.add_parser(
42
+ "check",
43
+ help="check the tests and markers of a compiled project, without dbt",
44
+ description="Check a dbt project's tests and markers from its target/manifest.json (run `dbt compile` first). "
45
+ f"Exit status: {EXIT_OK} every marker PROVED, {EXIT_FINDINGS} a marker UNKNOWN or a diagnostic reported, "
46
+ f"{EXIT_USAGE} the manifest cannot be read.",
47
+ )
48
+ _common(check)
49
+ check.add_argument("--manifest", help="manifest.json to read (default: PROJECT/target/manifest.json)")
50
+ check.add_argument(
51
+ "--trust-upstream-tests", action="store_true",
52
+ help="take the passing tests of what each model reads as facts about it: true when they run first, as in `dbt build`",
53
+ )
54
+ for name, verb in (("build", "dbt build"), ("test", "dbt test")):
55
+ command = commands.add_parser(
56
+ name,
57
+ help=f"`dbt compile`, check the markers, then `{verb}` without the tests the SQL proves",
58
+ description=f"Run `dbt compile`, check the project, then `{verb}` excluding every test the SQL proves. "
59
+ f"Arguments after -- go to dbt. Exit status: dbt's when it fails, else {EXIT_FINDINGS} when a marker is "
60
+ f"UNKNOWN or a diagnostic is reported, else {EXIT_OK}.",
61
+ )
62
+ _common(command)
63
+ command.add_argument("dbt_arguments", nargs=argparse.REMAINDER, help="-- then arguments for dbt")
64
+ arguments = parser.parse_args(argv)
65
+ project = Path(arguments.project)
66
+
67
+ if arguments.command == "check":
68
+ report = _analyze(project, arguments.manifest, arguments.dialect, arguments.trust_upstream_tests)
69
+ if report is None:
70
+ return EXIT_USAGE
71
+ sys.stdout.write(format_text(report, explain=arguments.explain))
72
+ return exit_code(report, require_proved=arguments.require_proved)
73
+
74
+ passed = [argument for argument in arguments.dbt_arguments if argument != "--"]
75
+ located = [*passed, "--project-dir", str(project)]
76
+ compiled = run_dbt(["compile", *located])
77
+ if compiled != 0:
78
+ return compiled
79
+ selecting = any(argument.split("=")[0] in SELECTION_FLAGS for argument in passed)
80
+ report = _analyze(project, None, arguments.dialect, not selecting)
81
+ if report is None:
82
+ return EXIT_USAGE
83
+ sys.stdout.write(format_text(report, explain=arguments.explain))
84
+ proved = proved_tests(report)
85
+ ran = run_dbt([arguments.command, *located, *(["--exclude", *proved] if proved else [])])
86
+ return ran if ran != 0 else exit_code(report, require_proved=arguments.require_proved)
87
+
88
+
89
+ def _common(command: argparse.ArgumentParser) -> None:
90
+ command.add_argument("project", nargs="?", default=".", help="dbt project directory (default: .)")
91
+ command.add_argument("--dialect", help="analyze under this dialect instead of the adapter's")
92
+ command.add_argument("--explain", action="store_true", help="print each proof and failed proof")
93
+ command.add_argument(
94
+ "--require-proved", action="store_true",
95
+ help=f"exit {EXIT_FINDINGS} when a dbt test is not proved from the SQL, too, not only a marker",
96
+ )
97
+
98
+
99
+ def proved_tests(report: DbtReport) -> list[str]:
100
+ """The names of the tests the SQL proves, to pass dbt as `--exclude`."""
101
+ return sorted({test.name for test in report.tests if test.skippable})
102
+
103
+
104
+ def _analyze(project: Path, manifest: str | None, dialect: str | None, trust: bool) -> DbtReport | None:
105
+ try:
106
+ return analyze_project(
107
+ project, manifest_path=Path(manifest) if manifest else None, dialect=dialect, trust_upstream_tests=trust,
108
+ )
109
+ except ProjectError as error:
110
+ print(f"sqlassert-dbt: {error}", file=sys.stderr)
111
+ return None
112
+
113
+
114
+ if __name__ == "__main__":
115
+ sys.exit(main())