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.
Files changed (209) hide show
  1. {interlaced-2.8.7/src/interlaced.egg-info → interlaced-2.8.8}/PKG-INFO +1 -1
  2. {interlaced-2.8.7 → interlaced-2.8.8}/pyproject.toml +1 -1
  3. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/duckdb.py +239 -64
  4. {interlaced-2.8.7 → interlaced-2.8.8/src/interlaced.egg-info}/PKG-INFO +1 -1
  5. {interlaced-2.8.7 → interlaced-2.8.8}/LICENSE +0 -0
  6. {interlaced-2.8.7 → interlaced-2.8.8}/MANIFEST.in +0 -0
  7. {interlaced-2.8.7 → interlaced-2.8.8}/README.md +0 -0
  8. {interlaced-2.8.7 → interlaced-2.8.8}/setup.cfg +0 -0
  9. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/__init__.py +0 -0
  10. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/__init__.py +0 -0
  11. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/decode.py +0 -0
  12. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/publish.py +0 -0
  13. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cdc/slot.py +0 -0
  14. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/__init__.py +0 -0
  15. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/builtin.py +0 -0
  16. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/runner.py +0 -0
  17. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/checks/spec.py +0 -0
  18. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/__init__.py +0 -0
  19. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/build.py +0 -0
  20. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/inspect.py +0 -0
  21. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/main.py +0 -0
  22. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/operate.py +0 -0
  23. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/cli/support.py +0 -0
  24. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/config/__init__.py +0 -0
  25. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/config/config.py +0 -0
  26. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/connections.py +0 -0
  27. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/contracts.py +0 -0
  28. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/__init__.py +0 -0
  29. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/decorators.py +0 -0
  30. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/discovery.py +0 -0
  31. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/dynamic.py +0 -0
  32. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/dsl/sql_config.py +0 -0
  33. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/__init__.py +0 -0
  34. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/adbc.py +0 -0
  35. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/base.py +0 -0
  36. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/bigquery.py +0 -0
  37. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/postgres.py +0 -0
  38. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/quack.py +0 -0
  39. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/redshift.py +0 -0
  40. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/registry.py +0 -0
  41. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/snowflake.py +0 -0
  42. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/engines/spark.py +0 -0
  43. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/exceptions.py +0 -0
  44. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/__init__.py +0 -0
  45. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/column_lineage.py +0 -0
  46. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/dag.py +0 -0
  47. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/project.py +0 -0
  48. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/graph/selectors.py +0 -0
  49. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/inputs.py +0 -0
  50. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/inspect.py +0 -0
  51. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/__init__.py +0 -0
  52. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/canonicalize.py +0 -0
  53. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/dialect.py +0 -0
  54. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/fingerprint.py +0 -0
  55. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/layout.py +0 -0
  56. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/macros.py +0 -0
  57. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/relation.py +0 -0
  58. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/ir/vars.py +0 -0
  59. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/mcp_server.py +0 -0
  60. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/__init__.py +0 -0
  61. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/changes.py +0 -0
  62. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/ddl.py +0 -0
  63. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/drift.py +0 -0
  64. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/reconcile.py +0 -0
  65. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/physical/spec.py +0 -0
  66. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/__init__.py +0 -0
  67. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/annotate.py +0 -0
  68. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/apply.py +0 -0
  69. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/backfill.py +0 -0
  70. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/comment.py +0 -0
  71. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/delivery.py +0 -0
  72. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/differ.py +0 -0
  73. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/files.py +0 -0
  74. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/fit.py +0 -0
  75. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/orchestrate.py +0 -0
  76. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/payload.py +0 -0
  77. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/plan.py +0 -0
  78. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/resolve.py +0 -0
  79. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/result.py +0 -0
  80. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/run.py +0 -0
  81. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/schedule.py +0 -0
  82. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/table_diff.py +0 -0
  83. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/plan/transfer.py +0 -0
  84. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/project.py +0 -0
  85. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/py.typed +0 -0
  86. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/query.py +0 -0
  87. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/runtime/__init__.py +0 -0
  88. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/runtime/handles.py +0 -0
  89. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/runtime/python_model.py +0 -0
  90. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scaffold.py +0 -0
  91. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/__init__.py +0 -0
  92. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/completion.py +0 -0
  93. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/daemon.py +0 -0
  94. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/engine.py +0 -0
  95. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/triggers.py +0 -0
  96. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/scheduler/worker.py +0 -0
  97. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/__init__.py +0 -0
  98. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/app.py +0 -0
  99. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/auth.py +0 -0
  100. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/lifespan.py +0 -0
  101. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/present.py +0 -0
  102. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/__init__.py +0 -0
  103. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/admin.py +0 -0
  104. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/catalog.py +0 -0
  105. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/environments.py +0 -0
  106. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/models.py +0 -0
  107. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/plan.py +0 -0
  108. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/runs.py +0 -0
  109. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/routes/streams.py +0 -0
  110. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/types.py +0 -0
  111. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/app.css +0 -0
  112. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/favicon.svg +0 -0
  113. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/index.html +0 -0
  114. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/api.js +0 -0
  115. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/app.js +0 -0
  116. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/dag.js +0 -0
  117. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/timeline.js +0 -0
  118. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/ui.js +0 -0
  119. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/checks.js +0 -0
  120. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/environments.js +0 -0
  121. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/lineage.js +0 -0
  122. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/models.js +0 -0
  123. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/overview.js +0 -0
  124. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/plan.js +0 -0
  125. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/query.js +0 -0
  126. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/runs.js +0 -0
  127. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/streams.js +0 -0
  128. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/service/ui/js/views/system.js +0 -0
  129. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sinks.py +0 -0
  130. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sources/__init__.py +0 -0
  131. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sources/auth.py +0 -0
  132. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/sources/rest.py +0 -0
  133. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/__init__.py +0 -0
  134. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/advisory.py +0 -0
  135. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/beat.py +0 -0
  136. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/cdc.py +0 -0
  137. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/checks.py +0 -0
  138. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/codec.py +0 -0
  139. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/conn.py +0 -0
  140. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/events.py +0 -0
  141. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/interval.py +0 -0
  142. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/janitor.py +0 -0
  143. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/keys.py +0 -0
  144. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/locks.py +0 -0
  145. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/pg.py +0 -0
  146. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/queue.py +0 -0
  147. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/schema.py +0 -0
  148. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/snapshot.py +0 -0
  149. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/snapshots.py +0 -0
  150. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/store.py +0 -0
  151. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/state/triggers.py +0 -0
  152. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/__init__.py +0 -0
  153. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/append.py +0 -0
  154. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/base.py +0 -0
  155. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/full_merge.py +0 -0
  156. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/hash_merge.py +0 -0
  157. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/incremental.py +0 -0
  158. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/merge.py +0 -0
  159. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/replace.py +0 -0
  160. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/replace_in_place.py +0 -0
  161. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/scd.py +0 -0
  162. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/strategies/view.py +0 -0
  163. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/__init__.py +0 -0
  164. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/log.py +0 -0
  165. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/materializer.py +0 -0
  166. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/streaming/schema.py +0 -0
  167. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/README.md +0 -0
  168. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/docker-compose.yml +0 -0
  169. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/init/seed.sql +0 -0
  170. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/interlace.yaml +0 -0
  171. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/models/orders.sql +0 -0
  172. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/models/orders_by_status.sql +0 -0
  173. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/models/orders_stream.py +0 -0
  174. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/push.py +0 -0
  175. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/cdc/template.yaml +0 -0
  176. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/README.md +0 -0
  177. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/generate.py +0 -0
  178. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/interlace.yaml +0 -0
  179. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/events.py +0 -0
  180. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
  181. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/events_by_type.sql +0 -0
  182. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/top_users.sql +0 -0
  183. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/models/user_spend.sql +0 -0
  184. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/events/template.yaml +0 -0
  185. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/README.md +0 -0
  186. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/interlace.yaml +0 -0
  187. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/models/github_issues.py +0 -0
  188. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
  189. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/github/template.yaml +0 -0
  190. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/README.md +0 -0
  191. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/docker-compose.yml +0 -0
  192. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/init/seed.sql +0 -0
  193. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/interlace.yaml +0 -0
  194. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/models/orders.py +0 -0
  195. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
  196. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/postgres/template.yaml +0 -0
  197. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/README.md +0 -0
  198. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/interlace.yaml +0 -0
  199. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
  200. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
  201. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
  202. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/templates/quickstart/template.yaml +0 -0
  203. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/testing/__init__.py +0 -0
  204. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlace/testing/golden.py +0 -0
  205. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/SOURCES.txt +0 -0
  206. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/dependency_links.txt +0 -0
  207. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/entry_points.txt +0 -0
  208. {interlaced-2.8.7 → interlaced-2.8.8}/src/interlaced.egg-info/requires.txt +0 -0
  209. {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.7
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"
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, statements that MUTATE THE CATALOG are serialised
11
- on ``_write_lock``. DuckLake's catalog layer is not safe against concurrent DDL
12
- on sibling cursors of one DatabaseInstance: under parallel builds a
13
- ``CREATE TABLE <schema>.<name>`` intermittently loses its schema qualification
14
- and creates the table in the catalog's default schema instead, silently — no
15
- error, no transaction conflict. The snapshot then disagrees with what the state
16
- store recorded, and every later run fails resolving the table. Reads stay
17
- unlocked, so parallelism is only lost where the catalog is actually being
18
- written.
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
- # Serialises catalog-mutating statements on DuckLake catalogs only (see
138
- # module docstring); a no-op context elsewhere so builds run in parallel.
139
- # Plain Lock, not RLock: no locked path calls another, and a plain Lock
140
- # turns an accidental nesting into an obvious deadlock rather than silent
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
- cur = self._cursor()
322
- try:
323
- cur.execute(sql)
324
- except Exception as exc:
325
- note_statement(exc, sql)
326
- raise
327
- finally:
328
- cur.close()
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
- counts: list[int] = []
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("BEGIN")
337
- for sql in sqls:
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 counts
518
+ return table.to_reader()
351
519
 
352
- def _fetch_sync(self, sql: str) -> pa.RecordBatchReader:
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
- cur.execute(f"CREATE OR REPLACE TABLE {target} AS SELECT * FROM {src}")
383
- else:
384
- cur.execute(f"INSERT INTO {target} SELECT * FROM {src}")
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
- cur = self._cursor()
394
- try:
395
- row = cur.execute(
396
- "SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? "
397
- "AND table_catalog = coalesce(?, current_database())",
398
- [table.schema, table.name, table.catalog],
399
- ).fetchone()
400
- finally:
401
- cur.close()
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
- cur = self._cursor()
406
- try:
407
- rows = cur.execute(
408
- "SELECT column_name, data_type FROM information_schema.columns "
409
- "WHERE table_schema = ? AND table_name = ? "
410
- "AND table_catalog = coalesce(?, current_database()) ORDER BY ordinal_position",
411
- [table.schema, table.name, table.catalog],
412
- ).fetchall()
413
- finally:
414
- cur.close()
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.7
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