duckstring 0.6.1__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. {duckstring-0.6.1 → duckstring-0.7.0}/CHANGELOG.md +180 -0
  2. {duckstring-0.6.1/src/duckstring.egg-info → duckstring-0.7.0}/PKG-INFO +17 -7
  3. {duckstring-0.6.1 → duckstring-0.7.0}/README.md +8 -4
  4. {duckstring-0.6.1 → duckstring-0.7.0}/pyproject.toml +32 -8
  5. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/app.py +3 -2
  6. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/auth.py +44 -3
  7. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/cloud.py +4 -2
  8. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/db.py +3 -1
  9. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/driver.py +492 -308
  10. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/egress_worker.py +39 -28
  11. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/flight_sql.py +25 -15
  12. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/pg_wire.py +21 -14
  13. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/poller.py +31 -20
  14. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/__init__.py +2 -0
  15. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/data.py +131 -49
  16. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/deploy.py +38 -21
  17. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/draw.py +1 -2
  18. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/duck.py +2 -2
  19. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/metrics.py +10 -0
  20. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/orchestrate.py +60 -57
  21. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/serving.py +60 -10
  22. duckstring-0.7.0/src/duckstring/catchment/routes/tap.py +64 -0
  23. duckstring-0.7.0/src/duckstring/catchment/schema/030_flock_accept.sql +5 -0
  24. duckstring-0.7.0/src/duckstring/catchment/schema/031_refresh.sql +83 -0
  25. duckstring-0.7.0/src/duckstring/catchment/schema/032_tap.sql +16 -0
  26. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/serving.py +42 -15
  27. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/404.html +1 -1
  28. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next.__PAGE__.txt +2 -2
  29. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._full.txt +2 -2
  30. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._head.txt +1 -1
  31. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._index.txt +1 -1
  32. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/__next._tree.txt +1 -1
  33. duckstring-0.7.0/src/duckstring/catchment/static/_next/static/chunks/0h6ulcnnp2foi.js +2 -0
  34. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._full.txt +1 -1
  35. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._head.txt +1 -1
  36. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._index.txt +1 -1
  37. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
  38. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._not-found.txt +1 -1
  39. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found/__next._tree.txt +1 -1
  40. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found.html +1 -1
  41. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_not-found.txt +1 -1
  42. duckstring-0.7.0/src/duckstring/catchment/static/index.html +1 -0
  43. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/index.txt +2 -2
  44. duckstring-0.7.0/src/duckstring/catchment/taps.py +236 -0
  45. duckstring-0.7.0/src/duckstring/changes.py +122 -0
  46. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/__init__.py +22 -41
  47. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/catchment.py +9 -7
  48. duckstring-0.7.0/src/duckstring/cli/control.py +346 -0
  49. duckstring-0.7.0/src/duckstring/cli/data.py +351 -0
  50. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/deploy.py +19 -2
  51. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/duck.py +10 -5
  52. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/duct.py +16 -13
  53. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/lineage.py +23 -15
  54. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/pond.py +1 -1
  55. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/puddle.py +3 -1
  56. duckstring-0.7.0/src/duckstring/cli/refresh.py +202 -0
  57. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/spout.py +5 -3
  58. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/status.py +10 -10
  59. duckstring-0.7.0/src/duckstring/cli/tap.py +135 -0
  60. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/window.py +61 -49
  61. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/core.py +309 -18
  62. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/dataplane.py +145 -43
  63. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_events/src/pond.py +1 -1
  64. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_repo_activity/README.md +2 -2
  65. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/src/pond.py +1 -1
  66. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/src/puddles.py +4 -4
  67. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/src/pond.py +1 -1
  68. duckstring-0.7.0/src/duckstring/demo/reports/pond.toml +11 -0
  69. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/README.md +1 -1
  70. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/src/puddles.py +2 -2
  71. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/README.md +1 -1
  72. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_reports/pond.toml +4 -0
  73. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_category_revenue/README.md +2 -2
  74. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_category_revenue/src/pond.py +2 -2
  75. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_items/src/pond.py +2 -2
  76. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_sales/src/pond.py +1 -1
  77. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_stores/src/pond.py +1 -1
  78. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/src/pond.py +1 -1
  79. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/__init__.py +2 -2
  80. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/__main__.py +6 -3
  81. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/client.py +7 -4
  82. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/core.py +30 -4
  83. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/dbt_executor.py +4 -4
  84. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/executor.py +17 -13
  85. duckstring-0.7.0/src/duckstring/egress/attached.py +178 -0
  86. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/base.py +6 -1
  87. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/destination.py +4 -3
  88. duckstring-0.7.0/src/duckstring/egress/ducklake.py +142 -0
  89. duckstring-0.7.0/src/duckstring/egress/postgres.py +55 -0
  90. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/__init__.py +32 -28
  91. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/catchment.py +430 -240
  92. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/core.py +115 -54
  93. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/worker.py +15 -15
  94. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/environments.py +31 -14
  95. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/flock/__init__.py +164 -81
  96. duckstring-0.7.0/src/duckstring/flock/engines/athena.py +154 -0
  97. duckstring-0.7.0/src/duckstring/flock/engines/motherduck.py +62 -0
  98. duckstring-0.7.0/src/duckstring/flock/engines/remote_duckdb.py +225 -0
  99. duckstring-0.7.0/src/duckstring/flock/engines/sql_engine.py +206 -0
  100. duckstring-0.7.0/src/duckstring/flock/engines/starrocks.py +305 -0
  101. duckstring-0.7.0/src/duckstring/flock/translate.py +289 -0
  102. duckstring-0.7.0/src/duckstring/flock/verify.py +123 -0
  103. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/runner.py +8 -5
  104. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/objects.py +75 -7
  105. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/schema_contract.py +19 -2
  106. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/sql_ripples.py +3 -2
  107. duckstring-0.7.0/src/duckstring/trickle/__init__.py +17 -0
  108. {duckstring-0.6.1 → duckstring-0.7.0/src/duckstring.egg-info}/PKG-INFO +17 -7
  109. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/SOURCES.txt +21 -16
  110. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/requires.txt +9 -2
  111. duckstring-0.6.1/src/duckstring/catchment/static/_next/static/chunks/09y-jd_5pt5my.js +0 -2
  112. duckstring-0.6.1/src/duckstring/catchment/static/index.html +0 -1
  113. duckstring-0.6.1/src/duckstring/cli/bulk.py +0 -135
  114. duckstring-0.6.1/src/duckstring/cli/control.py +0 -227
  115. duckstring-0.6.1/src/duckstring/cli/data.py +0 -273
  116. duckstring-0.6.1/src/duckstring/cli/serve.py +0 -97
  117. duckstring-0.6.1/src/duckstring/cli/trigger.py +0 -119
  118. duckstring-0.6.1/src/duckstring/demo/reports/pond.toml +0 -7
  119. duckstring-0.6.1/src/duckstring/egress/postgres.py +0 -170
  120. duckstring-0.6.1/src/duckstring/flock/engines/athena.py +0 -298
  121. duckstring-0.6.1/src/duckstring/flock/equivalence.py +0 -75
  122. duckstring-0.6.1/src/duckstring/trickle/__init__.py +0 -71
  123. duckstring-0.6.1/src/duckstring/trickle/acc.py +0 -122
  124. duckstring-0.6.1/src/duckstring/trickle/agg.py +0 -184
  125. duckstring-0.6.1/src/duckstring/trickle/builder.py +0 -1176
  126. duckstring-0.6.1/src/duckstring/trickle/capture.py +0 -363
  127. duckstring-0.6.1/src/duckstring/trickle/context.py +0 -53
  128. duckstring-0.6.1/src/duckstring/trickle/io.py +0 -2548
  129. duckstring-0.6.1/src/duckstring/trickle/lineage.py +0 -305
  130. {duckstring-0.6.1 → duckstring-0.7.0}/LICENSE +0 -0
  131. {duckstring-0.6.1 → duckstring-0.7.0}/MANIFEST.in +0 -0
  132. {duckstring-0.6.1 → duckstring-0.7.0}/setup.cfg +0 -0
  133. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/__init__.py +0 -0
  134. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/__main__.py +0 -0
  135. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/acc.py +0 -0
  136. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/agg.py +0 -0
  137. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/__init__.py +0 -0
  138. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/base.py +0 -0
  139. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/email.py +0 -0
  140. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/event.py +0 -0
  141. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/alerts/webhook.py +0 -0
  142. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/__init__.py +0 -0
  143. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/alert_worker.py +0 -0
  144. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/asgi.py +0 -0
  145. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/cloud_backends.py +0 -0
  146. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/cloud_deploy.py +0 -0
  147. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/dag.py +0 -0
  148. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/data_lease.py +0 -0
  149. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/dialback.py +0 -0
  150. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/ec2_launcher.py +0 -0
  151. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/fargate_launcher.py +0 -0
  152. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/launcher.py +0 -0
  153. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/pool_launcher.py +0 -0
  154. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/registry.py +0 -0
  155. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/relay.py +0 -0
  156. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/alerts.py +0 -0
  157. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/catchment.py +0 -0
  158. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/duct.py +0 -0
  159. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/pool.py +0 -0
  160. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/secrets.py +0 -0
  161. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/routes/view.py +0 -0
  162. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/001_init.sql +0 -0
  163. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/002_ducts.sql +0 -0
  164. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/003_pull_m.sql +0 -0
  165. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/004_identity.sql +0 -0
  166. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/005_pond_version_schema.sql +0 -0
  167. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/006_refresh.sql +0 -0
  168. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/007_catchment_key.sql +0 -0
  169. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/008_spout.sql +0 -0
  170. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/009_spout_state.sql +0 -0
  171. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/010_spout_wake.sql +0 -0
  172. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/011_spout_window.sql +0 -0
  173. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/012_spout_node.sql +0 -0
  174. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/013_changed_f.sql +0 -0
  175. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/014_alert.sql +0 -0
  176. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/016_alert_scope_major.sql +0 -0
  177. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/017_alert_renotify.sql +0 -0
  178. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/018_duck.sql +0 -0
  179. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/019_lineage.sql +0 -0
  180. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/020_column_lineage.sql +0 -0
  181. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/021_cloud.sql +0 -0
  182. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/022_retire_duck_size.sql +0 -0
  183. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/023_pool_provider.sql +0 -0
  184. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/024_serving.sql +0 -0
  185. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/025_dbt.sql +0 -0
  186. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/026_deploy_config.sql +0 -0
  187. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/027_persist.sql +0 -0
  188. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/028_persist_timing.sql +0 -0
  189. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/schema/029_source_pins.sql +0 -0
  190. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/secrets.py +0 -0
  191. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/state_sync.py +0 -0
  192. {duckstring-0.6.1/src/duckstring/catchment/static/_next/static/ZjcXhxxBm5wFt-ERTERYy → duckstring-0.7.0/src/duckstring/catchment/static/_next/static/FqgYlcTGJfIEhRPz1Nc4p}/_buildManifest.js +0 -0
  193. {duckstring-0.6.1/src/duckstring/catchment/static/_next/static/ZjcXhxxBm5wFt-ERTERYy → duckstring-0.7.0/src/duckstring/catchment/static/_next/static/FqgYlcTGJfIEhRPz1Nc4p}/_clientMiddlewareManifest.js +0 -0
  194. {duckstring-0.6.1/src/duckstring/catchment/static/_next/static/ZjcXhxxBm5wFt-ERTERYy → duckstring-0.7.0/src/duckstring/catchment/static/_next/static/FqgYlcTGJfIEhRPz1Nc4p}/_ssgManifest.js +0 -0
  195. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/03~yq9q893hmn.js +0 -0
  196. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/07lhk_q6pmm3r.js +0 -0
  197. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0_8xp0_1~w7l7.css +0 -0
  198. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0dbhjjzl8qfwv.js +0 -0
  199. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0jvmviuftg5e2.css +0 -0
  200. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/0kczw6usu-y-f.js +0 -0
  201. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/11kjahy2ntf0n.js +0 -0
  202. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/_next/static/chunks/turbopack-03~mbvk_uplk_.js +0 -0
  203. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/favicon.svg +0 -0
  204. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/logo-mark.svg +0 -0
  205. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/catchment/static/logo.svg +0 -0
  206. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/_http.py +0 -0
  207. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/alert.py +0 -0
  208. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/config.py +0 -0
  209. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/cli/secret.py +0 -0
  210. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/dbt_mode.py +0 -0
  211. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/.gitignore +0 -0
  212. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/README.md +0 -0
  213. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/pond.toml +0 -0
  214. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/catalog/src/pond.py +0 -0
  215. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_actors/README.md +0 -0
  216. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_actors/pond.toml +0 -0
  217. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_actors/src/pond.py +0 -0
  218. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_events/README.md +0 -0
  219. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_events/pond.toml +0 -0
  220. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_pushes/README.md +0 -0
  221. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_pushes/pond.toml +0 -0
  222. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_pushes/src/pond.py +0 -0
  223. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_repo_activity/pond.toml +0 -0
  224. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_repo_activity/src/pond.py +0 -0
  225. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_stars/README.md +0 -0
  226. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_stars/pond.toml +0 -0
  227. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_stars/src/pond.py +0 -0
  228. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_trending/README.md +0 -0
  229. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_trending/pond.toml +0 -0
  230. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/gh_trending/src/pond.py +0 -0
  231. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/.gitignore +0 -0
  232. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/README.md +0 -0
  233. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/orders/pond.toml +0 -0
  234. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/.gitignore +0 -0
  235. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/README.md +0 -0
  236. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/pond.toml +0 -0
  237. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/priced/src/pond.py +0 -0
  238. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/.gitignore +0 -0
  239. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/README.md +0 -0
  240. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/products/pond.toml +0 -0
  241. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/reports/.gitignore +0 -0
  242. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/reports/README.md +0 -0
  243. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/reports/src/pond.py +0 -0
  244. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/.gitignore +0 -0
  245. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/pond.toml +0 -0
  246. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/revenue/src/pond.py +0 -0
  247. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/.gitignore +0 -0
  248. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/README.md +0 -0
  249. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/pond.toml +0 -0
  250. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/src/pond.py +0 -0
  251. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sales/src/puddles.py +0 -0
  252. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/.gitignore +0 -0
  253. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/dbt_project.yml +0 -0
  254. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/orders_clean.sql +0 -0
  255. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/revenue_by_product.sql +0 -0
  256. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/sources.yml +0 -0
  257. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/dbt/models/top_products.sql +0 -0
  258. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_analytics/pond.toml +0 -0
  259. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/.gitignore +0 -0
  260. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/README.md +0 -0
  261. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/pond.toml +0 -0
  262. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/shop_orders/src/pond.py +0 -0
  263. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_reports/README.md +0 -0
  264. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_reports/sql/monthly_summary.sql +0 -0
  265. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/README.md +0 -0
  266. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/data/tier_bands.csv +0 -0
  267. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/pond.toml +0 -0
  268. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/sql/daily_sales.sql +0 -0
  269. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/sql/price_tiers.sql +0 -0
  270. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/sql/sale_line.sql +0 -0
  271. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/sql_sales/src/puddles.py +0 -0
  272. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_category_revenue/pond.toml +0 -0
  273. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_items/README.md +0 -0
  274. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_items/pond.toml +0 -0
  275. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_priced/README.md +0 -0
  276. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_priced/pond.toml +0 -0
  277. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_priced/src/pond.py +0 -0
  278. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_sales/README.md +0 -0
  279. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_sales/pond.toml +0 -0
  280. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_store_revenue/README.md +0 -0
  281. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_store_revenue/pond.toml +0 -0
  282. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_store_revenue/src/pond.py +0 -0
  283. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_stores/README.md +0 -0
  284. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/tpcds_stores/pond.toml +0 -0
  285. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/.gitignore +0 -0
  286. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/README.md +0 -0
  287. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/demo/transactions/pond.toml +0 -0
  288. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/discover.py +0 -0
  289. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/duck/pool_agent.py +0 -0
  290. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/__init__.py +0 -0
  291. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/credentials.py +0 -0
  292. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/egress/object_store.py +0 -0
  293. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/engine/pond.py +0 -0
  294. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/flock/engines/__init__.py +0 -0
  295. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/keys.py +0 -0
  296. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/__init__.py +0 -0
  297. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/hydrate.py +0 -0
  298. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/local/project.py +0 -0
  299. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/pondignore.py +0 -0
  300. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/storage.py +0 -0
  301. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/trickle_builder.py +0 -0
  302. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring/trickle_io.py +0 -0
  303. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/dependency_links.txt +0 -0
  304. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/entry_points.txt +0 -0
  305. {duckstring-0.6.1 → duckstring-0.7.0}/src/duckstring.egg-info/top_level.txt +0 -0
