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.
Files changed (45) hide show
  1. sverify_dbt-0.7.0/.gitignore +0 -0
  2. sverify_dbt-0.7.0/API-NOTES.md +416 -0
  3. sverify_dbt-0.7.0/LICENSE +373 -0
  4. sverify_dbt-0.7.0/PKG-INFO +195 -0
  5. sverify_dbt-0.7.0/README.md +181 -0
  6. sverify_dbt-0.7.0/examples/shop/.gitignore +8 -0
  7. sverify_dbt-0.7.0/examples/shop/PROOF-STATUS.md +73 -0
  8. sverify_dbt-0.7.0/examples/shop/README.md +27 -0
  9. sverify_dbt-0.7.0/examples/shop/dbt_project.yml +18 -0
  10. sverify_dbt-0.7.0/examples/shop/models/marts/_marts.yml +99 -0
  11. sverify_dbt-0.7.0/examples/shop/models/marts/customer_history.sql +9 -0
  12. sverify_dbt-0.7.0/examples/shop/models/marts/customer_revenue_by_category.sql +11 -0
  13. sverify_dbt-0.7.0/examples/shop/models/marts/events_unioned.sql +7 -0
  14. sverify_dbt-0.7.0/examples/shop/models/marts/order_status_counts.sql +8 -0
  15. sverify_dbt-0.7.0/examples/shop/models/marts/orders_with_city.sql +8 -0
  16. sverify_dbt-0.7.0/examples/shop/models/marts/org_depth.sql +11 -0
  17. sverify_dbt-0.7.0/examples/shop/models/marts/sales_by_quarter.sql +8 -0
  18. sverify_dbt-0.7.0/examples/shop/models/marts/subscription_arr.sql +3 -0
  19. sverify_dbt-0.7.0/examples/shop/models/staging/_sources.yml +139 -0
  20. sverify_dbt-0.7.0/examples/shop/models/staging/_staging.yml +24 -0
  21. sverify_dbt-0.7.0/examples/shop/models/staging/stg_order_items.sql +9 -0
  22. sverify_dbt-0.7.0/examples/shop/models/staging/stg_orders.sql +4 -0
  23. sverify_dbt-0.7.0/examples/shop/package-lock.yml +11 -0
  24. sverify_dbt-0.7.0/examples/shop/packages.yml +5 -0
  25. sverify_dbt-0.7.0/examples/shop/profiles.yml +7 -0
  26. sverify_dbt-0.7.0/examples/shop/seeds/_seeds.yml +29 -0
  27. sverify_dbt-0.7.0/examples/shop/seeds/customer_updates.csv +5 -0
  28. sverify_dbt-0.7.0/examples/shop/seeds/employees.csv +4 -0
  29. sverify_dbt-0.7.0/examples/shop/seeds/events.csv +3 -0
  30. sverify_dbt-0.7.0/examples/shop/seeds/order_items.csv +7 -0
  31. sverify_dbt-0.7.0/examples/shop/seeds/orders.csv +6 -0
  32. sverify_dbt-0.7.0/examples/shop/seeds/products.csv +4 -0
  33. sverify_dbt-0.7.0/examples/shop/seeds/sales_wide.csv +3 -0
  34. sverify_dbt-0.7.0/examples/shop/seeds/subscriptions.csv +3 -0
  35. sverify_dbt-0.7.0/pyproject.toml +72 -0
  36. sverify_dbt-0.7.0/src/sverify_dbt/__init__.py +30 -0
  37. sverify_dbt-0.7.0/src/sverify_dbt/check.py +568 -0
  38. sverify_dbt-0.7.0/src/sverify_dbt/cli.py +115 -0
  39. sverify_dbt-0.7.0/src/sverify_dbt/manifest.py +283 -0
  40. sverify_dbt-0.7.0/src/sverify_dbt/mapping.py +310 -0
  41. sverify_dbt-0.7.0/src/sverify_dbt/result.py +374 -0
  42. sverify_dbt-0.7.0/tests/conftest.py +69 -0
  43. sverify_dbt-0.7.0/tests/test_check.py +310 -0
  44. sverify_dbt-0.7.0/tests/test_mapping.py +176 -0
  45. 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.