@rocicorp/zero 1.12.0-canary.12 → 1.12.0-canary.13

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 (211) hide show
  1. package/out/replicache/src/btree/diff.d.ts.map +1 -1
  2. package/out/replicache/src/btree/diff.js +1 -2
  3. package/out/replicache/src/btree/diff.js.map +1 -1
  4. package/out/replicache/src/btree/read.d.ts +31 -1
  5. package/out/replicache/src/btree/read.d.ts.map +1 -1
  6. package/out/replicache/src/btree/read.js +74 -19
  7. package/out/replicache/src/btree/read.js.map +1 -1
  8. package/out/replicache/src/btree/splice.d.ts.map +1 -1
  9. package/out/replicache/src/btree/splice.js +2 -1
  10. package/out/replicache/src/btree/splice.js.map +1 -1
  11. package/out/replicache/src/btree/write.d.ts.map +1 -1
  12. package/out/replicache/src/btree/write.js +2 -1
  13. package/out/replicache/src/btree/write.js.map +1 -1
  14. package/out/replicache/src/persist/collect-idb-databases.d.ts +1 -0
  15. package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
  16. package/out/replicache/src/persist/collect-idb-databases.js +13 -5
  17. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  18. package/out/replicache/src/scan-iterator.js +1 -1
  19. package/out/replicache/src/sync/request-id.js +2 -2
  20. package/out/replicache/src/sync/request-id.js.map +1 -1
  21. package/out/shared/src/bigint-json.d.ts.map +1 -1
  22. package/out/shared/src/bigint-json.js +1 -0
  23. package/out/shared/src/bigint-json.js.map +1 -1
  24. package/out/shared/src/btree-set.d.ts +21 -0
  25. package/out/shared/src/btree-set.d.ts.map +1 -1
  26. package/out/shared/src/btree-set.js +54 -0
  27. package/out/shared/src/btree-set.js.map +1 -1
  28. package/out/shared/src/headers.js +20 -0
  29. package/out/shared/src/headers.js.map +1 -0
  30. package/out/shared/src/natural-runs.d.ts +191 -0
  31. package/out/shared/src/natural-runs.d.ts.map +1 -0
  32. package/out/shared/src/natural-runs.js +337 -0
  33. package/out/shared/src/natural-runs.js.map +1 -0
  34. package/out/shared/src/random-uint64.d.ts.map +1 -1
  35. package/out/shared/src/random-uint64.js +2 -2
  36. package/out/shared/src/random-uint64.js.map +1 -1
  37. package/out/shared/src/random-values.d.ts +19 -1
  38. package/out/shared/src/random-values.d.ts.map +1 -1
  39. package/out/shared/src/random-values.js +38 -4
  40. package/out/shared/src/random-values.js.map +1 -1
  41. package/out/shared/src/tuple-map.d.ts +22 -0
  42. package/out/shared/src/tuple-map.d.ts.map +1 -0
  43. package/out/shared/src/tuple-map.js +48 -0
  44. package/out/shared/src/tuple-map.js.map +1 -0
  45. package/out/zero/package.js +1 -1
  46. package/out/zero/package.js.map +1 -1
  47. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  48. package/out/zero-cache/src/auth/auth.js +5 -0
  49. package/out/zero-cache/src/auth/auth.js.map +1 -1
  50. package/out/zero-cache/src/config/zero-config.d.ts +21 -0
  51. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  52. package/out/zero-cache/src/config/zero-config.js +47 -9
  53. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  54. package/out/zero-cache/src/custom-queries/transform-query.js +2 -2
  55. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  56. package/out/zero-cache/src/db/pg-to-lite.d.ts.map +1 -1
  57. package/out/zero-cache/src/db/pg-to-lite.js +3 -2
  58. package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
  59. package/out/zero-cache/src/db/pg-type-parser.js +55 -2
  60. package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
  61. package/out/zero-cache/src/server/change-streamer.js +5 -2
  62. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  63. package/out/zero-cache/src/server/inspector-delegate.js +6 -29
  64. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  65. package/out/zero-cache/src/server/main.js +5 -0
  66. package/out/zero-cache/src/server/main.js.map +1 -1
  67. package/out/zero-cache/src/server/replicator.js +3 -2
  68. package/out/zero-cache/src/server/replicator.js.map +1 -1
  69. package/out/zero-cache/src/server/syncer.js +5 -4
  70. package/out/zero-cache/src/server/syncer.js.map +1 -1
  71. package/out/zero-cache/src/server/worker-dispatcher.d.ts.map +1 -1
  72. package/out/zero-cache/src/server/worker-dispatcher.js +2 -1
  73. package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
  74. package/out/zero-cache/src/services/admin-auth.d.ts +22 -0
  75. package/out/zero-cache/src/services/admin-auth.d.ts.map +1 -0
  76. package/out/zero-cache/src/services/admin-auth.js +42 -0
  77. package/out/zero-cache/src/services/admin-auth.js.map +1 -0
  78. package/out/zero-cache/src/services/auth-attempt-counter.d.ts +33 -0
  79. package/out/zero-cache/src/services/auth-attempt-counter.d.ts.map +1 -0
  80. package/out/zero-cache/src/services/auth-attempt-counter.js +61 -0
  81. package/out/zero-cache/src/services/auth-attempt-counter.js.map +1 -0
  82. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
  83. package/out/zero-cache/src/services/change-source/pg/change-source.js +110 -13
  84. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  85. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +13 -7
  86. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  87. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +38 -5
  88. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  89. package/out/zero-cache/src/services/change-source/pg/schema/init.js +10 -1
  90. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  91. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +4 -0
  92. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  93. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -3
  94. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  95. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +23 -2
  96. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  97. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
  98. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  99. package/out/zero-cache/src/services/change-streamer/change-streamer.js +10 -3
  100. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  101. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  102. package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
  103. package/out/zero-cache/src/services/heapz.js +4 -5
  104. package/out/zero-cache/src/services/heapz.js.map +1 -1
  105. package/out/zero-cache/src/services/mutagen/error.d.ts +5 -0
  106. package/out/zero-cache/src/services/mutagen/error.d.ts.map +1 -1
  107. package/out/zero-cache/src/services/mutagen/error.js +7 -1
  108. package/out/zero-cache/src/services/mutagen/error.js.map +1 -1
  109. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  110. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  111. package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
  112. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  113. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
  114. package/out/zero-cache/src/services/plannerz.js +3 -7
  115. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  116. package/out/zero-cache/src/services/profz.d.ts.map +1 -1
  117. package/out/zero-cache/src/services/profz.js +4 -13
  118. package/out/zero-cache/src/services/profz.js.map +1 -1
  119. package/out/zero-cache/src/services/replicator/change-processor.js +29 -2
  120. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  121. package/out/zero-cache/src/services/replicator/incremental-sync.js +3 -3
  122. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  123. package/out/zero-cache/src/services/statz.d.ts.map +1 -1
  124. package/out/zero-cache/src/services/statz.js +2 -7
  125. package/out/zero-cache/src/services/statz.js.map +1 -1
  126. package/out/zero-cache/src/services/view-syncer/client-handler.js +22 -1
  127. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  128. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +40 -2
  129. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  130. package/out/zero-cache/src/services/view-syncer/cvr-store.js +54 -17
  131. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  132. package/out/zero-cache/src/services/view-syncer/cvr.js +11 -0
  133. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  134. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js +107 -0
  135. package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js.map +1 -0
  136. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -6
  137. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  138. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +2 -1
  139. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  140. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +16 -14
  141. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  142. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +4 -1
  143. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  144. package/out/zero-cache/src/services/view-syncer/schema/init.js +3 -0
  145. package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
  146. package/out/zero-cache/src/services/view-syncer/view-syncer.js +89 -51
  147. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  148. package/out/zero-cache/src/types/client-group-log-id.d.ts +9 -0
  149. package/out/zero-cache/src/types/client-group-log-id.d.ts.map +1 -0
  150. package/out/zero-cache/src/types/client-group-log-id.js +16 -0
  151. package/out/zero-cache/src/types/client-group-log-id.js.map +1 -0
  152. package/out/zero-cache/src/types/lite.d.ts.map +1 -1
  153. package/out/zero-cache/src/types/lite.js +1 -0
  154. package/out/zero-cache/src/types/lite.js.map +1 -1
  155. package/out/zero-cache/src/types/pg.d.ts +13 -2
  156. package/out/zero-cache/src/types/pg.d.ts.map +1 -1
  157. package/out/zero-cache/src/types/pg.js +2 -2
  158. package/out/zero-cache/src/types/pg.js.map +1 -1
  159. package/out/zero-cache/src/types/poke-chunk.d.ts +25 -4
  160. package/out/zero-cache/src/types/poke-chunk.d.ts.map +1 -1
  161. package/out/zero-cache/src/types/poke-chunk.js +46 -5
  162. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  163. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  164. package/out/zero-cache/src/workers/connection.js +10 -3
  165. package/out/zero-cache/src/workers/connection.js.map +1 -1
  166. package/out/zero-cache/src/workers/rehome.d.ts +10 -0
  167. package/out/zero-cache/src/workers/rehome.d.ts.map +1 -1
  168. package/out/zero-cache/src/workers/rehome.js +4 -2
  169. package/out/zero-cache/src/workers/rehome.js.map +1 -1
  170. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +3 -2
  171. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  172. package/out/zero-cache/src/workers/syncer.js +15 -15
  173. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  174. package/out/zero-client/src/client/inspector/inspector.d.ts +4 -0
  175. package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
  176. package/out/zero-client/src/client/inspector/inspector.js +4 -0
  177. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  178. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +10 -2
  179. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
  180. package/out/zero-client/src/client/inspector/lazy-inspector.js +38 -31
  181. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  182. package/out/zero-client/src/client/inspector/query.d.ts +2 -2
  183. package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
  184. package/out/zero-client/src/client/inspector/query.js +4 -4
  185. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  186. package/out/zero-client/src/client/ivm-branch.d.ts +3 -0
  187. package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
  188. package/out/zero-client/src/client/ivm-branch.js +10 -5
  189. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  190. package/out/zero-client/src/client/reconnect-backoff.d.ts +31 -0
  191. package/out/zero-client/src/client/reconnect-backoff.d.ts.map +1 -0
  192. package/out/zero-client/src/client/reconnect-backoff.js +44 -0
  193. package/out/zero-client/src/client/reconnect-backoff.js.map +1 -0
  194. package/out/zero-client/src/client/version.js +1 -1
  195. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  196. package/out/zero-client/src/client/zero.js +7 -0
  197. package/out/zero-client/src/client/zero.js.map +1 -1
  198. package/out/zero-client/src/util/nanoid.js +2 -2
  199. package/out/zero-client/src/util/nanoid.js.map +1 -1
  200. package/out/zero-protocol/src/up.d.ts +42 -42
  201. package/out/zero-server/src/process-mutations.d.ts +10 -0
  202. package/out/zero-server/src/process-mutations.d.ts.map +1 -1
  203. package/out/zero-server/src/process-mutations.js +39 -6
  204. package/out/zero-server/src/process-mutations.js.map +1 -1
  205. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  206. package/out/zql/src/ivm/memory-source.js +89 -6
  207. package/out/zql/src/ivm/memory-source.js.map +1 -1
  208. package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
  209. package/out/zql/src/planner/planner-connection.js +37 -3
  210. package/out/zql/src/planner/planner-connection.js.map +1 -1
  211. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"planner-connection.js","names":[],"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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2EA,IAAa,oBAAb,MAA+B;CAC7B,OAAgB;CAKhB;CACA;CACA;;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;;;;CAOA;;;;;;CAUA;;;;;;;;CASA;;;;;;;;;;CAWA,oCAA+D,IAAI,IAAI;CAEvE;;;;;;CAOA,yCAAoD,IAAI,IAAI;;;;;CAM5D,kCAAiD,IAAI,IAAI;CAEzD,YACE,OACA,OACA,MACA,SACA,QACA,iBACA,OACA,MACA,QACA;EACA,KAAK,QAAQ;EACb,KAAK,OAAO,QAAQ;EACpB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,KAAK,UAAU;EACf,KAAK,mBAAmB,SACpB,iBAAiB,UAAS,MAAK,OAAO,IAAI,CAAC,CAAC,IAC5C;EACJ,KAAK,SAAS;EACd,KAAK,mBAAmB;EACxB,KAAK,aAAa;EAClB,KAAK,QAAQ;EACb,KAAK,+BAAe,IAAI,IAAI;EAC5B,KAAK,UAAU;EAEf,KAAK,cAAc,KAAK,oBAAoB,KAAK,gBAAgB;CACnE;CAEA,UAAU,MAAyB;EACjC,KAAK,UAAU;CACjB;CAEA,IAAI,SAAsB;EACxB,OAAO,KAAK,YAAY,KAAA,GAAW,gBAAgB;EACnD,OAAO,KAAK;CACd;CAEA,cAAc,MAAyB;EACrC,KAAK,cAAc;CACrB;CAEA,sBAAwC;EACtC,OAAO;CACT;;;;;;;;;;;;;;;CAgBA,qBACE,MACA,GACA,MACA,cACM;EACN,MAAM,MAAM,KAAK,KAAK,GAAG;EACzB,KAAK,aAAa,IAAI,KAAK,CAAC;EAE5B,KAAK,iBAAiB;EAEtB,cAAc,IAAI;GAChB,MAAM;GACN,UAAU;GACV,MAAM,KAAK;GACX,eAAe;GACf,YAAY;GACZ,MAAM,MAAM,QAAQ;EACtB,CAAC;CACH;;;;;;;CAQA,mBAAmB,MAAgB,QAAmC;EACpE,KAAK,kBAAkB,IAAI,KAAK,KAAK,GAAG,GAAG,MAAM;EAEjD,KAAK,iBAAiB;CACxB;CAEA,aACE,4BACA,eACA,cACc;EAEd,MAAM,MAAM,cAAc,KAAK,GAAG;EAGlC,IAAI,OAAO,KAAK,uBAAuB,IAAI,GAAG;EAC9C,IAAI,SAAS,KAAA,GACX,OAAO;EAIT,MAAM,aAAa,KAAK,aAAa,IAAI,GAAG;EAE5C,MAAM,mBAAmB,iBACvB,KAAK,kBACL,UACF;EAIA,MAAM,kBAAkB,KAAK,kBAAkB,IAAI,GAAG;EACtD,MAAM,UAAU,WACd,KAAK,YAAY,gBAAgB,GACjC,eACF;EACA,MAAM,EAAC,aAAa,QAAQ,MAAM,SAAQ,KAAK,OAC7C,KAAK,OACL,KAAK,OACL,SACA,gBACF;EACA,MAAM,cAAc,kBAChB,KAAK,oBACH,WAAW,KAAK,kBAAkB,eAAe,CACnD,IACA,KAAK;EACT,OAAO;GACL;GACA,SACE,KAAK,UAAU,KAAA,IACX,OACA,KAAK,IAAI,MAAM,KAAK,QAAQ,0BAA0B;GAC5D,MAAM;GACN,cAAc;GACd;GACA,OAAO,KAAK;GACZ;EACF;EACA,KAAK,uBAAuB,IAAI,KAAK,IAAI;EACzC,KAAK,gBAAgB,IAAI,KAAK;GAC5B,YAAY;GACZ;GACA;GACA;EACF,CAAC;EAED,IAAI,cACF,aAAa,IAAI;GACf,MAAM;GACN,UAAU;GACV,MAAM,KAAK;GACX;GACA;GACA,cAAc,WAAW,IAAI;GAC7B,SAAS,KAAK;GACd,UAAU,KAAK;EACjB,CAAC;EAGH,OAAO;CACT;;;;;;;;;;CAWA,YACE,YACuB;EACvB,MAAM,SAAS,KAAK;EACpB,IACE,CAAC,UACD,CAAC,cACD,KAAK,qBAAqB,KAAK,YAC/B,KAAK,aAAa,SAAS,WAE3B,OAAO,KAAK;EAEd,OAAO,iBACL,KAAK,WACL,MACE,OAAO,IAAI,CAAC,KACZ,EAAE,KAAK,SAAS,YAChB,OAAO,OAAO,YAAY,EAAE,KAAK,IAAI,CACzC;CACF;CAEA,oBAAoB,SAAwC;EAC1D,IAAI,KAAK,eAAe,KAAA,KAAa,YAAY,KAAA,GAC/C,OAAO;EAGT,MAAM,kBAAkB,KAAK,OAC3B,KAAK,OACL,KAAK,OACL,SACA,KAAA,CACF;EACA,MAAM,qBAAqB,KAAK,OAC9B,KAAK,OACL,KAAK,OACL,KAAA,GACA,KAAA,CACF;EACA,OAAO,mBAAmB,OAAO,IAC7B,gBAAgB,OAAO,mBAAmB,OAC1C;CACN;;;;;;CAOA,UAAgB;EACd,IAAI,KAAK,SAEP;EAEF,IAAI,KAAK,UAAU,KAAA,GACjB,KAAK,QAAQ,KAAA;CAKjB;;;;;CAMA,kCAAwC;EACtC,KAAK,QAAQ;CACf;CAEA,QAAQ;EACN,KAAK,aAAa,MAAM;EACxB,KAAK,kBAAkB,MAAM;EAC7B,KAAK,QAAQ,KAAK;EAElB,KAAK,iBAAiB;CACxB;CAEA,mBAAyB;EACvB,KAAK,uBAAuB,MAAM;EAClC,KAAK,gBAAgB,MAAM;CAC7B;;;;;CAMA,qBAAiE;EAC/D,OAAO,IAAI,IAAI,KAAK,YAAY;CAClC;;;;;;;;;;CAWA,mBACE,aACM;EACN,KAAK,aAAa,MAAM;EACxB,KAAK,kBAAkB,MAAM;EAC7B,KAAK,MAAM,CAAC,KAAK,UAAU,aACzB,KAAK,aAAa,IAAI,KAAK,KAAK;EAGlC,KAAK,iBAAiB;CACxB;CAEA,IAAI,OAAiB;EACnB,OAAO,KAAK;CACd;;;;;;CAOA,WAAuC;EACrC,OAAO,KAAK,gBAAgB,OAAO;CACrC;;CAGA,yBAAwE;EACtE,MAAM,SAAwD,CAAC;EAC/D,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK,cAC9B,OAAO,OAAO;EAEhB,OAAO;CACT;;CAGA,qBAA4C;EAC1C,OAAO,KAAK;CACd;;CAGA,kBAA4B;EAC1B,OAAO,KAAK;CACd;;CAGA,6BAA2D;EACzD,MAAM,SAAuC,CAAC;EAC9C,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK,wBAC9B,OAAO,OAAO;EAEhB,OAAO;CACT;AACF;AAEA,SAAS,WACP,GACA,GACuB;CACvB,OAAO,KAAK,IAAI;EAAC,MAAM;EAAO,YAAY,CAAC,GAAG,CAAC;CAAC,IAAK,KAAK;AAC5D;;;;;AAMA,SAAS,iBACP,GACA,QACuB;CACvB,QAAQ,GAAG,MAAX;EACE,KAAK,UACH,OAAO,OAAO,CAAC,IAAI,KAAA,IAAY;EACjC,KAAK,OAAO;GACV,MAAM,aAA0B,CAAC;GACjC,KAAK,MAAM,KAAK,EAAE,YAAY;IAC5B,MAAM,IAAI,iBAAiB,GAAG,MAAM;IACpC,IAAI,MAAM,KAAA,GACR,WAAW,KAAK,CAAC;GAErB;GACA,IACE,WAAW,WAAW,EAAE,WAAW,UACnC,WAAW,OAAO,GAAG,MAAM,MAAM,EAAE,WAAW,EAAE,GAEhD,OAAO;GAET,IAAI,WAAW,UAAU,GACvB,OAAO,WAAW;GAEpB,OAAO;IAAC,MAAM;IAAO;GAAU;EACjC;EACA,SACE,OAAO;CACX;AACF"}
