@rocicorp/zero 1.11.0-canary.13 → 1.11.0-canary.14

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 (249) hide show
  1. package/out/analyze-query/src/analyze-cli.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  3. package/out/ast-to-zql/src/ast-to-zql.d.ts +8 -1
  4. package/out/ast-to-zql/src/ast-to-zql.d.ts.map +1 -1
  5. package/out/ast-to-zql/src/ast-to-zql.js +25 -25
  6. package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
  7. package/out/otel/src/log-options.d.ts +16 -0
  8. package/out/otel/src/log-options.d.ts.map +1 -1
  9. package/out/otel/src/log-options.js +54 -1
  10. package/out/otel/src/log-options.js.map +1 -1
  11. package/out/replicache/src/get-kv-store-provider.d.ts +1 -1
  12. package/out/replicache/src/get-kv-store-provider.d.ts.map +1 -1
  13. package/out/replicache/src/get-kv-store-provider.js +4 -4
  14. package/out/replicache/src/get-kv-store-provider.js.map +1 -1
  15. package/out/replicache/src/kv/idb-store.d.ts +10 -1
  16. package/out/replicache/src/kv/idb-store.d.ts.map +1 -1
  17. package/out/replicache/src/kv/idb-store.js +18 -2
  18. package/out/replicache/src/kv/idb-store.js.map +1 -1
  19. package/out/replicache/src/kv/mem-fallback-store.d.ts +54 -0
  20. package/out/replicache/src/kv/mem-fallback-store.d.ts.map +1 -0
  21. package/out/replicache/src/kv/mem-fallback-store.js +177 -0
  22. package/out/replicache/src/kv/mem-fallback-store.js.map +1 -0
  23. package/out/replicache/src/kv/op-sqlite/store.js +3 -3
  24. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  25. package/out/replicache/src/kv/sqlite-store.d.ts +9 -0
  26. package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
  27. package/out/replicache/src/kv/sqlite-store.js +86 -6
  28. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  29. package/out/replicache/src/new-client-channel.js.map +1 -1
  30. package/out/replicache/src/persist/collect-idb-databases.d.ts +17 -3
  31. package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
  32. package/out/replicache/src/persist/collect-idb-databases.js +81 -11
  33. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  34. package/out/replicache/src/persist/idb-databases-store-db-name.js +1 -1
  35. package/out/replicache/src/persist/idb-databases-store-db-name.js.map +1 -1
  36. package/out/replicache/src/persist/persist.d.ts.map +1 -1
  37. package/out/replicache/src/persist/persist.js +6 -3
  38. package/out/replicache/src/persist/persist.js.map +1 -1
  39. package/out/replicache/src/replicache-impl.d.ts +43 -3
  40. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  41. package/out/replicache/src/replicache-impl.js +135 -17
  42. package/out/replicache/src/replicache-impl.js.map +1 -1
  43. package/out/replicache/src/storage-failure.d.ts +37 -0
  44. package/out/replicache/src/storage-failure.d.ts.map +1 -0
  45. package/out/replicache/src/storage-failure.js +31 -0
  46. package/out/replicache/src/storage-failure.js.map +1 -0
  47. package/out/shared/src/random-values.js +12 -1
  48. package/out/shared/src/random-values.js.map +1 -1
  49. package/out/zero/package.js +1 -1
  50. package/out/zero/package.js.map +1 -1
  51. package/out/zero/src/zero.js +2 -1
  52. package/out/zero-cache/src/config/zero-config.d.ts +26 -0
  53. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  54. package/out/zero-cache/src/config/zero-config.js +36 -0
  55. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  56. package/out/zero-cache/src/observability/log-throttle.d.ts +44 -0
  57. package/out/zero-cache/src/observability/log-throttle.d.ts.map +1 -0
  58. package/out/zero-cache/src/observability/log-throttle.js +76 -0
  59. package/out/zero-cache/src/observability/log-throttle.js.map +1 -0
  60. package/out/zero-cache/src/scripts/permissions.d.ts +16 -0
  61. package/out/zero-cache/src/scripts/permissions.d.ts.map +1 -1
  62. package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
  63. package/out/zero-cache/src/server/runner/zero-dispatcher.js +2 -0
  64. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  65. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  66. package/out/zero-cache/src/server/syncer.js +20 -2
  67. package/out/zero-cache/src/server/syncer.js.map +1 -1
  68. package/out/zero-cache/src/services/change-source/common/backfill-manager.d.ts.map +1 -1
  69. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +27 -15
  70. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  71. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts +1 -0
  72. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  73. package/out/zero-cache/src/services/change-source/common/replica-schema.js +15 -3
  74. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  75. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  76. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  77. package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts +9 -0
  78. package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts.map +1 -1
  79. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +204 -18
  80. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  81. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +11 -2
  82. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +11 -2
  84. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  85. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.d.ts +68 -0
  86. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.d.ts.map +1 -0
  87. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +141 -0
  88. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -0
  89. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +43 -0
  90. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/change-source/protocol/current/data.js +10 -1
  92. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  93. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +57 -0
  94. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
  95. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +20 -6
  96. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
  97. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js +17 -7
  98. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  99. package/out/zero-cache/src/services/change-source/protocol/current.js +2 -1
  100. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts +66 -0
  101. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts.map +1 -0
  102. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js +154 -0
  103. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -0
  104. package/out/zero-cache/src/services/change-streamer/backfill-state.d.ts +50 -0
  105. package/out/zero-cache/src/services/change-streamer/backfill-state.d.ts.map +1 -0
  106. package/out/zero-cache/src/services/change-streamer/backfill-state.js +268 -0
  107. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -0
  108. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +7 -6
  109. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  110. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  111. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +20 -4
  112. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  113. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +26 -1
  114. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  115. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  116. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  117. package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
  118. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  119. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +88 -0
  120. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
  121. package/out/zero-cache/src/services/change-streamer/subscribe.js +9 -1
  122. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  123. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +29 -0
  124. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  125. package/out/zero-cache/src/services/change-streamer/subscriber.js +34 -0
  126. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  127. package/out/zero-cache/src/services/plannerz.d.ts +159 -0
  128. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -0
  129. package/out/zero-cache/src/services/plannerz.js +263 -0
  130. package/out/zero-cache/src/services/plannerz.js.map +1 -0
  131. package/out/zero-cache/src/services/replicator/change-log-cookies.js +1 -1
  132. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  133. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  134. package/out/zero-cache/src/services/replicator/change-processor.js +44 -12
  135. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  136. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  137. package/out/zero-cache/src/services/replicator/incremental-sync.js +4 -3
  138. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  139. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts +23 -2
  140. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts.map +1 -1
  141. package/out/zero-cache/src/services/replicator/schema/backfilling.js +54 -2
  142. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  143. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts +11 -3
  144. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts.map +1 -1
  145. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  146. package/out/zero-cache/src/services/replicator/write-worker.js +5 -1
  147. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  148. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.d.ts +81 -0
  149. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.d.ts.map +1 -0
  150. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +128 -0
  151. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -0
  152. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -1
  153. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +36 -1
  154. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  155. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +362 -20
  156. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  157. package/out/zero-cache/src/services/view-syncer/plan-warnings.d.ts +7 -0
  158. package/out/zero-cache/src/services/view-syncer/plan-warnings.d.ts.map +1 -0
  159. package/out/zero-cache/src/services/view-syncer/plan-warnings.js +35 -0
  160. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -0
  161. package/out/zero-cache/src/services/view-syncer/query-shape.d.ts +25 -0
  162. package/out/zero-cache/src/services/view-syncer/query-shape.d.ts.map +1 -0
  163. package/out/zero-cache/src/services/view-syncer/query-shape.js +28 -0
  164. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -0
  165. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts +77 -0
  166. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts.map +1 -0
  167. package/out/zero-cache/src/services/view-syncer/query-stats.js +206 -0
  168. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -0
  169. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +11 -1
  170. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  171. package/out/zero-cache/src/services/view-syncer/snapshotter.js +7 -1
  172. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  173. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  174. package/out/zero-cache/src/services/view-syncer/view-syncer.js +49 -3
  175. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  176. package/out/zero-cache/src/workers/connection.js +3 -3
  177. package/out/zero-cache/src/workers/connection.js.map +1 -1
  178. package/out/zero-cache/src/workers/syncer.js +2 -2
  179. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  180. package/out/zero-client/src/client/options.d.ts +29 -0
  181. package/out/zero-client/src/client/options.d.ts.map +1 -1
  182. package/out/zero-client/src/client/options.js.map +1 -1
  183. package/out/zero-client/src/client/version.js +1 -1
  184. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  185. package/out/zero-client/src/client/zero.js +8 -23
  186. package/out/zero-client/src/client/zero.js.map +1 -1
  187. package/out/zero-client/src/mod.d.ts +1 -0
  188. package/out/zero-client/src/mod.d.ts.map +1 -1
  189. package/out/zero-client/src/mod.js +1 -0
  190. package/out/zero-protocol/src/down.d.ts +2 -2
  191. package/out/zero-react/src/zero.js +1 -0
  192. package/out/zero-solid/src/zero.js +1 -0
  193. package/out/zql/src/builder/builder.d.ts +6 -0
  194. package/out/zql/src/builder/builder.d.ts.map +1 -1
  195. package/out/zql/src/builder/builder.js +4 -3
  196. package/out/zql/src/builder/builder.js.map +1 -1
  197. package/out/zql/src/ivm/memory-source.d.ts +13 -0
  198. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  199. package/out/zql/src/ivm/memory-source.js +1 -1
  200. package/out/zql/src/ivm/memory-source.js.map +1 -1
  201. package/out/zql/src/planner/planner-builder.d.ts +12 -1
  202. package/out/zql/src/planner/planner-builder.d.ts.map +1 -1
  203. package/out/zql/src/planner/planner-builder.js +8 -1
  204. package/out/zql/src/planner/planner-builder.js.map +1 -1
  205. package/out/zql/src/planner/planner-connection.d.ts +43 -0
  206. package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
  207. package/out/zql/src/planner/planner-connection.js +31 -4
  208. package/out/zql/src/planner/planner-connection.js.map +1 -1
  209. package/out/zql/src/planner/planner-graph.d.ts +10 -0
  210. package/out/zql/src/planner/planner-graph.d.ts.map +1 -1
  211. package/out/zql/src/planner/planner-graph.js +14 -0
  212. package/out/zql/src/planner/planner-graph.js.map +1 -1
  213. package/out/zql/src/planner/planner-warnings.d.ts +83 -0
  214. package/out/zql/src/planner/planner-warnings.d.ts.map +1 -0
  215. package/out/zql/src/planner/planner-warnings.js +78 -0
  216. package/out/zql/src/planner/planner-warnings.js.map +1 -0
  217. package/out/zql/src/query/measure-push-operator.d.ts.map +1 -1
  218. package/out/zql/src/query/measure-push-operator.js +30 -8
  219. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  220. package/out/zqlite/src/pending-delta.d.ts +110 -0
  221. package/out/zqlite/src/pending-delta.d.ts.map +1 -0
  222. package/out/zqlite/src/pending-delta.js +278 -0
  223. package/out/zqlite/src/pending-delta.js.map +1 -0
  224. package/out/zqlite/src/query-delegate.d.ts.map +1 -1
  225. package/out/zqlite/src/query-delegate.js +5 -1
  226. package/out/zqlite/src/query-delegate.js.map +1 -1
  227. package/out/zqlite/src/sqlite-cost-model.d.ts.map +1 -1
  228. package/out/zqlite/src/sqlite-cost-model.js +52 -1
  229. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  230. package/out/zqlite/src/sqlite-stat-fanout.d.ts.map +1 -1
  231. package/out/zqlite/src/sqlite-stat-fanout.js +2 -26
  232. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  233. package/out/zqlite/src/sqlite-stat4-sample.d.ts +37 -0
  234. package/out/zqlite/src/sqlite-stat4-sample.d.ts.map +1 -0
  235. package/out/zqlite/src/sqlite-stat4-sample.js +61 -0
  236. package/out/zqlite/src/sqlite-stat4-sample.js.map +1 -0
  237. package/out/zqlite/src/table-source.d.ts +68 -0
  238. package/out/zqlite/src/table-source.d.ts.map +1 -1
  239. package/out/zqlite/src/table-source.js +206 -7
  240. package/out/zqlite/src/table-source.js.map +1 -1
  241. package/package.json +1 -1
  242. package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts +0 -35
  243. package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts.map +0 -1
  244. package/out/replicache/src/kv/idb-store-with-mem-fallback.js +0 -83
  245. package/out/replicache/src/kv/idb-store-with-mem-fallback.js.map +0 -1
  246. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts +0 -156
  247. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts.map +0 -1
  248. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js +0 -15
  249. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"planner-connection.js","names":["#sort","#filters","#pushed","#unpushedFilters","#model","#baseConstraints","#baseLimit","#constraints","#perBranchFilters","#isRoot","#computeSelectivity","#output","#parentJoin","#cachedConstraintCosts","#filtersFor"],"sources":["../../../../../zql/src/planner/planner-connection.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport type {\n Condition,\n Ordering,\n SimpleCondition,\n} from '../../../zero-protocol/src/ast.ts';\nimport type {NoSubqueryCondition} from '../builder/filter.ts';\nimport {\n mergeConstraints,\n type PlannerConstraint,\n} from './planner-constraint.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport type {PlannerJoin} from './planner-join.ts';\nimport {omitFanout} from './planner-node.ts';\nimport type {\n CostEstimate,\n JoinOrConnection,\n PlannerNode,\n} from './planner-node.ts';\n\n/**\n * Represents a connection to a source (table scan).\n *\n * # Dual State Pattern\n * Like all planner nodes, PlannerConnection separates:\n * 1. immutable structure: Ordering, filters, cost model (set at construction)\n * 2. mutable state: Pinned status, constraints (mutated during planning)\n *\n * # Cost Estimation\n * The ordering and filters determine the initial cost. As planning progresses,\n * constraints from parent joins refine the cost estimate.\n *\n * # Constraint Flow\n * When a connection is pinned as the outer loop, it reveals constraints for\n * connected joins. These constraints propagate through the graph, allowing\n * other connections to update their cost estimates.\n *\n * Example:\n *\n * ```ts\n * builder.issue.whereExists('assignee', a => a.where('name', 'Alice'))\n * ```\n *\n * ```\n * [issue] [assignee]\n * | |\n * | +-- where name = 'Alice'\n * \\ /\n * \\ /\n * [join]\n * |\n * ```\n *\n * - Initial state: Both connections have no constraints, costs are unconstrained\n * - If `issue` chosen first: Reveals constraint `assignee_id` for assignee connection\n * - If `assignee` chosen first: Reveals constraint `assignee_id` for issue connection\n * - Updated costs guide the next selection\n *\n * # Pushed conditions\n * Correlated predicate pushdown copies a parent's condition on a correlation\n * column into the child (`pushed`). When the parent drives the join into the\n * child, the join binds that column to a parent row, so the parent's\n * condition implies the copy and the copy removes no rows. The connection\n * leaves such a copy out of the cost model call. Another join that binds the\n * column does not imply the copy, because its rows need not hold the parent's\n * condition. The connection also leaves every copy out of `selectivity`: a\n * semi-join always binds the column, and a flipped join gets the copy's\n * reduction through `returnedRows` instead.\n *\n * # Lifecycle\n * 1. Construct with immutable structure (ordering, filters, cost model)\n * 2. Wire to output node during graph construction\n * 3. Planning mutates pinned status and accumulates constraints\n * 4. reset() clears mutable state for replanning\n */\nexport class PlannerConnection {\n readonly kind = 'connection' as const;\n\n // ========================================================================\n // IMMUTABLE STRUCTURE (set during construction, never changes)\n // ========================================================================\n readonly #sort: Ordering;\n readonly #filters: Condition | undefined;\n readonly #pushed: ReadonlySet<SimpleCondition> | undefined;\n /** `#filters` without the pushed conditions. */\n readonly #unpushedFilters: Condition | undefined;\n readonly #model: ConnectionCostModel;\n readonly table: string;\n readonly name: string; // Human-readable name for debugging (defaults to table name)\n readonly #baseConstraints: PlannerConstraint | undefined; // Constraints from parent correlation\n readonly #baseLimit: number | undefined; // Original limit from query structure (never modified)\n readonly selectivity: number; // Fraction of rows passing filters (1.0 = no filtering)\n #output?: PlannerNode | undefined; // Set once during graph construction\n /**\n * The join from the parent into this connection's EXISTS subquery. Set once\n * during graph construction. The pushed conditions are copies of that\n * parent's conditions. Undefined for a root connection, whose parent (if\n * it is a `related` subquery) always drives it.\n */\n #parentJoin: PlannerJoin | undefined;\n\n // ========================================================================\n // MUTABLE PLANNING STATE (changes during plan search)\n // ========================================================================\n /**\n * Current limit during planning. Can be cleared (set to undefined) when a\n * parent join is flipped, indicating this connection is now in an outer loop\n * and should not be limited by EXISTS semantics.\n */\n limit: number | undefined;\n\n /**\n * Constraints accumulated from parent joins during planning.\n * Key is a path through the graph (e.g., \"0,1\" for branch pattern [0,1]).\n *\n * Undefined constraints are possible when a FO converts to UFO and only\n * a single join in the UFO is flipped - other branches report undefined.\n */\n readonly #constraints: Map<string, PlannerConstraint | undefined>;\n\n /**\n * Per-branch filters registered by `PlannerFilter` nodes (simple OR\n * branches) when the enclosing FanIn is in UFI mode. Key is the same\n * `branchPattern.join(',')` used by `#constraints`.\n *\n * These are AND-merged with `#filters` when calling the cost model so\n * the model sees the true filter that the runtime would push to the\n * source for this specific branch.\n */\n readonly #perBranchFilters: Map<string, NoSubqueryCondition> = new Map();\n\n readonly #isRoot: boolean;\n\n /**\n * Cached per-constraint costs to avoid redundant cost model calls.\n * Maps constraint key (branch pattern string) to computed cost.\n * Invalidated when constraints change.\n */\n #cachedConstraintCosts: Map<string, CostEstimate> = new Map();\n\n constructor(\n table: string,\n model: ConnectionCostModel,\n sort: Ordering,\n filters: Condition | undefined,\n isRoot: boolean,\n baseConstraints?: PlannerConstraint,\n limit?: number,\n name?: string,\n pushed?: ReadonlySet<SimpleCondition>,\n ) {\n this.table = table;\n this.name = name ?? table;\n this.#sort = sort;\n this.#filters = filters;\n this.#pushed = pushed;\n this.#unpushedFilters = pushed\n ? withoutConjuncts(filters, c => pushed.has(c))\n : filters;\n this.#model = model;\n this.#baseConstraints = baseConstraints;\n this.#baseLimit = limit;\n this.limit = limit;\n this.#constraints = new Map();\n this.#isRoot = isRoot;\n\n this.selectivity = this.#computeSelectivity(this.#unpushedFilters);\n }\n\n setOutput(node: PlannerNode): void {\n this.#output = node;\n }\n\n get output(): PlannerNode {\n assert(this.#output !== undefined, 'Output not set');\n return this.#output;\n }\n\n setParentJoin(join: PlannerJoin): void {\n this.#parentJoin = join;\n }\n\n closestJoinOrSource(): JoinOrConnection {\n return 'connection';\n }\n\n /**\n * Constraints are uniquely identified by their path through the\n * graph.\n *\n * FO represents all sub-joins as a single path.\n * UFO represents each sub-join as a separate path.\n * The first branch in a UFO will match the path of FO so no re-set needs to happen\n * when swapping from FO to UFO.\n *\n * FO swaps to UFO when a join inside FO-FI gets flipped.\n *\n * The max of the last element of the paths is the number of\n * root branches.\n */\n propagateConstraints(\n path: number[],\n c: PlannerConstraint | undefined,\n from?: PlannerNode,\n planDebugger?: PlanDebugger,\n ): void {\n const key = path.join(',');\n this.#constraints.set(key, c);\n // Constraints changed, invalidate cost caches\n this.#cachedConstraintCosts.clear();\n\n planDebugger?.log({\n type: 'node-constraint',\n nodeType: 'connection',\n node: this.name,\n branchPattern: path,\n constraint: c,\n from: from?.kind ?? 'unknown',\n });\n }\n\n /**\n * Register a per-branch filter for the given branch pattern. Called by\n * `PlannerFilter` (a simple OR branch) when the enclosing FanIn is in\n * UFI mode. The filter is AND-merged with `this.#filters` when the cost\n * model is invoked for this branch.\n */\n setPerBranchFilter(path: number[], filter: NoSubqueryCondition): void {\n this.#perBranchFilters.set(path.join(','), filter);\n // The cost depends on the filter, so invalidate caches.\n this.#cachedConstraintCosts.clear();\n }\n\n estimateCost(\n downstreamChildSelectivity: number,\n branchPattern: number[],\n planDebugger?: PlanDebugger,\n ): CostEstimate {\n // Branch pattern specified - return cost for this specific branch\n const key = branchPattern.join(',');\n\n // Check per-constraint cache first\n let cost = this.#cachedConstraintCosts.get(key);\n if (cost !== undefined) {\n return cost;\n }\n\n // Cache miss - compute and cache\n const constraint = this.#constraints.get(key);\n // Merge base constraints with propagated constraints\n const mergedConstraint = mergeConstraints(\n this.#baseConstraints,\n constraint,\n );\n // AND the per-branch filter (registered by a PlannerFilter for a\n // simple OR branch in UFI mode) with the connection-time filter so\n // the cost model sees the same effective filter the runtime applies.\n const perBranchFilter = this.#perBranchFilters.get(key);\n const {startupCost, fanout, rows} = this.#model(\n this.table,\n this.#sort,\n andFilters(this.#filtersFor(mergedConstraint), perBranchFilter),\n mergedConstraint,\n );\n const selectivity = perBranchFilter\n ? this.#computeSelectivity(\n andFilters(this.#unpushedFilters, perBranchFilter),\n )\n : this.selectivity;\n cost = {\n startupCost,\n scanEst:\n this.limit === undefined\n ? rows\n : Math.min(rows, this.limit / downstreamChildSelectivity),\n cost: 0,\n returnedRows: rows,\n selectivity,\n limit: this.limit,\n fanout,\n };\n this.#cachedConstraintCosts.set(key, cost);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'node-cost',\n nodeType: 'connection',\n node: this.name,\n branchPattern,\n downstreamChildSelectivity,\n costEstimate: omitFanout(cost),\n filters: this.#filters,\n ordering: this.#sort,\n });\n }\n\n return cost;\n }\n\n /**\n * `#filters` without the pushed conditions that `constraint` binds the\n * column of, when the parent drives the join into this connection. The\n * runtime still applies them, but they remove no rows.\n *\n * When that join is flipped, this connection is the outer loop, so every\n * pushed condition counts, even where another flipped join binds its\n * column.\n */\n #filtersFor(\n constraint: PlannerConstraint | undefined,\n ): Condition | undefined {\n const pushed = this.#pushed;\n if (\n !pushed ||\n !constraint ||\n this.#unpushedFilters === this.#filters ||\n this.#parentJoin?.type === 'flipped'\n ) {\n return this.#filters;\n }\n return withoutConjuncts(\n this.#filters,\n c =>\n pushed.has(c) &&\n c.left.type === 'column' &&\n Object.hasOwn(constraint, c.left.name),\n );\n }\n\n #computeSelectivity(filters: Condition | undefined): number {\n if (this.#baseLimit === undefined || filters === undefined) {\n return 1.0;\n }\n\n const costWithFilters = this.#model(\n this.table,\n this.#sort,\n filters,\n undefined,\n );\n const costWithoutFilters = this.#model(\n this.table,\n this.#sort,\n undefined,\n undefined,\n );\n return costWithoutFilters.rows > 0\n ? costWithFilters.rows / costWithoutFilters.rows\n : 1.0;\n }\n\n /**\n * Remove the limit from this connection.\n * Called when a parent join is flipped, making this connection part of an\n * outer loop that should produce all rows rather than stopping at the limit.\n */\n unlimit(): void {\n if (this.#isRoot) {\n // We cannot unlimit root connections\n return;\n }\n if (this.limit !== undefined) {\n this.limit = undefined;\n // Limit changes do not impact connection costs.\n // Limit is taken into account at the join level.\n // Given that, we do not need to invalidate cost caches here.\n }\n }\n\n /**\n * Propagate unlimiting when a parent join is flipped.\n * For connections, we simply remove the limit.\n */\n propagateUnlimitFromFlippedJoin(): void {\n this.unlimit();\n }\n\n reset() {\n this.#constraints.clear();\n this.#perBranchFilters.clear();\n this.limit = this.#baseLimit;\n // Clear all cost caches\n this.#cachedConstraintCosts.clear();\n }\n\n /**\n * Capture constraint state for snapshotting.\n * Used by PlannerGraph to save/restore planning state.\n */\n captureConstraints(): Map<string, PlannerConstraint | undefined> {\n return new Map(this.#constraints);\n }\n\n /**\n * Restore constraint state from a snapshot.\n * Used by PlannerGraph to restore planning state.\n *\n * Per-branch filters are not snapshotted — they are re-derived by the\n * subsequent `propagateConstraints` pass. Clearing them here ensures\n * that any entries left over from the previous iteration don't linger\n * in case the restored plan registers a different set.\n */\n restoreConstraints(\n constraints: Map<string, PlannerConstraint | undefined>,\n ): void {\n this.#constraints.clear();\n this.#perBranchFilters.clear();\n for (const [key, value] of constraints) {\n this.#constraints.set(key, value);\n }\n // Constraints changed, invalidate cost caches\n this.#cachedConstraintCosts.clear();\n }\n\n /** Get current constraints for debugging. */\n getConstraintsForDebug(): Record<string, PlannerConstraint | undefined> {\n const record: Record<string, PlannerConstraint | undefined> = {};\n for (const [key, value] of this.#constraints) {\n record[key] = value;\n }\n return record;\n }\n\n /** Get filters for debugging. */\n getFiltersForDebug(): Condition | undefined {\n return this.#filters;\n }\n\n /** Get sort/ordering for debugging. */\n getSortForDebug(): Ordering {\n return this.#sort;\n }\n\n /** Get estimated cost for each constraint branch. */\n getConstraintCostsForDebug(): Record<string, CostEstimate> {\n const record: Record<string, CostEstimate> = {};\n for (const [key, value] of this.#cachedConstraintCosts) {\n record[key] = value;\n }\n return record;\n }\n}\n\nfunction andFilters(\n a: Condition | undefined,\n b: Condition | undefined,\n): Condition | undefined {\n return a && b ? {type: 'and', conditions: [a, b]} : (a ?? b);\n}\n\n/**\n * `c` without the simple conjuncts, including those of nested ANDs, for which\n * `remove` returns true. Returns `c` itself when it removes nothing.\n */\nfunction withoutConjuncts(\n c: Condition | undefined,\n remove: (c: SimpleCondition) => boolean,\n): Condition | undefined {\n switch (c?.type) {\n case 'simple':\n return remove(c) ? undefined : c;\n case 'and': {\n const conditions: Condition[] = [];\n for (const x of c.conditions) {\n const y = withoutConjuncts(x, remove);\n if (y !== undefined) {\n conditions.push(y);\n }\n }\n if (\n conditions.length === c.conditions.length &&\n conditions.every((x, i) => x === c.conditions[i])\n ) {\n return c;\n }\n if (conditions.length <= 1) {\n return conditions[0];\n }\n return {type: 'and', conditions};\n }\n default:\n return c;\n }\n}\n\ntype FanoutEst = {\n fanout: number;\n confidence: 'high' | 'med' | 'none';\n};\nexport type FanoutCostModel = (columns: string[]) => FanoutEst;\n\nexport type CostModelCost = {\n startupCost: number;\n rows: number;\n fanout: FanoutCostModel;\n};\nexport type ConnectionCostModel = (\n table: string,\n sort: Ordering,\n filters: Condition | undefined,\n constraint: PlannerConstraint | undefined,\n) => CostModelCost;\n"],"mappings":"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"}
