sverify 0.2.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-0.2.0/.gitignore +14 -0
- sverify-0.2.0/.python-version +1 -0
- sverify-0.2.0/PKG-INFO +241 -0
- sverify-0.2.0/README.md +207 -0
- sverify-0.2.0/great-docs.yml +134 -0
- sverify-0.2.0/hatch_build.py +60 -0
- sverify-0.2.0/lean/Main.lean +188 -0
- sverify-0.2.0/lean/SubstraitLean/Aggregate.lean +214 -0
- sverify-0.2.0/lean/SubstraitLean/Check.lean +53 -0
- sverify-0.2.0/lean/SubstraitLean/Completeness.lean +89 -0
- sverify-0.2.0/lean/SubstraitLean/Compose.lean +345 -0
- sverify-0.2.0/lean/SubstraitLean/ContractCheck.lean +237 -0
- sverify-0.2.0/lean/SubstraitLean/Contracts.lean +3070 -0
- sverify-0.2.0/lean/SubstraitLean/Examples.lean +202 -0
- sverify-0.2.0/lean/SubstraitLean/Expr.lean +253 -0
- sverify-0.2.0/lean/SubstraitLean/Float.lean +160 -0
- sverify-0.2.0/lean/SubstraitLean/Guards.lean +468 -0
- sverify-0.2.0/lean/SubstraitLean/Incremental.lean +295 -0
- sverify-0.2.0/lean/SubstraitLean/Keys.lean +824 -0
- sverify-0.2.0/lean/SubstraitLean/Order.lean +292 -0
- sverify-0.2.0/lean/SubstraitLean/Parse.lean +1862 -0
- sverify-0.2.0/lean/SubstraitLean/Partition.lean +1717 -0
- sverify-0.2.0/lean/SubstraitLean/Pretty.lean +106 -0
- sverify-0.2.0/lean/SubstraitLean/Rel.lean +274 -0
- sverify-0.2.0/lean/SubstraitLean/Rewrites.lean +93 -0
- sverify-0.2.0/lean/SubstraitLean/Rounding.lean +219 -0
- sverify-0.2.0/lean/SubstraitLean/Safety.lean +765 -0
- sverify-0.2.0/lean/SubstraitLean/Semantic.lean +1898 -0
- sverify-0.2.0/lean/SubstraitLean/ShardCheck.lean +126 -0
- sverify-0.2.0/lean/SubstraitLean/TableFormat.lean +400 -0
- sverify-0.2.0/lean/SubstraitLean/Terms.lean +99 -0
- sverify-0.2.0/lean/SubstraitLean/Text.lean +211 -0
- sverify-0.2.0/lean/SubstraitLean/Time.lean +501 -0
- sverify-0.2.0/lean/SubstraitLean/Value.lean +62 -0
- sverify-0.2.0/lean/SubstraitLean/Weighted.lean +335 -0
- sverify-0.2.0/lean/SubstraitLean/Window.lean +324 -0
- sverify-0.2.0/lean/SubstraitLean/Write.lean +703 -0
- sverify-0.2.0/lean/SubstraitLean.lean +29 -0
- sverify-0.2.0/lean/lake-manifest.json +6 -0
- sverify-0.2.0/lean/lakefile.toml +9 -0
- sverify-0.2.0/lean/lean-toolchain +1 -0
- sverify-0.2.0/pyproject.toml +199 -0
- sverify-0.2.0/scripts/bench_optimizations.py +153 -0
- sverify-0.2.0/scripts/capture_fixtures.py +171 -0
- sverify-0.2.0/scripts/compare_sql_producers.py +60 -0
- sverify-0.2.0/scripts/fetch_isthmus.sh +25 -0
- sverify-0.2.0/scripts/isthmus_catalogue.py +159 -0
- sverify-0.2.0/scripts/publish_notebooks.py +118 -0
- sverify-0.2.0/src/sverify/__init__.py +56 -0
- sverify-0.2.0/src/sverify/asof.py +79 -0
- sverify-0.2.0/src/sverify/catalogue.py +831 -0
- sverify-0.2.0/src/sverify/contracts.py +1355 -0
- sverify-0.2.0/src/sverify/counterexample.py +249 -0
- sverify-0.2.0/src/sverify/coverage.py +214 -0
- sverify-0.2.0/src/sverify/coverage_cases/__init__.py +66 -0
- sverify-0.2.0/src/sverify/coverage_cases/_common.py +104 -0
- sverify-0.2.0/src/sverify/coverage_cases/aggregation.py +78 -0
- sverify-0.2.0/src/sverify/coverage_cases/joins.py +99 -0
- sverify-0.2.0/src/sverify/coverage_cases/lists.py +36 -0
- sverify-0.2.0/src/sverify/coverage_cases/nulls.py +18 -0
- sverify-0.2.0/src/sverify/coverage_cases/numbers.py +29 -0
- sverify-0.2.0/src/sverify/coverage_cases/rows.py +65 -0
- sverify-0.2.0/src/sverify/coverage_cases/strings.py +22 -0
- sverify-0.2.0/src/sverify/coverage_cases/temporal.py +177 -0
- sverify-0.2.0/src/sverify/coverage_cases/windows.py +160 -0
- sverify-0.2.0/src/sverify/display.py +91 -0
- sverify-0.2.0/src/sverify/duckdb_engine.py +59 -0
- sverify-0.2.0/src/sverify/engine.py +839 -0
- sverify-0.2.0/src/sverify/engines.py +96 -0
- sverify-0.2.0/src/sverify/errors.py +35 -0
- sverify-0.2.0/src/sverify/examples.py +57 -0
- sverify-0.2.0/src/sverify/fuzz.py +631 -0
- sverify-0.2.0/src/sverify/ibis_substrait/LICENSE +201 -0
- sverify-0.2.0/src/sverify/ibis_substrait/README.md +10 -0
- sverify-0.2.0/src/sverify/ibis_substrait/__init__.py +1612 -0
- sverify-0.2.0/src/sverify/incremental.py +1283 -0
- sverify-0.2.0/src/sverify/io/__init__.py +55 -0
- sverify-0.2.0/src/sverify/io/_common.py +374 -0
- sverify-0.2.0/src/sverify/io/delta.py +631 -0
- sverify-0.2.0/src/sverify/io/iceberg.py +651 -0
- sverify-0.2.0/src/sverify/nodes.py +761 -0
- sverify-0.2.0/src/sverify/partition.py +312 -0
- sverify-0.2.0/src/sverify/polars_engine.py +1942 -0
- sverify-0.2.0/src/sverify/py.typed +0 -0
- sverify-0.2.0/src/sverify/skills/sverify/SKILL.md +141 -0
- sverify-0.2.0/src/sverify/skills/sverify/references/contracts.md +78 -0
- sverify-0.2.0/src/sverify/skills/sverify-engines/SKILL.md +138 -0
- sverify-0.2.0/src/sverify/skills/sverify-engines/references/semantics.md +30 -0
- sverify-0.2.0/src/sverify/skills/sverify-pipelines/SKILL.md +140 -0
- sverify-0.2.0/src/sverify/skills/sverify-pipelines/references/table-formats.md +116 -0
- sverify-0.2.0/src/sverify/spark_engine.py +124 -0
- sverify-0.2.0/src/sverify/sql/__init__.py +9 -0
- sverify-0.2.0/src/sverify/sql/core.py +3594 -0
- sverify-0.2.0/src/sverify/sql/duckdb.py +162 -0
- sverify-0.2.0/src/sverify/sql/snowflake.py +326 -0
- sverify-0.2.0/src/sverify/sql/spark.py +322 -0
- sverify-0.2.0/src/sverify/sql_model.py +89 -0
- sverify-0.2.0/src/sverify/testing.py +172 -0
- sverify-0.2.0/tests/conftest.py +118 -0
- sverify-0.2.0/tests/data/delta_deletion_vectors/_delta_log/00000000000000000000.json +3 -0
- sverify-0.2.0/tests/data/delta_deletion_vectors/_delta_log/00000000000000000001.json +2 -0
- sverify-0.2.0/tests/data/delta_deletion_vectors/_delta_log/00000000000000000002.json +3 -0
- sverify-0.2.0/tests/data/delta_deletion_vectors/deletion_vector_0045963d-73fc-48ff-ab94-2ecfbf35731a.bin +0 -0
- sverify-0.2.0/tests/data/delta_deletion_vectors/part-00000-a3785255-ffa6-42d4-b755-8b98ea6da28b-c000.snappy.parquet +0 -0
- sverify-0.2.0/tests/spark_session.py +46 -0
- sverify-0.2.0/tests/sql_snapshots/df_aggregate.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_aggregate.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_anti_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_anti_join.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_asof.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_asof.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_distinct.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_distinct.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_filter_project.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_filter_project.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_float_timestamp.duckdb.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_float_timestamp.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_float_timestamp.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_inner_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_inner_join.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_order_by.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_order_by.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_order_by_limit.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_order_by_limit.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_outer_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_outer_join.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_self_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_self_join.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_semi_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_semi_join.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_strings.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_strings.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_time.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_time.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_union_all.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_union_all.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_window.duckdb.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_window.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/df_window.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/filter_project.duckdb.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/filter_project.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/filter_project.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/group_by_without_measures.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/group_by_without_measures.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_char_literals.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_char_literals.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_compare_int_double.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_compare_int_double.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_date_compare.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_date_compare.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_date_iday.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_date_iday.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_is_true.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_is_true.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_nested_window.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_nested_window.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_power.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/isthmus_power.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter_delta0.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter_delta0.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter_delta1.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter_delta1.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter_delta2.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_cross_filter_delta2.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_fetch_inlined.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_fetch_inlined.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute_delta0.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute_delta0.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute_delta1.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute_delta1.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute_delta2.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_join_compute_delta2.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_project.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_project.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_project_delta0.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_filter_project_delta0.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join_delta0.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join_delta0.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join_delta1.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join_delta1.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join_delta2.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_inner_join_delta2.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join_delta0.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join_delta0.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join_delta1.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join_delta1.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join_delta2.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_self_join_delta2.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query_delta0.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query_delta0.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query_delta1.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query_delta1.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query_delta2.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_sql_query_delta2.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_union_all.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_union_all.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_union_all_delta0.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_union_all_delta0.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_union_all_delta1.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_union_all_delta1.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_window_inlined.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_window_inlined.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_window_tie_free.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/pipeline_window_tie_free.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/typed_greatest.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/typed_greatest.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/union_fetch.duckdb.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/union_fetch.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/union_fetch.spark.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/virtual_left_join.duckdb.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/virtual_left_join.snowflake.sql +1 -0
- sverify-0.2.0/tests/sql_snapshots/virtual_left_join.spark.sql +1 -0
- sverify-0.2.0/tests/test_aggregate.py +240 -0
- sverify-0.2.0/tests/test_asof.py +143 -0
- sverify-0.2.0/tests/test_backend_semantics.py +420 -0
- sverify-0.2.0/tests/test_backends.py +282 -0
- sverify-0.2.0/tests/test_catalogue.py +82 -0
- sverify-0.2.0/tests/test_compose.py +148 -0
- sverify-0.2.0/tests/test_contracts.py +250 -0
- sverify-0.2.0/tests/test_counterexample.py +115 -0
- sverify-0.2.0/tests/test_coverage.py +22 -0
- sverify-0.2.0/tests/test_datafusion_quirks.py +487 -0
- sverify-0.2.0/tests/test_floats.py +172 -0
- sverify-0.2.0/tests/test_fuzz.py +169 -0
- sverify-0.2.0/tests/test_guards.py +99 -0
- sverify-0.2.0/tests/test_ibis_semantics.py +125 -0
- sverify-0.2.0/tests/test_ibis_substrait.py +229 -0
- sverify-0.2.0/tests/test_incremental.py +143 -0
- sverify-0.2.0/tests/test_io_delta.py +804 -0
- sverify-0.2.0/tests/test_io_iceberg.py +390 -0
- sverify-0.2.0/tests/test_isthmus.py +208 -0
- sverify-0.2.0/tests/test_nodes.py +329 -0
- sverify-0.2.0/tests/test_numeric.py +243 -0
- sverify-0.2.0/tests/test_partition.py +267 -0
- sverify-0.2.0/tests/test_pipeline.py +216 -0
- sverify-0.2.0/tests/test_polars.py +665 -0
- sverify-0.2.0/tests/test_public_api.py +51 -0
- sverify-0.2.0/tests/test_publish_notebooks.py +88 -0
- sverify-0.2.0/tests/test_readme.py +65 -0
- sverify-0.2.0/tests/test_relations.py +127 -0
- sverify-0.2.0/tests/test_rewrites.py +170 -0
- sverify-0.2.0/tests/test_semantic.py +478 -0
- sverify-0.2.0/tests/test_skills.py +34 -0
- sverify-0.2.0/tests/test_sql.py +731 -0
- sverify-0.2.0/tests/test_sql_models.py +176 -0
- sverify-0.2.0/tests/test_sql_snowflake.py +463 -0
- sverify-0.2.0/tests/test_sql_spark.py +272 -0
- sverify-0.2.0/tests/test_text.py +152 -0
- sverify-0.2.0/tests/test_time.py +654 -0
- sverify-0.2.0/tests/test_windows.py +323 -0
- sverify-0.2.0/tests/test_without_datafusion.py +152 -0
- sverify-0.2.0/uv.lock +4043 -0
sverify-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/.lake
|
|
2
|
+
/float-bounds/.lake
|
|
3
|
+
/python/.venv
|
|
4
|
+
.hypothesis/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
__pycache__/
|
|
7
|
+
/python/src/sverify/_bin/
|
|
8
|
+
/python/build/
|
|
9
|
+
/python/dist/
|
|
10
|
+
/python/wheelhouse/
|
|
11
|
+
__marimo__/
|
|
12
|
+
# Great Docs build output (`poe docs`)
|
|
13
|
+
/python/great-docs/
|
|
14
|
+
/python/.great-docs*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
sverify-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sverify
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Contracts and incremental views, proven in Lean, on DuckDB, Polars, DataFusion, Snowflake and Spark
|
|
5
|
+
Project-URL: Homepage, https://github.com/AlexanderNenninger/substrait-lean
|
|
6
|
+
Project-URL: Documentation, https://alexandernenninger.github.io/substrait-lean/
|
|
7
|
+
Project-URL: Source, https://github.com/AlexanderNenninger/substrait-lean
|
|
8
|
+
Project-URL: Issues, https://github.com/AlexanderNenninger/substrait-lean/issues
|
|
9
|
+
Author-email: Alexander Nenninger <alexander.nenninger@live.de>
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: duckdb>=2.0.0
|
|
12
|
+
Requires-Dist: ibis-framework[duckdb]>=12.0.0
|
|
13
|
+
Requires-Dist: pyarrow>=26.0.0
|
|
14
|
+
Requires-Dist: substrait>=0.37.0
|
|
15
|
+
Provides-Extra: all
|
|
16
|
+
Requires-Dist: datafusion==51.0.0; extra == 'all'
|
|
17
|
+
Requires-Dist: deltalake>=1.6.6; extra == 'all'
|
|
18
|
+
Requires-Dist: numpy>=2.5.4; extra == 'all'
|
|
19
|
+
Requires-Dist: polars>=2.1.0; extra == 'all'
|
|
20
|
+
Requires-Dist: pyiceberg[pyarrow]>=0.12.0; extra == 'all'
|
|
21
|
+
Requires-Dist: pyspark>=4.0; extra == 'all'
|
|
22
|
+
Provides-Extra: datafusion
|
|
23
|
+
Requires-Dist: datafusion==51.0.0; extra == 'datafusion'
|
|
24
|
+
Provides-Extra: delta
|
|
25
|
+
Requires-Dist: deltalake>=1.6.6; extra == 'delta'
|
|
26
|
+
Provides-Extra: iceberg
|
|
27
|
+
Requires-Dist: pyiceberg[pyarrow]>=0.12.0; extra == 'iceberg'
|
|
28
|
+
Provides-Extra: polars
|
|
29
|
+
Requires-Dist: numpy>=2.5.4; extra == 'polars'
|
|
30
|
+
Requires-Dist: polars>=2.1.0; extra == 'polars'
|
|
31
|
+
Provides-Extra: spark
|
|
32
|
+
Requires-Dist: pyspark>=4.0; extra == 'spark'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# sverify
|
|
36
|
+
|
|
37
|
+
**Prove a data pipeline's contracts before it runs, get the same result on
|
|
38
|
+
[DuckDB](https://duckdb.org), [Polars](https://pola.rs),
|
|
39
|
+
[DataFusion](https://datafusion.apache.org), [Spark](https://spark.apache.org)
|
|
40
|
+
and Snowflake, and keep it up to date from Delta Lake and Iceberg change
|
|
41
|
+
feeds.**
|
|
42
|
+
|
|
43
|
+
You write a transformation in [Ibis](https://ibis-project.org) or SQL and say
|
|
44
|
+
what it assumes about its inputs (`requires`) and what it guarantees about
|
|
45
|
+
its output (`ensures`). A checker written in [Lean](https://lean-lang.org)
|
|
46
|
+
proves the guarantees for every possible input, or says why it can't. Then:
|
|
47
|
+
|
|
48
|
+
* **Contracts are proved, not tested.** `not_null`, `unique`, ranges and
|
|
49
|
+
relations between columns hold on every input that meets the
|
|
50
|
+
preconditions, and division by zero, overflow and bad casts are ruled out
|
|
51
|
+
before the job runs. Preconditions are checked on the data, or proved from
|
|
52
|
+
Delta and Iceberg metadata.
|
|
53
|
+
* **One plan gives the same answer on every engine.** The checked plan runs
|
|
54
|
+
on DuckDB, Polars or DataFusion, or is written as Snowflake or Spark SQL,
|
|
55
|
+
with NULLs, integer division, NaN, ties and overflow spelled out, or it is
|
|
56
|
+
refused where an engine can't match.
|
|
57
|
+
* **Results are kept up to date from the changes,** by delta plans proved
|
|
58
|
+
equal to a full recompute, read from Delta's Change Data Feed or Iceberg's
|
|
59
|
+
appends, and written to sinks exactly once, with the contract attached.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install sverify # or: uv add sverify
|
|
63
|
+
pip install 'sverify[all]' # with every engine and table format
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The core runs on DuckDB and writes DuckDB, Snowflake and Spark SQL. Extras:
|
|
67
|
+
`datafusion` (also compiles SQL strings), `polars`, `spark` (needs Java 17 or
|
|
68
|
+
21), `delta`, `iceberg`, or `all`. Wheels bundle the checker as a small
|
|
69
|
+
native binary, for Linux (x86_64, aarch64) and macOS (arm64, x86_64), Python
|
|
70
|
+
3.12 and later; elsewhere pip builds from the sdist, which needs
|
|
71
|
+
[elan](https://github.com/leanprover/elan).
|
|
72
|
+
|
|
73
|
+
The [API reference](https://alexandernenninger.github.io/substrait-lean/)
|
|
74
|
+
documents every public function and class, for the released version. For
|
|
75
|
+
coding agents, the site also publishes
|
|
76
|
+
[skills](https://alexandernenninger.github.io/substrait-lean/skills.html) for
|
|
77
|
+
writing contracts, pipelines and engine code with sverify
|
|
78
|
+
(`npx skills add https://alexandernenninger.github.io/substrait-lean/`); they
|
|
79
|
+
ship in the wheel too, under `sverify/skills/`.
|
|
80
|
+
|
|
81
|
+
## In thirty lines
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import pyarrow as pa
|
|
85
|
+
import sverify as sv
|
|
86
|
+
from ibis import _
|
|
87
|
+
from sverify.io import StaticSource
|
|
88
|
+
|
|
89
|
+
orders = StaticSource( # in memory; DeltaSource and IcebergSource work the same way
|
|
90
|
+
pa.table({"id": [1, 2], "customer": ["ada", "bob"], "amount": [30, 5]}), name="orders"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@sv.transform(
|
|
95
|
+
requires={"orders": [_.amount >= 0, sv.unique("id")]},
|
|
96
|
+
ensures=[_.doubled >= 22, _.doubled.notnull(), sv.unique("id")],
|
|
97
|
+
)
|
|
98
|
+
def big_orders(orders):
|
|
99
|
+
return orders.filter(orders.amount > 10).select(orders.id, doubled=orders.amount * 2)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
big = big_orders(orders) # proved here, before any data runs
|
|
103
|
+
big.refresh() # computed, on DuckDB by default
|
|
104
|
+
orders.apply(sv.Changes(inserts=[(3, "cy", 50)], deletes=[(1, "ada", 30)]))
|
|
105
|
+
big.refresh(engine="polars") # updated from the changes alone, on another engine
|
|
106
|
+
assert big.to_arrow().to_pylist() == [{"id": 3, "doubled": 100}]
|
|
107
|
+
print(big_orders.last_report)
|
|
108
|
+
# requires orders: (_.amount >= 0)
|
|
109
|
+
# requires orders: unique(id)
|
|
110
|
+
# ensures (_.doubled >= 22): proved
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A postcondition that doesn't follow is refused before anything runs:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
@sv.transform(requires={"orders": [_.amount >= 0]}, ensures=[_.total > 5])
|
|
117
|
+
def plus_one(orders):
|
|
118
|
+
return orders.select(orders.id, total=orders.amount + 1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
from sverify.errors import VerificationError
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
plus_one(orders)
|
|
125
|
+
except VerificationError as e:
|
|
126
|
+
print(e)
|
|
127
|
+
# could not prove: (_.total > 5) (it is TRUE or FALSE)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## SQL models
|
|
131
|
+
|
|
132
|
+
A transform's body can be SQL, over its inputs by name. It is proved,
|
|
133
|
+
composed and refreshed like any other:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
@sv.transform(
|
|
137
|
+
requires={"orders": ["amount >= 0", "amount IS NOT NULL"]},
|
|
138
|
+
ensures=["total >= 0", sv.unique("customer")],
|
|
139
|
+
)
|
|
140
|
+
def per_customer(orders):
|
|
141
|
+
return sv.sql(
|
|
142
|
+
"SELECT customer, coalesce(sum(amount), 0) AS total FROM orders GROUP BY customer",
|
|
143
|
+
orders=orders,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
totals = per_customer(orders) # proved here, like an Ibis transform
|
|
148
|
+
totals.refresh()
|
|
149
|
+
assert sorted(totals.to_arrow().to_pylist(), key=str) == [
|
|
150
|
+
{"customer": "bob", "total": 5},
|
|
151
|
+
{"customer": "cy", "total": 50},
|
|
152
|
+
]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Several inputs are several keywords (`sv.sql(query, orders=orders,
|
|
156
|
+
customers=customers)`). The SQL is planned by DataFusion
|
|
157
|
+
(`pip install 'sverify[datafusion]'`), or by Isthmus without it.
|
|
158
|
+
|
|
159
|
+
## Workflows
|
|
160
|
+
|
|
161
|
+
Three complete jobs from a catalogue of 139 real analytical queries, each
|
|
162
|
+
from source tables to a sink, with what goes wrong without the checker. Open
|
|
163
|
+
them in the browser:
|
|
164
|
+
|
|
165
|
+
* [Duplicate card payments](https://molab.marimo.io/github/AlexanderNenninger/substrait-lean/blob/published/notebooks/01_duplicate_payments.py):
|
|
166
|
+
a self-join over a Delta table, refreshed from its Change Data Feed, where
|
|
167
|
+
a watermark job would miss a late duplicate and keep a reversed one.
|
|
168
|
+
* [Sensor alerts](https://molab.marimo.io/github/AlexanderNenninger/substrait-lean/blob/published/notebooks/02_sensor_alerts.py):
|
|
169
|
+
`LAG` over an Iceberg table, where ties and a NaN reading change the alerts,
|
|
170
|
+
and Snowflake and Spark need proved tie-freedom.
|
|
171
|
+
* [An orders mart](https://molab.marimo.io/github/AlexanderNenninger/substrait-lean/blob/published/notebooks/03_orders_mart.py):
|
|
172
|
+
dbt tests as a proved contract on a point-in-time join to a type-2
|
|
173
|
+
dimension, in Ibis and in SQL.
|
|
174
|
+
|
|
175
|
+
The [whole tour](https://github.com/AlexanderNenninger/substrait-lean/tree/main/notebooks)
|
|
176
|
+
explains every feature in a chapter of its own; it runs locally with
|
|
177
|
+
`uvx marimo edit --sandbox notebooks` from a clone.
|
|
178
|
+
|
|
179
|
+
## The API in brief
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
import json
|
|
183
|
+
|
|
184
|
+
import ibis
|
|
185
|
+
from sverify.engines import SNOWFLAKE, to_sql # also DUCKDB, SPARK
|
|
186
|
+
from sverify.io import DeltaSink, DeltaSource # IcebergSource, IcebergSink likewise
|
|
187
|
+
|
|
188
|
+
sv.verify(
|
|
189
|
+
"select id, amount from orders where amount > 0",
|
|
190
|
+
tables={"orders": ibis.table({"id": "int64", "amount": "int64"}, name="orders")},
|
|
191
|
+
ensures=["amount > 0"],
|
|
192
|
+
) # SQL, checked; returns the report
|
|
193
|
+
to_sql(json.loads(big.plan), SNOWFLAKE) # the checked plan, or sverify.engines.Unsupported
|
|
194
|
+
|
|
195
|
+
# p = paid(DeltaSource("s3://.../orders")).write(DeltaSink("s3://.../paid")) # stored: a source
|
|
196
|
+
# r = revenue(p).write(DeltaSink("s3://.../revenue"))
|
|
197
|
+
# r.refresh(engine="polars"); sv.group(r, other).refresh(); r.to_pandas()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
* `@sv.transform(requires=..., ensures=..., on_unproved="error" | "runtime" | "warn")`
|
|
201
|
+
proves a contract on each call; `.last_report` says what was proved and
|
|
202
|
+
what is known about every output column. `sv.unique(...)` states keys.
|
|
203
|
+
* Called on sources (`sverify.io`) or other nodes, transforms build nodes
|
|
204
|
+
that compose directly: a downstream precondition is proved from the
|
|
205
|
+
upstream contract (`CompositionError` names a broken link).
|
|
206
|
+
* Without `.write` in between, transforms fuse into one plan; `.write(sink)`
|
|
207
|
+
stores a node, exactly once per state, and nodes downstream read it as a
|
|
208
|
+
versioned source. `refresh(engine=...)` updates the whole lineage, and
|
|
209
|
+
`sv.group(a, b)` several outputs against the same source versions.
|
|
210
|
+
* Filters, projections, inner and cross joins and `UNION ALL` are
|
|
211
|
+
maintained from the changes; anything else is recomputed and diffed.
|
|
212
|
+
* `sverify.engines` has the engines, SQL dialects and plans
|
|
213
|
+
(`run_sharded` runs a plan per shard and combines the results, with a
|
|
214
|
+
proof that the result is the same); `sverify.errors` the exceptions.
|
|
215
|
+
|
|
216
|
+
**Not supported yet:** DECIMAL columns (keep money as integer cents),
|
|
217
|
+
framed window aggregates, `RANK` and `NTILE`. The repository's README has
|
|
218
|
+
the coverage table, the full guide, and what is proved and what is trusted.
|
|
219
|
+
|
|
220
|
+
## Configuration
|
|
221
|
+
|
|
222
|
+
* `SVERIFY_CHECK`: path to a `substrait-check` binary to use instead of the
|
|
223
|
+
bundled one, for example your own `lake build`.
|
|
224
|
+
* `SVERIFY_ISTHMUS`: path to an [Isthmus](https://github.com/substrait-io/substrait-java/releases)
|
|
225
|
+
binary (otherwise `isthmus` on `PATH`), to compile SQL strings without
|
|
226
|
+
DataFusion; `sv.connect(sql_producer="isthmus")` picks it, and it is the
|
|
227
|
+
default when DataFusion is not installed. sverify doesn't ship it (~120 MB);
|
|
228
|
+
`scripts/fetch_isthmus.sh <dir>` downloads the pinned release.
|
|
229
|
+
* DataFusion is pinned to 51, because Ibis 12's DataFusion backend doesn't
|
|
230
|
+
connect to later versions.
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
git clone https://github.com/AlexanderNenninger/substrait-lean
|
|
236
|
+
cd substrait-lean/python
|
|
237
|
+
uv sync # builds substrait-check with lake (needs elan) and links it in
|
|
238
|
+
uv run poe qa # proofs, lint, typecheck, golden, Python tests, notebooks
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
See the [repository](https://github.com/AlexanderNenninger/substrait-lean) for setup details.
|
sverify-0.2.0/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# sverify
|
|
2
|
+
|
|
3
|
+
**Prove a data pipeline's contracts before it runs, get the same result on
|
|
4
|
+
[DuckDB](https://duckdb.org), [Polars](https://pola.rs),
|
|
5
|
+
[DataFusion](https://datafusion.apache.org), [Spark](https://spark.apache.org)
|
|
6
|
+
and Snowflake, and keep it up to date from Delta Lake and Iceberg change
|
|
7
|
+
feeds.**
|
|
8
|
+
|
|
9
|
+
You write a transformation in [Ibis](https://ibis-project.org) or SQL and say
|
|
10
|
+
what it assumes about its inputs (`requires`) and what it guarantees about
|
|
11
|
+
its output (`ensures`). A checker written in [Lean](https://lean-lang.org)
|
|
12
|
+
proves the guarantees for every possible input, or says why it can't. Then:
|
|
13
|
+
|
|
14
|
+
* **Contracts are proved, not tested.** `not_null`, `unique`, ranges and
|
|
15
|
+
relations between columns hold on every input that meets the
|
|
16
|
+
preconditions, and division by zero, overflow and bad casts are ruled out
|
|
17
|
+
before the job runs. Preconditions are checked on the data, or proved from
|
|
18
|
+
Delta and Iceberg metadata.
|
|
19
|
+
* **One plan gives the same answer on every engine.** The checked plan runs
|
|
20
|
+
on DuckDB, Polars or DataFusion, or is written as Snowflake or Spark SQL,
|
|
21
|
+
with NULLs, integer division, NaN, ties and overflow spelled out, or it is
|
|
22
|
+
refused where an engine can't match.
|
|
23
|
+
* **Results are kept up to date from the changes,** by delta plans proved
|
|
24
|
+
equal to a full recompute, read from Delta's Change Data Feed or Iceberg's
|
|
25
|
+
appends, and written to sinks exactly once, with the contract attached.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install sverify # or: uv add sverify
|
|
29
|
+
pip install 'sverify[all]' # with every engine and table format
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The core runs on DuckDB and writes DuckDB, Snowflake and Spark SQL. Extras:
|
|
33
|
+
`datafusion` (also compiles SQL strings), `polars`, `spark` (needs Java 17 or
|
|
34
|
+
21), `delta`, `iceberg`, or `all`. Wheels bundle the checker as a small
|
|
35
|
+
native binary, for Linux (x86_64, aarch64) and macOS (arm64, x86_64), Python
|
|
36
|
+
3.12 and later; elsewhere pip builds from the sdist, which needs
|
|
37
|
+
[elan](https://github.com/leanprover/elan).
|
|
38
|
+
|
|
39
|
+
The [API reference](https://alexandernenninger.github.io/substrait-lean/)
|
|
40
|
+
documents every public function and class, for the released version. For
|
|
41
|
+
coding agents, the site also publishes
|
|
42
|
+
[skills](https://alexandernenninger.github.io/substrait-lean/skills.html) for
|
|
43
|
+
writing contracts, pipelines and engine code with sverify
|
|
44
|
+
(`npx skills add https://alexandernenninger.github.io/substrait-lean/`); they
|
|
45
|
+
ship in the wheel too, under `sverify/skills/`.
|
|
46
|
+
|
|
47
|
+
## In thirty lines
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import pyarrow as pa
|
|
51
|
+
import sverify as sv
|
|
52
|
+
from ibis import _
|
|
53
|
+
from sverify.io import StaticSource
|
|
54
|
+
|
|
55
|
+
orders = StaticSource( # in memory; DeltaSource and IcebergSource work the same way
|
|
56
|
+
pa.table({"id": [1, 2], "customer": ["ada", "bob"], "amount": [30, 5]}), name="orders"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@sv.transform(
|
|
61
|
+
requires={"orders": [_.amount >= 0, sv.unique("id")]},
|
|
62
|
+
ensures=[_.doubled >= 22, _.doubled.notnull(), sv.unique("id")],
|
|
63
|
+
)
|
|
64
|
+
def big_orders(orders):
|
|
65
|
+
return orders.filter(orders.amount > 10).select(orders.id, doubled=orders.amount * 2)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
big = big_orders(orders) # proved here, before any data runs
|
|
69
|
+
big.refresh() # computed, on DuckDB by default
|
|
70
|
+
orders.apply(sv.Changes(inserts=[(3, "cy", 50)], deletes=[(1, "ada", 30)]))
|
|
71
|
+
big.refresh(engine="polars") # updated from the changes alone, on another engine
|
|
72
|
+
assert big.to_arrow().to_pylist() == [{"id": 3, "doubled": 100}]
|
|
73
|
+
print(big_orders.last_report)
|
|
74
|
+
# requires orders: (_.amount >= 0)
|
|
75
|
+
# requires orders: unique(id)
|
|
76
|
+
# ensures (_.doubled >= 22): proved
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A postcondition that doesn't follow is refused before anything runs:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
@sv.transform(requires={"orders": [_.amount >= 0]}, ensures=[_.total > 5])
|
|
83
|
+
def plus_one(orders):
|
|
84
|
+
return orders.select(orders.id, total=orders.amount + 1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
from sverify.errors import VerificationError
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
plus_one(orders)
|
|
91
|
+
except VerificationError as e:
|
|
92
|
+
print(e)
|
|
93
|
+
# could not prove: (_.total > 5) (it is TRUE or FALSE)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## SQL models
|
|
97
|
+
|
|
98
|
+
A transform's body can be SQL, over its inputs by name. It is proved,
|
|
99
|
+
composed and refreshed like any other:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
@sv.transform(
|
|
103
|
+
requires={"orders": ["amount >= 0", "amount IS NOT NULL"]},
|
|
104
|
+
ensures=["total >= 0", sv.unique("customer")],
|
|
105
|
+
)
|
|
106
|
+
def per_customer(orders):
|
|
107
|
+
return sv.sql(
|
|
108
|
+
"SELECT customer, coalesce(sum(amount), 0) AS total FROM orders GROUP BY customer",
|
|
109
|
+
orders=orders,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
totals = per_customer(orders) # proved here, like an Ibis transform
|
|
114
|
+
totals.refresh()
|
|
115
|
+
assert sorted(totals.to_arrow().to_pylist(), key=str) == [
|
|
116
|
+
{"customer": "bob", "total": 5},
|
|
117
|
+
{"customer": "cy", "total": 50},
|
|
118
|
+
]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Several inputs are several keywords (`sv.sql(query, orders=orders,
|
|
122
|
+
customers=customers)`). The SQL is planned by DataFusion
|
|
123
|
+
(`pip install 'sverify[datafusion]'`), or by Isthmus without it.
|
|
124
|
+
|
|
125
|
+
## Workflows
|
|
126
|
+
|
|
127
|
+
Three complete jobs from a catalogue of 139 real analytical queries, each
|
|
128
|
+
from source tables to a sink, with what goes wrong without the checker. Open
|
|
129
|
+
them in the browser:
|
|
130
|
+
|
|
131
|
+
* [Duplicate card payments](https://molab.marimo.io/github/AlexanderNenninger/substrait-lean/blob/published/notebooks/01_duplicate_payments.py):
|
|
132
|
+
a self-join over a Delta table, refreshed from its Change Data Feed, where
|
|
133
|
+
a watermark job would miss a late duplicate and keep a reversed one.
|
|
134
|
+
* [Sensor alerts](https://molab.marimo.io/github/AlexanderNenninger/substrait-lean/blob/published/notebooks/02_sensor_alerts.py):
|
|
135
|
+
`LAG` over an Iceberg table, where ties and a NaN reading change the alerts,
|
|
136
|
+
and Snowflake and Spark need proved tie-freedom.
|
|
137
|
+
* [An orders mart](https://molab.marimo.io/github/AlexanderNenninger/substrait-lean/blob/published/notebooks/03_orders_mart.py):
|
|
138
|
+
dbt tests as a proved contract on a point-in-time join to a type-2
|
|
139
|
+
dimension, in Ibis and in SQL.
|
|
140
|
+
|
|
141
|
+
The [whole tour](https://github.com/AlexanderNenninger/substrait-lean/tree/main/notebooks)
|
|
142
|
+
explains every feature in a chapter of its own; it runs locally with
|
|
143
|
+
`uvx marimo edit --sandbox notebooks` from a clone.
|
|
144
|
+
|
|
145
|
+
## The API in brief
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import json
|
|
149
|
+
|
|
150
|
+
import ibis
|
|
151
|
+
from sverify.engines import SNOWFLAKE, to_sql # also DUCKDB, SPARK
|
|
152
|
+
from sverify.io import DeltaSink, DeltaSource # IcebergSource, IcebergSink likewise
|
|
153
|
+
|
|
154
|
+
sv.verify(
|
|
155
|
+
"select id, amount from orders where amount > 0",
|
|
156
|
+
tables={"orders": ibis.table({"id": "int64", "amount": "int64"}, name="orders")},
|
|
157
|
+
ensures=["amount > 0"],
|
|
158
|
+
) # SQL, checked; returns the report
|
|
159
|
+
to_sql(json.loads(big.plan), SNOWFLAKE) # the checked plan, or sverify.engines.Unsupported
|
|
160
|
+
|
|
161
|
+
# p = paid(DeltaSource("s3://.../orders")).write(DeltaSink("s3://.../paid")) # stored: a source
|
|
162
|
+
# r = revenue(p).write(DeltaSink("s3://.../revenue"))
|
|
163
|
+
# r.refresh(engine="polars"); sv.group(r, other).refresh(); r.to_pandas()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
* `@sv.transform(requires=..., ensures=..., on_unproved="error" | "runtime" | "warn")`
|
|
167
|
+
proves a contract on each call; `.last_report` says what was proved and
|
|
168
|
+
what is known about every output column. `sv.unique(...)` states keys.
|
|
169
|
+
* Called on sources (`sverify.io`) or other nodes, transforms build nodes
|
|
170
|
+
that compose directly: a downstream precondition is proved from the
|
|
171
|
+
upstream contract (`CompositionError` names a broken link).
|
|
172
|
+
* Without `.write` in between, transforms fuse into one plan; `.write(sink)`
|
|
173
|
+
stores a node, exactly once per state, and nodes downstream read it as a
|
|
174
|
+
versioned source. `refresh(engine=...)` updates the whole lineage, and
|
|
175
|
+
`sv.group(a, b)` several outputs against the same source versions.
|
|
176
|
+
* Filters, projections, inner and cross joins and `UNION ALL` are
|
|
177
|
+
maintained from the changes; anything else is recomputed and diffed.
|
|
178
|
+
* `sverify.engines` has the engines, SQL dialects and plans
|
|
179
|
+
(`run_sharded` runs a plan per shard and combines the results, with a
|
|
180
|
+
proof that the result is the same); `sverify.errors` the exceptions.
|
|
181
|
+
|
|
182
|
+
**Not supported yet:** DECIMAL columns (keep money as integer cents),
|
|
183
|
+
framed window aggregates, `RANK` and `NTILE`. The repository's README has
|
|
184
|
+
the coverage table, the full guide, and what is proved and what is trusted.
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
* `SVERIFY_CHECK`: path to a `substrait-check` binary to use instead of the
|
|
189
|
+
bundled one, for example your own `lake build`.
|
|
190
|
+
* `SVERIFY_ISTHMUS`: path to an [Isthmus](https://github.com/substrait-io/substrait-java/releases)
|
|
191
|
+
binary (otherwise `isthmus` on `PATH`), to compile SQL strings without
|
|
192
|
+
DataFusion; `sv.connect(sql_producer="isthmus")` picks it, and it is the
|
|
193
|
+
default when DataFusion is not installed. sverify doesn't ship it (~120 MB);
|
|
194
|
+
`scripts/fetch_isthmus.sh <dir>` downloads the pinned release.
|
|
195
|
+
* DataFusion is pinned to 51, because Ibis 12's DataFusion backend doesn't
|
|
196
|
+
connect to later versions.
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git clone https://github.com/AlexanderNenninger/substrait-lean
|
|
202
|
+
cd substrait-lean/python
|
|
203
|
+
uv sync # builds substrait-check with lake (needs elan) and links it in
|
|
204
|
+
uv run poe qa # proofs, lint, typecheck, golden, Python tests, notebooks
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
See the [repository](https://github.com/AlexanderNenninger/substrait-lean) for setup details.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Great Docs configuration for the sverify API reference (`uv run poe docs`).
|
|
2
|
+
# See https://posit-dev.github.io/great-docs/user-guide/03-configuration.html
|
|
3
|
+
#
|
|
4
|
+
# The landing page is this directory's README.md (the PyPI page). The notebook
|
|
5
|
+
# tour is linked on molab, not embedded: the notebooks call the native Lean
|
|
6
|
+
# checker, so they can't run in the browser.
|
|
7
|
+
|
|
8
|
+
module: sverify
|
|
9
|
+
|
|
10
|
+
display_name: "sverify"
|
|
11
|
+
|
|
12
|
+
navbar_style: "sky"
|
|
13
|
+
|
|
14
|
+
parser: google
|
|
15
|
+
|
|
16
|
+
# Static analysis (griffe reads the source). Runtime introspection resolves
|
|
17
|
+
# `io.DeltaSource` as an attribute of `sverify`, and `import sverify` doesn't
|
|
18
|
+
# import all of its submodules (`sverify.io` loads delta-rs and pyiceberg
|
|
19
|
+
# lazily).
|
|
20
|
+
dynamic: false
|
|
21
|
+
|
|
22
|
+
# Sections in the order a data engineer looks for things: what a transform
|
|
23
|
+
# promises, how its result is built, stored and kept up to date, where its
|
|
24
|
+
# tables live, where it runs, what can go wrong. The public API is the ten
|
|
25
|
+
# top-level names plus `sverify.io`, `sverify.engines` and `sverify.errors`;
|
|
26
|
+
# everything else is internal (CHANGELOG.md, 0.2.0). `Node`'s results
|
|
27
|
+
# (`Refresh`, `Status`, `Group`) live in `sverify.nodes` and are documented
|
|
28
|
+
# because users hold them, not because they are imported.
|
|
29
|
+
reference:
|
|
30
|
+
- title: Contracts
|
|
31
|
+
desc: >
|
|
32
|
+
State what a transformation assumes and promises, and have it proved
|
|
33
|
+
before any data runs.
|
|
34
|
+
contents:
|
|
35
|
+
- transform
|
|
36
|
+
- verify
|
|
37
|
+
- unique
|
|
38
|
+
- references
|
|
39
|
+
- Report
|
|
40
|
+
|
|
41
|
+
- title: Transforms, nodes and refresh
|
|
42
|
+
desc: >
|
|
43
|
+
A contracted transform called on a source builds a node. Nodes compose,
|
|
44
|
+
are stored with `.write(sink)`, and are kept up to date by `.refresh()`,
|
|
45
|
+
with update plans proved equal to a full recompute.
|
|
46
|
+
contents:
|
|
47
|
+
- Node
|
|
48
|
+
- group
|
|
49
|
+
- Changes
|
|
50
|
+
- nodes.Group
|
|
51
|
+
- nodes.Refresh
|
|
52
|
+
- nodes.Status
|
|
53
|
+
- nodes.NodeStatus
|
|
54
|
+
- nodes.StageRefresh
|
|
55
|
+
|
|
56
|
+
- title: Sources and sinks
|
|
57
|
+
desc: >
|
|
58
|
+
Delta Lake and Iceberg tables as versioned sources, sources of
|
|
59
|
+
preconditions, and sinks that carry the contract (`sverify.io`).
|
|
60
|
+
contents:
|
|
61
|
+
- io.DeltaSource
|
|
62
|
+
- io.DeltaSink
|
|
63
|
+
- io.IcebergSource
|
|
64
|
+
- io.IcebergSink
|
|
65
|
+
- io.StaticSource
|
|
66
|
+
- io.SinkWrite
|
|
67
|
+
- io.Contract
|
|
68
|
+
|
|
69
|
+
- title: Engines and SQL
|
|
70
|
+
desc: >
|
|
71
|
+
Run a checked plan on DuckDB, Polars, DataFusion or Spark, or write it
|
|
72
|
+
as DuckDB, Snowflake or Spark SQL (`sverify.engines`).
|
|
73
|
+
contents:
|
|
74
|
+
- connect
|
|
75
|
+
- engines.run
|
|
76
|
+
- engines.ENGINES
|
|
77
|
+
- engines.Connection
|
|
78
|
+
- engines.create_table
|
|
79
|
+
- engines.table_rows
|
|
80
|
+
- engines.to_sql
|
|
81
|
+
- engines.Dialect
|
|
82
|
+
- engines.DUCKDB
|
|
83
|
+
- engines.SNOWFLAKE
|
|
84
|
+
- engines.SPARK
|
|
85
|
+
- engines.Unsupported
|
|
86
|
+
- engines.run_spark
|
|
87
|
+
- engines.SPARK_CONF
|
|
88
|
+
- engines.run_sharded
|
|
89
|
+
|
|
90
|
+
- title: ASOF joins
|
|
91
|
+
desc: Align two time series, each row with the latest earlier row of the other.
|
|
92
|
+
contents:
|
|
93
|
+
- asof_join
|
|
94
|
+
|
|
95
|
+
- title: Plans
|
|
96
|
+
desc: >
|
|
97
|
+
The low-level layer: Substrait plans, Lean's written form of them, and
|
|
98
|
+
the update plans of incremental maintenance (`sverify.engines`).
|
|
99
|
+
contents:
|
|
100
|
+
- engines.substrait_json
|
|
101
|
+
- engines.isthmus_json
|
|
102
|
+
- engines.SQL_PRODUCERS
|
|
103
|
+
- engines.written_plan
|
|
104
|
+
- engines.run_substrait_json
|
|
105
|
+
- engines.delta_plans
|
|
106
|
+
|
|
107
|
+
- title: Errors
|
|
108
|
+
desc: What sverify raises, and when (`sverify.errors`).
|
|
109
|
+
contents:
|
|
110
|
+
- errors.VerificationError
|
|
111
|
+
- errors.PreconditionError
|
|
112
|
+
- errors.PostconditionError
|
|
113
|
+
- errors.CompositionError
|
|
114
|
+
- errors.NotIncremental
|
|
115
|
+
- errors.AuditError
|
|
116
|
+
- errors.SchemaError
|
|
117
|
+
- errors.SqlError
|
|
118
|
+
|
|
119
|
+
# Agent skills for sverify users, written by hand and shipped in the wheel
|
|
120
|
+
# (`sverify/skills/<name>/SKILL.md`, with their `references/`). The site
|
|
121
|
+
# publishes them under `.well-known/agent-skills/` with a Skills page;
|
|
122
|
+
# `tests/test_skills.py` runs their code.
|
|
123
|
+
skill:
|
|
124
|
+
enabled: true
|
|
125
|
+
well_known: true
|
|
126
|
+
skills:
|
|
127
|
+
- name: sverify
|
|
128
|
+
file: src/sverify/skills/sverify/SKILL.md
|
|
129
|
+
- name: sverify-pipelines
|
|
130
|
+
file: src/sverify/skills/sverify-pipelines/SKILL.md
|
|
131
|
+
- name: sverify-engines
|
|
132
|
+
file: src/sverify/skills/sverify-engines/SKILL.md
|
|
133
|
+
|
|
134
|
+
jupyter: python3
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Build `substrait-check` with lake and ship it inside the wheel.
|
|
2
|
+
|
|
3
|
+
The wheel is tagged `py3-none-<platform>`: the Python code is pure, the binary
|
|
4
|
+
is not. A regular wheel gets a stripped copy in `sverify/_bin/`; an editable
|
|
5
|
+
install gets a symlink to `.lake/build/bin/substrait-check`, so a later
|
|
6
|
+
`lake build` takes effect without reinstalling.
|
|
7
|
+
|
|
8
|
+
The Lean project is the repository root, or `lean/` in an unpacked sdist.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import platform
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
20
|
+
|
|
21
|
+
BINARY = "substrait-check"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def platform_tag() -> str:
|
|
25
|
+
machine = platform.machine().lower()
|
|
26
|
+
if sys.platform == "darwin":
|
|
27
|
+
# Lean's linker records no real minimum macOS version (`minos 99.0`),
|
|
28
|
+
# so use the oldest one each architecture's toolchain supports.
|
|
29
|
+
return "macosx_11_0_arm64" if machine == "arm64" else "macosx_10_15_x86_64"
|
|
30
|
+
if sys.platform == "linux":
|
|
31
|
+
# `auditwheel repair` turns this into a manylinux tag in CI.
|
|
32
|
+
return f"linux_{machine}"
|
|
33
|
+
raise RuntimeError(f"sverify wheels are not supported on {sys.platform}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LakeBuildHook(BuildHookInterface):
|
|
37
|
+
def initialize(self, version: str, build_data: dict) -> None:
|
|
38
|
+
repo = Path(self.root) / "lean"
|
|
39
|
+
if not repo.is_dir():
|
|
40
|
+
repo = Path(self.root).parent
|
|
41
|
+
lake = shutil.which("lake") or str(Path.home() / ".elan" / "bin" / "lake")
|
|
42
|
+
if not Path(lake).exists():
|
|
43
|
+
raise RuntimeError("building sverify needs `lake` (install elan) on PATH")
|
|
44
|
+
subprocess.run([lake, "build", BINARY], cwd=repo, check=True)
|
|
45
|
+
|
|
46
|
+
built = repo / ".lake" / "build" / "bin" / BINARY
|
|
47
|
+
if version == "editable":
|
|
48
|
+
link = Path(self.root) / "src" / "sverify" / "_bin" / BINARY
|
|
49
|
+
link.parent.mkdir(exist_ok=True)
|
|
50
|
+
link.unlink(missing_ok=True)
|
|
51
|
+
link.symlink_to(built)
|
|
52
|
+
else:
|
|
53
|
+
staged = Path(self.root) / "build" / BINARY
|
|
54
|
+
staged.parent.mkdir(exist_ok=True)
|
|
55
|
+
shutil.copy2(built, staged)
|
|
56
|
+
subprocess.run(["strip", str(staged)], check=True)
|
|
57
|
+
build_data["force_include"][str(staged)] = f"sverify/_bin/{BINARY}"
|
|
58
|
+
|
|
59
|
+
build_data["pure_python"] = False
|
|
60
|
+
build_data["tag"] = f"py3-none-{platform_tag()}"
|