1
+ {"version":3,"file":"planner-connection.js","names":[],"sources":["../../../../../zql/src/planner/planner-connection.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {getOrInsertComputed} from '../../../shared/src/map.ts';\nimport {TupleMap} from '../../../shared/src/tuple-map.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 /**\n * The cost model's results, by everything its arguments depend on besides\n * the table and the sort, which never change: whether the\n * pushed conditions all count (see {@link #filtersFor}), the per-branch\n * filter, and the columns of the constraint.\n *\n * Unlike the caches above, this one is kept for the lifetime of the\n * connection. The planner tries every flip pattern and most patterns read\n * this connection in a way an earlier pattern already did, and a cost model\n * call can be expensive (the SQLite one prepares a statement). A connection\n * lives for one planning run, so the costs are not kept across changes to\n * the table statistics.\n */\n readonly #modelCosts = new TupleMap<readonly unknown[], ModelCost>();\n\n /** The selectivity with each per-branch filter. Kept like `#modelCosts`. */\n readonly #perBranchSelectivities: Map<NoSubqueryCondition, number> =\n 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 {\n filters,\n cost: {startupCost, fanout, rows, plan},\n } = this.#modelCost(mergedConstraint, perBranchFilter);\n const selectivity = perBranchFilter\n ? this.#perBranchSelectivity(perBranchFilter)\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 #modelCost(\n constraint: PlannerConstraint | undefined,\n perBranchFilter: NoSubqueryCondition | undefined,\n ): ModelCost {\n const allPushedCount =\n this.#pushed !== undefined && this.#parentJoin?.type === 'flipped';\n return this.#modelCosts.getOrInsertComputed(\n [\n allPushedCount,\n perBranchFilter,\n // Column names are strings, so `undefined` is not one of them.\n ...(constraint ? Object.keys(constraint) : [undefined]),\n ],\n () => {\n const filters = andFilters(\n this.#filtersFor(constraint),\n perBranchFilter,\n );\n return {\n filters,\n cost: this.#model(this.table, this.#sort, filters, constraint),\n };\n },\n );\n }\n\n #perBranchSelectivity(perBranchFilter: NoSubqueryCondition): number {\n return getOrInsertComputed(\n this.#perBranchSelectivities,\n perBranchFilter,\n f => this.#computeSelectivity(andFilters(this.#unpushedFilters, f)),\n );\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\n/** A cost model result and the filters it was computed for. */\ntype ModelCost = {\n readonly filters: Condition | undefined;\n readonly cost: CostModelCost;\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocicorp/zero",
3
- "version": "1.12.0-canary.12",
3
+ "version": "1.12.0-canary.13",
4
4
  "description": "Zero is a web framework for serverless web development.",
5
5
  "homepage": "https://zero.rocicorp.dev",
6
6
  "bugs": {