1
+ {"version":3,"file":"planner-connection.js","names":["#sort","#filters","#pushed","#unpushedFilters","#model","#baseConstraints","#baseLimit","#constraints","#perBranchFilters","#isRoot","#computeSelectivity","#output","#parentJoin","#clearCostCaches","#cachedConstraintCosts","#filtersFor","#cachedAccesses"],"sources":["../../../../../zql/src/planner/planner-connection.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport type {\n Condition,\n Ordering,\n SimpleCondition,\n} from '../../../zero-protocol/src/ast.ts';\nimport type {NoSubqueryCondition} from '../builder/filter.ts';\nimport {\n mergeConstraints,\n type PlannerConstraint,\n} from './planner-constraint.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport type {PlannerJoin} from './planner-join.ts';\nimport {omitFanout} from './planner-node.ts';\nimport type {\n CostEstimate,\n JoinOrConnection,\n PlannerNode,\n} from './planner-node.ts';\n\n/**\n * Represents a connection to a source (table scan).\n *\n * # Dual State Pattern\n * Like all planner nodes, PlannerConnection separates:\n * 1. immutable structure: Ordering, filters, cost model (set at construction)\n * 2. mutable state: Pinned status, constraints (mutated during planning)\n *\n * # Cost Estimation\n * The ordering and filters determine the initial cost. As planning progresses,\n * constraints from parent joins refine the cost estimate.\n *\n * # Constraint Flow\n * When a connection is pinned as the outer loop, it reveals constraints for\n * connected joins. These constraints propagate through the graph, allowing\n * other connections to update their cost estimates.\n *\n * Example:\n *\n * ```ts\n * builder.issue.whereExists('assignee', a => a.where('name', 'Alice'))\n * ```\n *\n * ```\n * [issue] [assignee]\n * | |\n * | +-- where name = 'Alice'\n * \\ /\n * \\ /\n * [join]\n * |\n * ```\n *\n * - Initial state: Both connections have no constraints, costs are unconstrained\n * - If `issue` chosen first: Reveals constraint `assignee_id` for assignee connection\n * - If `assignee` chosen first: Reveals constraint `assignee_id` for issue connection\n * - Updated costs guide the next selection\n *\n * # Pushed conditions\n * Correlated predicate pushdown copies a parent's condition on a correlation\n * column into the child (`pushed`). When the parent drives the join into the\n * child, the join binds that column to a parent row, so the parent's\n * condition implies the copy and the copy removes no rows. The connection\n * leaves such a copy out of the cost model call. Another join that binds the\n * column does not imply the copy, because its rows need not hold the parent's\n * condition. The connection also leaves every copy out of `selectivity`: a\n * semi-join always binds the column, and a flipped join gets the copy's\n * reduction through `returnedRows` instead.\n *\n * # Lifecycle\n * 1. Construct with immutable structure (ordering, filters, cost model)\n * 2. Wire to output node during graph construction\n * 3. Planning mutates pinned status and accumulates constraints\n * 4. reset() clears mutable state for replanning\n */\nexport class PlannerConnection {\n readonly kind = 'connection' as const;\n\n // ========================================================================\n // IMMUTABLE STRUCTURE (set during construction, never changes)\n // ========================================================================\n readonly #sort: Ordering;\n readonly #filters: Condition | undefined;\n readonly #pushed: ReadonlySet<SimpleCondition> | undefined;\n /** `#filters` without the pushed conditions. */\n readonly #unpushedFilters: Condition | undefined;\n readonly #model: ConnectionCostModel;\n readonly table: string;\n readonly name: string; // Human-readable name for debugging (defaults to table name)\n readonly #baseConstraints: PlannerConstraint | undefined; // Constraints from parent correlation\n readonly #baseLimit: number | undefined; // Original limit from query structure (never modified)\n readonly selectivity: number; // Fraction of rows passing filters (1.0 = no filtering)\n #output?: PlannerNode | undefined; // Set once during graph construction\n /**\n * The join from the parent into this connection's EXISTS subquery. Set once\n * during graph construction. The pushed conditions are copies of that\n * parent's conditions. Undefined for a root connection, whose parent (if\n * it is a `related` subquery) always drives it.\n */\n #parentJoin: PlannerJoin | undefined;\n\n // ========================================================================\n // MUTABLE PLANNING STATE (changes during plan search)\n // ========================================================================\n /**\n * Current limit during planning. Can be cleared (set to undefined) when a\n * parent join is flipped, indicating this connection is now in an outer loop\n * and should not be limited by EXISTS semantics.\n */\n limit: number | undefined;\n\n /**\n * Constraints accumulated from parent joins during planning.\n * Key is a path through the graph (e.g., \"0,1\" for branch pattern [0,1]).\n *\n * Undefined constraints are possible when a FO converts to UFO and only\n * a single join in the UFO is flipped - other branches report undefined.\n */\n readonly #constraints: Map<string, PlannerConstraint | undefined>;\n\n /**\n * Per-branch filters registered by `PlannerFilter` nodes (simple OR\n * branches) when the enclosing FanIn is in UFI mode. Key is the same\n * `branchPattern.join(',')` used by `#constraints`.\n *\n * These are AND-merged with `#filters` when calling the cost model so\n * the model sees the true filter that the runtime would push to the\n * source for this specific branch.\n */\n readonly #perBranchFilters: Map<string, NoSubqueryCondition> = new Map();\n\n readonly #isRoot: boolean;\n\n /**\n * Cached per-constraint costs to avoid redundant cost model calls.\n * Maps constraint key (branch pattern string) to computed cost.\n * Invalidated when constraints change.\n */\n #cachedConstraintCosts: Map<string, CostEstimate> = new Map();\n\n /**\n * How the source reads rows for each constraint key with a cached cost.\n * Cleared along with {@link #cachedConstraintCosts}.\n */\n #cachedAccesses: Map<string, ConnectionAccess> = new Map();\n\n constructor(\n table: string,\n model: ConnectionCostModel,\n sort: Ordering,\n filters: Condition | undefined,\n isRoot: boolean,\n baseConstraints?: PlannerConstraint,\n limit?: number,\n name?: string,\n pushed?: ReadonlySet<SimpleCondition>,\n ) {\n this.table = table;\n this.name = name ?? table;\n this.#sort = sort;\n this.#filters = filters;\n this.#pushed = pushed;\n this.#unpushedFilters = pushed\n ? withoutConjuncts(filters, c => pushed.has(c))\n : filters;\n this.#model = model;\n this.#baseConstraints = baseConstraints;\n this.#baseLimit = limit;\n this.limit = limit;\n this.#constraints = new Map();\n this.#isRoot = isRoot;\n\n this.selectivity = this.#computeSelectivity(this.#unpushedFilters);\n }\n\n setOutput(node: PlannerNode): void {\n this.#output = node;\n }\n\n get output(): PlannerNode {\n assert(this.#output !== undefined, 'Output not set');\n return this.#output;\n }\n\n setParentJoin(join: PlannerJoin): void {\n this.#parentJoin = join;\n }\n\n closestJoinOrSource(): JoinOrConnection {\n return 'connection';\n }\n\n /**\n * Constraints are uniquely identified by their path through the\n * graph.\n *\n * FO represents all sub-joins as a single path.\n * UFO represents each sub-join as a separate path.\n * The first branch in a UFO will match the path of FO so no re-set needs to happen\n * when swapping from FO to UFO.\n *\n * FO swaps to UFO when a join inside FO-FI gets flipped.\n *\n * The max of the last element of the paths is the number of\n * root branches.\n */\n propagateConstraints(\n path: number[],\n c: PlannerConstraint | undefined,\n from?: PlannerNode,\n planDebugger?: PlanDebugger,\n ): void {\n const key = path.join(',');\n this.#constraints.set(key, c);\n // Constraints changed, invalidate cost caches\n this.#clearCostCaches();\n\n planDebugger?.log({\n type: 'node-constraint',\n nodeType: 'connection',\n node: this.name,\n branchPattern: path,\n constraint: c,\n from: from?.kind ?? 'unknown',\n });\n }\n\n /**\n * Register a per-branch filter for the given branch pattern. Called by\n * `PlannerFilter` (a simple OR branch) when the enclosing FanIn is in\n * UFI mode. The filter is AND-merged with `this.#filters` when the cost\n * model is invoked for this branch.\n */\n setPerBranchFilter(path: number[], filter: NoSubqueryCondition): void {\n this.#perBranchFilters.set(path.join(','), filter);\n // The cost depends on the filter, so invalidate caches.\n this.#clearCostCaches();\n }\n\n estimateCost(\n downstreamChildSelectivity: number,\n branchPattern: number[],\n planDebugger?: PlanDebugger,\n ): CostEstimate {\n // Branch pattern specified - return cost for this specific branch\n const key = branchPattern.join(',');\n\n // Check per-constraint cache first\n let cost = this.#cachedConstraintCosts.get(key);\n if (cost !== undefined) {\n return cost;\n }\n\n // Cache miss - compute and cache\n const constraint = this.#constraints.get(key);\n // Merge base constraints with propagated constraints\n const mergedConstraint = mergeConstraints(\n this.#baseConstraints,\n constraint,\n );\n // AND the per-branch filter (registered by a PlannerFilter for a\n // simple OR branch in UFI mode) with the connection-time filter so\n // the cost model sees the same effective filter the runtime applies.\n const perBranchFilter = this.#perBranchFilters.get(key);\n const filters = andFilters(\n this.#filtersFor(mergedConstraint),\n perBranchFilter,\n );\n const {startupCost, fanout, rows, plan} = this.#model(\n this.table,\n this.#sort,\n filters,\n mergedConstraint,\n );\n const selectivity = perBranchFilter\n ? this.#computeSelectivity(\n andFilters(this.#unpushedFilters, perBranchFilter),\n )\n : this.selectivity;\n cost = {\n startupCost,\n scanEst:\n this.limit === undefined\n ? rows\n : Math.min(rows, this.limit / downstreamChildSelectivity),\n cost: 0,\n returnedRows: rows,\n selectivity,\n limit: this.limit,\n fanout,\n };\n this.#cachedConstraintCosts.set(key, cost);\n this.#cachedAccesses.set(key, {\n constraint: mergedConstraint,\n filters,\n rows,\n plan,\n });\n\n if (planDebugger) {\n planDebugger.log({\n type: 'node-cost',\n nodeType: 'connection',\n node: this.name,\n branchPattern,\n downstreamChildSelectivity,\n costEstimate: omitFanout(cost),\n filters: this.#filters,\n ordering: this.#sort,\n });\n }\n\n return cost;\n }\n\n /**\n * `#filters` without the pushed conditions that `constraint` binds the\n * column of, when the parent drives the join into this connection. The\n * runtime still applies them, but they remove no rows.\n *\n * When that join is flipped, this connection is the outer loop, so every\n * pushed condition counts, even where another flipped join binds its\n * column.\n */\n #filtersFor(\n constraint: PlannerConstraint | undefined,\n ): Condition | undefined {\n const pushed = this.#pushed;\n if (\n !pushed ||\n !constraint ||\n this.#unpushedFilters === this.#filters ||\n this.#parentJoin?.type === 'flipped'\n ) {\n return this.#filters;\n }\n return withoutConjuncts(\n this.#filters,\n c =>\n pushed.has(c) &&\n c.left.type === 'column' &&\n Object.hasOwn(constraint, c.left.name),\n );\n }\n\n #computeSelectivity(filters: Condition | undefined): number {\n if (this.#baseLimit === undefined || filters === undefined) {\n return 1.0;\n }\n\n const costWithFilters = this.#model(\n this.table,\n this.#sort,\n filters,\n undefined,\n );\n const costWithoutFilters = this.#model(\n this.table,\n this.#sort,\n undefined,\n undefined,\n );\n return costWithoutFilters.rows > 0\n ? costWithFilters.rows / costWithoutFilters.rows\n : 1.0;\n }\n\n /**\n * Remove the limit from this connection.\n * Called when a parent join is flipped, making this connection part of an\n * outer loop that should produce all rows rather than stopping at the limit.\n */\n unlimit(): void {\n if (this.#isRoot) {\n // We cannot unlimit root connections\n return;\n }\n if (this.limit !== undefined) {\n this.limit = undefined;\n // Limit changes do not impact connection costs.\n // Limit is taken into account at the join level.\n // Given that, we do not need to invalidate cost caches here.\n }\n }\n\n /**\n * Propagate unlimiting when a parent join is flipped.\n * For connections, we simply remove the limit.\n */\n propagateUnlimitFromFlippedJoin(): void {\n this.unlimit();\n }\n\n reset() {\n this.#constraints.clear();\n this.#perBranchFilters.clear();\n this.limit = this.#baseLimit;\n // Clear all cost caches\n this.#clearCostCaches();\n }\n\n #clearCostCaches(): void {\n this.#cachedConstraintCosts.clear();\n this.#cachedAccesses.clear();\n }\n\n /**\n * Capture constraint state for snapshotting.\n * Used by PlannerGraph to save/restore planning state.\n */\n captureConstraints(): Map<string, PlannerConstraint | undefined> {\n return new Map(this.#constraints);\n }\n\n /**\n * Restore constraint state from a snapshot.\n * Used by PlannerGraph to restore planning state.\n *\n * Per-branch filters are not snapshotted — they are re-derived by the\n * subsequent `propagateConstraints` pass. Clearing them here ensures\n * that any entries left over from the previous iteration don't linger\n * in case the restored plan registers a different set.\n */\n restoreConstraints(\n constraints: Map<string, PlannerConstraint | undefined>,\n ): void {\n this.#constraints.clear();\n this.#perBranchFilters.clear();\n for (const [key, value] of constraints) {\n this.#constraints.set(key, value);\n }\n // Constraints changed, invalidate cost caches\n this.#clearCostCaches();\n }\n\n get sort(): Ordering {\n return this.#sort;\n }\n\n /**\n * How the source reads rows for the current plan: one access per branch\n * pattern this connection was costed for since its constraints last\n * changed. Estimate the plan's cost first to populate them.\n */\n accesses(): Iterable<ConnectionAccess> {\n return this.#cachedAccesses.values();\n }\n\n /** Get current constraints for debugging. */\n getConstraintsForDebug(): Record<string, PlannerConstraint | undefined> {\n const record: Record<string, PlannerConstraint | undefined> = {};\n for (const [key, value] of this.#constraints) {\n record[key] = value;\n }\n return record;\n }\n\n /** Get filters for debugging. */\n getFiltersForDebug(): Condition | undefined {\n return this.#filters;\n }\n\n /** Get sort/ordering for debugging. */\n getSortForDebug(): Ordering {\n return this.#sort;\n }\n\n /** Get estimated cost for each constraint branch. */\n getConstraintCostsForDebug(): Record<string, CostEstimate> {\n const record: Record<string, CostEstimate> = {};\n for (const [key, value] of this.#cachedConstraintCosts) {\n record[key] = value;\n }\n return record;\n }\n}\n\nfunction andFilters(\n a: Condition | undefined,\n b: Condition | undefined,\n): Condition | undefined {\n return a && b ? {type: 'and', conditions: [a, b]} : (a ?? b);\n}\n\n/**\n * `c` without the simple conjuncts, including those of nested ANDs, for which\n * `remove` returns true. Returns `c` itself when it removes nothing.\n */\nfunction withoutConjuncts(\n c: Condition | undefined,\n remove: (c: SimpleCondition) => boolean,\n): Condition | undefined {\n switch (c?.type) {\n case 'simple':\n return remove(c) ? undefined : c;\n case 'and': {\n const conditions: Condition[] = [];\n for (const x of c.conditions) {\n const y = withoutConjuncts(x, remove);\n if (y !== undefined) {\n conditions.push(y);\n }\n }\n if (\n conditions.length === c.conditions.length &&\n conditions.every((x, i) => x === c.conditions[i])\n ) {\n return c;\n }\n if (conditions.length <= 1) {\n return conditions[0];\n }\n return {type: 'and', conditions};\n }\n default:\n return c;\n }\n}\n\ntype FanoutEst = {\n fanout: number;\n confidence: 'high' | 'med' | 'none';\n};\nexport type FanoutCostModel = (columns: string[]) => FanoutEst;\n\n/**\n * How a source would read the rows of a connection, as far as the cost model\n * can tell. This is used to warn about slow plans, not to choose between\n * them.\n */\nexport type AccessPlan = {\n /**\n * `search` if the source uses an index to find the matching rows. `scan`\n * if it reads the whole table (or a whole index, for its order) and tests\n * each row, stopping early only if the reader stops.\n */\n readonly access: 'search' | 'scan';\n\n /**\n * Whether the rows are sorted after they are read. `full` means that every\n * matching row is read and sorted before the first one is returned, so\n * reading fewer rows (e.g. for a limit) does not help. `partial` means that\n * only runs of rows that tie on a prefix of the ordering are sorted.\n */\n readonly sort: 'none' | 'partial' | 'full';\n\n /** The number of rows in the table, if the cost model knows it. */\n readonly tableRows: number | undefined;\n};\n\nexport type CostModelCost = {\n startupCost: number;\n rows: number;\n fanout: FanoutCostModel;\n /** Omitted by cost models that cannot tell. */\n plan?: AccessPlan | undefined;\n};\n\n/**\n * A read of a connection's source under a particular constraint, as costed\n * by the cost model.\n */\nexport type ConnectionAccess = {\n /** The columns bound to a single value by the time of the read. */\n readonly constraint: PlannerConstraint | undefined;\n readonly filters: Condition | undefined;\n /** The estimated number of rows returned. */\n readonly rows: number;\n readonly plan: AccessPlan | undefined;\n};\nexport type ConnectionCostModel = (\n table: string,\n sort: Ordering,\n filters: Condition | undefined,\n constraint: PlannerConstraint | undefined,\n) => CostModelCost;\n"],"mappings":"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"}
@@ -6,6 +6,7 @@ import type { PlannerFanIn } from './planner-fan-in.ts';
6
6
  import type { PlannerFanOut } from './planner-fan-out.ts';
