interlaced 2.8.7__tar.gz → 2.8.8__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.7/src/interlaced.egg-info → interlaced-2.8.8}/PKG-INFO +1 -1
- {interlaced-2.8.7 → interlaced-2.8.8}/pyproject.toml +1 -1
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/duckdb.py +239 -64
- {interlaced-2.8.7 → interlaced-2.8.8/src/interlaced.egg-info}/PKG-INFO +1 -1
- {interlaced-2.8.7 → interlaced-2.8.8}/LICENSE +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/MANIFEST.in +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/README.md +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/setup.cfg +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/decode.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/publish.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/slot.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/builtin.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/runner.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/spec.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/build.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/inspect.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/main.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/operate.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/support.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/config/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/config/config.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/connections.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/contracts.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/decorators.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/discovery.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/dynamic.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/sql_config.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/adbc.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/base.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/bigquery.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/postgres.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/quack.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/redshift.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/registry.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/snowflake.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/spark.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/exceptions.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/column_lineage.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/dag.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/project.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/selectors.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/inputs.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/inspect.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/canonicalize.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/dialect.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/fingerprint.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/layout.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/macros.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/relation.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/vars.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/mcp_server.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/changes.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/ddl.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/drift.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/reconcile.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/spec.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/annotate.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/apply.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/backfill.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/comment.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/delivery.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/differ.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/files.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/fit.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/orchestrate.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/payload.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/plan.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/resolve.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/result.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/run.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/schedule.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/table_diff.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/transfer.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/project.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/py.typed +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/query.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/runtime/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/runtime/handles.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/runtime/python_model.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scaffold.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/completion.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/daemon.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/engine.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/triggers.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/worker.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/app.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/auth.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/lifespan.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/present.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/admin.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/catalog.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/environments.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/models.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/plan.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/runs.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/streams.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/types.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/app.css +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/favicon.svg +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/index.html +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/api.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/app.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/dag.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/timeline.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/ui.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/checks.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/environments.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/lineage.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/models.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/overview.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/plan.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/query.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/runs.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/streams.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/system.js +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sinks.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sources/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sources/auth.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sources/rest.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/advisory.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/beat.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/cdc.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/checks.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/codec.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/conn.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/events.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/interval.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/janitor.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/keys.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/locks.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/pg.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/queue.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/schema.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/snapshot.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/snapshots.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/store.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/triggers.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/append.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/base.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/full_merge.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/hash_merge.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/incremental.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/merge.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/replace.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/replace_in_place.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/scd.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/view.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/log.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/materializer.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/schema.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/README.md +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/docker-compose.yml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/init/seed.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/interlace.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/models/orders.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/models/orders_by_status.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/models/orders_stream.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/push.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/template.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/README.md +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/generate.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/interlace.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/events.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/events_by_type.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/top_users.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/user_spend.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/template.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/README.md +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/interlace.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/models/github_issues.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/template.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/README.md +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/docker-compose.yml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/init/seed.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/interlace.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/models/orders.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/template.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/README.md +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/interlace.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/template.yaml +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/testing/__init__.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/testing/golden.py +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/SOURCES.txt +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/dependency_links.txt +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/entry_points.txt +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/requires.txt +0 -0
- {interlaced-2.8.7 → interlaced-2.8.8}/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.8
|
|
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
|
|
@@ -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.8"
|
|
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"
|
|
@@ -7,15 +7,17 @@ DuckDB calls run in a worker thread; each call uses its own ``cursor()`` so read
|
|
|
7
7
|
proceed concurrently (DuckDB MVCC), while the DAG guarantees no two tasks write
|
|
8
8
|
the same table at once.
|
|
9
9
|
|
|
10
|
-
On **DuckLake** connections
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
On **DuckLake** connections every cursor — reads included — is serialised on
|
|
11
|
+
``_write_lock``. DuckLake's catalog layer is not safe against sibling cursors of
|
|
12
|
+
one DatabaseInstance: under parallel builds a ``CREATE TABLE <schema>.<name>``
|
|
13
|
+
intermittently commits with an empty schema name and lands in the catalog's
|
|
14
|
+
default schema (``main``), silently. Serialising writes alone does not stop it;
|
|
15
|
+
a read cursor open across the commit is enough. Reads are materialised inside
|
|
16
|
+
the lock so that cursor is closed first. On an attached catalog the CREATE is
|
|
17
|
+
rewritten to ``catalog.schema.table`` so the schema is part of the name, not
|
|
18
|
+
resolved through the default schema. If the table still is not in the schema the
|
|
19
|
+
statement named, the transaction is rolled back and retried — it is not kept
|
|
20
|
+
and copied out of ``main``.
|
|
19
21
|
|
|
20
22
|
Plain DuckDB catalogs don't have that bug, and the DAG already guarantees no
|
|
21
23
|
two tasks write the same table — so there the "lock" is a no-op context and
|
|
@@ -36,6 +38,7 @@ from uuid import uuid4
|
|
|
36
38
|
|
|
37
39
|
import duckdb
|
|
38
40
|
import pyarrow as pa
|
|
41
|
+
import sqlglot
|
|
39
42
|
import tenacity
|
|
40
43
|
from sqlglot import exp
|
|
41
44
|
|
|
@@ -113,6 +116,89 @@ def _affected(cur: duckdb.DuckDBPyConnection) -> int:
|
|
|
113
116
|
return 0
|
|
114
117
|
|
|
115
118
|
|
|
119
|
+
class CatalogPlacementError(Exception):
|
|
120
|
+
"""DuckLake committed a CREATE under a schema other than the one the statement named.
|
|
121
|
+
|
|
122
|
+
A plain Exception, not an :class:`~interlace.exceptions.InterlaceError`: the scheduler
|
|
123
|
+
wraps those as ``model '…' failed: …`` and does not record the snapshot.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _qualified(schema: str, name: str) -> str:
|
|
128
|
+
return f"{exp.to_identifier(schema).sql(dialect='duckdb')}.{exp.to_identifier(name).sql(dialect='duckdb')}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _created_tables(sql: str) -> list[tuple[str, str, str]]:
|
|
132
|
+
"""``(schema, name, kind)`` for a CREATE TABLE/VIEW that names a schema. Empty when the
|
|
133
|
+
statement is not one, or names no schema — an unqualified create cannot be told
|
|
134
|
+
apart from the DuckLake bug that drops the schema."""
|
|
135
|
+
try:
|
|
136
|
+
expression = sqlglot.parse_one(sql, read="duckdb")
|
|
137
|
+
except Exception:
|
|
138
|
+
return []
|
|
139
|
+
if not isinstance(expression, exp.Create):
|
|
140
|
+
return []
|
|
141
|
+
kind = str(expression.args.get("kind") or "").upper()
|
|
142
|
+
if kind not in {"TABLE", "VIEW"}:
|
|
143
|
+
return []
|
|
144
|
+
target = expression.this
|
|
145
|
+
if isinstance(target, exp.Schema):
|
|
146
|
+
target = target.this
|
|
147
|
+
if not isinstance(target, exp.Table) or not target.db or not target.name:
|
|
148
|
+
return []
|
|
149
|
+
return [(target.db, target.name, kind)]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _placed_schemas(cur: duckdb.DuckDBPyConnection, name: str, *, kind: str) -> set[str]:
|
|
153
|
+
"""Schemas in the current catalog that hold this table, or this view — not both.
|
|
154
|
+
|
|
155
|
+
Environment views share a short name (``main.a`` and ``dev__main.a``). Mixing the
|
|
156
|
+
two types made a view promotion look like the DuckLake bug and drop the other view.
|
|
157
|
+
"""
|
|
158
|
+
table_type = "VIEW" if kind == "VIEW" else "BASE TABLE"
|
|
159
|
+
rows = cur.execute(
|
|
160
|
+
"SELECT table_schema FROM information_schema.tables "
|
|
161
|
+
"WHERE table_name = ? AND table_type = ? AND table_catalog = current_database()",
|
|
162
|
+
[name, table_type],
|
|
163
|
+
).fetchall()
|
|
164
|
+
return {str(row[0]) for row in rows if row and row[0]}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _drop_elsewhere(cur: duckdb.DuckDBPyConnection, schema: str, name: str, *, kind: str) -> None:
|
|
168
|
+
"""Drop a table copy in ``main`` left by an autocommitted CREATE. A transaction
|
|
169
|
+
rolls back instead. Views are never dropped: another environment's view may share
|
|
170
|
+
the name."""
|
|
171
|
+
if kind != "TABLE":
|
|
172
|
+
return
|
|
173
|
+
for placed in _placed_schemas(cur, name, kind=kind):
|
|
174
|
+
if placed.casefold() == "main" and placed.casefold() != schema.casefold():
|
|
175
|
+
cur.execute(f"DROP TABLE IF EXISTS {_qualified(placed, name)}")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _ensure_placed(cur: duckdb.DuckDBPyConnection, schema: str, name: str, *, kind: str = "TABLE") -> None:
|
|
179
|
+
"""Reject a CREATE whose schema name did not survive.
|
|
180
|
+
|
|
181
|
+
DuckLake sometimes records the new table under ``main`` (an empty schema name in
|
|
182
|
+
the commit). That CREATE is wrong — copying the rows across would publish a table
|
|
183
|
+
that was born without its schema. The caller rolls the statement back and retries
|
|
184
|
+
it. A leftover *table* already in ``main`` beside one that did land in the named
|
|
185
|
+
schema is the previous occurrence of this bug and is dropped. Views are left alone.
|
|
186
|
+
"""
|
|
187
|
+
if not schema or schema.casefold() == "main":
|
|
188
|
+
return
|
|
189
|
+
found = _placed_schemas(cur, name, kind=kind)
|
|
190
|
+
if any(placed.casefold() == schema.casefold() for placed in found):
|
|
191
|
+
if kind == "TABLE":
|
|
192
|
+
for placed in found:
|
|
193
|
+
if placed.casefold() == "main":
|
|
194
|
+
cur.execute(f"DROP TABLE IF EXISTS {_qualified(placed, name)}")
|
|
195
|
+
return
|
|
196
|
+
if not found:
|
|
197
|
+
return
|
|
198
|
+
where = ", ".join(sorted(found))
|
|
199
|
+
raise CatalogPlacementError(f"DuckLake created {schema}.{name} in schema {where} with an empty schema name")
|
|
200
|
+
|
|
201
|
+
|
|
116
202
|
class DuckDBAdapter(EngineAdapter):
|
|
117
203
|
"""Executes canonical ASTs and moves Arrow data in and out of a DuckDB database."""
|
|
118
204
|
|
|
@@ -126,19 +212,23 @@ class DuckDBAdapter(EngineAdapter):
|
|
|
126
212
|
session_init: Sequence[str] = (),
|
|
127
213
|
*,
|
|
128
214
|
serialise_writes: bool = False,
|
|
215
|
+
catalog_alias: str | None = None,
|
|
129
216
|
) -> None:
|
|
130
217
|
self._conn = connection
|
|
218
|
+
# Attached DuckLake alias (``warehouse``, or the project name). CREATE targets
|
|
219
|
+
# are written ``alias.schema.table`` so the schema is not resolved through the
|
|
220
|
+
# session default, which is ``main``.
|
|
221
|
+
self._catalog_alias = catalog_alias
|
|
131
222
|
# Statements re-applied on every cursor — SESSION-LOCAL state only (USE).
|
|
132
223
|
# Anything instance-wide (LOAD, secrets, ATTACH) belongs at connect time:
|
|
133
224
|
# re-running catalog writes here races across concurrent cursors.
|
|
134
225
|
self._session_init = list(session_init)
|
|
135
226
|
self._attached: list[str] = [] # aliases to DETACH on close (see close())
|
|
136
227
|
self.caps = _DUCKLAKE_CAPS if serialise_writes else _DUCKDB_CAPS
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# re-entry.
|
|
228
|
+
# One cursor at a time on DuckLake (see module docstring); a no-op context
|
|
229
|
+
# elsewhere so builds run in parallel. Plain Lock, not RLock: no locked
|
|
230
|
+
# path calls another, and a plain Lock turns an accidental nesting into an
|
|
231
|
+
# obvious deadlock rather than silent re-entry.
|
|
142
232
|
self._write_lock: contextlib.AbstractContextManager[object] = (
|
|
143
233
|
threading.Lock() if serialise_writes else contextlib.nullcontext()
|
|
144
234
|
)
|
|
@@ -195,7 +285,7 @@ class DuckDBAdapter(EngineAdapter):
|
|
|
195
285
|
# cursor and must run ONCE (re-running CREATE OR REPLACE SECRET per cursor
|
|
196
286
|
# races: concurrent cursors hit "catalog write-write conflict on alter").
|
|
197
287
|
# Only the default catalog is session state, so that is all a cursor re-applies.
|
|
198
|
-
return cls(conn, session_init=[f"USE {alias_sql}"], serialise_writes=True)
|
|
288
|
+
return cls(conn, session_init=[f"USE {alias_sql}"], serialise_writes=True, catalog_alias=alias)
|
|
199
289
|
|
|
200
290
|
def close(self) -> None:
|
|
201
291
|
# DETACH long-lived attaches first: DuckLake leaks its DatabaseInstance when
|
|
@@ -315,45 +405,119 @@ class DuckDBAdapter(EngineAdapter):
|
|
|
315
405
|
|
|
316
406
|
# --- sync workers (run in a thread) -------------------------------------
|
|
317
407
|
|
|
408
|
+
def _pin_create_catalog(self, sql: str) -> str:
|
|
409
|
+
"""Write an attached DuckLake CREATE as ``catalog.schema.table``.
|
|
410
|
+
|
|
411
|
+
A two-part ``schema.table`` is resolved in the session's default schema when
|
|
412
|
+
DuckLake drops the qualifier. The catalog alias makes the schema part of the
|
|
413
|
+
name DuckLake commits.
|
|
414
|
+
"""
|
|
415
|
+
alias = self._catalog_alias
|
|
416
|
+
if not alias:
|
|
417
|
+
return sql
|
|
418
|
+
try:
|
|
419
|
+
expression = sqlglot.parse_one(sql, read="duckdb")
|
|
420
|
+
except Exception:
|
|
421
|
+
return sql
|
|
422
|
+
if not isinstance(expression, exp.Create):
|
|
423
|
+
return sql
|
|
424
|
+
target = expression.this
|
|
425
|
+
if isinstance(target, exp.Schema):
|
|
426
|
+
target = target.this
|
|
427
|
+
if not isinstance(target, exp.Table) or not target.db or target.catalog:
|
|
428
|
+
return sql
|
|
429
|
+
target.set("catalog", exp.to_identifier(alias))
|
|
430
|
+
return expression.sql(dialect="duckdb")
|
|
431
|
+
|
|
432
|
+
def _guard_creates(self, cur: duckdb.DuckDBPyConnection, sql: str) -> None:
|
|
433
|
+
for schema, name, kind in _created_tables(sql):
|
|
434
|
+
_ensure_placed(cur, schema, name, kind=kind)
|
|
435
|
+
|
|
436
|
+
def _execute_once(self, sql: str) -> None:
|
|
437
|
+
cur = self._cursor()
|
|
438
|
+
try:
|
|
439
|
+
cur.execute(sql)
|
|
440
|
+
self._guard_creates(cur, sql)
|
|
441
|
+
except CatalogPlacementError:
|
|
442
|
+
# Autocommit: the empty-schema table is already stored. Drop it so the
|
|
443
|
+
# retry creates the named schema, and so a failed retry leaves nothing in main.
|
|
444
|
+
for schema, name, kind in _created_tables(sql):
|
|
445
|
+
_drop_elsewhere(cur, schema, name, kind=kind)
|
|
446
|
+
raise
|
|
447
|
+
except Exception as exc:
|
|
448
|
+
note_statement(exc, sql)
|
|
449
|
+
raise
|
|
450
|
+
finally:
|
|
451
|
+
cur.close()
|
|
452
|
+
|
|
318
453
|
@_commit_retry
|
|
319
454
|
def _execute_sync(self, sql: str) -> None:
|
|
455
|
+
sql = self._pin_create_catalog(sql)
|
|
320
456
|
with self._write_lock: # may be DDL (create_schema / create_view / migrations)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
457
|
+
last: CatalogPlacementError | None = None
|
|
458
|
+
for _attempt in range(3):
|
|
459
|
+
try:
|
|
460
|
+
self._execute_once(sql)
|
|
461
|
+
return
|
|
462
|
+
except CatalogPlacementError as exc:
|
|
463
|
+
note_statement(exc, sql)
|
|
464
|
+
last = exc
|
|
465
|
+
assert last is not None
|
|
466
|
+
raise last
|
|
467
|
+
|
|
468
|
+
def _execute_all_once(self, sqls: list[str]) -> list[int]:
|
|
469
|
+
counts: list[int] = []
|
|
470
|
+
cur = self._cursor()
|
|
471
|
+
try:
|
|
472
|
+
cur.execute("BEGIN")
|
|
473
|
+
for sql in sqls:
|
|
474
|
+
try:
|
|
475
|
+
cur.execute(sql)
|
|
476
|
+
counts.append(_affected(cur))
|
|
477
|
+
# Before COMMIT. A missing schema rolls this transaction back;
|
|
478
|
+
# the table never becomes the recorded snapshot.
|
|
479
|
+
self._guard_creates(cur, sql)
|
|
480
|
+
except Exception as exc:
|
|
481
|
+
note_statement(exc, sql)
|
|
482
|
+
raise
|
|
483
|
+
cur.execute("COMMIT")
|
|
484
|
+
except Exception:
|
|
485
|
+
cur.execute("ROLLBACK")
|
|
486
|
+
raise
|
|
487
|
+
finally:
|
|
488
|
+
cur.close()
|
|
489
|
+
return counts
|
|
329
490
|
|
|
330
491
|
@_commit_retry
|
|
331
492
|
def _execute_all_sync(self, sqls: list[str]) -> list[int]:
|
|
332
|
-
|
|
493
|
+
sqls = [self._pin_create_catalog(sql) for sql in sqls]
|
|
333
494
|
with self._write_lock: # a strategy's CREATE / DELETE / INSERT / DROP batch
|
|
495
|
+
last: CatalogPlacementError | None = None
|
|
496
|
+
for _attempt in range(3):
|
|
497
|
+
try:
|
|
498
|
+
return self._execute_all_once(sqls)
|
|
499
|
+
except CatalogPlacementError as exc:
|
|
500
|
+
last = exc
|
|
501
|
+
assert last is not None
|
|
502
|
+
raise last
|
|
503
|
+
|
|
504
|
+
def _fetch_sync(self, sql: str) -> pa.RecordBatchReader:
|
|
505
|
+
# Plain DuckDB streams: the cursor must outlive the reader, and is closed
|
|
506
|
+
# when that stream ends rather than left for the GC. DuckLake cannot: a
|
|
507
|
+
# cursor left open across another task's CREATE is what drops the schema,
|
|
508
|
+
# so the scan is materialised inside the lock and the cursor closed first.
|
|
509
|
+
if isinstance(self._write_lock, contextlib.nullcontext):
|
|
510
|
+
return self._stream_sync(sql)
|
|
511
|
+
with self._write_lock:
|
|
334
512
|
cur = self._cursor()
|
|
335
513
|
try:
|
|
336
|
-
cur.execute(
|
|
337
|
-
|
|
338
|
-
try:
|
|
339
|
-
cur.execute(sql)
|
|
340
|
-
except Exception as exc:
|
|
341
|
-
note_statement(exc, sql)
|
|
342
|
-
raise
|
|
343
|
-
counts.append(_affected(cur))
|
|
344
|
-
cur.execute("COMMIT")
|
|
345
|
-
except Exception:
|
|
346
|
-
cur.execute("ROLLBACK")
|
|
347
|
-
raise
|
|
514
|
+
cur.execute(sql)
|
|
515
|
+
table = cur.to_arrow_reader().read_all()
|
|
348
516
|
finally:
|
|
349
517
|
cur.close()
|
|
350
|
-
return
|
|
518
|
+
return table.to_reader()
|
|
351
519
|
|
|
352
|
-
def
|
|
353
|
-
# Read-only: deliberately not locked, so scans run concurrently (DuckDB MVCC).
|
|
354
|
-
# The cursor must outlive the stream that reads from it, so it is closed when
|
|
355
|
-
# that stream ends rather than left for the garbage collector to reclaim on
|
|
356
|
-
# whatever thread happens to drop the last reference.
|
|
520
|
+
def _stream_sync(self, sql: str) -> pa.RecordBatchReader:
|
|
357
521
|
cur = self._cursor()
|
|
358
522
|
cur.execute(sql)
|
|
359
523
|
reader = cur.to_arrow_reader()
|
|
@@ -379,9 +543,18 @@ class DuckDBAdapter(EngineAdapter):
|
|
|
379
543
|
try:
|
|
380
544
|
target = self._table_sql(table)
|
|
381
545
|
if mode == "create":
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
546
|
+
sql = self._pin_create_catalog(f"CREATE OR REPLACE TABLE {target} AS SELECT * FROM {src}")
|
|
547
|
+
cur.execute(sql)
|
|
548
|
+
# Row count before the placement probe: that probe runs its own statements.
|
|
549
|
+
written = _affected(cur)
|
|
550
|
+
try:
|
|
551
|
+
self._guard_creates(cur, sql)
|
|
552
|
+
except CatalogPlacementError:
|
|
553
|
+
for schema, name, kind in _created_tables(sql):
|
|
554
|
+
_drop_elsewhere(cur, schema, name, kind=kind)
|
|
555
|
+
raise
|
|
556
|
+
return written
|
|
557
|
+
cur.execute(f"INSERT INTO {target} SELECT * FROM {src}")
|
|
385
558
|
return _affected(cur)
|
|
386
559
|
finally:
|
|
387
560
|
cur.unregister(src)
|
|
@@ -390,26 +563,28 @@ class DuckDBAdapter(EngineAdapter):
|
|
|
390
563
|
def _table_exists_sync(self, table: TableRef) -> bool:
|
|
391
564
|
# information_schema spans every attached catalog: pin to the ref's catalog
|
|
392
565
|
# (or the session default) so same-named tables elsewhere don't collide.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
566
|
+
with self._write_lock:
|
|
567
|
+
cur = self._cursor()
|
|
568
|
+
try:
|
|
569
|
+
row = cur.execute(
|
|
570
|
+
"SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? "
|
|
571
|
+
"AND table_catalog = coalesce(?, current_database())",
|
|
572
|
+
[table.schema, table.name, table.catalog],
|
|
573
|
+
).fetchone()
|
|
574
|
+
finally:
|
|
575
|
+
cur.close()
|
|
402
576
|
return bool(row and row[0])
|
|
403
577
|
|
|
404
578
|
def _describe_sync(self, table: TableRef) -> dict[str, str]:
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
579
|
+
with self._write_lock:
|
|
580
|
+
cur = self._cursor()
|
|
581
|
+
try:
|
|
582
|
+
rows = cur.execute(
|
|
583
|
+
"SELECT column_name, data_type FROM information_schema.columns "
|
|
584
|
+
"WHERE table_schema = ? AND table_name = ? "
|
|
585
|
+
"AND table_catalog = coalesce(?, current_database()) ORDER BY ordinal_position",
|
|
586
|
+
[table.schema, table.name, table.catalog],
|
|
587
|
+
).fetchall()
|
|
588
|
+
finally:
|
|
589
|
+
cur.close()
|
|
415
590
|
return dict(rows)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: interlaced
|
|
3
|
-
Version: 2.8.
|
|
3
|
+
Version: 2.8.8
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|