@@ -2,6 +2,186 @@
2
2
 
3
3
  Notable changes per release. Versions before 0.5.0 are recorded in the git history and the `v*` tags.
4
4
 
5
+ ## 0.7.0 — 2026-10-22
6
+
7
+ A single trigger, Refresh, replaces Tap, Wave, Pulse and Tide, and a Tap is now an app's access point to a
8
+ Pond. Also DuckDB 2.0 support and Spouts that deliver to a DuckLake. The old trigger commands and routes are
9
+ removed with no compatibility period, so read each section's Upgrading notes. Needs `duckstring-trickle` 0.2.1.
10
+
11
+ ### One trigger: Refresh
12
+
13
+ Tap, Wave, Pulse and Tide are replaced by a single trigger, **Refresh**, which asks for a Pond's data to be at
14
+ least as fresh as now. A recurring Refresh repeats it:
15
+
16
+ - `duckstring refresh add reports --every 1d`: a day after the later of the last request and the last refresh of
17
+ the data (the old Tide).
18
+ - `duckstring refresh add reports --every 1d --start 06:00 [--on MON,TUE,WED,THU,FRI] [--until T]`: at set times,
19
+ on a window's grid, whatever else refreshes the Pond. New. A Catchment that was down asks once, for the latest
20
+ occurrence, when it returns.
21
+ - `duckstring refresh add reports --continuous` (or `--every 0s`): as fresh as the pipeline allows, paced to its
22
+ slowest step (the old Wave).
23
+
24
+ `duckstring refresh now reports` refreshes once (the old Pulse). A Pond can have any number of scheduled Refreshes and
25
+ one `--every` or continuous Refresh; `refresh ls` lists them with when each next asks, and `refresh rm` removes
26
+ one. In the web UI, a Pond's trigger buttons became a Refresh section (Refresh Now, and Recurring Refresh for
27
+ Continuous, Every N and schedules), and its recurring Refreshes and Taps are listed in a strip at the bottom of its
28
+ box on the canvas.
29
+
30
+ Upgrading:
31
+
32
+ - **Existing Waves become continuous Refreshes and Tides become `--every` Refreshes** (migration `031`).
33
+ - **`POST /api/ponds/{name}/refresh` changed meaning.** It used to mark a Pond for a rebuild; it now refreshes the
34
+ Pond (at `demand` level). The rebuild flag moved to `POST /api/ponds/{name}/rebuild`, and the command to
35
+ `duckstring control rebuild` (and the batch operation `refresh` to `rebuild`).
36
+ Scripts that called `/refresh` for a rebuild need updating.
37
+ - **Pull is gone from the engine.** A Tap's "anything newer" request no longer exists: a Refresh always asks for
38
+ data as of now. Wake now runs once if the Sources are fresher at that moment, rather than waiting for them.
39
+ - `duckstring trigger window POND add|list|remove` is now `duckstring window add|ls|rm POND`.
40
+ - **The old commands and routes are removed, with no compatibility period**: `duckstring trigger …`, `control
41
+ refresh`, `do --refresh`, and `POST /tap`, `/pulse`, `/wave`, `/tide` and `/untrigger`. Scripts need the new ones.
42
+ - **Both ends of a duct need this release.** A Draw sends no requests to an upstream Catchment older than Refresh
43
+ (there, `/refresh` would mark the Pond for a rebuild) and logs that it needs upgrading.
44
+ - **`catchment open --tap-on-get` is removed** (it went with Tap, and fired on every reader, including the web
45
+ UI's data viewer). An open Pond stays open. Taps, below, replace it.
46
+ - `/api/status` lists each Pond's `refreshes` (with `next_due`), `demand_f` and `rebuild_pending`, in place of
47
+ `trigger`, `has_pull`, `target_f` and `refresh_pending`.
48
+ - The Orchestration Theory page was removed from the docs; the Scheduling guide and the `refresh` and `window`
49
+ CLI reference cover what it said that users need.
50
+
51
+ ### Taps: freshness that follows usage
52
+
53
+ A **Tap** is an access point on a Pond for one app: `duckstring refresh tap add reports --name dashboard --fresh-within
54
+ 5m`. It has its own key, shown once, which reads the Pond's exposed tables (as a read-level key does) over HTTP
55
+ (`/api/query`, `/api/serve/query`), the Postgres wire or Arrow Flight, and is refused everywhere else. A query through it sends the Pond a Refresh when the newest request on the Pond, or the
56
+ data, is at least `--fresh-within` old (the `--every` clock, checked per query; `0`, the default, refreshes on every
57
+ query unless a request is already waiting), so the pipeline keeps up while the app is in use and goes quiet when it
58
+ isn't. A query is always answered at once from what is published. Queries with any other key never send demand.
59
+
60
+ - `refresh tap ls|rm|rotate POND`; the routes `/api/ponds/{name}/taps` (full). The web UI lists a Pond's Taps at the
61
+ foot of its Refresh section, adds them in a modal, and shows a Tap's details, Rotate and Delete when clicked.
62
+ - `/metrics` gains `duckstring_tap_queries_total` and `duckstring_tap_refreshes_total`; `/api/status` lists each
63
+ Pond's `taps` (never keys).
64
+ - **`GET /api/ponds/{name}/freshness`** says how fresh a Pond's data is: `freshness` (the point in time it
65
+ reflects), `changed` (when its output last changed), `refreshing`, and when each table and Object last changed. A
66
+ Tap's key can call it for its own Pond, and `Catchment.freshness()` wraps it. It replaces a route of the same name
67
+ that the HTTP reference described as the Pond's freshness but that listed a Trickle table's run freshnesses for
68
+ the data viewer; that list moved to `/api/ponds/{name}/tables/{table}/runs`.
69
+ - The Python `Catchment` client takes a Tap's key as `api_key`, and then needs no `pond`.
70
+ - The demo `reports` exposes `monthly_summary` (`[serve] tables`), so a Tap on it works out of the box.
71
+ - Migration `032` (`pond_tap`).
72
+
73
+ ### The CLI, regrouped
74
+
75
+ The top level goes from 21 commands to 13: `status`, `query`, `get`, `refresh`, `window`, `control`, `data`, `pond`,
76
+ `catchment`, `duck`, `spout`, `secret` and `alert`. Tables and Objects are named `POND.NAME` everywhere, as in the
77
+ Python API. No old forms are kept:
78
+
79
+ | Before | Now |
80
+ |---|---|
81
+ | `query POND [TABLE] [--sql SQL]` | `query POND.TABLE`, or `query SQL [--pond POND]` |
82
+ | `serve query SQL` | `query SQL` (with `--csv`, `--json` and `--parquet` too) |
83
+ | `get POND TABLE [--path]`, `get-object POND NAME [--out]` | `get POND.NAME [--out]` |
84
+ | `objects POND`, `serve status` | `data ls [POND]` (with each table's exposure and last change) |
85
+ | `delete-table POND TABLE`, `delete-object POND NAME` | `data delete POND.NAME` |
86
+ | `serve expose POND TABLE` | `data expose POND.TABLE` |
87
+ | `lineage [POND] [--table T]`, `trace POND.TABLE` | `data lineage [POND[.TABLE]]`, `data trace POND.TABLE` |
88
+ | `serve promote POND` | `pond promote POND` |
89
+ | `puddle ls\|show\|query` | `pond puddle ls\|show\|query` |
90
+ | `catchment open\|close` | `catchment duct open\|close` |
91
+ | `do PONDS --kill …` | `control kill PONDS …`: every `control` verb takes several Ponds and `--all`, `--tree`, `--between`, `--downstream`; `--wipe` is `control wipe`, `--remove` is `pond remove` |
92
+
93
+ `POST /api/serve/query` takes a `format` (`csv`, `json` or `parquet`) to return a file.
94
+
95
+ ### Orchestration
96
+
97
+ - **Requests that arrive faster than the pipeline can serve them are paced to its slowest step** (the throttle),
98
+ always on, with no setting. A step that receives two requests during one run is held there, and one request goes
99
+ upstream each time it starts a new run.
100
+ - **A Refresh no longer refreshes an optional Source after its first build.** Demand propagates only to a Pond's
101
+ required Sources: a Pond never waits for an optional Source, so asking it ran it for nothing. An optional Source
102
+ that has never run still gets one request, so its Sink has output to read, and a Pond whose Sources are all
103
+ optional still asks every one of them. If an optional Source needs a cadence of its own, give it its own Refresh.
104
+ - **A repeat request for the freshness a Pond is already running is ignored.** It used to start a second
105
+ Run at the same freshness, which the no-change skip then completed as a pass, ending the Run in flight
106
+ early. A duct re-sending its downstream's request could cause it.
107
+ - **A join's Sources read together under saturation.** When some of a Pond's Sources are throttled and others
108
+ aren't, its requests reach the faster Sources only once the throttled ones start their next run, so a join
109
+ isn't held back by a fast Source that read early.
110
+ - **A Pond already throttled through another Refresh's Source still sends its first request.** It used to hold
111
+ that request until its own first completion, which the request itself was needed for.
112
+ - **Sleep keeps other Ponds' demand.** Sleeping a Pond (or a path with `--upstream`) clears its own demand, but a
113
+ pending request from another Pond it was serving is now sent to it again; before, that request stranded.
114
+ - **Demand is no longer lost at a halted Source.** A Pond that stops being blocked (after Clear, a recovery or a
115
+ redeploy) re-sends its newest unmet request upstream. Before, a request waiting on a Source whose Run had failed
116
+ or been killed stranded after Clear until something else demanded the path.
117
+
118
+ ### Redeploys and contract failures
119
+
120
+ - **A redeploy takes effect while a recurring Refresh keeps the Pond busy.** A warm Duck used to keep running the
121
+ code it started with until it went idle, which a continuous Refresh never let it do, so a fix redeploy changed
122
+ nothing. A Duck now says which version it runs when it asks for work; a Duck on a replaced version finishes the
123
+ Runs it holds and exits, and a Duck on the new version picks up the next Run.
124
+ - **A Run in flight across a redeploy is recorded against the version that started it.** Its events were recorded
125
+ against the new version, which the database refused, and the Duck retried the refused event forever: the Pond
126
+ showed as running for good, and nothing it did afterwards was recorded.
127
+ - **A contract violation fails the Pond.** The Duck reported the Run's last Ripple as a success before it found the
128
+ output broke the version's contract, so the Catchment counted the Run done and dropped the failure that followed:
129
+ the Pond never showed as failed, and its Sinks ran on output that was never published. A Duck from before this
130
+ release still reports the old way, so redeploy Ducks with the Catchment.
131
+
132
+ ### DuckDB 2.0
133
+
134
+ Duckstring and Trickle support DuckDB 2.0 as well as 1.5 (`duckdb>=1.5,<3`). DuckDB 2.0 needs Python 3.11 or
135
+ newer, so on Python 3.10 DuckDB stays at 1.5.
136
+
137
+ Upgrading:
138
+
139
+ - **Ripple code may need a change.** In DuckDB 2.0 a query run with `execute()` holds its connection until its rows
140
+ are read to the end, so `pond.con.execute(...).fetchone()` followed by another statement fails, and relations
141
+ no longer have `fetchone()`. Read with `fetchall()`. A Pond whose lock keeps DuckDB 1.5 stays on 1.5 while the
142
+ Catchment runs 2.0, so Ponds can move over one at a time.
143
+ - A column that is NULL in every row and has no other type (`SELECT NULL AS note`) is published as `INTEGER`, as
144
+ DuckDB 1.5 typed it. DuckDB 2.0 would give it its own NULL type, which refuses later values.
145
+ - Tables written with `if_changed=True` republish once after the upgrade, since change fingerprints are tied to
146
+ the DuckDB version.
147
+ - A Pond database written by DuckDB 2.0 can't be opened by 1.5. A Pond moved back to 1.5 rebuilds its database
148
+ from what it published, as it would after losing it.
149
+ - On DuckDB 2.0 an integer `//` or `%` by zero raises. A Flock job on StarRocks now fails in the same place and
150
+ runs locally, rather than publishing NULL.
151
+ - Trickle 0.2.1 is required.
152
+
153
+ ### DuckLake Spouts
154
+
155
+ A Spout can deliver to a [DuckLake](https://ducklake.select):
156
+ `duckstring spout add reports --to 'ducklake:sqlite:/srv/lake/meta.sqlite?data_path=s3://lakehouse/files/'`. The
157
+ destination is DuckDB's own attach string for the lake (SQLite, Postgres or DuckDB catalogs), with `data_path`,
158
+ `schema` and the data path's credentials as options. Any table can be delivered: a merge Trickle by its primary
159
+ key, an append Trickle's new rows, a plain table replaced whole. A run's tables go in one transaction, so the
160
+ lake gains one snapshot per delivery, authored `duckstring`. Delivery is exactly-once. Duckstring never
161
+ migrates a lake's catalog, and leaves compaction and snapshot expiry to the lake's owner.
162
+
163
+ Also for Spouts:
164
+
165
+ - `spout resync` now re-delivers in full to Postgres too. Before, a destination that keeps its own record of
166
+ what it has received (Postgres) treated a resync as already delivered.
167
+ - A Postgres table first filled by a full load no longer gains a `_duckstring_f` column that later deliveries
168
+ left empty, and a column a table gains is added to the destination table.
169
+ - A Spout delivering all of a Pond's tables no longer delivers a merge Trickle's changelog as a table of its own.
170
+
171
+ ### Contract checks on remote Ducks
172
+
173
+ A merge or append Trickle with an integer `SUM` column failed its version contract on the first run of a fresh
174
+ Duck (every run on Fargate, EC2 or a pool, after its idle shutdown). `SUM` of an integer is `HUGEINT`, which
175
+ Parquet stores as `DOUBLE`, so a Duck that rebuilt its database from what it had published held `DOUBLE` where the
176
+ contract had recorded `HUGEINT`. The check now compares types as published. Before this release, contract
177
+ failures found at that point were dropped without a trace, which hid it.
178
+
179
+ ### Documentation and Playground
180
+
181
+ The Concepts and Guides have screenshots and recordings of the web UI and the Playground, and the landing page
182
+ leads with the Playground. The Playground has a `chain` example (`?example=chain`), the 1 s → 3 s → 1 s chain the
183
+ Orchestration page uses.
184
+
5
185
  ## 0.6.1 — 2026-10-08
6
186
 
7
187
  ### Performance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duckstring
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: An open source data engineering platform for DuckDB: versioned transformations, pull orchestration, incremental processing and a built-in catalog.
5
5
  Author-email: Duckstring <dev@duckstring.com>
6
6
  License: Apache License
@@ -212,7 +212,8 @@ Project-URL: Playground, https://playground.duckstring.com
212
212
  Requires-Python: >=3.10
213
213
  Description-Content-Type: text/markdown
214
214
  License-File: LICENSE
215
- Requires-Dist: duckdb<2,>=1.5
215
+ Requires-Dist: duckdb<3,>=1.5
216
+ Requires-Dist: duckstring-trickle<0.3,>=0.2.1
216
217
  Requires-Dist: rich>=13.7
217
218
  Requires-Dist: typer>=0.12
218
219
  Requires-Dist: fastapi>=0.111
@@ -232,6 +233,10 @@ Requires-Dist: sqlglot>=23; extra == "lineage"
232
233
  Provides-Extra: aws
233
234
  Requires-Dist: s3fs>=2023.1; extra == "aws"
234
235
  Requires-Dist: boto3>=1.34; extra == "aws"
236
+ Requires-Dist: sqlglot<31,>=30.21; extra == "aws"
237
+ Provides-Extra: starrocks
238
+ Requires-Dist: pymysql>=1.1; extra == "starrocks"
239
+ Requires-Dist: sqlglot<31,>=30.21; extra == "starrocks"
235
240
  Provides-Extra: dev
236
241
  Requires-Dist: pytest>=8.0; extra == "dev"
237
242
  Requires-Dist: moto[server]>=5.0; extra == "dev"
@@ -239,10 +244,11 @@ Requires-Dist: pytest-timeout>=0.5; extra == "dev"
239
244
  Requires-Dist: ruff>=0.3; extra == "dev"
240
245
  Requires-Dist: mypy>=1.8; extra == "dev"
241
246
  Requires-Dist: dbt-duckdb>=1.9; extra == "dev"
242
- Requires-Dist: sqlglot>=23; extra == "dev"
247
+ Requires-Dist: sqlglot<31,>=30.21; extra == "dev"
243
248
  Requires-Dist: s3fs>=2023.1; extra == "dev"
244
249
  Requires-Dist: boto3>=1.34; extra == "dev"
245
250
  Requires-Dist: pg8000>=1.31; extra == "dev"
251
+ Requires-Dist: pymysql>=1.1; extra == "dev"
246
252
  Dynamic: license-file
247
253
 
248
254
  # Duckstring
@@ -274,9 +280,13 @@ The pipeline follows from those declarations, so it never has to be drawn by han
274
280
  Schedules are set on the Ponds whose output is actually used, at the end of the pipeline. Duckstring works back from there, running each upstream Pond only as often as something downstream needs it, so a path nobody consumes never runs. A daily job is one command:
275
281
 
276
282
  ```bash
277
- duckstring trigger tide reports 1d
283
+ duckstring refresh add reports --every 1d
278
284
  ```
279
285
 
286
+ Data can also stay fresh just because it's being used: a [Tap](https://docs.duckstring.com/guides/serving_apps_with_taps) gives an app its own key, and its queries refresh the Pond when the data is older than you allow, so nothing runs while the app is idle.
287
+
288
+ The [Playground](https://playground.duckstring.com) runs the demo pipeline in your browser, with a short guided tour of how it's scheduled.
289
+
280
290
  ## Incremental processing
281
291
 
282
292
  A *Trickle* stores a table's changes as a Z-set: rows weighted +1 when added and -1 when removed. Joins and aggregations written with the Trickle builder recompute only the keys that changed since the last run, and handle updates and deletes correctly across any shape of join:
@@ -325,8 +335,8 @@ This starts a local Catchment, with its web UI at http://127.0.0.1:7474. Leave i
325
335
  mkdir demo && cd demo
326
336
  duckstring pond demo # transactions, products → sales → reports
327
337
  duckstring pond deploy --all --yes
328
- duckstring trigger pulse reports # run the pipeline once, from the end
329
- duckstring query reports monthly_summary
338
+ duckstring refresh now reports # run the pipeline once, from the end
339
+ duckstring query reports.monthly_summary
330
340
  ```
331
341
 
332
342
  The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same steps with explanations.
@@ -343,7 +353,7 @@ The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same s
343
353
 
344
354
  ## Documentation
345
355
 
346
- [docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything, use the [Playground](https://playground.duckstring.com).
356
+ [docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything, the [Playground](https://playground.duckstring.com) simulates it in the browser, with a guided tour.
347
357
 
348
358
  ## Status
349
359
 
@@ -27,9 +27,13 @@ The pipeline follows from those declarations, so it never has to be drawn by han
27
27
  Schedules are set on the Ponds whose output is actually used, at the end of the pipeline. Duckstring works back from there, running each upstream Pond only as often as something downstream needs it, so a path nobody consumes never runs. A daily job is one command:
28
28
 
29
29
  ```bash
30
- duckstring trigger tide reports 1d
30
+ duckstring refresh add reports --every 1d
31
31
  ```
32
32
 
33
+ Data can also stay fresh just because it's being used: a [Tap](https://docs.duckstring.com/guides/serving_apps_with_taps) gives an app its own key, and its queries refresh the Pond when the data is older than you allow, so nothing runs while the app is idle.
34
+
35
+ The [Playground](https://playground.duckstring.com) runs the demo pipeline in your browser, with a short guided tour of how it's scheduled.
36
+
33
37
  ## Incremental processing
34
38
 
35
39
  A *Trickle* stores a table's changes as a Z-set: rows weighted +1 when added and -1 when removed. Joins and aggregations written with the Trickle builder recompute only the keys that changed since the last run, and handle updates and deletes correctly across any shape of join:
@@ -78,8 +82,8 @@ This starts a local Catchment, with its web UI at http://127.0.0.1:7474. Leave i
78
82
  mkdir demo && cd demo
79
83
  duckstring pond demo # transactions, products → sales → reports
80
84
  duckstring pond deploy --all --yes
81
- duckstring trigger pulse reports # run the pipeline once, from the end
82
- duckstring query reports monthly_summary
85
+ duckstring refresh now reports # run the pipeline once, from the end
86
+ duckstring query reports.monthly_summary
83
87
  ```
84
88
 
85
89
  The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same steps with explanations.
@@ -96,7 +100,7 @@ The [Quickstart](https://docs.duckstring.com/quickstart) goes through the same s
96
100
 
97
101
  ## Documentation
98
102
 
99
- [docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything, use the [Playground](https://playground.duckstring.com).
103
+ [docs.duckstring.com](https://docs.duckstring.com) has the concepts, task guides, and a reference for the CLI, Python API, `pond.toml` and HTTP API. To try pull orchestration without installing anything, the [Playground](https://playground.duckstring.com) simulates it in the browser, with a guided tour.
100
104
 
101
105
  ## Status
102
106
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "duckstring"
3
- version = "0.6.1"
3
+ version = "0.7.0"
4
4
  description = "An open source data engineering platform for DuckDB: versioned transformations, pull orchestration, incremental processing and a built-in catalog."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -10,10 +10,13 @@ authors = [
10
10
  ]
11
11
 
12
12
  dependencies = [
13
- # The tested range: CI runs the suite on the floor and the latest release, and against the pre-release
14
- # in an allowed-to-fail job. Raise the ceiling once the suite passes on the next major's release
15
- # (plans/duckdb-2-ready.md). A Pond environment gets this Duckstring, so these bounds hold there too.
16
- "duckdb>=1.5,<2",
13
+ # The tested range: CI runs the suite on the floor (1.5, an LTS) and the latest release, and against the
14
+ # pre-release in an allowed-to-fail job. Raise the ceiling once the suite passes on the next major's release
15
+ # (plans/duckdb-2.md). A Pond environment gets this Duckstring, so these bounds hold there too.
16
+ "duckdb>=1.5,<3",
17
+ # The incremental engine (packages/trickle), released separately. A compatible range: while it's 0.x a
18
+ # minor release can change its API or storage format, so Duckstring moves to one deliberately.
19
+ "duckstring-trickle>=0.2.1,<0.3",
17
20
  "rich>=13.7",
18
21
  "typer>=0.12",
19
22
  "fastapi>=0.111",
@@ -48,10 +51,19 @@ lineage = [
48
51
  ]
49
52
  # AWS / cloud (plans/cloud-config.md): the object-store data plane + egress (s3fs — fsspec's small-file
50
53
  # work; DuckDB does the bulk Parquet I/O over httpfs on its own), and the remote-compute launchers
51
- # (EC2/Fargate/relay) + the Athena Flock engine (boto3). Opt-in — a local/offline Catchment needs none.
54
+ # (EC2/Fargate/relay) + the Athena Flock engine (boto3, and sqlglot to translate its jobs). Opt-in: a
55
+ # local/offline Catchment needs none.
52
56
  aws = [
53
57
  "s3fs>=2023.1",
54
58
  "boto3>=1.34",
59
+ "sqlglot>=30.21,<31",
60
+ ]
61
+ # The StarRocks Flock engine (plans/flock-coverage.md): the MySQL-protocol client, and sqlglot to translate
62
+ # jobs. sqlglot is pinned to one major because what an engine is trusted with is measured against its
63
+ # translations: re-run tests/test_flock_starrocks_conformance.py before widening it.
64
+ starrocks = [
65
+ "pymysql>=1.1",
66
+ "sqlglot>=30.21,<31",
55
67
  ]
56
68
  dev = [
57
69
  "pytest>=8.0",
@@ -61,10 +73,11 @@ dev = [
61
73
  "ruff>=0.3",
62
74
  "mypy>=1.8",
63
75
  "dbt-duckdb>=1.9",
64
- "sqlglot>=23",
76
+ "sqlglot>=30.21,<31",
65
77
  "s3fs>=2023.1",
66
78
  "boto3>=1.34",
67
79
  "pg8000>=1.31",
80
+ "pymysql>=1.1",
68
81
  ]
69
82
 
70
83
  [tool.setuptools]
@@ -77,6 +90,12 @@ package-dir = { "" = "src" }
77
90
  [tool.setuptools.packages.find]
78
91
  where = ["src"]
79
92
 
93
+ [tool.uv.workspace]
94
+ members = ["packages/trickle"]
95
+
96
+ [tool.uv.sources]
97
+ duckstring-trickle = { workspace = true }
98
+
80
99
  [project.urls]
81
100
  Homepage = "https://duckstring.com"
82
101
  Documentation = "https://docs.duckstring.com"
@@ -88,7 +107,7 @@ duckstring = "duckstring.cli:main"
88
107
  ds = "duckstring.cli:main"
89
108
 
90
109
  [tool.pytest.ini_options]
91
- testpaths = ["tests"]
110
+ testpaths = ["tests", "packages/trickle/tests"]
92
111
  # Some suites share fixtures by importing `tests.<module>`; put the repo root on the path so a bare
93
112
  # `pytest` (what CI runs) resolves them the same as `python -m pytest`.
94
113
  pythonpath = ["."]
@@ -104,3 +123,8 @@ extend-exclude = ["experiment"]
104
123
  [tool.ruff.lint]
105
124
  select = ["E", "F", "I", "B"]
106
125
  ignore = ["B008"] # typer.Option/Argument in defaults is intentional
126
+
127
+ [tool.ruff.lint.isort]
128
+ # Explicit, because ruff versions disagree on modules that only resolve through an alias: newer ones class
129
+ # duckstring.trickle.* (now packages/trickle, aliased) as third-party. `trickle` itself is third-party here.
130
+ known-first-party = ["duckstring"]
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  from fastapi import FastAPI
10
10
  from fastapi.staticfiles import StaticFiles
11
11
 
12
+ from ..trickle_io import first_row
12
13
  from . import auth
13
14
  from .db import connect, ensure_identity, migrate
14
15
  from .driver import Driver
@@ -19,7 +20,7 @@ _STATIC_DIR = Path(__file__).parent / "static"
19
20
 
20
21
 
21
22
  async def _scheduler(driver: Driver) -> None:
22
- """Drive clock processes (Tide deadlines, window boundaries, Wave-on-idle) at next_wake."""
23
+ """Drive clock processes (recurring Refreshes, window boundaries) at next_wake."""
23
24
  while True:
24
25
  nw = driver.next_wake()
25
26
  now = datetime.now(timezone.utc)
@@ -205,7 +206,7 @@ def create_app(
205
206
  from ..storage import get_storage
206
207
  from .data_lease import acquire_lease
207
208
 
208
- cid = con.execute("SELECT value FROM catchment_meta WHERE key = 'id'").fetchone()
209
+ cid = first_row(con.execute("SELECT value FROM catchment_meta WHERE key = 'id'"))
209
210
  owner_id = cid[0] if cid else "unknown"
210
211
  lease_store = get_storage(data_root)
211
212
  acquire_lease(lease_store, owner_id) # raises LeaseConflict → the server does not start
@@ -4,9 +4,14 @@ The single-key model is split into a **total-ordered** ladder — ``read ⊂ dem
4
4
  authorization check stays one integer comparison:
5
5
 
6
6
  - **read** — read & query data only (no ducts, no demand).
7
- - **demand** — read + create demand (tap/wave/pulse/tide) + connect a downstream duct.
7
+ - **demand** — read + create demand (refresh now, recurring Refreshes) + connect a downstream duct.
8
8
  - **full** — everything: deploy, the control verbs, windows, ducts, key rotation.
9
9
 
10
+ Besides the ladder, a **Tap** key (plans/taps.md) is a principal of its own: it names one Tap on one Pond line,
11
+ can query only that Tap's tables, and its queries may send that Pond a Refresh. It is accepted only on the
12
+ routes guarded by ``read_or_tap`` (the query routes) and refused (403) everywhere else. Tap keys are recognised
13
+ in open mode too, since what they add is the Refresh, not access.
14
+
10
15
  Each route declares a *minimum* level via the ready-made dependencies ``read`` / ``demand`` / ``full``
11
16
  (or ``duck`` for the internal worker channel). A request's level is the matched key's level. Routes are
12
17
  **fail-closed**: :func:`audit_routes` runs at app construction and raises if any ``/api`` route (bar the
@@ -31,6 +36,7 @@ from enum import IntEnum
31
36
 
32
37
  from fastapi import Depends, HTTPException, Request
33
38
 
39
+ from ..trickle_io import first_row
34
40
  from .db import locked
35
41
 
36
42
  # Routes that need no credential at all (parity with the old open `/api/health`).
@@ -79,7 +85,7 @@ def generate(con: sqlite3.Connection, levels: list[str] | None = None) -> dict[s
79
85
  def ensure_duck_token(con: sqlite3.Connection) -> str:
80
86
  """Mint the internal Duck dial-back token once (persisted in ``catchment_meta``), then return it.
81
87
  Stable across restarts so a Duck that outlived the Catchment keeps authenticating."""
82
- row = con.execute("SELECT value FROM catchment_meta WHERE key = 'duck_token'").fetchone()
88
+ row = first_row(con.execute("SELECT value FROM catchment_meta WHERE key = 'duck_token'"))
83
89
  if row is not None:
84
90
  return row[0]
85
91
  token = secrets.token_urlsafe(32)
@@ -128,10 +134,26 @@ def auth_configured(con: sqlite3.Connection | None, api_key: str | None) -> bool
128
134
  # ─── Request-time resolution ─────────────────────────────────────────────────
129
135
 
130
136
 
137
+ @dataclass(frozen=True)
138
+ class TapScope:
139
+ """The Tap a key belongs to: its Pond line (a pond key) and its name."""
140
+ pond: str
141
+ name: str
142
+
143
+
131
144
  @dataclass
132
145
  class Principal:
133
146
  level: Level | None # the access level of a user credential (None = unauthenticated)
134
147
  is_duck: bool # the internal worker token (only valid on the duck channel)
148
+ tap: TapScope | None = None # a Tap's key: queries only that Tap's tables (level is None)
149
+
150
+
151
+ def tap_for_key(driver, supplied: str) -> TapScope | None:
152
+ """The Tap a raw key belongs to, if any (the Driver holds the Taps' key hashes in memory)."""
153
+ if not supplied or driver is None:
154
+ return None
155
+ lookup = getattr(driver, "tap_for_hash", None)
156
+ return lookup(hash_key(supplied)) if lookup is not None else None
135
157
 
136
158
 
137
159
  def _extract_token(request: Request) -> str:
@@ -152,6 +174,10 @@ def get_principal(request: Request) -> Principal:
152
174
  if duck_token and supplied and secrets.compare_digest(supplied, duck_token):
153
175
  return Principal(level=None, is_duck=True)
154
176
 
177
+ tap = tap_for_key(getattr(state, "driver", None), supplied)
178
+ if tap is not None:
179
+ return Principal(level=None, is_duck=False, tap=tap)
180
+
155
181
  if not auth_configured(con, api_key):
156
182
  return Principal(level=Level.FULL, is_duck=False) # open mode — no credential required
157
183
 
@@ -167,6 +193,16 @@ def get_principal(request: Request) -> Principal:
167
193
  return Principal(level=None, is_duck=False)
168
194
 
169
195
 
196
+ def principal_for_key(driver, api_key: str | None, supplied: str) -> Principal | None:
197
+ """The principal for a raw key on a non-HTTP front door (the pg / Flight wires): a Tap, else the key's
198
+ access level (open mode → FULL). None for a bad or absent key."""
199
+ tap = tap_for_key(driver, supplied)
200
+ if tap is not None:
201
+ return Principal(level=None, is_duck=False, tap=tap)
202
+ level = level_for_key(getattr(driver, "db", None), api_key, supplied)
203
+ return Principal(level=level, is_duck=False) if level is not None else None
204
+
205
+
170
206
  def level_for_key(con: sqlite3.Connection | None, api_key: str | None, supplied: str) -> Level | None:
171
207
  """The access :class:`Level` for a raw key — the reusable core of :func:`get_principal`, for
172
208
  non-HTTP front doors (the pg / Flight wire servers, where the key arrives as a password). Open mode
@@ -187,10 +223,14 @@ def level_for_key(con: sqlite3.Connection | None, api_key: str | None, supplied:
187
223
  # ─── Route guards (declared on each route as `dependencies=[...]`) ────────────
188
224
 
189
225
 
190
- def _level_guard(required: Level):
226
+ def _level_guard(required: Level, allow_tap: bool = False):
191
227
  def dep(principal: Principal = Depends(get_principal)) -> Principal:
192
228
  if principal.is_duck: # the worker token is for the duck channel only
193
229
  raise HTTPException(status_code=403, detail="endpoint not available to the worker token")
230
+ if principal.tap is not None:
231
+ if allow_tap:
232
+ return principal
233
+ raise HTTPException(status_code=403, detail="a Tap's key can only query its Tap")
194
234
  if principal.level is None:
195
235
  raise HTTPException(status_code=401, detail="Invalid or missing API key")
196
236
  if principal.level < required:
@@ -219,6 +259,7 @@ def _duck_guard():
219
259
  read = _level_guard(Level.READ)
220
260
  demand = _level_guard(Level.DEMAND)
221
261
  full = _level_guard(Level.FULL)
262
+ read_or_tap = _level_guard(Level.READ, allow_tap=True) # the query routes: a read key or a Tap's key
222
263
  duck = _duck_guard()
223
264
 
224
265
 
@@ -13,6 +13,8 @@ import functools
13
13
  import os
14
14
  import sqlite3
15
15
 
16
+ from ..trickle_io import first_row
17
+
16
18
  DATA_ROOT_KEY = "data_root"
17
19
  _REMOTE_SCHEMES = ("s3", "gs")
18
20
 
@@ -20,7 +22,7 @@ _REMOTE_SCHEMES = ("s3", "gs")
20
22
  # ─── the generic key/value setting store ─────────────────────────────────────────
21
23
 
22
24
  def get_setting(con: sqlite3.Connection, key: str) -> str | None:
23
- row = con.execute("SELECT value FROM catchment_setting WHERE key = ?", (key,)).fetchone()
25
+ row = first_row(con.execute("SELECT value FROM catchment_setting WHERE key = ?", (key,)))
24
26
  return row[0] if row else None
25
27
 
26
28
 
@@ -44,7 +46,7 @@ def has_published_data(con: sqlite3.Connection) -> bool:
44
46
  """Whether the Catchment has ever published output — a successful Pond Run. The set-once guard
45
47
  keys off this: while it is False the data root is freely settable; once True, switching it would
46
48
  strand the published data, so the setter refuses."""
47
- row = con.execute("SELECT 1 FROM pond_run WHERE status = 'success' LIMIT 1").fetchone()
49
+ row = first_row(con.execute("SELECT 1 FROM pond_run WHERE status = 'success' LIMIT 1"))
48
50
  return row is not None
49
51
 
50
52
 
@@ -5,13 +5,15 @@ from contextlib import nullcontext
5
5
  from datetime import datetime, timezone
6
6
  from pathlib import Path
7
7
 
8
+ from ..trickle_io import first_row
9
+
8
10
  _SCHEMA_DIR = Path(__file__).parent / "schema"
9
11
 
10
12
 
11
13
  def ensure_identity(con: sqlite3.Connection, name: str | None = None) -> None:
12
14
  """Mint this Catchment's stable id on first start (never changes), and set/refresh its optional
13
15
  display name. Call after migrate()."""
14
- row = con.execute("SELECT value FROM catchment_meta WHERE key = 'id'").fetchone()
16
+ row = first_row(con.execute("SELECT value FROM catchment_meta WHERE key = 'id'"))
15
17
  if row is None:
16
18
  con.execute("INSERT INTO catchment_meta (key, value) VALUES ('id', ?)", (str(uuid.uuid4()),))
17
19
  if name: