sverify-dbt 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sverify_dbt-0.7.0/.gitignore +0 -0
- sverify_dbt-0.7.0/API-NOTES.md +416 -0
- sverify_dbt-0.7.0/LICENSE +373 -0
- sverify_dbt-0.7.0/PKG-INFO +195 -0
- sverify_dbt-0.7.0/README.md +181 -0
- sverify_dbt-0.7.0/examples/shop/.gitignore +8 -0
- sverify_dbt-0.7.0/examples/shop/PROOF-STATUS.md +73 -0
- sverify_dbt-0.7.0/examples/shop/README.md +27 -0
- sverify_dbt-0.7.0/examples/shop/dbt_project.yml +18 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/_marts.yml +99 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/customer_history.sql +9 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/customer_revenue_by_category.sql +11 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/events_unioned.sql +7 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/order_status_counts.sql +8 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/orders_with_city.sql +8 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/org_depth.sql +11 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/sales_by_quarter.sql +8 -0
- sverify_dbt-0.7.0/examples/shop/models/marts/subscription_arr.sql +3 -0
- sverify_dbt-0.7.0/examples/shop/models/staging/_sources.yml +139 -0
- sverify_dbt-0.7.0/examples/shop/models/staging/_staging.yml +24 -0
- sverify_dbt-0.7.0/examples/shop/models/staging/stg_order_items.sql +9 -0
- sverify_dbt-0.7.0/examples/shop/models/staging/stg_orders.sql +4 -0
- sverify_dbt-0.7.0/examples/shop/package-lock.yml +11 -0
- sverify_dbt-0.7.0/examples/shop/packages.yml +5 -0
- sverify_dbt-0.7.0/examples/shop/profiles.yml +7 -0
- sverify_dbt-0.7.0/examples/shop/seeds/_seeds.yml +29 -0
- sverify_dbt-0.7.0/examples/shop/seeds/customer_updates.csv +5 -0
- sverify_dbt-0.7.0/examples/shop/seeds/employees.csv +4 -0
- sverify_dbt-0.7.0/examples/shop/seeds/events.csv +3 -0
- sverify_dbt-0.7.0/examples/shop/seeds/order_items.csv +7 -0
- sverify_dbt-0.7.0/examples/shop/seeds/orders.csv +6 -0
- sverify_dbt-0.7.0/examples/shop/seeds/products.csv +4 -0
- sverify_dbt-0.7.0/examples/shop/seeds/sales_wide.csv +3 -0
- sverify_dbt-0.7.0/examples/shop/seeds/subscriptions.csv +3 -0
- sverify_dbt-0.7.0/pyproject.toml +72 -0
- sverify_dbt-0.7.0/src/sverify_dbt/__init__.py +30 -0
- sverify_dbt-0.7.0/src/sverify_dbt/check.py +568 -0
- sverify_dbt-0.7.0/src/sverify_dbt/cli.py +115 -0
- sverify_dbt-0.7.0/src/sverify_dbt/manifest.py +283 -0
- sverify_dbt-0.7.0/src/sverify_dbt/mapping.py +310 -0
- sverify_dbt-0.7.0/src/sverify_dbt/result.py +374 -0
- sverify_dbt-0.7.0/tests/conftest.py +69 -0
- sverify_dbt-0.7.0/tests/test_check.py +310 -0
- sverify_dbt-0.7.0/tests/test_mapping.py +176 -0
- sverify_dbt-0.7.0/uv.lock +1818 -0
|
File without changes
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# Notes on sverify's API, from building sverify-dbt
|
|
2
|
+
|
|
3
|
+
Sverify-dbt uses only what `sverify` exports, as an outside user would: in
|
|
4
|
+
practice `sv.verify` on a SQL string with `tables=`, `requires=`, `ensures=`,
|
|
5
|
+
`semantics="duckdb"` and `on_inferred="error"`, then the `Report`. Each note
|
|
6
|
+
says what happened, what sverify-dbt did about it, and a proposed change.
|
|
7
|
+
They were written against `main` at the 0.5.0 release (b87a8963). The
|
|
8
|
+
VS Code extension keeps its own notes in `vscode/UX-NOTES.md`.
|
|
9
|
+
|
|
10
|
+
The repros run in `dbt/` with `uv run python`, after `import ibis, sverify as sv,
|
|
11
|
+
sverify.engines`; they show the behaviour before substrait-lean#685.
|
|
12
|
+
|
|
13
|
+
## Public API gaps
|
|
14
|
+
|
|
15
|
+
Gaps in what sverify exports, with the signature sverify-dbt wanted. The
|
|
16
|
+
first group is closed in [substrait-lean#685](https://github.com/AlexanderNenninger/substrait-lean/pull/685) (issue [substrait-lean#177](https://github.com/AlexanderNenninger/substrait-lean/issues/177)), each a small
|
|
17
|
+
documented addition with tests (substrait-lean's `python/tests/test_report_data.py` and
|
|
18
|
+
`test_sql_lift_core.py`) and sverify-dbt now uses it. The second group is
|
|
19
|
+
larger, for an issue each.
|
|
20
|
+
|
|
21
|
+
Closed in substrait-lean#685:
|
|
22
|
+
|
|
23
|
+
| Note | Addition |
|
|
24
|
+
|---|---|
|
|
25
|
+
| 1 | `sverify.errors.Unsupported(ValueError)`, the base of every refusal (the lift's, the producer's, the SQL writer's, the Polars translator's); `sverify.engines.Unsupported` is the same class |
|
|
26
|
+
| 4 | `CheckedCondition.reason -> Literal["observation", "not_a_key", "ties", "always_false", "may_be_false", "may_be_null", "not_proved_kind", "unknown"] \| None` |
|
|
27
|
+
| 5 | `Report.needs -> list[Needed]`, `Needed(table, column, op, value, sql, label, code, issue)`, `code` being the Python for `requires` (`_.qty != 0`) for an editor's quick fix (`sverify.contracts.Needed`) |
|
|
28
|
+
| 13 | `verify(tables={"t": {"id": "BIGINT", "s": "VARCHAR NOT NULL"}})`: DuckDB's type names beside Ibis's (`sverify.contracts.input_schema`) |
|
|
29
|
+
| 16 | The SQL lift reads `x IS [NOT] TRUE` and `x IS [NOT] FALSE` |
|
|
30
|
+
| 19 | `CheckedCondition.condition`: the condition as given in `ensures` |
|
|
31
|
+
| 22 | The checker's pipes close when it stops |
|
|
32
|
+
|
|
33
|
+
Filed (larger than substrait-lean#685; the checklist is [substrait-lean#692](https://github.com/AlexanderNenninger/substrait-lean/issues/692)):
|
|
34
|
+
|
|
35
|
+
| Note | Issue | Wanted |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| 2 | [substrait-lean#699](https://github.com/AlexanderNenninger/substrait-lean/issues/699) | `class ConditionError(Unsupported)` with `.condition` and `.reason`, raised before the proof; or `CheckedCondition.status == "unreadable"` and the rest proved |
|
|
38
|
+
| 3 | [substrait-lean#700](https://github.com/AlexanderNenninger/substrait-lean/issues/700) | `Unsupported.construct: str \| None` (`"function md5"`, `"WITH RECURSIVE"`) and `Unsupported.sql: str \| None`, set at each refusal site of the lift and the producer |
|
|
39
|
+
| 6 | [substrait-lean#701](https://github.com/AlexanderNenninger/substrait-lean/issues/701) | `CheckedCondition.needs: tuple[str, ...]`, the needed preconditions its proof used, and `proved` False when it is not empty; needs the checker to report which conditions used which inferred bound |
|
|
40
|
+
| 7, 8, 9 | [substrait-lean#687](https://github.com/AlexanderNenninger/substrait-lean/issues/687), [substrait-lean#702](https://github.com/AlexanderNenninger/substrait-lean/issues/702), [substrait-lean#703](https://github.com/AlexanderNenninger/substrait-lean/issues/703) | `Issue.shown` from the user's SQL only; no domain error and no assumed `SUM` under `semantics="duckdb"` where DuckDB completes the operation or widens the type |
|
|
41
|
+
| 10, 11 | [substrait-lean#703](https://github.com/AlexanderNenninger/substrait-lean/issues/703), [substrait-lean#704](https://github.com/AlexanderNenninger/substrait-lean/issues/704) | `Report.output` with DuckDB's types under `semantics="duckdb"`, `nullable=False` where proved; `Report.column_facts: dict[str, ColumnFacts]` (nullability, bounds, value set) |
|
|
42
|
+
| 12 | [substrait-lean#705](https://github.com/AlexanderNenninger/substrait-lean/issues/705) | `Report.ensured -> dict[str, list[Condition]]` (keys, NOT NULL, bounds of the result), in the form `requires` takes, to chain `verify` calls |
|
|
43
|
+
| 14 | [substrait-lean#706](https://github.com/AlexanderNenninger/substrait-lean/issues/706) | `tables` keys qualified as DuckDB resolves names (`"shop.main.orders"`) |
|
|
44
|
+
| 15 | [substrait-lean#707](https://github.com/AlexanderNenninger/substrait-lean/issues/707) | `sv.unique(*cols, nulls="distinct")`, a key that lets NULLs repeat (needs a key kind in Lean's `Rel.keys`) |
|
|
45
|
+
| 17 | [substrait-lean#708](https://github.com/AlexanderNenninger/substrait-lean/issues/708) | A finite value set per string column in the range analysis (Lean) |
|
|
46
|
+
| 18 | [substrait-lean#441](https://github.com/AlexanderNenninger/substrait-lean/issues/441) | Exact signs through double products, quotients and sums of non-negatives (Lean's float bounds) |
|
|
47
|
+
| 20 | [substrait-lean#709](https://github.com/AlexanderNenninger/substrait-lean/issues/709) | `sverify.sql_schema(query, tables, semantics="reference") -> ibis.Schema`, the lift's output schema without a proof (a new top-level name) |
|
|
48
|
+
| 21 | [substrait-lean#710](https://github.com/AlexanderNenninger/substrait-lean/issues/710) | `Report.engine_version`, and a warning from `run` when the installed engine is another |
|
|
49
|
+
| — | [substrait-lean#688](https://github.com/AlexanderNenninger/substrait-lean/issues/688) | `CheckedCondition.suggested` as `Needed`-like records too: sverify-dbt still parses their SQL to write a dbt test (`mapping.as_dbt_test`) |
|
|
50
|
+
|
|
51
|
+
## Errors
|
|
52
|
+
|
|
53
|
+
### 1. `sverify.engines.Unsupported` is not what `verify` raises
|
|
54
|
+
|
|
55
|
+
`sv.verify` raises `sverify.plan_json.Unsupported` for SQL the lift cannot
|
|
56
|
+
read. The public `sverify.engines.Unsupported` is `sverify.sql.Unsupported`,
|
|
57
|
+
another class, so `except sverify.engines.Unsupported` lets every refusal
|
|
58
|
+
through.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
t = ibis.schema({"x": "int64"})
|
|
62
|
+
try:
|
|
63
|
+
sv.verify("with recursive c as (select 1 as x) select * from c", tables={"t": t})
|
|
64
|
+
except sverify.engines.Unsupported:
|
|
65
|
+
print("caught") # never: sverify.plan_json.Unsupported escapes
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Sverify-dbt catches `ValueError`, the base of both
|
|
69
|
+
([check.py](src/sverify_dbt/check.py), `_verify`).
|
|
70
|
+
|
|
71
|
+
Proposed: one `Unsupported`, exported from `sverify.errors`, that every
|
|
72
|
+
refusal raises (the lift, the producer, the checker), with the others as
|
|
73
|
+
aliases of it.
|
|
74
|
+
|
|
75
|
+
Closed in substrait-lean#685: `sverify.errors.Unsupported`. Sverify-dbt still catches `ValueError`, since a bad condition raises one too (note 2).
|
|
76
|
+
|
|
77
|
+
### 2. A bad condition and an unreadable query raise the same error
|
|
78
|
+
|
|
79
|
+
A condition on a column the result does not have (`ensures=["zz > 1"]`)
|
|
80
|
+
raises `Unsupported("cannot resolve the SQL's names: Column 'zz' could not
|
|
81
|
+
be resolved")`, as an unreadable query does, and a condition sverify cannot
|
|
82
|
+
read (`IS NOT FALSE`) refuses the whole call. A caller with twenty
|
|
83
|
+
conditions cannot tell which one failed, or whether the query did.
|
|
84
|
+
|
|
85
|
+
Sverify-dbt checks test columns against the result's columns first, then, on
|
|
86
|
+
an error, verifies the query with no conditions, then each condition on its
|
|
87
|
+
own, to find the culprits: up to one checker call per condition
|
|
88
|
+
(`_verify`).
|
|
89
|
+
|
|
90
|
+
Proposed: `verify` reads every condition before it proves anything, and
|
|
91
|
+
raises `ConditionError` (a subclass of `ValueError`) with the condition and
|
|
92
|
+
the reason; or it reports an unreadable condition in the report
|
|
93
|
+
(`CheckedCondition.status = "unreadable"`) and proves the rest.
|
|
94
|
+
|
|
95
|
+
### 3. Refusals name the construct only as truncated SQL
|
|
96
|
+
|
|
97
|
+
`dbt_utils.generate_surrogate_key` compiles to `md5(...)`, which the SQL
|
|
98
|
+
lift refuses with `MD5(CAST(order_items.order_id AS TEXT) || '-' ||
|
|
99
|
+
COALESCE... is not supported yet`. The function is the first word of a
|
|
100
|
+
70-character prefix of the expression; a tool that wants to say "md5 is not
|
|
101
|
+
supported" has to parse the message.
|
|
102
|
+
|
|
103
|
+
Sverify-dbt rephrases the message with a regex (`_refusal`).
|
|
104
|
+
|
|
105
|
+
Proposed: `Unsupported` carries structured fields: `construct`
|
|
106
|
+
(`"function md5"`, `"WITH RECURSIVE"`), `sql` (the fragment) and, where
|
|
107
|
+
there is one, an issue link. `str(e)` stays as it is.
|
|
108
|
+
|
|
109
|
+
## Reports and their wording
|
|
110
|
+
|
|
111
|
+
### 4. `CheckedCondition.why` speaks to a Python user
|
|
112
|
+
|
|
113
|
+
An unproved key says "the preconditions do not make it a key: add
|
|
114
|
+
sv.unique('order_id') on the input it comes from to requires". A dbt user
|
|
115
|
+
writes tests, and has no `requires` or `sv.unique`. The reason exists only
|
|
116
|
+
as this sentence and as `value` (`"not shown to be a key"`,
|
|
117
|
+
`"TRUE, FALSE or NULL"`).
|
|
118
|
+
|
|
119
|
+
Sverify-dbt rewrites the reason from `value`, matching its strings
|
|
120
|
+
(`_why`), which breaks when the wording changes.
|
|
121
|
+
|
|
122
|
+
Proposed: `CheckedCondition.reason`, an enum (`not_a_key`, `may_be_false`,
|
|
123
|
+
`may_be_null`, `ties`, `observation`, `needs_precondition`), with `why` the
|
|
124
|
+
current text built from it, so that a frontend phrases it in its own terms.
|
|
125
|
+
|
|
126
|
+
Closed in substrait-lean#685: `CheckedCondition.reason`; sverify-dbt words each reason from it (`_WHY`).
|
|
127
|
+
|
|
128
|
+
### 5. `Report.needed` gives Ibis code; the SQL and the column are hidden
|
|
129
|
+
|
|
130
|
+
With `on_inferred="error"`, `report.needed` is `{"subscriptions":
|
|
131
|
+
["(_.mrr < 7 * 10**17)"]}`: Ibis code, which a SQL or dbt user cannot
|
|
132
|
+
paste. The SQL is in `report.inferred`, as an `Inferred`, a `str` subclass
|
|
133
|
+
sverify does not export, whose `text` attribute matches the label. The
|
|
134
|
+
column, the operator and the bound are in neither. `needs_lines()` writes
|
|
135
|
+
advice for the Python API ("declare it in requires", "`subscriptions.filter(...)`
|
|
136
|
+
first", "on_inferred='warn' or 'silent' assumes it").
|
|
137
|
+
|
|
138
|
+
Sverify-dbt pairs `needed` with `inferred` through `Inferred.text`, parses
|
|
139
|
+
the SQL with SQLGlot to get the column and bound, and writes a
|
|
140
|
+
`dbt_utils.accepted_range` from them ([mapping.py](src/sverify_dbt/mapping.py),
|
|
141
|
+
`bound`, `as_dbt_test`).
|
|
142
|
+
|
|
143
|
+
Proposed: `Report.needed` (or a new `Report.needs`) as a list of
|
|
144
|
+
`Needed(table, column, op, bound, sql, label, issue)`, with `issue` the
|
|
145
|
+
`Issue` it rules out (today `inferred_from[label]`).
|
|
146
|
+
|
|
147
|
+
Closed in substrait-lean#685: `Report.needs`; sverify-dbt writes the dbt test from its column, `op` and `value` (`mapping.dbt_test`).
|
|
148
|
+
|
|
149
|
+
### 6. A condition proved with a needed precondition reads as proved
|
|
150
|
+
|
|
151
|
+
Under `on_inferred="error"`, `Report.proved` is False when `needed` is not
|
|
152
|
+
empty, but each `CheckedCondition.proved` stays True for a condition whose
|
|
153
|
+
proof used the inferred bound. A tool that reads the conditions one by one
|
|
154
|
+
reports them as proved.
|
|
155
|
+
|
|
156
|
+
Sverify-dbt marks every test unproved when `report.needed` is not empty
|
|
157
|
+
(`_read`), which also marks the tests that did not need the bound.
|
|
158
|
+
|
|
159
|
+
Proposed: `CheckedCondition.needs`, the needed preconditions its proof used
|
|
160
|
+
(empty when none), and `proved` False when it is not empty under
|
|
161
|
+
`on_inferred="error"`.
|
|
162
|
+
|
|
163
|
+
### 7. `Issue.shown` leaks the plan's internals
|
|
164
|
+
|
|
165
|
+
A query `select o.customer_id, sum(i.qty * i.price) ...` reports
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
may fail: domain error: IF ((qty = 0) AND ((price = infd) OR (price = -infd))) THEN NaNd ELSE ...
|
|
169
|
+
hint: `sum(qty)` is inside an AND or OR (`&`, `|`), and DuckDB and Polars evaluate both operands ...
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`infd`, `NaNd` and the `IF` come from the plan, and `Issue.expr` has `$6`.
|
|
173
|
+
The query has no AND or OR; the hint follows `under_and_or=True` on the
|
|
174
|
+
`SUM`, which the guard sverify adds for NaN sets.
|
|
175
|
+
|
|
176
|
+
Proposed: `Issue.shown` written from the user's SQL (the `qty * price` of
|
|
177
|
+
the first issue), the guard's internal expression left out, and
|
|
178
|
+
`under_and_or` set only for an AND or OR of the user's.
|
|
179
|
+
|
|
180
|
+
### 8. Under `semantics="duckdb"`, double arithmetic "may fail"
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
t = ibis.schema({"r": "float64", "p": "float64"})
|
|
184
|
+
sv.verify("select r * p as y from t", tables={"t": t}, semantics="duckdb").errors
|
|
185
|
+
# [Issue(kind='domain error', shown='r * p', ...)]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
DuckDB gives NaN for `0 * inf` and `inf` for `1.0 / 0.0`, without an error,
|
|
189
|
+
and `semantics="duckdb"` is documented as "NaN and ±inf included". Either
|
|
190
|
+
the issue is a false positive under this profile, or the docs should say
|
|
191
|
+
which operations still raise. To confirm.
|
|
192
|
+
|
|
193
|
+
Sverify-dbt lists these as "may fail at run time".
|
|
194
|
+
|
|
195
|
+
Proposed: under `semantics="duckdb"`, no domain error for an operation
|
|
196
|
+
DuckDB completes; or a field on `Issue` that says under which profile it
|
|
197
|
+
raises.
|
|
198
|
+
|
|
199
|
+
### 9. `Report.assumed` assumes a job that dbt does not run
|
|
200
|
+
|
|
201
|
+
`select x, sum(y) as s from t group by x` assumes "SUM of sum(y) fits 64
|
|
202
|
+
bits in every group (inferred; checked on the data while the job runs,
|
|
203
|
+
which raises if it does not)". Under dbt, DuckDB runs the SQL itself, and
|
|
204
|
+
DuckDB's `SUM` of a `BIGINT` is a `HUGEINT`, so it cannot overflow 64 bits
|
|
205
|
+
(see 10).
|
|
206
|
+
|
|
207
|
+
Sverify-dbt prints the assumption as it is.
|
|
208
|
+
|
|
209
|
+
Proposed: under `semantics="duckdb"`, a `SUM` follows DuckDB's widening and
|
|
210
|
+
assumes nothing; elsewhere, `Issue` says who checks the assumption (the
|
|
211
|
+
sverify job, or the engine).
|
|
212
|
+
|
|
213
|
+
## Schemas and types
|
|
214
|
+
|
|
215
|
+
### 10. `Report.output` has the plan's types, not DuckDB's
|
|
216
|
+
|
|
217
|
+
Under `semantics="duckdb"`, `report.output` for `sum(cents)`, with `cents`
|
|
218
|
+
an `INTEGER`, is `Int64`; DuckDB gives `HUGEINT`. A dbt contract that
|
|
219
|
+
declares `bigint` passes against `report.output` and fails at `dbt build`
|
|
220
|
+
(`tests/test_check.py`, `test_contract_mismatch_and_inputs_without_types`).
|
|
221
|
+
|
|
222
|
+
Sverify-dbt asks DuckDB for the result's types (`DESCRIBE` of the
|
|
223
|
+
compiled SQL over empty tables) and checks contracts against those, noting
|
|
224
|
+
where sverify's type differs (`_contract`).
|
|
225
|
+
|
|
226
|
+
Proposed: under `semantics="duckdb"`, `Report.output` has DuckDB's types (or
|
|
227
|
+
a `Report.engine_output`), and `sverify` documents where its plan's type
|
|
228
|
+
and the engine's differ.
|
|
229
|
+
|
|
230
|
+
### 11. Proved NOT NULL columns are nullable in `Report.output`
|
|
231
|
+
|
|
232
|
+
Every column of `report.output` is `nullable=True`, `count(*)` included,
|
|
233
|
+
though `report.facts["c"]` is "an integer ≥ 1". The proved facts are
|
|
234
|
+
strings only.
|
|
235
|
+
|
|
236
|
+
Sverify-dbt proves `c IS NOT NULL` as a condition instead of reading it.
|
|
237
|
+
|
|
238
|
+
Proposed: `Report.output` types with `nullable=False` where the checker
|
|
239
|
+
proves the column never NULL, and the facts as data (`Report.column_facts`:
|
|
240
|
+
nullability, integer bounds, the set of a string's values).
|
|
241
|
+
|
|
242
|
+
### 12. Facts proved of one model do not reach the next
|
|
243
|
+
|
|
244
|
+
`verify` checks one query. What it proves of the result beyond the
|
|
245
|
+
`ensures` (a key, a bound, NOT NULL) is in `facts` as text, so a caller
|
|
246
|
+
cannot pass it on as `requires` to the query that reads the result.
|
|
247
|
+
Sverify's own answer is fusion (`Node.pipe`), which sverify-dbt cannot use:
|
|
248
|
+
dbt builds and tests each model on its own, and an upstream test that
|
|
249
|
+
`dbt build` runs (a precondition downstream) has no place in the middle of a
|
|
250
|
+
fused pipeline.
|
|
251
|
+
|
|
252
|
+
Sverify-dbt carries only tests: a key that sverify proves of a model but
|
|
253
|
+
that no test states is unknown downstream (see the README).
|
|
254
|
+
|
|
255
|
+
Proposed: `Report.ensured`, the conditions the checker proves of the result
|
|
256
|
+
(keys, NOT NULL, bounds, value sets), in the form `requires` takes, so a
|
|
257
|
+
caller can chain `verify` calls; or `requires` in the middle of a fused
|
|
258
|
+
pipeline, for a node whose output is checked at run time.
|
|
259
|
+
|
|
260
|
+
### 13. `tables=` takes Ibis types, and `verify` reads DuckDB's SQL
|
|
261
|
+
|
|
262
|
+
`ibis.schema({"a": "bigint"})` fails (`integer`, `hugeint` and `text`
|
|
263
|
+
too), so a SQL user who knows the table as DuckDB declares it has to
|
|
264
|
+
translate each type to Ibis's names.
|
|
265
|
+
|
|
266
|
+
Sverify-dbt creates each input as an empty DuckDB table and reads its
|
|
267
|
+
schema through Ibis's DuckDB backend (`ibis.duckdb.from_connection(...)`).
|
|
268
|
+
|
|
269
|
+
Proposed: `tables=` accepts DuckDB type names (`{"orders": {"id": "BIGINT"}}`,
|
|
270
|
+
or a `CREATE TABLE` column list), parsed with the query's `dialect`.
|
|
271
|
+
|
|
272
|
+
Closed in substrait-lean#685: `tables=` takes DuckDB's type names; sverify-dbt passes DuckDB's `DESCRIBE` of each input.
|
|
273
|
+
|
|
274
|
+
### 14. Inputs must be bare names
|
|
275
|
+
|
|
276
|
+
dbt's compiled SQL reads `"shop"."main"."orders"`; `verify` refuses it
|
|
277
|
+
("only the inputs' tables can be read") and `tables` keys are bare names.
|
|
278
|
+
The refusal is safe, but every caller with schemas has to rewrite the SQL.
|
|
279
|
+
|
|
280
|
+
Sverify-dbt rewrites each relation with SQLGlot to a name of its own,
|
|
281
|
+
keeping the alias (`_rewrite`).
|
|
282
|
+
|
|
283
|
+
Proposed: `tables` keys may be qualified (`"shop.main.orders"`), matched as
|
|
284
|
+
DuckDB resolves names (catalog, schema, table, with the defaults).
|
|
285
|
+
|
|
286
|
+
## Conditions
|
|
287
|
+
|
|
288
|
+
### 15. No key that lets NULLs repeat
|
|
289
|
+
|
|
290
|
+
dbt's `unique` test, and a SQL `UNIQUE` constraint, let several rows have
|
|
291
|
+
NULL; `sv.unique` counts NULLs as one value (`_weighted.duplicate_keys`:
|
|
292
|
+
"NULLs equal"). Proving `sv.unique` implies dbt's test, but assuming it
|
|
293
|
+
from dbt's test is unsound for a nullable column.
|
|
294
|
+
|
|
295
|
+
Sverify-dbt assumes a `unique` test only where a `not_null` test covers the
|
|
296
|
+
column ([mapping.py](src/sverify_dbt/mapping.py), `_unique`).
|
|
297
|
+
|
|
298
|
+
Proposed: `sv.unique(*cols, nulls="distinct")`, a key over the rows with no
|
|
299
|
+
NULL in `cols`, which `Rel.keys_sound` can use for joins on those columns.
|
|
300
|
+
|
|
301
|
+
### 16. `IS [NOT] TRUE` and `IS [NOT] FALSE` are not read
|
|
302
|
+
|
|
303
|
+
dbt's tests let NULL pass, and the natural condition is `expr IS NOT
|
|
304
|
+
FALSE`. Sverify refuses it ("IS FALSE is not supported; only IS [NOT] NULL
|
|
305
|
+
is").
|
|
306
|
+
|
|
307
|
+
Sverify-dbt writes `coalesce((expr), TRUE)`.
|
|
308
|
+
|
|
309
|
+
Proposed: read `IS [NOT] TRUE/FALSE` (it is `coalesce` with a constant).
|
|
310
|
+
|
|
311
|
+
Closed in substrait-lean#685: the lift reads `IS [NOT] TRUE/FALSE`.
|
|
312
|
+
|
|
313
|
+
### 17. Strings are known only by the exact condition
|
|
314
|
+
|
|
315
|
+
On a string column, sverify proves a condition only from the same condition:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
t = ibis.schema({"s": "string"})
|
|
319
|
+
v = lambda q, req, ens: (
|
|
320
|
+
sv.verify(q, tables={"t": t}, semantics="duckdb", requires={"t": req}, ensures=ens)
|
|
321
|
+
.ensures[0]
|
|
322
|
+
.proved
|
|
323
|
+
)
|
|
324
|
+
v("select 'a' as s from t", [], ["s = 'a'"]) # False
|
|
325
|
+
v("select s from t where s <> 'c'", ["s IN ('a', 'b', 'c')"], ["s IN ('a', 'b')"]) # False
|
|
326
|
+
v("select s from t", ["s IN ('a', 'b')"], ["s IS NULL OR s IN ('a', 'b')"]) # False
|
|
327
|
+
v("select s from t", ["s IN ('a', 'b')"], ["s IN ('a', 'b')"]) # True
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`accepted_values` is dbt's most common test after `not_null` and `unique`,
|
|
331
|
+
and four of the seven unproved tests of the sample project are this case:
|
|
332
|
+
a status filtered from a wider set, a tag that is a constant in each branch
|
|
333
|
+
of a `UNION ALL`, a quarter named by a constant.
|
|
334
|
+
|
|
335
|
+
Proposed: a finite set of values in the range analysis for strings (and
|
|
336
|
+
other types compared only by equality), through constants, `IN`, `=`, `<>`,
|
|
337
|
+
`COALESCE`, `CASE` and `UNION ALL`. A key over a `UNION ALL` whose
|
|
338
|
+
branches differ in a constant column follows from it too (the sample's
|
|
339
|
+
`unique_combination_of_columns(region, quarter)`).
|
|
340
|
+
|
|
341
|
+
### 18. The sign of a product of doubles is lost
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
t = ibis.schema({"q": "int64", "x": "float64"})
|
|
345
|
+
sv.verify(
|
|
346
|
+
"select q * x as y from t",
|
|
347
|
+
tables={"t": t},
|
|
348
|
+
semantics="duckdb",
|
|
349
|
+
requires={"t": ["q >= 1 AND q <= 10", "x >= 0 AND x <= 10"]},
|
|
350
|
+
ensures=["y >= 0"],
|
|
351
|
+
).facts["y"]
|
|
352
|
+
# 'a double > -8.881784197001252e-14 and < 100.00000000000009'
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
A product of two non-negative doubles is non-negative in IEEE 754, but the
|
|
356
|
+
rounding error widens the bound below 0, so `y >= 0` and the sample's
|
|
357
|
+
`accepted_range(books, min 0)` over `sum(qty * price)` stay unproved.
|
|
358
|
+
|
|
359
|
+
Proposed: keep a bound exact where rounding cannot cross it (the sign of a
|
|
360
|
+
product or quotient, 0 as a bound of a sum of non-negatives).
|
|
361
|
+
|
|
362
|
+
## Smaller points
|
|
363
|
+
|
|
364
|
+
### 19. `CheckedCondition` holds the label, not the condition
|
|
365
|
+
|
|
366
|
+
To pair a result with the condition passed, sverify-dbt matches
|
|
367
|
+
`CheckedCondition.label` against the SQL string or `Unique.label`. Proposed:
|
|
368
|
+
`CheckedCondition.condition`, the object as given.
|
|
369
|
+
|
|
370
|
+
Closed in substrait-lean#685: `CheckedCondition.condition`; sverify-dbt pairs results with its conditions by it.
|
|
371
|
+
|
|
372
|
+
### 20. No schema of a query without a proof
|
|
373
|
+
|
|
374
|
+
Sverify-dbt needs a model's result columns before it builds the `ensures`
|
|
375
|
+
(to leave out a test on a missing column, see 2), and gets them from
|
|
376
|
+
DuckDB. Proposed: `sverify.sql_schema(query, tables, semantics=...)`, the
|
|
377
|
+
lift's output schema (the internal `sql_model_schema`).
|
|
378
|
+
|
|
379
|
+
### 21. Which DuckDB the profile describes
|
|
380
|
+
|
|
381
|
+
`semantics="duckdb"` models DuckDB at the versions of the engine facts; dbt
|
|
382
|
+
runs whatever DuckDB is installed. Proposed: `Report.engine_version` (the
|
|
383
|
+
version the profile was characterized at) and a warning from `run` and
|
|
384
|
+
`check_inputs` when the installed DuckDB is another.
|
|
385
|
+
|
|
386
|
+
### 22. The checker's pipes leak at exit
|
|
387
|
+
|
|
388
|
+
A script that calls `verify` and exits with warnings on prints
|
|
389
|
+
`ResourceWarning: unclosed file <_io.BufferedWriter name=4>` from
|
|
390
|
+
`sverify/engine.py:522`: the `substrait-check --serve` process's pipes are
|
|
391
|
+
not closed. Proposed: close them in the `atexit` handler.
|
|
392
|
+
|
|
393
|
+
Closed in substrait-lean#685: `_Checker.close` closes the pipes.
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
## From `sverify-dbt suggest`
|
|
397
|
+
|
|
398
|
+
### 23. Suggestions for unproved postconditions are rare and unstructured
|
|
399
|
+
|
|
400
|
+
`sverify-dbt suggest` turns `Report.needs` and `CheckedCondition.suggested`
|
|
401
|
+
into dbt tests. On the sample project, `needs` gave the one test that
|
|
402
|
+
mattered (`mrr`'s upper bound), and `suggested` was empty for all nine
|
|
403
|
+
tests left unproved. Their blockers are string value sets, keys through
|
|
404
|
+
`UNION ALL`, signs of double products and range joins, where sverify
|
|
405
|
+
suggests nothing (`examples/shop/PROOF-STATUS.md`). Where it does suggest
|
|
406
|
+
something, `suggested` holds conditions, so sverify-dbt parses their SQL
|
|
407
|
+
(`mapping.as_dbt_test`). Each `Needed` is also one side of a bound, so a
|
|
408
|
+
column with both sides gets two `accepted_range` tests where a user writes
|
|
409
|
+
one.
|
|
410
|
+
|
|
411
|
+
Proposed: `CheckedCondition.suggested` as `Needed` records, as
|
|
412
|
+
[substrait-lean#688](https://github.com/AlexanderNenninger/substrait-lean/issues/688)
|
|
413
|
+
asks, and a `Needed` per column carrying both sides (`lower`, `upper`).
|
|
414
|
+
The weakest-precondition search of
|
|
415
|
+
[substrait-lean#663](https://github.com/AlexanderNenninger/substrait-lean/issues/663)
|
|
416
|
+
would give suggestions for more kinds of postcondition.
|