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.
- sqlassert_dbt-0.3.1/LICENSE +21 -0
- sqlassert_dbt-0.3.1/PKG-INFO +153 -0
- sqlassert_dbt-0.3.1/README.md +129 -0
- sqlassert_dbt-0.3.1/pyproject.toml +58 -0
- sqlassert_dbt-0.3.1/setup.cfg +4 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/__init__.py +19 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/__main__.py +115 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/expectations.py +336 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/jinja.py +183 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/manifest.py +254 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/program.py +686 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/project.py +340 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt/report.py +158 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/PKG-INFO +153 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/SOURCES.txt +36 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/dependency_links.txt +1 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/entry_points.txt +2 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/requires.txt +7 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/scm_file_list.json +32 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/scm_version.json +8 -0
- sqlassert_dbt-0.3.1/sqlassert_dbt.egg-info/top_level.txt +1 -0
- sqlassert_dbt-0.3.1/tests/conftest.py +16 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/dbt_project.yml +5 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/active_customers.sql +4 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/customer_keys.sql +4 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/customer_orders.sql +6 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/customer_signups.sql +2 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/customers.sql +12 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/orders_appended.sql +7 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/orders_incremental.sql +8 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/schema.yml +85 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/stg_customers.sql +3 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/models/stg_orders.sql +2 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/profiles.yml +7 -0
- sqlassert_dbt-0.3.1/tests/dbt_project/seeds/raw_payments.csv +2 -0
- sqlassert_dbt-0.3.1/tests/dbt_runs.py +26 -0
- sqlassert_dbt-0.3.1/tests/test_contracts.py +56 -0
- 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,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())
|