7
7
  import type { PlannerFilter } from './planner-filter.ts';
8
8
  import type { PlannerJoin } from './planner-join.ts';
9
+ import type { CostEstimate } from './planner-node.ts';
9
10
  import { PlannerSource, type ConnectionCostModel } from './planner-source.ts';
10
11
  import type { PlannerTerminus } from './planner-terminus.ts';
11
12
  /**
@@ -68,6 +69,15 @@ export declare class PlannerGraph {
68
69
  * Total cost includes both startup cost (one-time, e.g., sorting) and running cost.
69
70
  */
70
71
  getTotalCost(planDebugger?: PlanDebugger): number;
72
+ /**
73
+ * Estimates the cost of the current plan, e.g. the one {@link plan} chose,
74
+ * leaving each connection's {@link PlannerConnection.accesses} for it.
75
+ *
76
+ * This derives the same state that {@link plan} derives for each plan it
77
+ * tries, so it also works for a graph {@link plan} had nothing to choose
78
+ * for. For a plan that {@link plan} restored it changes nothing.
79
+ */
80
+ estimateCurrentPlan(): CostEstimate;
71
81
  /**
72
82
  * Capture a lightweight snapshot of the current planning state.
73
83
  * Used for backtracking during multi-start greedy search.
@@ -1 +1 @@
1
- {"version":3,"file":"planner-graph.d.ts","sourceRoot":"","sources":["../../../../../zql/src/planner/planner-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAGjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAGnD,OAAO,EAAC,aAAa,EAAE,KAAK,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,WAAW,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,CAAC,CAAC;IAChD,KAAK,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,CAAC,CAAC;IACzC,OAAO,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAC,CAAC,CAAC;IACrC,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAC,CAAC,CAAC;IACpC,qBAAqB,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC,CAAC,CAAC;CAC1E,CAAC;AAkBF,qBAAa,YAAY;;IAQvB,KAAK,EAAE,WAAW,EAAE,CAAM;IAC1B,OAAO,EAAE,aAAa,EAAE,CAAM;IAC9B,MAAM,EAAE,YAAY,EAAE,CAAM;IAC5B,WAAW,EAAE,iBAAiB,EAAE,CAAM;IACtC,OAAO,EAAE,aAAa,EAAE,CAAM;IAE9B;;;;;OAKG;IACH,kBAAkB,SAMjB;IAED;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,aAAa,CAQjE;IAED;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAIrC;IAED;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/B;IAED;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAE3C;IAED;;;;OAIG;IACH,oBAAoB,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAMtD;IAED;;;OAGG;IACH,YAAY,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,MAAM,CAGhD;IAED;;;;;;;;OAQG;IACH,uBAAuB,IAAI,SAAS,CAUnC;IAED;;;;;;;;OAQG;IACH,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAK9C;IAiFD;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,YAAY,CAAC,EAAE,YAAY,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI,CA8HvD;CACF"}
1
+ {"version":3,"file":"planner-graph.d.ts","sourceRoot":"","sources":["../../../../../zql/src/planner/planner-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAGjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAEnD,OAAO,KAAK,EAAC,YAAY,EAAc,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAC,aAAa,EAAE,KAAK,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,WAAW,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,CAAC,CAAC;IAChD,KAAK,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,CAAC,CAAC;IACzC,OAAO,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAC,CAAC,CAAC;IACrC,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAA;KAAC,CAAC,CAAC;IACpC,qBAAqB,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC,CAAC,CAAC;CAC1E,CAAC;AAkBF,qBAAa,YAAY;;IAQvB,KAAK,EAAE,WAAW,EAAE,CAAM;IAC1B,OAAO,EAAE,aAAa,EAAE,CAAM;IAC9B,MAAM,EAAE,YAAY,EAAE,CAAM;IAC5B,WAAW,EAAE,iBAAiB,EAAE,CAAM;IACtC,OAAO,EAAE,aAAa,EAAE,CAAM;IAE9B;;;;;OAKG;IACH,kBAAkB,SAMjB;IAED;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,aAAa,CAQjE;IAED;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAIrC;IAED;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/B;IAED;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAE3C;IAED;;;;OAIG;IACH,oBAAoB,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAMtD;IAED;;;OAGG;IACH,YAAY,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,MAAM,CAGhD;IAED;;;;;;;OAOG;IACH,mBAAmB,IAAI,YAAY,CAKlC;IAED;;;;;;;;OAQG;IACH,uBAAuB,IAAI,SAAS,CAUnC;IAED;;;;;;;;OAQG;IACH,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAK9C;IAiFD;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,YAAY,CAAC,EAAE,YAAY,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI,CA8HvD;CACF"}
@@ -78,6 +78,20 @@ var PlannerGraph = class {
78
78
  return estimate.cost + estimate.startupCost;
79
79
  }
80
80
  /**
81
+ * Estimates the cost of the current plan, e.g. the one {@link plan} chose,
82
+ * leaving each connection's {@link PlannerConnection.accesses} for it.
83
+ *
84
+ * This derives the same state that {@link plan} derives for each plan it
85
+ * tries, so it also works for a graph {@link plan} had nothing to choose
86
+ * for. For a plan that {@link plan} restored it changes nothing.
87
+ */
88
+ estimateCurrentPlan() {
89
+ checkAndConvertFOFI(buildFOFICache(this));
90
+ propagateUnlimitForFlippedJoins(this);
91
+ this.propagateConstraints();
92
+ return must(this.#terminus).estimateCost();
93
+ }
94
+ /**
81
95
  * Capture a lightweight snapshot of the current planning state.
82
96
  * Used for backtracking during multi-start greedy search.
83
97
  *
@@ -1 +1 @@
1
- {"version":3,"file":"planner-graph.js","names":["#sources","#terminus","#validateSnapshotShape","#restoreConnections","#restoreJoins","#restoreFanNodes"],"sources":["../../../../../zql/src/planner/planner-graph.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {PlannerConnection} from './planner-connection.ts';\nimport type {PlannerConstraint} from './planner-constraint.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport type {PlannerFanIn} from './planner-fan-in.ts';\nimport type {PlannerFanOut} from './planner-fan-out.ts';\nimport type {PlannerFilter} from './planner-filter.ts';\nimport type {PlannerJoin} from './planner-join.ts';\nimport {omitFanout} from './planner-node.ts';\nimport type {PlannerNode} from './planner-node.ts';\nimport {PlannerSource, type ConnectionCostModel} from './planner-source.ts';\nimport type {PlannerTerminus} from './planner-terminus.ts';\n\n/**\n * Captured state of a plan for comparison and restoration.\n */\nexport type PlanState = {\n connections: Array<{limit: number | undefined}>;\n joins: Array<{type: 'semi' | 'flipped'}>;\n fanOuts: Array<{type: 'FO' | 'UFO'}>;\n fanIns: Array<{type: 'FI' | 'UFI'}>;\n connectionConstraints: Array<Map<string, PlannerConstraint | undefined>>;\n};\n\n/**\n * Maximum number of flippable joins to attempt exhaustive enumeration.\n * With n flippable joins, we explore 2^n plans.\n * 10 joins = 1024 plans (~100-200ms), 12 joins = 4096 plans (~400ms - 1 second)\n */\nconst MAX_FLIPPABLE_JOINS = 9;\n\n/**\n * Cached information about FanOut→FanIn relationships.\n * Computed once during planning to avoid redundant BFS traversals.\n */\ntype FOFIInfo = {\n fi: PlannerFanIn | undefined;\n joinsBetween: PlannerJoin[];\n};\n\nexport class PlannerGraph {\n // Sources indexed by table name\n readonly #sources = new Map<string, PlannerSource>();\n\n // The final output node where constraint propagation starts\n #terminus: PlannerTerminus | undefined = undefined;\n\n // Collections of nodes with mutable planning state\n joins: PlannerJoin[] = [];\n fanOuts: PlannerFanOut[] = [];\n fanIns: PlannerFanIn[] = [];\n connections: PlannerConnection[] = [];\n filters: PlannerFilter[] = [];\n\n /**\n * Reset all planning state back to initial values for another planning pass.\n * Resets only mutable planning state - graph structure is unchanged.\n *\n * This allows replanning the same query graph with different strategies.\n */\n resetPlanningState() {\n for (const j of this.joins) j.reset();\n for (const fo of this.fanOuts) fo.reset();\n for (const fi of this.fanIns) fi.reset();\n for (const c of this.connections) c.reset();\n for (const f of this.filters) f.reset();\n }\n\n /**\n * Create and register a source (table) in the graph.\n */\n addSource(name: string, model: ConnectionCostModel): PlannerSource {\n assert(\n !this.#sources.has(name),\n `Source ${name} already exists in the graph`,\n );\n const source = new PlannerSource(name, model);\n this.#sources.set(name, source);\n return source;\n }\n\n /**\n * Get a source by table name.\n */\n getSource(name: string): PlannerSource {\n const source = this.#sources.get(name);\n assert(source !== undefined, `Source ${name} not found in the graph`);\n return source;\n }\n\n /**\n * Check if a source exists by table name.\n */\n hasSource(name: string): boolean {\n return this.#sources.has(name);\n }\n\n /**\n * Set the terminus (final output) node of the graph.\n * Constraint propagation starts from this node.\n */\n setTerminus(terminus: PlannerTerminus): void {\n this.#terminus = terminus;\n }\n\n /**\n * Initiate constraint propagation from the terminus node.\n * This sends constraints up through the graph to update\n * connection cost estimates.\n */\n propagateConstraints(planDebugger?: PlanDebugger): void {\n assert(\n this.#terminus !== undefined,\n 'Cannot propagate constraints without a terminus node',\n );\n this.#terminus.propagateConstraints(planDebugger);\n }\n\n /**\n * Calculate total cost of the current plan.\n * Total cost includes both startup cost (one-time, e.g., sorting) and running cost.\n */\n getTotalCost(planDebugger?: PlanDebugger): number {\n const estimate = must(this.#terminus).estimateCost(planDebugger);\n return estimate.cost + estimate.startupCost;\n }\n\n /**\n * Capture a lightweight snapshot of the current planning state.\n * Used for backtracking during multi-start greedy search.\n *\n * Captures mutable state including pinned flags, join types, and\n * constraint maps to avoid needing repropagation on restore.\n *\n * @returns A snapshot that can be restored via restorePlanningSnapshot()\n */\n capturePlanningSnapshot(): PlanState {\n return {\n connections: this.connections.map(c => ({\n limit: c.limit,\n })),\n joins: this.joins.map(j => ({type: j.type})),\n fanOuts: this.fanOuts.map(fo => ({type: fo.type})),\n fanIns: this.fanIns.map(fi => ({type: fi.type})),\n connectionConstraints: this.connections.map(c => c.captureConstraints()),\n };\n }\n\n /**\n * Restore planning state from a previously captured snapshot.\n * Used for backtracking when a planning attempt fails.\n *\n * Restores pinned flags, join types, and constraint maps, eliminating\n * the need for repropagation.\n *\n * @param state - Snapshot created by capturePlanningSnapshot()\n */\n restorePlanningSnapshot(state: PlanState): void {\n this.#validateSnapshotShape(state);\n this.#restoreConnections(state);\n this.#restoreJoins(state);\n this.#restoreFanNodes(state);\n }\n\n /**\n * Validate that snapshot shape matches current graph structure.\n */\n #validateSnapshotShape(state: PlanState): void {\n assert(\n this.connections.length === state.connections.length,\n 'Plan state mismatch: connections',\n );\n assert(\n this.joins.length === state.joins.length,\n 'Plan state mismatch: joins',\n );\n assert(\n this.fanOuts.length === state.fanOuts.length,\n 'Plan state mismatch: fanOuts',\n );\n assert(\n this.fanIns.length === state.fanIns.length,\n 'Plan state mismatch: fanIns',\n );\n assert(\n this.connections.length === state.connectionConstraints.length,\n 'Plan state mismatch: connectionConstraints',\n );\n }\n\n /**\n * Restore connection pinned flags, limits, and constraint maps.\n */\n #restoreConnections(state: PlanState): void {\n for (let i = 0; i < this.connections.length; i++) {\n this.connections[i].limit = state.connections[i].limit;\n this.connections[i].restoreConstraints(state.connectionConstraints[i]);\n }\n }\n\n /**\n * Restore join types and pinned flags.\n */\n #restoreJoins(state: PlanState): void {\n for (let i = 0; i < this.joins.length; i++) {\n const join = this.joins[i];\n const targetState = state.joins[i];\n\n // Reset to initial state first\n join.reset();\n\n // Apply target state\n if (targetState.type === 'flipped' && join.type !== 'flipped') {\n join.flip();\n }\n assert(\n targetState.type === join.type,\n 'join is not in the correct state after reset',\n );\n }\n }\n\n /**\n * Restore FanOut and FanIn types.\n */\n #restoreFanNodes(state: PlanState): void {\n for (let i = 0; i < this.fanOuts.length; i++) {\n const fo = this.fanOuts[i];\n const targetType = state.fanOuts[i].type;\n if (targetType === 'UFO' && fo.type === 'FO') {\n fo.convertToUFO();\n }\n }\n\n for (let i = 0; i < this.fanIns.length; i++) {\n const fi = this.fanIns[i];\n const targetType = state.fanIns[i].type;\n if (targetType === 'UFI' && fi.type === 'FI') {\n fi.convertToUFI();\n }\n }\n }\n\n /**\n * Main planning algorithm using exhaustive join flip enumeration.\n *\n * Enumerates all possible flip patterns for flippable joins (2^n for n flippable joins).\n * Each pattern represents a different query execution plan. We evaluate the cost of each\n * plan and select the one with the lowest cost.\n *\n * Connections are used only for cost estimation - the flip patterns determine the plan.\n * FanOut/FanIn states (FO/UFO and FI/UFI) are automatically derived from join flip states.\n *\n * @param planDebugger - Optional debugger to receive structured events during planning\n * @param lc - Optional logger for warnings\n */\n plan(planDebugger?: PlanDebugger, lc?: LogContext): void {\n // Get all flippable joins\n const flippableJoins = this.joins.filter(j => j.isFlippable());\n\n // Too many flippable joins - skip optimization and run as-is\n if (flippableJoins.length > MAX_FLIPPABLE_JOINS) {\n lc?.warn?.(\n `Query has ${flippableJoins.length} EXISTS checks which would require ` +\n `${2 ** flippableJoins.length} plan evaluations. Skipping optimization.`,\n );\n return;\n }\n\n // Build FO→FI cache once to avoid redundant BFS traversals in each iteration\n const fofiCache = buildFOFICache(this);\n\n const numPatterns =\n flippableJoins.length === 0 ? 0 : 2 ** flippableJoins.length;\n let bestCost = Infinity;\n let bestPlan: PlanState | undefined = undefined;\n let bestAttemptNumber = -1;\n\n // Enumerate all flip patterns\n for (let pattern = 0; pattern < numPatterns; pattern++) {\n // Reset to initial state\n this.resetPlanningState();\n\n if (planDebugger) {\n planDebugger.log({\n type: 'attempt-start',\n attemptNumber: pattern,\n totalAttempts: numPatterns,\n });\n }\n\n // Apply flip pattern (treat pattern as bitmask)\n // Bit i set to 1 means flip join i\n for (let i = 0; i < flippableJoins.length; i++) {\n if (pattern & (1 << i)) {\n flippableJoins[i].flip();\n }\n }\n\n // Derive FO/UFO and FI/UFI states from join flip states\n checkAndConvertFOFI(fofiCache);\n\n // Propagate unlimiting for flipped joins\n propagateUnlimitForFlippedJoins(this);\n\n // Propagate constraints through the graph\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'constraints-propagated',\n attemptNumber: pattern,\n connectionConstraints: this.connections.map(c => {\n const constraintCosts = c.getConstraintCostsForDebug();\n const constraintCostsWithoutFanout: Record<\n string,\n Omit<(typeof constraintCosts)[string], 'fanout'>\n > = {};\n for (const [key, cost] of Object.entries(constraintCosts)) {\n constraintCostsWithoutFanout[key] = omitFanout(cost);\n }\n return {\n connection: c.name,\n constraints: c.getConstraintsForDebug(),\n constraintCosts: constraintCostsWithoutFanout,\n };\n }),\n });\n }\n\n // Evaluate this plan\n const totalCost = this.getTotalCost(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'plan-complete',\n attemptNumber: pattern,\n totalCost,\n flipPattern: pattern, // Bitmask of which joins are flipped\n planSnapshot: this.capturePlanningSnapshot(),\n joinStates: this.joins.map(j => {\n const info = j.getDebugInfo();\n return {\n join: info.name,\n type: info.type,\n };\n }),\n });\n }\n\n // Track best plan\n if (totalCost < bestCost) {\n bestCost = totalCost;\n bestPlan = this.capturePlanningSnapshot();\n bestAttemptNumber = pattern;\n }\n }\n\n // Restore best plan\n if (bestPlan) {\n this.restorePlanningSnapshot(bestPlan);\n // Propagate constraints to ensure all derived state is consistent\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'best-plan-selected',\n bestAttemptNumber,\n totalCost: bestCost,\n flipPattern: bestAttemptNumber, // The best attempt number is also the flip pattern\n joinStates: this.joins.map(j => ({\n join: j.getName(),\n type: j.type,\n })),\n });\n }\n } else {\n assert(\n numPatterns === 0,\n 'no plan was found but flippable joins did exist!',\n );\n }\n }\n}\n\n/**\n * Build cache of FO→FI relationships and joins between them.\n * Called once at the start of planning to avoid redundant BFS traversals.\n */\nfunction buildFOFICache(graph: PlannerGraph): Map<PlannerFanOut, FOFIInfo> {\n const cache = new Map<PlannerFanOut, FOFIInfo>();\n\n for (const fo of graph.fanOuts) {\n const info = findFIAndJoins(fo);\n cache.set(fo, info);\n }\n\n return cache;\n}\n\n/**\n * Check if any joins downstream of a FanOut (before reaching FanIn) are flipped.\n * If so, convert the FO to UFO and the FI to UFI.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction checkAndConvertFOFI(fofiCache: Map<PlannerFanOut, FOFIInfo>): void {\n for (const [fo, info] of fofiCache) {\n const hasFlippedJoin = info.joinsBetween.some(j => j.type === 'flipped');\n if (info.fi && hasFlippedJoin) {\n fo.convertToUFO();\n info.fi.convertToUFI();\n }\n }\n}\n\n/**\n * Traverse from a FanOut through its outputs to find the corresponding FanIn\n * and collect all joins along the way.\n */\nfunction findFIAndJoins(fo: PlannerFanOut): FOFIInfo {\n const joinsBetween: PlannerJoin[] = [];\n let fi: PlannerFanIn | undefined = undefined;\n\n // BFS through FO outputs to find FI and collect joins\n const queue: PlannerNode[] = [...fo.outputs];\n const visited = new Set<PlannerNode>();\n\n while (queue.length > 0) {\n const node = must(queue.shift());\n if (visited.has(node)) continue;\n visited.add(node);\n\n switch (node.kind) {\n case 'join':\n joinsBetween.push(node);\n queue.push(node.output);\n break;\n case 'fan-out':\n // Nested FO - traverse its outputs\n queue.push(...node.outputs);\n break;\n case 'fan-in':\n // Found the FI - this is the boundary, don't traverse further\n fi = node;\n break;\n case 'connection':\n // Shouldn't happen in a well-formed graph\n break;\n case 'terminus':\n // Reached the end without finding FI\n break;\n }\n }\n\n return {fi, joinsBetween};\n}\n\n/**\n * Propagate unlimiting to all flipped joins in the graph.\n * When a join is flipped, its child becomes the outer loop and should no longer\n * be limited by EXISTS semantics.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction propagateUnlimitForFlippedJoins(graph: PlannerGraph): void {\n for (const join of graph.joins) {\n if (join.type === 'flipped') {\n join.propagateUnlimit();\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"planner-graph.js","names":["#sources","#terminus","#validateSnapshotShape","#restoreConnections","#restoreJoins","#restoreFanNodes"],"sources":["../../../../../zql/src/planner/planner-graph.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {PlannerConnection} from './planner-connection.ts';\nimport type {PlannerConstraint} from './planner-constraint.ts';\nimport type {PlanDebugger} from './planner-debug.ts';\nimport type {PlannerFanIn} from './planner-fan-in.ts';\nimport type {PlannerFanOut} from './planner-fan-out.ts';\nimport type {PlannerFilter} from './planner-filter.ts';\nimport type {PlannerJoin} from './planner-join.ts';\nimport {omitFanout} from './planner-node.ts';\nimport type {CostEstimate, PlannerNode} from './planner-node.ts';\nimport {PlannerSource, type ConnectionCostModel} from './planner-source.ts';\nimport type {PlannerTerminus} from './planner-terminus.ts';\n\n/**\n * Captured state of a plan for comparison and restoration.\n */\nexport type PlanState = {\n connections: Array<{limit: number | undefined}>;\n joins: Array<{type: 'semi' | 'flipped'}>;\n fanOuts: Array<{type: 'FO' | 'UFO'}>;\n fanIns: Array<{type: 'FI' | 'UFI'}>;\n connectionConstraints: Array<Map<string, PlannerConstraint | undefined>>;\n};\n\n/**\n * Maximum number of flippable joins to attempt exhaustive enumeration.\n * With n flippable joins, we explore 2^n plans.\n * 10 joins = 1024 plans (~100-200ms), 12 joins = 4096 plans (~400ms - 1 second)\n */\nconst MAX_FLIPPABLE_JOINS = 9;\n\n/**\n * Cached information about FanOut→FanIn relationships.\n * Computed once during planning to avoid redundant BFS traversals.\n */\ntype FOFIInfo = {\n fi: PlannerFanIn | undefined;\n joinsBetween: PlannerJoin[];\n};\n\nexport class PlannerGraph {\n // Sources indexed by table name\n readonly #sources = new Map<string, PlannerSource>();\n\n // The final output node where constraint propagation starts\n #terminus: PlannerTerminus | undefined = undefined;\n\n // Collections of nodes with mutable planning state\n joins: PlannerJoin[] = [];\n fanOuts: PlannerFanOut[] = [];\n fanIns: PlannerFanIn[] = [];\n connections: PlannerConnection[] = [];\n filters: PlannerFilter[] = [];\n\n /**\n * Reset all planning state back to initial values for another planning pass.\n * Resets only mutable planning state - graph structure is unchanged.\n *\n * This allows replanning the same query graph with different strategies.\n */\n resetPlanningState() {\n for (const j of this.joins) j.reset();\n for (const fo of this.fanOuts) fo.reset();\n for (const fi of this.fanIns) fi.reset();\n for (const c of this.connections) c.reset();\n for (const f of this.filters) f.reset();\n }\n\n /**\n * Create and register a source (table) in the graph.\n */\n addSource(name: string, model: ConnectionCostModel): PlannerSource {\n assert(\n !this.#sources.has(name),\n `Source ${name} already exists in the graph`,\n );\n const source = new PlannerSource(name, model);\n this.#sources.set(name, source);\n return source;\n }\n\n /**\n * Get a source by table name.\n */\n getSource(name: string): PlannerSource {\n const source = this.#sources.get(name);\n assert(source !== undefined, `Source ${name} not found in the graph`);\n return source;\n }\n\n /**\n * Check if a source exists by table name.\n */\n hasSource(name: string): boolean {\n return this.#sources.has(name);\n }\n\n /**\n * Set the terminus (final output) node of the graph.\n * Constraint propagation starts from this node.\n */\n setTerminus(terminus: PlannerTerminus): void {\n this.#terminus = terminus;\n }\n\n /**\n * Initiate constraint propagation from the terminus node.\n * This sends constraints up through the graph to update\n * connection cost estimates.\n */\n propagateConstraints(planDebugger?: PlanDebugger): void {\n assert(\n this.#terminus !== undefined,\n 'Cannot propagate constraints without a terminus node',\n );\n this.#terminus.propagateConstraints(planDebugger);\n }\n\n /**\n * Calculate total cost of the current plan.\n * Total cost includes both startup cost (one-time, e.g., sorting) and running cost.\n */\n getTotalCost(planDebugger?: PlanDebugger): number {\n const estimate = must(this.#terminus).estimateCost(planDebugger);\n return estimate.cost + estimate.startupCost;\n }\n\n /**\n * Estimates the cost of the current plan, e.g. the one {@link plan} chose,\n * leaving each connection's {@link PlannerConnection.accesses} for it.\n *\n * This derives the same state that {@link plan} derives for each plan it\n * tries, so it also works for a graph {@link plan} had nothing to choose\n * for. For a plan that {@link plan} restored it changes nothing.\n */\n estimateCurrentPlan(): CostEstimate {\n checkAndConvertFOFI(buildFOFICache(this));\n propagateUnlimitForFlippedJoins(this);\n this.propagateConstraints();\n return must(this.#terminus).estimateCost();\n }\n\n /**\n * Capture a lightweight snapshot of the current planning state.\n * Used for backtracking during multi-start greedy search.\n *\n * Captures mutable state including pinned flags, join types, and\n * constraint maps to avoid needing repropagation on restore.\n *\n * @returns A snapshot that can be restored via restorePlanningSnapshot()\n */\n capturePlanningSnapshot(): PlanState {\n return {\n connections: this.connections.map(c => ({\n limit: c.limit,\n })),\n joins: this.joins.map(j => ({type: j.type})),\n fanOuts: this.fanOuts.map(fo => ({type: fo.type})),\n fanIns: this.fanIns.map(fi => ({type: fi.type})),\n connectionConstraints: this.connections.map(c => c.captureConstraints()),\n };\n }\n\n /**\n * Restore planning state from a previously captured snapshot.\n * Used for backtracking when a planning attempt fails.\n *\n * Restores pinned flags, join types, and constraint maps, eliminating\n * the need for repropagation.\n *\n * @param state - Snapshot created by capturePlanningSnapshot()\n */\n restorePlanningSnapshot(state: PlanState): void {\n this.#validateSnapshotShape(state);\n this.#restoreConnections(state);\n this.#restoreJoins(state);\n this.#restoreFanNodes(state);\n }\n\n /**\n * Validate that snapshot shape matches current graph structure.\n */\n #validateSnapshotShape(state: PlanState): void {\n assert(\n this.connections.length === state.connections.length,\n 'Plan state mismatch: connections',\n );\n assert(\n this.joins.length === state.joins.length,\n 'Plan state mismatch: joins',\n );\n assert(\n this.fanOuts.length === state.fanOuts.length,\n 'Plan state mismatch: fanOuts',\n );\n assert(\n this.fanIns.length === state.fanIns.length,\n 'Plan state mismatch: fanIns',\n );\n assert(\n this.connections.length === state.connectionConstraints.length,\n 'Plan state mismatch: connectionConstraints',\n );\n }\n\n /**\n * Restore connection pinned flags, limits, and constraint maps.\n */\n #restoreConnections(state: PlanState): void {\n for (let i = 0; i < this.connections.length; i++) {\n this.connections[i].limit = state.connections[i].limit;\n this.connections[i].restoreConstraints(state.connectionConstraints[i]);\n }\n }\n\n /**\n * Restore join types and pinned flags.\n */\n #restoreJoins(state: PlanState): void {\n for (let i = 0; i < this.joins.length; i++) {\n const join = this.joins[i];\n const targetState = state.joins[i];\n\n // Reset to initial state first\n join.reset();\n\n // Apply target state\n if (targetState.type === 'flipped' && join.type !== 'flipped') {\n join.flip();\n }\n assert(\n targetState.type === join.type,\n 'join is not in the correct state after reset',\n );\n }\n }\n\n /**\n * Restore FanOut and FanIn types.\n */\n #restoreFanNodes(state: PlanState): void {\n for (let i = 0; i < this.fanOuts.length; i++) {\n const fo = this.fanOuts[i];\n const targetType = state.fanOuts[i].type;\n if (targetType === 'UFO' && fo.type === 'FO') {\n fo.convertToUFO();\n }\n }\n\n for (let i = 0; i < this.fanIns.length; i++) {\n const fi = this.fanIns[i];\n const targetType = state.fanIns[i].type;\n if (targetType === 'UFI' && fi.type === 'FI') {\n fi.convertToUFI();\n }\n }\n }\n\n /**\n * Main planning algorithm using exhaustive join flip enumeration.\n *\n * Enumerates all possible flip patterns for flippable joins (2^n for n flippable joins).\n * Each pattern represents a different query execution plan. We evaluate the cost of each\n * plan and select the one with the lowest cost.\n *\n * Connections are used only for cost estimation - the flip patterns determine the plan.\n * FanOut/FanIn states (FO/UFO and FI/UFI) are automatically derived from join flip states.\n *\n * @param planDebugger - Optional debugger to receive structured events during planning\n * @param lc - Optional logger for warnings\n */\n plan(planDebugger?: PlanDebugger, lc?: LogContext): void {\n // Get all flippable joins\n const flippableJoins = this.joins.filter(j => j.isFlippable());\n\n // Too many flippable joins - skip optimization and run as-is\n if (flippableJoins.length > MAX_FLIPPABLE_JOINS) {\n lc?.warn?.(\n `Query has ${flippableJoins.length} EXISTS checks which would require ` +\n `${2 ** flippableJoins.length} plan evaluations. Skipping optimization.`,\n );\n return;\n }\n\n // Build FO→FI cache once to avoid redundant BFS traversals in each iteration\n const fofiCache = buildFOFICache(this);\n\n const numPatterns =\n flippableJoins.length === 0 ? 0 : 2 ** flippableJoins.length;\n let bestCost = Infinity;\n let bestPlan: PlanState | undefined = undefined;\n let bestAttemptNumber = -1;\n\n // Enumerate all flip patterns\n for (let pattern = 0; pattern < numPatterns; pattern++) {\n // Reset to initial state\n this.resetPlanningState();\n\n if (planDebugger) {\n planDebugger.log({\n type: 'attempt-start',\n attemptNumber: pattern,\n totalAttempts: numPatterns,\n });\n }\n\n // Apply flip pattern (treat pattern as bitmask)\n // Bit i set to 1 means flip join i\n for (let i = 0; i < flippableJoins.length; i++) {\n if (pattern & (1 << i)) {\n flippableJoins[i].flip();\n }\n }\n\n // Derive FO/UFO and FI/UFI states from join flip states\n checkAndConvertFOFI(fofiCache);\n\n // Propagate unlimiting for flipped joins\n propagateUnlimitForFlippedJoins(this);\n\n // Propagate constraints through the graph\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'constraints-propagated',\n attemptNumber: pattern,\n connectionConstraints: this.connections.map(c => {\n const constraintCosts = c.getConstraintCostsForDebug();\n const constraintCostsWithoutFanout: Record<\n string,\n Omit<(typeof constraintCosts)[string], 'fanout'>\n > = {};\n for (const [key, cost] of Object.entries(constraintCosts)) {\n constraintCostsWithoutFanout[key] = omitFanout(cost);\n }\n return {\n connection: c.name,\n constraints: c.getConstraintsForDebug(),\n constraintCosts: constraintCostsWithoutFanout,\n };\n }),\n });\n }\n\n // Evaluate this plan\n const totalCost = this.getTotalCost(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'plan-complete',\n attemptNumber: pattern,\n totalCost,\n flipPattern: pattern, // Bitmask of which joins are flipped\n planSnapshot: this.capturePlanningSnapshot(),\n joinStates: this.joins.map(j => {\n const info = j.getDebugInfo();\n return {\n join: info.name,\n type: info.type,\n };\n }),\n });\n }\n\n // Track best plan\n if (totalCost < bestCost) {\n bestCost = totalCost;\n bestPlan = this.capturePlanningSnapshot();\n bestAttemptNumber = pattern;\n }\n }\n\n // Restore best plan\n if (bestPlan) {\n this.restorePlanningSnapshot(bestPlan);\n // Propagate constraints to ensure all derived state is consistent\n this.propagateConstraints(planDebugger);\n\n if (planDebugger) {\n planDebugger.log({\n type: 'best-plan-selected',\n bestAttemptNumber,\n totalCost: bestCost,\n flipPattern: bestAttemptNumber, // The best attempt number is also the flip pattern\n joinStates: this.joins.map(j => ({\n join: j.getName(),\n type: j.type,\n })),\n });\n }\n } else {\n assert(\n numPatterns === 0,\n 'no plan was found but flippable joins did exist!',\n );\n }\n }\n}\n\n/**\n * Build cache of FO→FI relationships and joins between them.\n * Called once at the start of planning to avoid redundant BFS traversals.\n */\nfunction buildFOFICache(graph: PlannerGraph): Map<PlannerFanOut, FOFIInfo> {\n const cache = new Map<PlannerFanOut, FOFIInfo>();\n\n for (const fo of graph.fanOuts) {\n const info = findFIAndJoins(fo);\n cache.set(fo, info);\n }\n\n return cache;\n}\n\n/**\n * Check if any joins downstream of a FanOut (before reaching FanIn) are flipped.\n * If so, convert the FO to UFO and the FI to UFI.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction checkAndConvertFOFI(fofiCache: Map<PlannerFanOut, FOFIInfo>): void {\n for (const [fo, info] of fofiCache) {\n const hasFlippedJoin = info.joinsBetween.some(j => j.type === 'flipped');\n if (info.fi && hasFlippedJoin) {\n fo.convertToUFO();\n info.fi.convertToUFI();\n }\n }\n}\n\n/**\n * Traverse from a FanOut through its outputs to find the corresponding FanIn\n * and collect all joins along the way.\n */\nfunction findFIAndJoins(fo: PlannerFanOut): FOFIInfo {\n const joinsBetween: PlannerJoin[] = [];\n let fi: PlannerFanIn | undefined = undefined;\n\n // BFS through FO outputs to find FI and collect joins\n const queue: PlannerNode[] = [...fo.outputs];\n const visited = new Set<PlannerNode>();\n\n while (queue.length > 0) {\n const node = must(queue.shift());\n if (visited.has(node)) continue;\n visited.add(node);\n\n switch (node.kind) {\n case 'join':\n joinsBetween.push(node);\n queue.push(node.output);\n break;\n case 'fan-out':\n // Nested FO - traverse its outputs\n queue.push(...node.outputs);\n break;\n case 'fan-in':\n // Found the FI - this is the boundary, don't traverse further\n fi = node;\n break;\n case 'connection':\n // Shouldn't happen in a well-formed graph\n break;\n case 'terminus':\n // Reached the end without finding FI\n break;\n }\n }\n\n return {fi, joinsBetween};\n}\n\n/**\n * Propagate unlimiting to all flipped joins in the graph.\n * When a join is flipped, its child becomes the outer loop and should no longer\n * be limited by EXISTS semantics.\n *\n * This must be called after join flipping and before propagateConstraints.\n */\nfunction propagateUnlimitForFlippedJoins(graph: PlannerGraph): void {\n for (const join of graph.joins) {\n if (join.type === 'flipped') {\n join.propagateUnlimit();\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import type { Ordering } from '../../../zero-protocol/src/ast.ts';
2
+ import type { Plans } from './planner-builder.ts';
3
+ /**
4
+ * When the planner reports a {@link PlanWarning}. Each threshold is compared
5
+ * against the cost model's estimates, and 0 disables the warnings it gates.
6
+ */
7
+ export type PlanWarningThresholds = {
8
+ /**
9
+ * The number of rows a single read of a table must scan (for a
10
+ * {@link MissingIndexWarning}) or sort (for a {@link FullSortWarning}).
11
+ */
12
+ readonly rows: number;
13
+ /**
14
+ * The number of rows the chosen plan must be estimated to process, for a
15
+ * {@link HighCostWarning}.
16
+ */
17
+ readonly cost: number;
18
+ };
19
+ type TableRead = {
20
+ readonly table: string;
21
+ /**
22
+ * The aliases of the `related` subqueries leading to the read, starting
23
+ * from the root query. Empty for the root query and its EXISTS subqueries.
24
+ */
25
+ readonly path: readonly string[];
26
+ /**
27
+ * Whether the read runs once per row of another table, because a join
28
+ * binds some of its columns, rather than once per query.
29
+ */
30
+ readonly perRow: boolean;
31
+ /**
32
+ * An index that would let the source find the rows it reads and return
33
+ * them in order: the columns the rows are found by, then the ordering.
34
+ */
35
+ readonly suggestedIndex: readonly string[];
36
+ };
37
+ /**
38
+ * The rows of a table are found by columns that no index covers, so every
39
+ * read scans the whole table. This is typically a relationship whose foreign
40
+ * key is not indexed.
41
+ */
42
+ export type MissingIndexWarning = TableRead & {
43
+ readonly type: 'missing-index';
44
+ /**
45
+ * The columns the rows are found by: the columns a join binds and the
46
+ * columns filtered for equality.
47
+ */
48
+ readonly columns: readonly string[];
49
+ /** The rows each read scans. */
50
+ readonly rows: number;
51
+ };
52
+ /**
53
+ * No index returns the rows of a table in the requested order, so every read
54
+ * sorts all of the matching rows before returning the first one. A limit does
55
+ * not reduce the rows read.
56
+ */
57
+ export type FullSortWarning = TableRead & {
58
+ readonly type: 'full-sort';
59
+ readonly orderBy: Ordering;
60
+ /** The estimated number of rows each read sorts. */
61
+ readonly rows: number;
62
+ };
63
+ /**
64
+ * The best plan the planner found is still estimated to process a lot of
65
+ * rows. Only reported when every table the query reads has statistics, since
66
+ * the estimates are guesses otherwise.
67
+ */
68
+ export type HighCostWarning = {
69
+ readonly type: 'high-cost';
70
+ readonly table: string;
71
+ /** The estimated number of rows the plan processes. */
72
+ readonly cost: number;
73
+ };
74
+ export type PlanWarning = MissingIndexWarning | FullSortWarning | HighCostWarning;
75
+ /**
76
+ * Warns about what the cost model reports the plans in `plans` will do
77
+ * slowly. Call this after planning: it estimates each graph's current plan.
78
+ *
79
+ * @param table The table of the query that `plans` was built for.
80
+ */
81
+ export declare function collectPlanWarnings(table: string, plans: Plans, thresholds: PlanWarningThresholds): PlanWarning[];
82
+ export {};
83
+ //# sourceMappingURL=planner-warnings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner-warnings.d.ts","sourceRoot":"","sources":["../../../../../zql/src/planner/planner-warnings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,QAAQ,EAAC,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAC,KAAK,EAAC,MAAM,sBAAsB,CAAC;AAOhD;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG;IAC5C,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG;IACxC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GACnB,mBAAmB,GACnB,eAAe,GACf,eAAe,CAAC;AAEpB;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,qBAAqB,GAChC,WAAW,EAAE,CASf"}
@@ -0,0 +1,78 @@
1
+ //#region ../zql/src/planner/planner-warnings.ts
2
+ /**
3
+ * Warns about what the cost model reports the plans in `plans` will do
4
+ * slowly. Call this after planning: it estimates each graph's current plan.
5
+ *
6
+ * @param table The table of the query that `plans` was built for.
7
+ */
8
+ function collectPlanWarnings(table, plans, thresholds) {
9
+ const warnings = /* @__PURE__ */ new Map();
10
+ const state = {
11
+ warnings,
12
+ estimated: true
13
+ };
14
+ const cost = visit(plans, [], thresholds, state);
15
+ const result = [...warnings.values()];
16
+ if (state.estimated && thresholds.cost > 0 && cost >= thresholds.cost) result.push({
17
+ type: "high-cost",
18
+ table,
19
+ cost: Math.round(cost)
20
+ });
21
+ return result;
22
+ }
23
+ /**
24
+ * Collects the warnings for `plans` and its subplans, and returns the
25
+ * estimated number of rows they process.
26
+ */
27
+ function visit(plans, path, thresholds, state) {
28
+ const estimate = plans.plan.estimateCurrentPlan();
29
+ for (const connection of plans.plan.connections) for (const access of connection.accesses()) if (access.plan?.tableRows === void 0) state.estimated = false;
30
+ else if (thresholds.rows > 0) for (const warning of accessWarnings(connection, access, access.plan, access.plan.tableRows, path, thresholds.rows)) state.warnings.set(JSON.stringify(warning), warning);
31
+ let subCost = 0;
32
+ for (const [alias, subPlans] of Object.entries(plans.subPlans)) subCost += visit(subPlans, [...path, alias], thresholds, state);
33
+ const rowsOut = estimate.limit === void 0 ? estimate.returnedRows : Math.min(estimate.returnedRows, estimate.limit);
34
+ return estimate.startupCost + estimate.cost + estimate.scanEst + rowsOut * subCost;
35
+ }
36
+ function* accessWarnings(connection, access, plan, tableRows, path, minRows) {
37
+ const constrained = Object.keys(access.constraint ?? {});
38
+ const columns = unique([...constrained, ...equalityColumns(access.filters)]);
39
+ const read = {
40
+ table: connection.table,
41
+ path,
42
+ perRow: constrained.length > 0,
43
+ suggestedIndex: unique([...columns, ...connection.sort.map(([column]) => column)])
44
+ };
45
+ if (plan.access === "scan" && columns.length > 0 && (read.perRow || connection.limit === void 0) && tableRows >= minRows) yield {
46
+ type: "missing-index",
47
+ ...read,
48
+ columns,
49
+ rows: tableRows
50
+ };
51
+ if (plan.sort === "full" && access.rows >= minRows) yield {
52
+ type: "full-sort",
53
+ ...read,
54
+ orderBy: connection.sort,
55
+ rows: Math.round(access.rows)
56
+ };
57
+ }
58
+ /**
59
+ * The columns that the top-level conjuncts of `condition` compare for
60
+ * equality with a value, which an index lookup can use.
61
+ */
62
+ function equalityColumns(condition) {
63
+ switch (condition?.type) {
64
+ case "simple": {
65
+ const { left, op } = condition;
66
+ return (op === "=" || op === "IS" || op === "IN") && left.type === "column" ? [left.name] : [];
67
+ }
68
+ case "and": return condition.conditions.flatMap(equalityColumns);
69
+ default: return [];
70
+ }
71
+ }
72
+ function unique(values) {
73
+ return [...new Set(values)];
74
+ }
75
+ //#endregion
76
+ export { collectPlanWarnings };
77
+
78
+ //# sourceMappingURL=planner-warnings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner-warnings.js","names":[],"sources":["../../../../../zql/src/planner/planner-warnings.ts"],"sourcesContent":["import type {Condition, Ordering} from '../../../zero-protocol/src/ast.ts';\nimport type {Plans} from './planner-builder.ts';\nimport type {\n AccessPlan,\n ConnectionAccess,\n PlannerConnection,\n} from './planner-connection.ts';\n\n/**\n * When the planner reports a {@link PlanWarning}. Each threshold is compared\n * against the cost model's estimates, and 0 disables the warnings it gates.\n */\nexport type PlanWarningThresholds = {\n /**\n * The number of rows a single read of a table must scan (for a\n * {@link MissingIndexWarning}) or sort (for a {@link FullSortWarning}).\n */\n readonly rows: number;\n\n /**\n * The number of rows the chosen plan must be estimated to process, for a\n * {@link HighCostWarning}.\n */\n readonly cost: number;\n};\n\ntype TableRead = {\n readonly table: string;\n\n /**\n * The aliases of the `related` subqueries leading to the read, starting\n * from the root query. Empty for the root query and its EXISTS subqueries.\n */\n readonly path: readonly string[];\n\n /**\n * Whether the read runs once per row of another table, because a join\n * binds some of its columns, rather than once per query.\n */\n readonly perRow: boolean;\n\n /**\n * An index that would let the source find the rows it reads and return\n * them in order: the columns the rows are found by, then the ordering.\n */\n readonly suggestedIndex: readonly string[];\n};\n\n/**\n * The rows of a table are found by columns that no index covers, so every\n * read scans the whole table. This is typically a relationship whose foreign\n * key is not indexed.\n */\nexport type MissingIndexWarning = TableRead & {\n readonly type: 'missing-index';\n /**\n * The columns the rows are found by: the columns a join binds and the\n * columns filtered for equality.\n */\n readonly columns: readonly string[];\n /** The rows each read scans. */\n readonly rows: number;\n};\n\n/**\n * No index returns the rows of a table in the requested order, so every read\n * sorts all of the matching rows before returning the first one. A limit does\n * not reduce the rows read.\n */\nexport type FullSortWarning = TableRead & {\n readonly type: 'full-sort';\n readonly orderBy: Ordering;\n /** The estimated number of rows each read sorts. */\n readonly rows: number;\n};\n\n/**\n * The best plan the planner found is still estimated to process a lot of\n * rows. Only reported when every table the query reads has statistics, since\n * the estimates are guesses otherwise.\n */\nexport type HighCostWarning = {\n readonly type: 'high-cost';\n readonly table: string;\n /** The estimated number of rows the plan processes. */\n readonly cost: number;\n};\n\nexport type PlanWarning =\n | MissingIndexWarning\n | FullSortWarning\n | HighCostWarning;\n\n/**\n * Warns about what the cost model reports the plans in `plans` will do\n * slowly. Call this after planning: it estimates each graph's current plan.\n *\n * @param table The table of the query that `plans` was built for.\n */\nexport function collectPlanWarnings(\n table: string,\n plans: Plans,\n thresholds: PlanWarningThresholds,\n): PlanWarning[] {\n const warnings = new Map<string, PlanWarning>();\n const state = {warnings, estimated: true};\n const cost = visit(plans, [], thresholds, state);\n const result = [...warnings.values()];\n if (state.estimated && thresholds.cost > 0 && cost >= thresholds.cost) {\n result.push({type: 'high-cost', table, cost: Math.round(cost)});\n }\n return result;\n}\n\ntype VisitState = {\n readonly warnings: Map<string, PlanWarning>;\n /** Whether every access read a table with statistics. */\n estimated: boolean;\n};\n\n/**\n * Collects the warnings for `plans` and its subplans, and returns the\n * estimated number of rows they process.\n */\nfunction visit(\n plans: Plans,\n path: readonly string[],\n thresholds: PlanWarningThresholds,\n state: VisitState,\n): number {\n const estimate = plans.plan.estimateCurrentPlan();\n for (const connection of plans.plan.connections) {\n for (const access of connection.accesses()) {\n if (access.plan?.tableRows === undefined) {\n state.estimated = false;\n } else if (thresholds.rows > 0) {\n for (const warning of accessWarnings(\n connection,\n access,\n access.plan,\n access.plan.tableRows,\n path,\n thresholds.rows,\n )) {\n state.warnings.set(JSON.stringify(warning), warning);\n }\n }\n }\n }\n\n // A `related` subquery runs once for each row of its parent.\n let subCost = 0;\n for (const [alias, subPlans] of Object.entries(plans.subPlans)) {\n subCost += visit(subPlans, [...path, alias], thresholds, state);\n }\n const rowsOut =\n estimate.limit === undefined\n ? estimate.returnedRows\n : Math.min(estimate.returnedRows, estimate.limit);\n return (\n estimate.startupCost + estimate.cost + estimate.scanEst + rowsOut * subCost\n );\n}\n\nfunction* accessWarnings(\n connection: PlannerConnection,\n access: ConnectionAccess,\n plan: AccessPlan,\n tableRows: number,\n path: readonly string[],\n minRows: number,\n): Iterable<MissingIndexWarning | FullSortWarning> {\n const constrained = Object.keys(access.constraint ?? {});\n const columns = unique([...constrained, ...equalityColumns(access.filters)]);\n const read: TableRead = {\n table: connection.table,\n path,\n perRow: constrained.length > 0,\n suggestedIndex: unique([\n ...columns,\n ...connection.sort.map(([column]) => column),\n ]),\n };\n\n // A scan that stops at a limit reads an unknown share of the table, unless\n // it runs per row: then even a limit of one (an EXISTS) scans the table\n // for every row that has no match.\n if (\n plan.access === 'scan' &&\n columns.length > 0 &&\n (read.perRow || connection.limit === undefined) &&\n tableRows >= minRows\n ) {\n yield {type: 'missing-index', ...read, columns, rows: tableRows};\n }\n\n if (plan.sort === 'full' && access.rows >= minRows) {\n yield {\n type: 'full-sort',\n ...read,\n orderBy: connection.sort,\n rows: Math.round(access.rows),\n };\n }\n}\n\n/**\n * The columns that the top-level conjuncts of `condition` compare for\n * equality with a value, which an index lookup can use.\n */\nfunction equalityColumns(condition: Condition | undefined): string[] {\n switch (condition?.type) {\n case 'simple': {\n const {left, op} = condition;\n return (op === '=' || op === 'IS' || op === 'IN') &&\n left.type === 'column'\n ? [left.name]\n : [];\n }\n case 'and':\n return condition.conditions.flatMap(equalityColumns);\n default:\n return [];\n }\n}\n\nfunction unique(values: string[]): string[] {\n return [...new Set(values)];\n}\n"],"mappings":";;;;;;;AAmGA,SAAgB,oBACd,OACA,OACA,YACe;CACf,MAAM,2BAAW,IAAI,IAAyB;CAC9C,MAAM,QAAQ;EAAC;EAAU,WAAW;CAAI;CACxC,MAAM,OAAO,MAAM,OAAO,CAAC,GAAG,YAAY,KAAK;CAC/C,MAAM,SAAS,CAAC,GAAG,SAAS,OAAO,CAAC;CACpC,IAAI,MAAM,aAAa,WAAW,OAAO,KAAK,QAAQ,WAAW,MAC/D,OAAO,KAAK;EAAC,MAAM;EAAa;EAAO,MAAM,KAAK,MAAM,IAAI;CAAC,CAAC;CAEhE,OAAO;AACT;;;;;AAYA,SAAS,MACP,OACA,MACA,YACA,OACQ;CACR,MAAM,WAAW,MAAM,KAAK,oBAAoB;CAChD,KAAK,MAAM,cAAc,MAAM,KAAK,aAClC,KAAK,MAAM,UAAU,WAAW,SAAS,GACvC,IAAI,OAAO,MAAM,cAAc,KAAA,GAC7B,MAAM,YAAY;MACb,IAAI,WAAW,OAAO,GAC3B,KAAK,MAAM,WAAW,eACpB,YACA,QACA,OAAO,MACP,OAAO,KAAK,WACZ,MACA,WAAW,IACb,GACE,MAAM,SAAS,IAAI,KAAK,UAAU,OAAO,GAAG,OAAO;CAO3D,IAAI,UAAU;CACd,KAAK,MAAM,CAAC,OAAO,aAAa,OAAO,QAAQ,MAAM,QAAQ,GAC3D,WAAW,MAAM,UAAU,CAAC,GAAG,MAAM,KAAK,GAAG,YAAY,KAAK;CAEhE,MAAM,UACJ,SAAS,UAAU,KAAA,IACf,SAAS,eACT,KAAK,IAAI,SAAS,cAAc,SAAS,KAAK;CACpD,OACE,SAAS,cAAc,SAAS,OAAO,SAAS,UAAU,UAAU;AAExE;AAEA,UAAU,eACR,YACA,QACA,MACA,WACA,MACA,SACiD;CACjD,MAAM,cAAc,OAAO,KAAK,OAAO,cAAc,CAAC,CAAC;CACvD,MAAM,UAAU,OAAO,CAAC,GAAG,aAAa,GAAG,gBAAgB,OAAO,OAAO,CAAC,CAAC;CAC3E,MAAM,OAAkB;EACtB,OAAO,WAAW;EAClB;EACA,QAAQ,YAAY,SAAS;EAC7B,gBAAgB,OAAO,CACrB,GAAG,SACH,GAAG,WAAW,KAAK,KAAK,CAAC,YAAY,MAAM,CAC7C,CAAC;CACH;CAKA,IACE,KAAK,WAAW,UAChB,QAAQ,SAAS,MAChB,KAAK,UAAU,WAAW,UAAU,KAAA,MACrC,aAAa,SAEb,MAAM;EAAC,MAAM;EAAiB,GAAG;EAAM;EAAS,MAAM;CAAS;CAGjE,IAAI,KAAK,SAAS,UAAU,OAAO,QAAQ,SACzC,MAAM;EACJ,MAAM;EACN,GAAG;EACH,SAAS,WAAW;EACpB,MAAM,KAAK,MAAM,OAAO,IAAI;CAC9B;AAEJ;;;;;AAMA,SAAS,gBAAgB,WAA4C;CACnE,QAAQ,WAAW,MAAnB;EACE,KAAK,UAAU;GACb,MAAM,EAAC,MAAM,OAAM;GACnB,QAAQ,OAAO,OAAO,OAAO,QAAQ,OAAO,SAC1C,KAAK,SAAS,WACZ,CAAC,KAAK,IAAI,IACV,CAAC;EACP;EACA,KAAK,OACH,OAAO,UAAU,WAAW,QAAQ,eAAe;EACrD,SACE,OAAO,CAAC;CACZ;AACF;AAEA,SAAS,OAAO,QAA4B;CAC1C,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAC5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"measure-push-operator.d.ts","sourceRoot":"","sources":["../../../../../zql/src/query/measure-push-operator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D,KAAK,UAAU,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAEhE,qBAAa,mBAAoB,YAAW,QAAQ;;IAQlD,YACE,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,UAAU,EAOvB;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE9B;IAED,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,CAE/C;IAED,SAAS,IAAI,YAAY,CAExB;IAED,OAAO,IAAI,IAAI,CAEd;IAEA,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAQrC;IAEA,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,CAU9C;CACF"}
1
+ {"version":3,"file":"measure-push-operator.d.ts","sourceRoot":"","sources":["../../../../../zql/src/query/measure-push-operator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D,KAAK,UAAU,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAEhE,qBAAa,mBAAoB,YAAW,QAAQ;;IAQlD,YACE,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,eAAe,EAChC,UAAU,EAAE,UAAU,EAOvB;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE9B;IAED,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,CAE/C;IAED,SAAS,IAAI,YAAY,CAExB;IAED,OAAO,IAAI,IAAI,CAEd;IAED,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAEpC;IAEA,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,CAK9C;CA+BF"}
@@ -25,16 +25,38 @@ var MeasurePushOperator = class {
25
25
  destroy() {
26
26
  this.#input.destroy();
27
27
  }
28
- *push(change) {
29
- const startTime = performance.now();
30
- yield* this.#output.push(change, this);
31
- this.#metricsDelegate.addMetric(this.#metricName, performance.now() - startTime, this.#queryID);
28
+ push(change) {
29
+ return this.#measure(() => this.#output.push(change, this));
32
30
  }
33
31
  *reconcile(_pusher) {
34
- if (this.#output.reconcile) {
35
- const startTime = performance.now();
36
- yield* this.#output.reconcile(this);
37
- this.#metricsDelegate.addMetric(this.#metricName, performance.now() - startTime, this.#queryID);
32
+ const reconcile = this.#output.reconcile?.bind(this.#output);
33
+ if (reconcile) yield* this.#measure(() => reconcile(this));
34
+ }
35
+ /**
36
+ * Runs the stream returned by `run` and records the time spent running it.
37
+ *
38
+ * The time the stream spends suspended at a `yield` is not counted: the
39
+ * caller yields the thread there, and whatever else runs in the meantime
40
+ * is not this query's work.
41
+ *
42
+ * The time is recorded even if the stream throws or is abandoned, so that
43
+ * a push aborted for taking too long is accounted to its query.
44
+ */
45
+ *#measure(run) {
46
+ let elapsed = 0;
47
+ let start = performance.now();
48
+ let running = true;
49
+ try {
50
+ for (const result of run()) {
51
+ elapsed += performance.now() - start;
52
+ running = false;
53
+ yield result;
54
+ running = true;
55
+ start = performance.now();
56
+ }
57
+ } finally {
58
+ if (running) elapsed += performance.now() - start;
59
+ this.#metricsDelegate.addMetric(this.#metricName, elapsed, this.#queryID);
38
60
  }
39
61
  }
40
62
  };
@@ -1 +1 @@
1
- {"version":3,"file":"measure-push-operator.js","names":["#input","#queryID","#metricsDelegate","#metricName","#output"],"sources":["../../../../../zql/src/query/measure-push-operator.ts"],"sourcesContent":["import type {Change} from '../ivm/change.ts';\nimport type {Node} from '../ivm/data.ts';\nimport {\n throwOutput,\n type FetchRequest,\n type Input,\n type InputBase,\n type Operator,\n type Output,\n} from '../ivm/operator.ts';\nimport type {SourceSchema} from '../ivm/schema.ts';\nimport type {Stream} from '../ivm/stream.ts';\nimport type {MetricsDelegate} from './metrics-delegate.ts';\n\ntype MetricName = 'query-update-client' | 'query-update-server';\n\nexport class MeasurePushOperator implements Operator {\n readonly #input: Input;\n readonly #queryID: string;\n readonly #metricsDelegate: MetricsDelegate;\n\n #output: Output = throwOutput;\n readonly #metricName: MetricName;\n\n constructor(\n input: Input,\n queryID: string,\n metricsDelegate: MetricsDelegate,\n metricName: MetricName,\n ) {\n this.#input = input;\n this.#queryID = queryID;\n this.#metricsDelegate = metricsDelegate;\n this.#metricName = metricName;\n input.setOutput(this);\n }\n\n setOutput(output: Output): void {\n this.#output = output;\n }\n\n fetch(req: FetchRequest): Stream<Node | 'yield'> {\n return this.#input.fetch(req);\n }\n\n getSchema(): SourceSchema {\n return this.#input.getSchema();\n }\n\n destroy(): void {\n this.#input.destroy();\n }\n\n *push(change: Change): Stream<'yield'> {\n const startTime = performance.now();\n yield* this.#output.push(change, this);\n this.#metricsDelegate.addMetric(\n this.#metricName,\n performance.now() - startTime,\n this.#queryID,\n );\n }\n\n *reconcile(_pusher: InputBase): Stream<'yield'> {\n if (this.#output.reconcile) {\n const startTime = performance.now();\n yield* this.#output.reconcile(this);\n this.#metricsDelegate.addMetric(\n this.#metricName,\n performance.now() - startTime,\n this.#queryID,\n );\n }\n }\n}\n"],"mappings":";;AAgBA,IAAa,sBAAb,MAAqD;CACnD;CACA;CACA;CAEA,UAAkB;CAClB;CAEA,YACE,OACA,SACA,iBACA,YACA;EACA,KAAKA,SAAS;EACd,KAAKC,WAAW;EAChB,KAAKC,mBAAmB;EACxB,KAAKC,cAAc;EACnB,MAAM,UAAU,IAAI;CACtB;CAEA,UAAU,QAAsB;EAC9B,KAAKC,UAAU;CACjB;CAEA,MAAM,KAA2C;EAC/C,OAAO,KAAKJ,OAAO,MAAM,GAAG;CAC9B;CAEA,YAA0B;EACxB,OAAO,KAAKA,OAAO,UAAU;CAC/B;CAEA,UAAgB;EACd,KAAKA,OAAO,QAAQ;CACtB;CAEA,CAAC,KAAK,QAAiC;EACrC,MAAM,YAAY,YAAY,IAAI;EAClC,OAAO,KAAKI,QAAQ,KAAK,QAAQ,IAAI;EACrC,KAAKF,iBAAiB,UACpB,KAAKC,aACL,YAAY,IAAI,IAAI,WACpB,KAAKF,QACP;CACF;CAEA,CAAC,UAAU,SAAqC;EAC9C,IAAI,KAAKG,QAAQ,WAAW;GAC1B,MAAM,YAAY,YAAY,IAAI;GAClC,OAAO,KAAKA,QAAQ,UAAU,IAAI;GAClC,KAAKF,iBAAiB,UACpB,KAAKC,aACL,YAAY,IAAI,IAAI,WACpB,KAAKF,QACP;EACF;CACF;AACF"}
1
+ {"version":3,"file":"measure-push-operator.js","names":["#input","#queryID","#metricsDelegate","#metricName","#output","#measure"],"sources":["../../../../../zql/src/query/measure-push-operator.ts"],"sourcesContent":["import type {Change} from '../ivm/change.ts';\nimport type {Node} from '../ivm/data.ts';\nimport {\n throwOutput,\n type FetchRequest,\n type Input,\n type InputBase,\n type Operator,\n type Output,\n} from '../ivm/operator.ts';\nimport type {SourceSchema} from '../ivm/schema.ts';\nimport type {Stream} from '../ivm/stream.ts';\nimport type {MetricsDelegate} from './metrics-delegate.ts';\n\ntype MetricName = 'query-update-client' | 'query-update-server';\n\nexport class MeasurePushOperator implements Operator {\n readonly #input: Input;\n readonly #queryID: string;\n readonly #metricsDelegate: MetricsDelegate;\n\n #output: Output = throwOutput;\n readonly #metricName: MetricName;\n\n constructor(\n input: Input,\n queryID: string,\n metricsDelegate: MetricsDelegate,\n metricName: MetricName,\n ) {\n this.#input = input;\n this.#queryID = queryID;\n this.#metricsDelegate = metricsDelegate;\n this.#metricName = metricName;\n input.setOutput(this);\n }\n\n setOutput(output: Output): void {\n this.#output = output;\n }\n\n fetch(req: FetchRequest): Stream<Node | 'yield'> {\n return this.#input.fetch(req);\n }\n\n getSchema(): SourceSchema {\n return this.#input.getSchema();\n }\n\n destroy(): void {\n this.#input.destroy();\n }\n\n push(change: Change): Stream<'yield'> {\n return this.#measure(() => this.#output.push(change, this));\n }\n\n *reconcile(_pusher: InputBase): Stream<'yield'> {\n const reconcile = this.#output.reconcile?.bind(this.#output);\n if (reconcile) {\n yield* this.#measure(() => reconcile(this));\n }\n }\n\n /**\n * Runs the stream returned by `run` and records the time spent running it.\n *\n * The time the stream spends suspended at a `yield` is not counted: the\n * caller yields the thread there, and whatever else runs in the meantime\n * is not this query's work.\n *\n * The time is recorded even if the stream throws or is abandoned, so that\n * a push aborted for taking too long is accounted to its query.\n */\n *#measure(run: () => Stream<'yield'>): Stream<'yield'> {\n let elapsed = 0;\n let start = performance.now();\n let running = true;\n try {\n for (const result of run()) {\n elapsed += performance.now() - start;\n running = false;\n yield result;\n running = true;\n start = performance.now();\n }\n } finally {\n if (running) {\n elapsed += performance.now() - start;\n }\n this.#metricsDelegate.addMetric(this.#metricName, elapsed, this.#queryID);\n }\n }\n}\n"],"mappings":";;AAgBA,IAAa,sBAAb,MAAqD;CACnD;CACA;CACA;CAEA,UAAkB;CAClB;CAEA,YACE,OACA,SACA,iBACA,YACA;EACA,KAAKA,SAAS;EACd,KAAKC,WAAW;EAChB,KAAKC,mBAAmB;EACxB,KAAKC,cAAc;EACnB,MAAM,UAAU,IAAI;CACtB;CAEA,UAAU,QAAsB;EAC9B,KAAKC,UAAU;CACjB;CAEA,MAAM,KAA2C;EAC/C,OAAO,KAAKJ,OAAO,MAAM,GAAG;CAC9B;CAEA,YAA0B;EACxB,OAAO,KAAKA,OAAO,UAAU;CAC/B;CAEA,UAAgB;EACd,KAAKA,OAAO,QAAQ;CACtB;CAEA,KAAK,QAAiC;EACpC,OAAO,KAAKK,eAAe,KAAKD,QAAQ,KAAK,QAAQ,IAAI,CAAC;CAC5D;CAEA,CAAC,UAAU,SAAqC;EAC9C,MAAM,YAAY,KAAKA,QAAQ,WAAW,KAAK,KAAKA,OAAO;EAC3D,IAAI,WACF,OAAO,KAAKC,eAAe,UAAU,IAAI,CAAC;CAE9C;;;;;;;;;;;CAYA,CAACA,SAAS,KAA6C;EACrD,IAAI,UAAU;EACd,IAAI,QAAQ,YAAY,IAAI;EAC5B,IAAI,UAAU;EACd,IAAI;GACF,KAAK,MAAM,UAAU,IAAI,GAAG;IAC1B,WAAW,YAAY,IAAI,IAAI;IAC/B,UAAU;IACV,MAAM;IACN,UAAU;IACV,QAAQ,YAAY,IAAI;GAC1B;EACF,UAAU;GACR,IAAI,SACF,WAAW,YAAY,IAAI,IAAI;GAEjC,KAAKH,iBAAiB,UAAU,KAAKC,aAAa,SAAS,KAAKF,QAAQ;EAC1E;CACF;AACF"}