interlaced 2.8.0__tar.gz → 2.8.2__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.
- {interlaced-2.8.0/src/interlaced.egg-info → interlaced-2.8.2}/PKG-INFO +2 -2
- {interlaced-2.8.0 → interlaced-2.8.2}/pyproject.toml +2 -2
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cdc/publish.py +2 -2
- interlaced-2.8.2/src/interlace/cdc/slot.py +110 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/runner.py +47 -2
- interlaced-2.8.2/src/interlace/cli/build.py +496 -0
- interlaced-2.8.2/src/interlace/cli/inspect.py +522 -0
- interlaced-2.8.2/src/interlace/cli/main.py +29 -0
- interlaced-2.8.2/src/interlace/cli/operate.py +578 -0
- interlaced-2.8.2/src/interlace/cli/support.py +310 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/decorators.py +3 -2
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/base.py +25 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/duckdb.py +8 -1
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/quack.py +5 -1
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/spark.py +2 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/inspect.py +38 -0
- interlaced-2.8.2/src/interlace/ir/layout.py +30 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/mcp_server.py +10 -14
- interlaced-2.8.2/src/interlace/physical/changes.py +45 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/drift.py +8 -3
- {interlaced-2.8.0/src/interlace/physical → interlaced-2.8.2/src/interlace/plan}/annotate.py +3 -2
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/apply.py +237 -174
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/differ.py +9 -11
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/orchestrate.py +1 -1
- interlaced-2.8.2/src/interlace/plan/payload.py +86 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/plan.py +41 -64
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/run.py +11 -24
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/project.py +24 -2
- interlaced-2.8.2/src/interlace/scheduler/daemon.py +217 -0
- interlaced-2.8.2/src/interlace/service/app.py +383 -0
- interlaced-2.8.2/src/interlace/service/present.py +235 -0
- interlaced-2.8.2/src/interlace/service/routes/__init__.py +1 -0
- interlaced-2.8.2/src/interlace/service/routes/admin.py +259 -0
- interlaced-2.8.2/src/interlace/service/routes/catalog.py +243 -0
- interlaced-2.8.2/src/interlace/service/routes/environments.py +75 -0
- interlaced-2.8.2/src/interlace/service/routes/models.py +183 -0
- interlaced-2.8.2/src/interlace/service/routes/plan.py +187 -0
- interlaced-2.8.2/src/interlace/service/routes/runs.py +149 -0
- interlaced-2.8.2/src/interlace/service/routes/streams.py +242 -0
- interlaced-2.8.2/src/interlace/state/advisory.py +84 -0
- interlaced-2.8.2/src/interlace/state/cdc.py +70 -0
- interlaced-2.8.2/src/interlace/state/checks.py +62 -0
- interlaced-2.8.2/src/interlace/state/codec.py +127 -0
- interlaced-2.8.2/src/interlace/state/conn.py +40 -0
- interlaced-2.8.2/src/interlace/state/events.py +129 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/interval.py +13 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/janitor.py +31 -6
- interlaced-2.8.2/src/interlace/state/keys.py +92 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/locks.py +4 -4
- interlaced-2.8.2/src/interlace/state/queue.py +291 -0
- interlaced-2.8.2/src/interlace/state/schema.py +182 -0
- interlaced-2.8.2/src/interlace/state/snapshots.py +353 -0
- interlaced-2.8.2/src/interlace/state/store.py +377 -0
- interlaced-2.8.2/src/interlace/state/triggers.py +35 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/__init__.py +19 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/append.py +3 -7
- interlaced-2.8.2/src/interlace/strategies/base.py +141 -0
- interlaced-2.8.2/src/interlace/strategies/full_merge.py +154 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/hash_merge.py +5 -7
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/incremental.py +7 -9
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/merge.py +10 -14
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/replace.py +8 -11
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/replace_in_place.py +3 -7
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/scd.py +10 -10
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/view.py +7 -7
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/materializer.py +43 -27
- interlaced-2.8.2/src/interlace/templates/cdc/README.md +66 -0
- interlaced-2.8.2/src/interlace/templates/cdc/docker-compose.yml +29 -0
- interlaced-2.8.2/src/interlace/templates/cdc/init/seed.sql +22 -0
- interlaced-2.8.2/src/interlace/templates/cdc/interlace.yaml +21 -0
- interlaced-2.8.2/src/interlace/templates/cdc/models/orders.sql +27 -0
- interlaced-2.8.2/src/interlace/templates/cdc/models/orders_by_status.sql +14 -0
- interlaced-2.8.2/src/interlace/templates/cdc/models/orders_stream.py +25 -0
- interlaced-2.8.2/src/interlace/templates/cdc/push.py +56 -0
- interlaced-2.8.2/src/interlace/templates/cdc/template.yaml +4 -0
- {interlaced-2.8.0 → interlaced-2.8.2/src/interlaced.egg-info}/PKG-INFO +2 -2
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/SOURCES.txt +38 -1
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/requires.txt +1 -1
- interlaced-2.8.0/src/interlace/cdc/slot.py +0 -97
- interlaced-2.8.0/src/interlace/cli/main.py +0 -1867
- interlaced-2.8.0/src/interlace/service/app.py +0 -1988
- interlaced-2.8.0/src/interlace/state/store.py +0 -1413
- interlaced-2.8.0/src/interlace/strategies/base.py +0 -89
- interlaced-2.8.0/src/interlace/strategies/full_merge.py +0 -96
- {interlaced-2.8.0 → interlaced-2.8.2}/LICENSE +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/MANIFEST.in +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/README.md +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/setup.cfg +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cdc/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cdc/decode.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/builtin.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/spec.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cli/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/config/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/config/config.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/connections.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/contracts.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/discovery.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/dynamic.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/sql_config.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/adbc.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/bigquery.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/postgres.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/redshift.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/registry.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/snowflake.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/exceptions.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/column_lineage.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/dag.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/project.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/selectors.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/inputs.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/canonicalize.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/fingerprint.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/macros.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/relation.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/ddl.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/reconcile.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/spec.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/comment.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/resolve.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/table_diff.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/py.typed +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/query.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/runtime/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/runtime/handles.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/runtime/python_model.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scaffold.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/engine.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/triggers.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/worker.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/auth.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/types.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/app.css +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/favicon.svg +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/index.html +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/api.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/app.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/dag.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/timeline.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/ui.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/checks.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/environments.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/lineage.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/models.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/overview.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/plan.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/query.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/runs.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/streams.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/system.js +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sinks.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sources/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sources/auth.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sources/rest.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/snapshot.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/log.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/schema.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/README.md +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/generate.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/interlace.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/events.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/events_by_type.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/top_users.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/user_spend.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/template.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/README.md +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/interlace.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/models/github_issues.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/template.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/README.md +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/docker-compose.yml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/init/seed.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/interlace.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/models/orders.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/template.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/README.md +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/interlace.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/template.yaml +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/testing/__init__.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/testing/golden.py +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/dependency_links.txt +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/entry_points.txt +0 -0
- {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: interlaced
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.2
|
|
4
4
|
Summary: Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres.
|
|
5
5
|
Author-email: Mark <mark@interlace.sh>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,7 +22,7 @@ Classifier: Operating System :: POSIX :: Linux
|
|
|
22
22
|
Requires-Python: >=3.12
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: sqlglot<31.0,>=
|
|
25
|
+
Requires-Dist: sqlglot<31.0,>=30.0
|
|
26
26
|
Requires-Dist: duckdb>=1.5.3
|
|
27
27
|
Requires-Dist: pyarrow>=17.0
|
|
28
28
|
Requires-Dist: pydantic<3.0,>=2.5
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "interlaced"
|
|
7
|
-
version = "2.8.
|
|
7
|
+
version = "2.8.2"
|
|
8
8
|
description = "Python and SQL models in one DAG. Transformation, orchestration and durable streaming in one process, on DuckDB and Postgres."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -30,7 +30,7 @@ classifiers = [
|
|
|
30
30
|
# Core: the IR + engine + state + plan spine (Phase 1). Everything is Arrow- and
|
|
31
31
|
# sqlglot-native. See docs/architecture/architecture.md.
|
|
32
32
|
dependencies = [
|
|
33
|
-
"sqlglot>=
|
|
33
|
+
"sqlglot>=30.0,<31.0", # canonical IR, transpilation, semantic diff, column lineage
|
|
34
34
|
"duckdb>=1.5.3", # default engine + federation hub; DuckLake storage + quack serving
|
|
35
35
|
"pyarrow>=17.0", # the wire format: RecordBatchReader everywhere
|
|
36
36
|
"pydantic>=2.5,<3.0", # config + manifest validation (cold paths only)
|
|
@@ -18,10 +18,10 @@ async def publish_changes(log: StreamLog, store: SqliteStateStore, stream: str,
|
|
|
18
18
|
stream,
|
|
19
19
|
[Event(payload=payload_of(change), idempotency_key=f"cdc:{change.lsn}") for change in changes],
|
|
20
20
|
)
|
|
21
|
-
await store.cdc_note_pending(stream, list(zip(result.offsets, (change.lsn for change in changes), strict=True)))
|
|
21
|
+
await store.cdc.cdc_note_pending(stream, list(zip(result.offsets, (change.lsn for change in changes), strict=True)))
|
|
22
22
|
return result.offsets
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
async def confirm_flushed(store: SqliteStateStore, stream: str, watermark: int) -> str | None:
|
|
26
26
|
"""Advance the stored LSN up to the offset ``flush_streams`` has committed."""
|
|
27
|
-
return await store.cdc_advance(stream, watermark)
|
|
27
|
+
return await store.cdc.cdc_advance(stream, watermark)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Read a Postgres logical replication slot with ``psycopg`` and decode ``pgoutput``.
|
|
2
|
+
|
|
3
|
+
psycopg 3 has no replication cursor, so this peeks the slot with
|
|
4
|
+
``pg_logical_slot_peek_binary_changes`` (Postgres 14+) and does not move it.
|
|
5
|
+
:meth:`SlotReader.feedback` advances the slot only after
|
|
6
|
+
:func:`interlace.cdc.publish.confirm_flushed` has stored the LSN — never when
|
|
7
|
+
the message is merely appended to the stream log.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from interlace.cdc.decode import Change, Relation, decode_message
|
|
15
|
+
from interlace.config.config import CdcConfig
|
|
16
|
+
from interlace.exceptions import ConfigurationError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SlotReader:
|
|
20
|
+
"""One Postgres connection for one slot. ``poll`` does not advance the slot."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, dsn: str, source: CdcConfig) -> None:
|
|
23
|
+
self._dsn = dsn
|
|
24
|
+
self._source = source
|
|
25
|
+
self._conn: Any = None
|
|
26
|
+
self._relations: dict[int, Relation] = {}
|
|
27
|
+
self._tables = set(source.tables)
|
|
28
|
+
|
|
29
|
+
def poll(self, confirmed_lsn: str | None, *, limit: int = 100) -> list[Change]:
|
|
30
|
+
"""Read up to ``limit`` decoded row changes. Returns immediately when the slot is idle.
|
|
31
|
+
|
|
32
|
+
``confirmed_lsn`` is the highest LSN already flushed. Changes at or below it are
|
|
33
|
+
skipped, so a peek that still contains them (feedback has not advanced the slot yet)
|
|
34
|
+
does not publish them again.
|
|
35
|
+
"""
|
|
36
|
+
confirmed = _lsn_key(confirmed_lsn) if confirmed_lsn else None
|
|
37
|
+
changes: list[Change] = []
|
|
38
|
+
# Begin, relation, and commit messages sit beside each row change.
|
|
39
|
+
for lsn, data in self._peek(max(limit * 4, limit)):
|
|
40
|
+
payload = bytes(data)
|
|
41
|
+
change = decode_message(payload, self._relations, lsn)
|
|
42
|
+
if change is None or not self._wanted(change.table):
|
|
43
|
+
continue
|
|
44
|
+
if confirmed is not None and _lsn_key(change.lsn) <= confirmed:
|
|
45
|
+
continue
|
|
46
|
+
changes.append(change)
|
|
47
|
+
if len(changes) >= limit:
|
|
48
|
+
break
|
|
49
|
+
return changes
|
|
50
|
+
|
|
51
|
+
def feedback(self, lsn: str) -> None:
|
|
52
|
+
"""Advance the slot to ``lsn``. Safe to call only after confirm_flushed."""
|
|
53
|
+
if not lsn or self._conn is None:
|
|
54
|
+
return
|
|
55
|
+
self._fetch("SELECT pg_replication_slot_advance(%s, %s::pg_lsn)", (self._source.slot, lsn))
|
|
56
|
+
|
|
57
|
+
def close(self) -> None:
|
|
58
|
+
conn = self._conn
|
|
59
|
+
self._conn = None
|
|
60
|
+
close = getattr(conn, "close", None)
|
|
61
|
+
if close is not None:
|
|
62
|
+
close()
|
|
63
|
+
|
|
64
|
+
def _wanted(self, table: str) -> bool:
|
|
65
|
+
if table in self._tables:
|
|
66
|
+
return True
|
|
67
|
+
return any(item == table.rsplit(".", 1)[-1] for item in self._tables)
|
|
68
|
+
|
|
69
|
+
def _peek(self, nchanges: int) -> list[tuple[str, bytes]]:
|
|
70
|
+
rows = self._fetch(
|
|
71
|
+
"""
|
|
72
|
+
SELECT lsn::text, data
|
|
73
|
+
FROM pg_logical_slot_peek_binary_changes(
|
|
74
|
+
%s, NULL, %s, 'proto_version', '1', 'publication_names', %s
|
|
75
|
+
)
|
|
76
|
+
""",
|
|
77
|
+
(self._source.slot, nchanges, self._source.publication),
|
|
78
|
+
)
|
|
79
|
+
return [(str(row[0]), bytes(row[1])) for row in rows]
|
|
80
|
+
|
|
81
|
+
def _fetch(self, sql: str, params: tuple[object, ...]) -> list[Any]:
|
|
82
|
+
conn = self._connection()
|
|
83
|
+
try:
|
|
84
|
+
with conn.cursor() as cur:
|
|
85
|
+
cur.execute(sql, params)
|
|
86
|
+
return list(cur.fetchall())
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
if "pg_logical_slot_peek_binary_changes" in str(exc):
|
|
89
|
+
raise ConfigurationError(
|
|
90
|
+
"CDC needs Postgres 14 or newer (pg_logical_slot_peek_binary_changes)."
|
|
91
|
+
) from exc
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
def _connection(self) -> Any:
|
|
95
|
+
if self._conn is not None:
|
|
96
|
+
return self._conn
|
|
97
|
+
try:
|
|
98
|
+
import psycopg # optional postgres extra
|
|
99
|
+
except ImportError as exc:
|
|
100
|
+
raise ConfigurationError("CDC needs the postgres extra (psycopg). Install interlaced[postgres].") from exc
|
|
101
|
+
self._conn = psycopg.connect(self._dsn, autocommit=True)
|
|
102
|
+
return self._conn
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _lsn_key(lsn: str) -> tuple[int, int]:
|
|
106
|
+
"""Order key for a ``pg_lsn`` text value (``high/low``, both hex)."""
|
|
107
|
+
high, sep, low = lsn.partition("/")
|
|
108
|
+
if not sep:
|
|
109
|
+
raise ValueError(f"not a pg_lsn: {lsn!r}")
|
|
110
|
+
return (int(high, 16), int(low, 16))
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import inspect
|
|
14
|
-
from collections.abc import Mapping
|
|
14
|
+
from collections.abc import Mapping, Sequence
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
17
|
import pyarrow as pa
|
|
@@ -21,10 +21,13 @@ from interlace.checks.builtin import build_check_query
|
|
|
21
21
|
from interlace.checks.spec import CheckSpec
|
|
22
22
|
from interlace.dsl.decorators import CheckDef
|
|
23
23
|
from interlace.engines.base import EngineAdapter
|
|
24
|
-
from interlace.
|
|
24
|
+
from interlace.engines.registry import EngineRegistry
|
|
25
|
+
from interlace.exceptions import DefinitionError, PlanError
|
|
25
26
|
from interlace.graph.project import CompiledModel, CompiledProject
|
|
26
27
|
from interlace.ir.relation import TableRef
|
|
27
28
|
from interlace.runtime.handles import RelationHandle
|
|
29
|
+
from interlace.sinks import target_ref
|
|
30
|
+
from interlace.state.store import SqliteStateStore
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
@dataclass(frozen=True)
|
|
@@ -101,3 +104,45 @@ async def run_checks(
|
|
|
101
104
|
outcomes = [await _run_declared(spec, model, compiled, engine, table, physical) for spec in model.checks]
|
|
102
105
|
outcomes += [await _run_python(check, engine, table, model.name) for check in python_checks]
|
|
103
106
|
return outcomes
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def run_promoted_checks(
|
|
110
|
+
compiled: CompiledProject,
|
|
111
|
+
store: SqliteStateStore,
|
|
112
|
+
engines: EngineRegistry,
|
|
113
|
+
environment: str,
|
|
114
|
+
selectors: Sequence[str] = (),
|
|
115
|
+
) -> tuple[list[CheckOutcome], list[str]]:
|
|
116
|
+
"""Re-check an environment's promoted tables. ``state:`` selectors see the ledger.
|
|
117
|
+
|
|
118
|
+
Returns ``(outcomes, skipped)``. Models that were never promoted in this
|
|
119
|
+
environment are skipped. A missing environment is a plan error.
|
|
120
|
+
"""
|
|
121
|
+
from interlace.plan.orchestrate import resolve_selection
|
|
122
|
+
|
|
123
|
+
chosen = await resolve_selection(compiled, store, environment, selectors)
|
|
124
|
+
promoted = await store.get_environment(environment)
|
|
125
|
+
if not promoted:
|
|
126
|
+
raise PlanError(f"no environment {environment!r} — run `interlace apply` first")
|
|
127
|
+
snapshots = await store.get_snapshots(promoted.items())
|
|
128
|
+
physical = {name: snapshot.physical_table for (name, _), snapshot in snapshots.items()}
|
|
129
|
+
outcomes: list[CheckOutcome] = []
|
|
130
|
+
skipped: list[str] = []
|
|
131
|
+
for name, model in compiled.models.items():
|
|
132
|
+
if chosen is not None and name not in chosen:
|
|
133
|
+
continue
|
|
134
|
+
if not model.checks and not compiled.python_checks.get(name):
|
|
135
|
+
continue
|
|
136
|
+
if model.materialise == "file" or (model.is_terminal and environment not in model.environments):
|
|
137
|
+
continue
|
|
138
|
+
snapshot = snapshots.get((name, promoted.get(name, "")))
|
|
139
|
+
if snapshot is None:
|
|
140
|
+
skipped.append(name)
|
|
141
|
+
continue
|
|
142
|
+
engine = engines.require(snapshot.engine, model=name)
|
|
143
|
+
check_table = target_ref(model.target or "") if model.materialise == "table" else snapshot.physical_table
|
|
144
|
+
results = await run_checks(model, compiled, engine, check_table, compiled.python_checks.get(name, ()), physical)
|
|
145
|
+
if results:
|
|
146
|
+
await store.record_check_results(environment, snapshot.fingerprint, results)
|
|
147
|
+
outcomes.extend(results)
|
|
148
|
+
return outcomes, skipped
|