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.
Files changed (202) hide show
  1. {interlaced-2.8.0/src/interlaced.egg-info → interlaced-2.8.2}/PKG-INFO +2 -2
  2. {interlaced-2.8.0 → interlaced-2.8.2}/pyproject.toml +2 -2
  3. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cdc/publish.py +2 -2
  4. interlaced-2.8.2/src/interlace/cdc/slot.py +110 -0
  5. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/runner.py +47 -2
  6. interlaced-2.8.2/src/interlace/cli/build.py +496 -0
  7. interlaced-2.8.2/src/interlace/cli/inspect.py +522 -0
  8. interlaced-2.8.2/src/interlace/cli/main.py +29 -0
  9. interlaced-2.8.2/src/interlace/cli/operate.py +578 -0
  10. interlaced-2.8.2/src/interlace/cli/support.py +310 -0
  11. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/decorators.py +3 -2
  12. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/base.py +25 -0
  13. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/duckdb.py +8 -1
  14. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/quack.py +5 -1
  15. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/spark.py +2 -0
  16. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/inspect.py +38 -0
  17. interlaced-2.8.2/src/interlace/ir/layout.py +30 -0
  18. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/mcp_server.py +10 -14
  19. interlaced-2.8.2/src/interlace/physical/changes.py +45 -0
  20. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/drift.py +8 -3
  21. {interlaced-2.8.0/src/interlace/physical → interlaced-2.8.2/src/interlace/plan}/annotate.py +3 -2
  22. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/apply.py +237 -174
  23. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/differ.py +9 -11
  24. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/orchestrate.py +1 -1
  25. interlaced-2.8.2/src/interlace/plan/payload.py +86 -0
  26. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/plan.py +41 -64
  27. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/run.py +11 -24
  28. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/project.py +24 -2
  29. interlaced-2.8.2/src/interlace/scheduler/daemon.py +217 -0
  30. interlaced-2.8.2/src/interlace/service/app.py +383 -0
  31. interlaced-2.8.2/src/interlace/service/present.py +235 -0
  32. interlaced-2.8.2/src/interlace/service/routes/__init__.py +1 -0
  33. interlaced-2.8.2/src/interlace/service/routes/admin.py +259 -0
  34. interlaced-2.8.2/src/interlace/service/routes/catalog.py +243 -0
  35. interlaced-2.8.2/src/interlace/service/routes/environments.py +75 -0
  36. interlaced-2.8.2/src/interlace/service/routes/models.py +183 -0
  37. interlaced-2.8.2/src/interlace/service/routes/plan.py +187 -0
  38. interlaced-2.8.2/src/interlace/service/routes/runs.py +149 -0
  39. interlaced-2.8.2/src/interlace/service/routes/streams.py +242 -0
  40. interlaced-2.8.2/src/interlace/state/advisory.py +84 -0
  41. interlaced-2.8.2/src/interlace/state/cdc.py +70 -0
  42. interlaced-2.8.2/src/interlace/state/checks.py +62 -0
  43. interlaced-2.8.2/src/interlace/state/codec.py +127 -0
  44. interlaced-2.8.2/src/interlace/state/conn.py +40 -0
  45. interlaced-2.8.2/src/interlace/state/events.py +129 -0
  46. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/interval.py +13 -0
  47. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/janitor.py +31 -6
  48. interlaced-2.8.2/src/interlace/state/keys.py +92 -0
  49. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/locks.py +4 -4
  50. interlaced-2.8.2/src/interlace/state/queue.py +291 -0
  51. interlaced-2.8.2/src/interlace/state/schema.py +182 -0
  52. interlaced-2.8.2/src/interlace/state/snapshots.py +353 -0
  53. interlaced-2.8.2/src/interlace/state/store.py +377 -0
  54. interlaced-2.8.2/src/interlace/state/triggers.py +35 -0
  55. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/__init__.py +19 -0
  56. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/append.py +3 -7
  57. interlaced-2.8.2/src/interlace/strategies/base.py +141 -0
  58. interlaced-2.8.2/src/interlace/strategies/full_merge.py +154 -0
  59. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/hash_merge.py +5 -7
  60. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/incremental.py +7 -9
  61. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/merge.py +10 -14
  62. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/replace.py +8 -11
  63. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/replace_in_place.py +3 -7
  64. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/scd.py +10 -10
  65. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/strategies/view.py +7 -7
  66. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/materializer.py +43 -27
  67. interlaced-2.8.2/src/interlace/templates/cdc/README.md +66 -0
  68. interlaced-2.8.2/src/interlace/templates/cdc/docker-compose.yml +29 -0
  69. interlaced-2.8.2/src/interlace/templates/cdc/init/seed.sql +22 -0
  70. interlaced-2.8.2/src/interlace/templates/cdc/interlace.yaml +21 -0
  71. interlaced-2.8.2/src/interlace/templates/cdc/models/orders.sql +27 -0
  72. interlaced-2.8.2/src/interlace/templates/cdc/models/orders_by_status.sql +14 -0
  73. interlaced-2.8.2/src/interlace/templates/cdc/models/orders_stream.py +25 -0
  74. interlaced-2.8.2/src/interlace/templates/cdc/push.py +56 -0
  75. interlaced-2.8.2/src/interlace/templates/cdc/template.yaml +4 -0
  76. {interlaced-2.8.0 → interlaced-2.8.2/src/interlaced.egg-info}/PKG-INFO +2 -2
  77. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/SOURCES.txt +38 -1
  78. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/requires.txt +1 -1
  79. interlaced-2.8.0/src/interlace/cdc/slot.py +0 -97
  80. interlaced-2.8.0/src/interlace/cli/main.py +0 -1867
  81. interlaced-2.8.0/src/interlace/service/app.py +0 -1988
  82. interlaced-2.8.0/src/interlace/state/store.py +0 -1413
  83. interlaced-2.8.0/src/interlace/strategies/base.py +0 -89
  84. interlaced-2.8.0/src/interlace/strategies/full_merge.py +0 -96
  85. {interlaced-2.8.0 → interlaced-2.8.2}/LICENSE +0 -0
  86. {interlaced-2.8.0 → interlaced-2.8.2}/MANIFEST.in +0 -0
  87. {interlaced-2.8.0 → interlaced-2.8.2}/README.md +0 -0
  88. {interlaced-2.8.0 → interlaced-2.8.2}/setup.cfg +0 -0
  89. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/__init__.py +0 -0
  90. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cdc/__init__.py +0 -0
  91. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cdc/decode.py +0 -0
  92. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/__init__.py +0 -0
  93. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/builtin.py +0 -0
  94. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/checks/spec.py +0 -0
  95. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/cli/__init__.py +0 -0
  96. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/config/__init__.py +0 -0
  97. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/config/config.py +0 -0
  98. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/connections.py +0 -0
  99. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/contracts.py +0 -0
  100. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/__init__.py +0 -0
  101. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/discovery.py +0 -0
  102. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/dynamic.py +0 -0
  103. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/dsl/sql_config.py +0 -0
  104. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/__init__.py +0 -0
  105. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/adbc.py +0 -0
  106. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/bigquery.py +0 -0
  107. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/postgres.py +0 -0
  108. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/redshift.py +0 -0
  109. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/registry.py +0 -0
  110. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/engines/snowflake.py +0 -0
  111. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/exceptions.py +0 -0
  112. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/__init__.py +0 -0
  113. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/column_lineage.py +0 -0
  114. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/dag.py +0 -0
  115. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/project.py +0 -0
  116. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/graph/selectors.py +0 -0
  117. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/inputs.py +0 -0
  118. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/__init__.py +0 -0
  119. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/canonicalize.py +0 -0
  120. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/fingerprint.py +0 -0
  121. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/macros.py +0 -0
  122. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/ir/relation.py +0 -0
  123. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/__init__.py +0 -0
  124. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/ddl.py +0 -0
  125. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/reconcile.py +0 -0
  126. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/physical/spec.py +0 -0
  127. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/__init__.py +0 -0
  128. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/comment.py +0 -0
  129. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/resolve.py +0 -0
  130. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/plan/table_diff.py +0 -0
  131. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/py.typed +0 -0
  132. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/query.py +0 -0
  133. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/runtime/__init__.py +0 -0
  134. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/runtime/handles.py +0 -0
  135. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/runtime/python_model.py +0 -0
  136. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scaffold.py +0 -0
  137. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/__init__.py +0 -0
  138. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/engine.py +0 -0
  139. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/triggers.py +0 -0
  140. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/scheduler/worker.py +0 -0
  141. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/__init__.py +0 -0
  142. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/auth.py +0 -0
  143. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/types.py +0 -0
  144. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/app.css +0 -0
  145. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/favicon.svg +0 -0
  146. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/index.html +0 -0
  147. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/api.js +0 -0
  148. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/app.js +0 -0
  149. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/dag.js +0 -0
  150. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/timeline.js +0 -0
  151. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/ui.js +0 -0
  152. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/checks.js +0 -0
  153. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/environments.js +0 -0
  154. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/lineage.js +0 -0
  155. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/models.js +0 -0
  156. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/overview.js +0 -0
  157. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/plan.js +0 -0
  158. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/query.js +0 -0
  159. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/runs.js +0 -0
  160. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/streams.js +0 -0
  161. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/service/ui/js/views/system.js +0 -0
  162. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sinks.py +0 -0
  163. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sources/__init__.py +0 -0
  164. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sources/auth.py +0 -0
  165. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/sources/rest.py +0 -0
  166. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/__init__.py +0 -0
  167. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/state/snapshot.py +0 -0
  168. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/__init__.py +0 -0
  169. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/log.py +0 -0
  170. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/streaming/schema.py +0 -0
  171. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/README.md +0 -0
  172. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/generate.py +0 -0
  173. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/interlace.yaml +0 -0
  174. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/events.py +0 -0
  175. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/events_by_minute.sql +0 -0
  176. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/events_by_type.sql +0 -0
  177. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/top_users.sql +0 -0
  178. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/models/user_spend.sql +0 -0
  179. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/events/template.yaml +0 -0
  180. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/README.md +0 -0
  181. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/interlace.yaml +0 -0
  182. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/models/github_issues.py +0 -0
  183. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/models/issues_by_state.sql +0 -0
  184. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/github/template.yaml +0 -0
  185. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/README.md +0 -0
  186. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/docker-compose.yml +0 -0
  187. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/init/seed.sql +0 -0
  188. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/interlace.yaml +0 -0
  189. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/models/orders.py +0 -0
  190. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/models/orders_by_status.sql +0 -0
  191. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/postgres/template.yaml +0 -0
  192. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/README.md +0 -0
  193. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/interlace.yaml +0 -0
  194. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/enriched_events.py +0 -0
  195. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/event_summary.sql +0 -0
  196. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/models/raw_events.sql +0 -0
  197. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/templates/quickstart/template.yaml +0 -0
  198. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/testing/__init__.py +0 -0
  199. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlace/testing/golden.py +0 -0
  200. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/dependency_links.txt +0 -0
  201. {interlaced-2.8.0 → interlaced-2.8.2}/src/interlaced.egg-info/entry_points.txt +0 -0
  202. {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.0
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.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.0"
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>=25.0,<31.0", # canonical IR, transpilation, semantic diff, column lineage
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.exceptions import DefinitionError
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