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

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 (252) 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/normalize.d.ts.map +1 -1
  53. package/out/zero-cache/src/config/normalize.js +1 -1
  54. package/out/zero-cache/src/config/normalize.js.map +1 -1
  55. package/out/zero-cache/src/config/zero-config.d.ts +26 -0
  56. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  57. package/out/zero-cache/src/config/zero-config.js +36 -0
  58. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  59. package/out/zero-cache/src/observability/log-throttle.d.ts +44 -0
  60. package/out/zero-cache/src/observability/log-throttle.d.ts.map +1 -0
  61. package/out/zero-cache/src/observability/log-throttle.js +76 -0
  62. package/out/zero-cache/src/observability/log-throttle.js.map +1 -0
  63. package/out/zero-cache/src/scripts/permissions.d.ts +16 -0
  64. package/out/zero-cache/src/scripts/permissions.d.ts.map +1 -1
  65. package/out/zero-cache/src/server/runner/zero-dispatcher.d.ts.map +1 -1
  66. package/out/zero-cache/src/server/runner/zero-dispatcher.js +2 -0
  67. package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
  68. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  69. package/out/zero-cache/src/server/syncer.js +20 -2
  70. package/out/zero-cache/src/server/syncer.js.map +1 -1
  71. package/out/zero-cache/src/services/change-source/common/backfill-manager.d.ts.map +1 -1
  72. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +27 -15
  73. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  74. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts +1 -0
  75. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  76. package/out/zero-cache/src/services/change-source/common/replica-schema.js +15 -3
  77. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  78. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  79. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  80. package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts +9 -0
  81. package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts.map +1 -1
  82. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +204 -18
  83. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  84. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +11 -2
  85. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  86. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +11 -2
  87. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  88. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.d.ts +68 -0
  89. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.d.ts.map +1 -0
  90. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js +141 -0
  91. package/out/zero-cache/src/services/change-source/protocol/backfill-progress.js.map +1 -0
  92. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +43 -0
  93. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
  94. package/out/zero-cache/src/services/change-source/protocol/current/data.js +10 -1
  95. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  96. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +57 -0
  97. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
  98. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +20 -6
  99. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
  100. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js +17 -7
  101. package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
  102. package/out/zero-cache/src/services/change-source/protocol/current.js +2 -1
  103. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts +66 -0
  104. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts.map +1 -0
  105. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js +154 -0
  106. package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -0
  107. package/out/zero-cache/src/services/change-streamer/backfill-state.d.ts +50 -0
  108. package/out/zero-cache/src/services/change-streamer/backfill-state.d.ts.map +1 -0
  109. package/out/zero-cache/src/services/change-streamer/backfill-state.js +268 -0
  110. package/out/zero-cache/src/services/change-streamer/backfill-state.js.map +1 -0
  111. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +7 -6
  112. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  113. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  114. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +20 -4
  115. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  116. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +26 -1
  117. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  118. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  119. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  120. package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
  121. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  122. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +88 -0
  123. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
  124. package/out/zero-cache/src/services/change-streamer/subscribe.js +9 -1
  125. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  126. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +29 -0
  127. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  128. package/out/zero-cache/src/services/change-streamer/subscriber.js +34 -0
  129. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  130. package/out/zero-cache/src/services/plannerz.d.ts +159 -0
  131. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -0
  132. package/out/zero-cache/src/services/plannerz.js +263 -0
  133. package/out/zero-cache/src/services/plannerz.js.map +1 -0
  134. package/out/zero-cache/src/services/replicator/change-log-cookies.js +1 -1
  135. package/out/zero-cache/src/services/replicator/change-log-cookies.js.map +1 -1
  136. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  137. package/out/zero-cache/src/services/replicator/change-processor.js +44 -12
  138. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  139. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  140. package/out/zero-cache/src/services/replicator/incremental-sync.js +4 -3
  141. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  142. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts +23 -2
  143. package/out/zero-cache/src/services/replicator/schema/backfilling.d.ts.map +1 -1
  144. package/out/zero-cache/src/services/replicator/schema/backfilling.js +54 -2
  145. package/out/zero-cache/src/services/replicator/schema/backfilling.js.map +1 -1
  146. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts +11 -3
  147. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts.map +1 -1
  148. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  149. package/out/zero-cache/src/services/replicator/write-worker.js +5 -1
  150. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  151. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.d.ts +81 -0
  152. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.d.ts.map +1 -0
  153. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js +128 -0
  154. package/out/zero-cache/src/services/view-syncer/deferred-writes-budget.js.map +1 -0
  155. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -1
  156. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +36 -1
  157. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  158. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +362 -20
  159. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  160. package/out/zero-cache/src/services/view-syncer/plan-warnings.d.ts +7 -0
  161. package/out/zero-cache/src/services/view-syncer/plan-warnings.d.ts.map +1 -0
  162. package/out/zero-cache/src/services/view-syncer/plan-warnings.js +35 -0
  163. package/out/zero-cache/src/services/view-syncer/plan-warnings.js.map +1 -0
  164. package/out/zero-cache/src/services/view-syncer/query-shape.d.ts +25 -0
  165. package/out/zero-cache/src/services/view-syncer/query-shape.d.ts.map +1 -0
  166. package/out/zero-cache/src/services/view-syncer/query-shape.js +28 -0
  167. package/out/zero-cache/src/services/view-syncer/query-shape.js.map +1 -0
  168. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts +77 -0
  169. package/out/zero-cache/src/services/view-syncer/query-stats.d.ts.map +1 -0
  170. package/out/zero-cache/src/services/view-syncer/query-stats.js +206 -0
  171. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -0
  172. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +11 -1
  173. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  174. package/out/zero-cache/src/services/view-syncer/snapshotter.js +7 -1
  175. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  176. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  177. package/out/zero-cache/src/services/view-syncer/view-syncer.js +49 -3
  178. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  179. package/out/zero-cache/src/workers/connection.js +3 -3
  180. package/out/zero-cache/src/workers/connection.js.map +1 -1
  181. package/out/zero-cache/src/workers/syncer.js +2 -2
  182. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  183. package/out/zero-client/src/client/options.d.ts +29 -0
  184. package/out/zero-client/src/client/options.d.ts.map +1 -1
  185. package/out/zero-client/src/client/options.js.map +1 -1
  186. package/out/zero-client/src/client/version.js +1 -1
  187. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  188. package/out/zero-client/src/client/zero.js +8 -23
  189. package/out/zero-client/src/client/zero.js.map +1 -1
  190. package/out/zero-client/src/mod.d.ts +1 -0
  191. package/out/zero-client/src/mod.d.ts.map +1 -1
  192. package/out/zero-client/src/mod.js +1 -0
  193. package/out/zero-protocol/src/down.d.ts +2 -2
  194. package/out/zero-react/src/zero.js +1 -0
  195. package/out/zero-solid/src/zero.js +1 -0
  196. package/out/zql/src/builder/builder.d.ts +6 -0
  197. package/out/zql/src/builder/builder.d.ts.map +1 -1
  198. package/out/zql/src/builder/builder.js +4 -3
  199. package/out/zql/src/builder/builder.js.map +1 -1
  200. package/out/zql/src/ivm/memory-source.d.ts +13 -0
  201. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  202. package/out/zql/src/ivm/memory-source.js +1 -1
  203. package/out/zql/src/ivm/memory-source.js.map +1 -1
  204. package/out/zql/src/planner/planner-builder.d.ts +12 -1
  205. package/out/zql/src/planner/planner-builder.d.ts.map +1 -1
  206. package/out/zql/src/planner/planner-builder.js +8 -1
  207. package/out/zql/src/planner/planner-builder.js.map +1 -1
  208. package/out/zql/src/planner/planner-connection.d.ts +43 -0
  209. package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
  210. package/out/zql/src/planner/planner-connection.js +31 -4
  211. package/out/zql/src/planner/planner-connection.js.map +1 -1
  212. package/out/zql/src/planner/planner-graph.d.ts +10 -0
  213. package/out/zql/src/planner/planner-graph.d.ts.map +1 -1
  214. package/out/zql/src/planner/planner-graph.js +14 -0
  215. package/out/zql/src/planner/planner-graph.js.map +1 -1
  216. package/out/zql/src/planner/planner-warnings.d.ts +83 -0
  217. package/out/zql/src/planner/planner-warnings.d.ts.map +1 -0
  218. package/out/zql/src/planner/planner-warnings.js +78 -0
  219. package/out/zql/src/planner/planner-warnings.js.map +1 -0
  220. package/out/zql/src/query/measure-push-operator.d.ts.map +1 -1
  221. package/out/zql/src/query/measure-push-operator.js +30 -8
  222. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  223. package/out/zqlite/src/pending-delta.d.ts +110 -0
  224. package/out/zqlite/src/pending-delta.d.ts.map +1 -0
  225. package/out/zqlite/src/pending-delta.js +278 -0
  226. package/out/zqlite/src/pending-delta.js.map +1 -0
  227. package/out/zqlite/src/query-delegate.d.ts.map +1 -1
  228. package/out/zqlite/src/query-delegate.js +5 -1
  229. package/out/zqlite/src/query-delegate.js.map +1 -1
  230. package/out/zqlite/src/sqlite-cost-model.d.ts.map +1 -1
  231. package/out/zqlite/src/sqlite-cost-model.js +52 -1
  232. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  233. package/out/zqlite/src/sqlite-stat-fanout.d.ts.map +1 -1
  234. package/out/zqlite/src/sqlite-stat-fanout.js +2 -26
  235. package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
  236. package/out/zqlite/src/sqlite-stat4-sample.d.ts +37 -0
  237. package/out/zqlite/src/sqlite-stat4-sample.d.ts.map +1 -0
  238. package/out/zqlite/src/sqlite-stat4-sample.js +61 -0
  239. package/out/zqlite/src/sqlite-stat4-sample.js.map +1 -0
  240. package/out/zqlite/src/table-source.d.ts +68 -0
  241. package/out/zqlite/src/table-source.d.ts.map +1 -1
  242. package/out/zqlite/src/table-source.js +206 -7
  243. package/out/zqlite/src/table-source.js.map +1 -1
  244. package/package.json +1 -1
  245. package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts +0 -35
  246. package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts.map +0 -1
  247. package/out/replicache/src/kv/idb-store-with-mem-fallback.js +0 -83
  248. package/out/replicache/src/kv/idb-store-with-mem-fallback.js.map +0 -1
  249. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts +0 -156
  250. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts.map +0 -1
  251. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js +0 -15
  252. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js.map +0 -1
@@ -0,0 +1,110 @@
1
+ import type { Ordering } from '../../zero-protocol/src/ast.ts';
2
+ import type { Row } from '../../zero-protocol/src/data.ts';
3
+ import type { PrimaryKey } from '../../zero-protocol/src/primary-key.ts';
4
+ import { type Constraint } from '../../zql/src/ivm/constraint.ts';
5
+ import type { Comparator } from '../../zql/src/ivm/data.ts';
6
+ import type { MultiConstraint } from '../../zql/src/ivm/operator.ts';
7
+ /**
8
+ * The batch overlay that lets a `TableSource` derive a transaction without
9
+ * writing to the database it is reading from.
10
+ *
11
+ * A `TableSource` normally makes each change durable in its backing SQLite
12
+ * connection as soon as it has been vended to every output, because change
13
+ * *k+1* has to read a base that already includes changes *1..k* -- IVM's
14
+ * joins fetch from the source mid-push. On the server that backing connection
15
+ * is a `BEGIN CONCURRENT` snapshot that is never committed and always rolled
16
+ * back, so those writes exist only to serve reads for the rest of the pass.
17
+ *
18
+ * A `PendingDelta` holds them in memory instead. The base stays pinned at the
19
+ * state it had when the pass began and this object supplies *1..k*, merged
20
+ * into every leaf scan. The durable copy comes from the replicator's own
21
+ * commit, which the source picks up when it leapfrogs to the next snapshot.
22
+ *
23
+ * State is coalesced by primary key: whatever the batch did to a row, all that
24
+ * matters downstream is the row's final value, or that it is gone. A key that
25
+ * appears here overrides the base unconditionally -- an added row has no base
26
+ * to override, an edited row's base copy is suppressed in favor of the new
27
+ * value, and a removed row's base copy is suppressed with nothing to put in
28
+ * its place.
29
+ *
30
+ * Live rows are additionally kept in lazily built sorted indexes, one per
31
+ * `[...constraintKeys, ...sort]` shape a fetch has asked for, so that merging
32
+ * the delta into a constrained scan costs `O(log D + matches)` rather than a
33
+ * walk of the whole delta. This mirrors `MemorySource`'s index handling; see
34
+ * its `#fetch` for the bound construction the scans here reproduce.
35
+ */
36
+ export declare class PendingDelta {
37
+ #private;
38
+ constructor(primaryKey: PrimaryKey);
39
+ /**
40
+ * True when the source can take its untouched fast path: no suppression
41
+ * test per base row, no merge, no index maintenance.
42
+ */
43
+ get isEmpty(): boolean;
44
+ /** The number of rows the batch has touched, tombstones included. */
45
+ get size(): number;
46
+ /**
47
+ * Estimated retained bytes, including keys, tombstones, and sorted indexes.
48
+ * This is a budgeting heuristic, not a measurement of the JavaScript heap.
49
+ */
50
+ get estimatedBytes(): number;
51
+ /** Records that `row` is now present with this value. */
52
+ set(row: Row): void;
53
+ /** Records that `row` is now absent. */
54
+ delete(row: Row): void;
55
+ /**
56
+ * Whether the batch overrides this base row. Callers use it to suppress base
57
+ * rows during a merge, so it must be answered by primary key -- the value
58
+ * carried by the base row is exactly what the delta is replacing.
59
+ */
60
+ overrides(row: Row): boolean;
61
+ /**
62
+ * The batch's value for a row, by primary key. Returns `NOT_OVERRIDDEN` when
63
+ * the batch has not touched the key at all (so the caller should consult the
64
+ * base) and `undefined` when the batch removed it.
65
+ */
66
+ get(row: Row): Row | undefined | typeof NOT_OVERRIDDEN;
67
+ /**
68
+ * Looks a row up by an arbitrary (not necessarily primary) unique key. Only
69
+ * reachable when the delta is non-empty, which is never the case for the one
70
+ * caller that uses a non-primary key, so a walk is fine here.
71
+ */
72
+ getByColumns(keyCols: readonly string[], keyRow: Row): Row | undefined;
73
+ /**
74
+ * The live rows this batch contributes to a fetch, in the same order the
75
+ * backing SQL query returns its rows, so the two streams can be merged.
76
+ *
77
+ * `constraint`, `multiConstraints` and `filterPredicate` are the ones SQL
78
+ * already applied to the base rows; they are applied here so the delta rows
79
+ * are subject to the same query. `start` is the fetch's start row: the scan
80
+ * begins there rather than at the start of the constraint span. It is
81
+ * inclusive, so the caller still applies an `after` basis.
82
+ */
83
+ rowsFor(sort: Ordering, constraint: Constraint | undefined, reverse: boolean | undefined, filterPredicate: ((row: Row) => boolean | undefined) | undefined, multiConstraints: readonly MultiConstraint[] | undefined, start?: Row | undefined): Iterable<Row>;
84
+ /**
85
+ * A row with the primary key of each row the batch has touched, removed
86
+ * rows included. A live row is returned as is.
87
+ */
88
+ touchedKeys(): Iterable<Row>;
89
+ /** The rows the batch has added or edited, and not since removed. */
90
+ liveRows(): Iterable<Row>;
91
+ /** Drops the batch. Called when the source moves to a snapshot that has it. */
92
+ clear(): void;
93
+ }
94
+ export declare const NOT_OVERRIDDEN: unique symbol;
95
+ /**
96
+ * Merges a batch's rows into an ordered base stream and drops the base rows
97
+ * the batch overrides.
98
+ *
99
+ * Both streams must already be in `compare` order. A delta row that ties with
100
+ * a base row is the *same* row by primary key -- every fetch ordering includes
101
+ * the primary key -- so the base copy is suppressed and the delta's value
102
+ * takes its place at that position.
103
+ */
104
+ export declare function generateWithPendingDelta(baseRows: Iterable<Row>, deltaRows: Iterable<Row>, delta: PendingDelta, compare: Comparator): IterableIterator<Row>;
105
+ /**
106
+ * The unordered counterpart. Nothing downstream depends on position, so the
107
+ * surviving base rows are emitted first and the batch's rows follow.
108
+ */
109
+ export declare function generateWithPendingDeltaUnordered(baseRows: Iterable<Row>, deltaRows: Iterable<Row>, delta: PendingDelta): IterableIterator<Row>;
110
+ //# sourceMappingURL=pending-delta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-delta.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/pending-delta.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAC,GAAG,EAAQ,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,2BAA2B,CAAC;AAQ1D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,+BAA+B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,YAAY;;IA0BvB,YAAY,UAAU,EAAE,UAAU,EAGjC;IAED;;;OAGG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,qEAAqE;IACrE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,IAAI,cAAc,IAAI,MAAM,CAO3B;IAqBD,yDAAyD;IACzD,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAgBlB;IAED,wCAAwC;IACxC,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAUrB;IAED;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAQ3B;IAED;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS,GAAG,OAAO,cAAc,CAMrD;IAED;;;;OAIG;IACH,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS,CAOrE;IA8BD;;;;;;;;;OASG;IACF,OAAO,CACN,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,UAAU,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,eAAe,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,SAAS,CAAC,GAAG,SAAS,EAChE,gBAAgB,EAAE,SAAS,eAAe,EAAE,GAAG,SAAS,EACxD,KAAK,CAAC,EAAE,GAAG,GAAG,SAAS,GACtB,QAAQ,CAAC,GAAG,CAAC,CAiEf;IAED;;;OAGG;IACF,WAAW,IAAI,QAAQ,CAAC,GAAG,CAAC,CAc5B;IAED,qEAAqE;IACpE,QAAQ,IAAI,QAAQ,CAAC,GAAG,CAAC,CAMzB;IAED,+EAA+E;IAC/E,KAAK,IAAI,IAAI,CAOZ;CACF;AAwBD,eAAO,MAAM,cAAc,eAA2B,CAAC;AAwBvD;;;;;;;;GAQG;AACH,wBAAiB,wBAAwB,CACvC,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,EACvB,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,EACxB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,UAAU,GAClB,gBAAgB,CAAC,GAAG,CAAC,CAwBvB;AAED;;;GAGG;AACH,wBAAiB,iCAAiC,CAChD,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,EACvB,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,EACxB,KAAK,EAAE,YAAY,GAClB,gBAAgB,CAAC,GAAG,CAAC,CAOvB"}
@@ -0,0 +1,278 @@
1
+ import { hasOwn } from "../../shared/src/has-own.js";
2
+ import { constraintMatchesPrimaryKey, constraintMatchesRow } from "../../zql/src/ivm/constraint.js";
3
+ import { BTreeSet } from "../../shared/src/btree-set.js";
4
+ import { generateRows, makeBoundComparator, maxValue, minValue } from "../../zql/src/ivm/memory-source.js";
5
+ //#region ../zqlite/src/pending-delta.ts
6
+ /**
7
+ * The batch overlay that lets a `TableSource` derive a transaction without
8
+ * writing to the database it is reading from.
9
+ *
10
+ * A `TableSource` normally makes each change durable in its backing SQLite
11
+ * connection as soon as it has been vended to every output, because change
12
+ * *k+1* has to read a base that already includes changes *1..k* -- IVM's
13
+ * joins fetch from the source mid-push. On the server that backing connection
14
+ * is a `BEGIN CONCURRENT` snapshot that is never committed and always rolled
15
+ * back, so those writes exist only to serve reads for the rest of the pass.
16
+ *
17
+ * A `PendingDelta` holds them in memory instead. The base stays pinned at the
18
+ * state it had when the pass began and this object supplies *1..k*, merged
19
+ * into every leaf scan. The durable copy comes from the replicator's own
20
+ * commit, which the source picks up when it leapfrogs to the next snapshot.
21
+ *
22
+ * State is coalesced by primary key: whatever the batch did to a row, all that
23
+ * matters downstream is the row's final value, or that it is gone. A key that
24
+ * appears here overrides the base unconditionally -- an added row has no base
25
+ * to override, an edited row's base copy is suppressed in favor of the new
26
+ * value, and a removed row's base copy is suppressed with nothing to put in
27
+ * its place.
28
+ *
29
+ * Live rows are additionally kept in lazily built sorted indexes, one per
30
+ * `[...constraintKeys, ...sort]` shape a fetch has asked for, so that merging
31
+ * the delta into a constrained scan costs `O(log D + matches)` rather than a
32
+ * walk of the whole delta. This mirrors `MemorySource`'s index handling; see
33
+ * its `#fetch` for the bound construction the scans here reproduce.
34
+ */
35
+ var PendingDelta = class {
36
+ #primaryKey;
37
+ #singleColumnKey;
38
+ /**
39
+ * For a composite primary key, the first key column's value of every key in
40
+ * `#byKey`. {@link overrides} runs for every base row of a merged fetch, and
41
+ * building a composite key allocates, so this rules most rows out first.
42
+ */
43
+ #firstKeyValues = /* @__PURE__ */ new Set();
44
+ /**
45
+ * The coalesced net state, keyed by primary key. A `undefined` value is a
46
+ * tombstone: the batch removed the row, so the base copy is suppressed and
47
+ * nothing replaces it. Membership alone is what suppresses a base row, so
48
+ * this map is also the "is this key overridden" test.
49
+ */
50
+ #byKey = /* @__PURE__ */ new Map();
51
+ /** Sorted views over the live (non-tombstone) rows, keyed by `JSON(sort)`. */
52
+ #indexes = /* @__PURE__ */ new Map();
53
+ #liveCount = 0;
54
+ #rowBytes = 0;
55
+ #indexBytes = 0;
56
+ constructor(primaryKey) {
57
+ this.#primaryKey = primaryKey;
58
+ this.#singleColumnKey = primaryKey.length === 1 ? primaryKey[0] : void 0;
59
+ }
60
+ /**
61
+ * True when the source can take its untouched fast path: no suppression
62
+ * test per base row, no merge, no index maintenance.
63
+ */
64
+ get isEmpty() {
65
+ return this.#byKey.size === 0;
66
+ }
67
+ /** The number of rows the batch has touched, tombstones included. */
68
+ get size() {
69
+ return this.#byKey.size;
70
+ }
71
+ /**
72
+ * Estimated retained bytes, including keys, tombstones, and sorted indexes.
73
+ * This is a budgeting heuristic, not a measurement of the JavaScript heap.
74
+ */
75
+ get estimatedBytes() {
76
+ return this.#rowBytes + this.#indexBytes + this.#liveCount * this.#indexes.size * 32;
77
+ }
78
+ #key(row) {
79
+ const single = this.#singleColumnKey;
80
+ if (single !== void 0) return row[single];
81
+ const key = this.#primaryKey.map((k) => row[k]);
82
+ return JSON.stringify(key);
83
+ }
84
+ #track(row, key) {
85
+ if (this.#byKey.has(key)) return;
86
+ this.#rowBytes += 64 + estimateBytes(key);
87
+ if (this.#singleColumnKey === void 0) this.#firstKeyValues.add(row[this.#primaryKey[0]]);
88
+ }
89
+ /** Records that `row` is now present with this value. */
90
+ set(row) {
91
+ const key = this.#key(row);
92
+ const existing = this.#byKey.get(key);
93
+ this.#track(row, key);
94
+ this.#rowBytes += estimateBytes(row) - (existing === void 0 ? 0 : estimateBytes(existing));
95
+ if (existing !== void 0) {
96
+ this.#removeFromIndexes(existing);
97
+ this.#liveCount--;
98
+ }
99
+ this.#byKey.set(key, row);
100
+ this.#addToIndexes(row);
101
+ this.#liveCount++;
102
+ }
103
+ /** Records that `row` is now absent. */
104
+ delete(row) {
105
+ const key = this.#key(row);
106
+ const existing = this.#byKey.get(key);
107
+ this.#track(row, key);
108
+ if (existing !== void 0) {
109
+ this.#rowBytes -= estimateBytes(existing);
110
+ this.#removeFromIndexes(existing);
111
+ this.#liveCount--;
112
+ }
113
+ this.#byKey.set(key, void 0);
114
+ }
115
+ /**
116
+ * Whether the batch overrides this base row. Callers use it to suppress base
117
+ * rows during a merge, so it must be answered by primary key -- the value
118
+ * carried by the base row is exactly what the delta is replacing.
119
+ */
120
+ overrides(row) {
121
+ if (this.#singleColumnKey === void 0 && !this.#firstKeyValues.has(row[this.#primaryKey[0]])) return false;
122
+ return this.#byKey.has(this.#key(row));
123
+ }
124
+ /**
125
+ * The batch's value for a row, by primary key. Returns `NOT_OVERRIDDEN` when
126
+ * the batch has not touched the key at all (so the caller should consult the
127
+ * base) and `undefined` when the batch removed it.
128
+ */
129
+ get(row) {
130
+ const key = this.#key(row);
131
+ if (!this.#byKey.has(key)) return NOT_OVERRIDDEN;
132
+ return this.#byKey.get(key);
133
+ }
134
+ /**
135
+ * Looks a row up by an arbitrary (not necessarily primary) unique key. Only
136
+ * reachable when the delta is non-empty, which is never the case for the one
137
+ * caller that uses a non-primary key, so a walk is fine here.
138
+ */
139
+ getByColumns(keyCols, keyRow) {
140
+ for (const row of this.#byKey.values()) if (row !== void 0 && keyCols.every((c) => row[c] === keyRow[c])) return row;
141
+ }
142
+ #addToIndexes(row) {
143
+ for (const data of this.#indexes.values()) data.add(row);
144
+ }
145
+ #removeFromIndexes(row) {
146
+ for (const data of this.#indexes.values()) data.delete(row);
147
+ }
148
+ #getOrCreateIndex(sort) {
149
+ const indexKey = JSON.stringify(sort);
150
+ let data = this.#indexes.get(indexKey);
151
+ if (data === void 0) {
152
+ data = new BTreeSet(makeBoundComparator(sort));
153
+ for (const row of this.#byKey.values()) if (row !== void 0) data.add(row);
154
+ this.#indexes.set(indexKey, data);
155
+ this.#indexBytes += 128 + estimateBytes(indexKey) + estimateBytes(sort);
156
+ }
157
+ return data;
158
+ }
159
+ /**
160
+ * The live rows this batch contributes to a fetch, in the same order the
161
+ * backing SQL query returns its rows, so the two streams can be merged.
162
+ *
163
+ * `constraint`, `multiConstraints` and `filterPredicate` are the ones SQL
164
+ * already applied to the base rows; they are applied here so the delta rows
165
+ * are subject to the same query. `start` is the fetch's start row: the scan
166
+ * begins there rather than at the start of the constraint span. It is
167
+ * inclusive, so the caller still applies an `after` basis.
168
+ */
169
+ *rowsFor(sort, constraint, reverse, filterPredicate, multiConstraints, start) {
170
+ if (this.#liveCount === 0) return;
171
+ const indexSort = [];
172
+ if (constraint) for (const key of Object.keys(constraint)) indexSort.push([key, "asc"]);
173
+ const sorted = !constraint || !constraintMatchesPrimaryKey(constraint, this.#primaryKey);
174
+ if (sorted) indexSort.push(...sort);
175
+ const data = this.#getOrCreateIndex(indexSort);
176
+ let scanStart;
177
+ if (start !== void 0 && sorted && (!constraint || constraintMatchesRow(constraint, start))) scanStart = start;
178
+ else if (constraint) {
179
+ scanStart = {};
180
+ for (const [key, dir] of indexSort) if (hasOwn(constraint, key)) scanStart[key] = constraint[key];
181
+ else if (reverse) scanStart[key] = dir === "asc" ? maxValue : minValue;
182
+ else scanStart[key] = dir === "asc" ? minValue : maxValue;
183
+ }
184
+ for (const row of generateRows(data, scanStart, reverse)) {
185
+ if (constraint && !constraintMatchesRow(constraint, row)) break;
186
+ if (multiConstraints && !matchesMultiConstraints(row, multiConstraints)) continue;
187
+ if (filterPredicate && !filterPredicate(row)) continue;
188
+ yield row;
189
+ }
190
+ }
191
+ /**
192
+ * A row with the primary key of each row the batch has touched, removed
193
+ * rows included. A live row is returned as is.
194
+ */
195
+ *touchedKeys() {
196
+ const single = this.#singleColumnKey;
197
+ for (const [key, row] of this.#byKey) if (row !== void 0) yield row;
198
+ else if (single !== void 0) yield { [single]: key };
199
+ else {
200
+ const values = JSON.parse(key);
201
+ yield Object.fromEntries(this.#primaryKey.map((k, i) => [k, values[i]]));
202
+ }
203
+ }
204
+ /** The rows the batch has added or edited, and not since removed. */
205
+ *liveRows() {
206
+ for (const row of this.#byKey.values()) if (row !== void 0) yield row;
207
+ }
208
+ /** Drops the batch. Called when the source moves to a snapshot that has it. */
209
+ clear() {
210
+ this.#byKey.clear();
211
+ this.#firstKeyValues.clear();
212
+ this.#indexes.clear();
213
+ this.#liveCount = 0;
214
+ this.#rowBytes = 0;
215
+ this.#indexBytes = 0;
216
+ }
217
+ };
218
+ function estimateBytes(value) {
219
+ if (typeof value === "string") return 24 + value.length * 2;
220
+ if (value === null || typeof value !== "object") return 8;
221
+ let bytes = 32;
222
+ if (Array.isArray(value)) for (const item of value) bytes += 8 + estimateBytes(item);
223
+ else for (const key in value) bytes += 16 + estimateBytes(key) + estimateBytes(value[key]);
224
+ return bytes;
225
+ }
226
+ var NOT_OVERRIDDEN = Symbol("not-overridden");
227
+ function matchesMultiConstraints(row, multiConstraints) {
228
+ for (const mc of multiConstraints) {
229
+ if (mc.length === 0) continue;
230
+ let any = false;
231
+ for (const c of mc) if (constraintMatchesRow(c, row)) {
232
+ any = true;
233
+ break;
234
+ }
235
+ if (!any) return false;
236
+ }
237
+ return true;
238
+ }
239
+ /**
240
+ * Merges a batch's rows into an ordered base stream and drops the base rows
241
+ * the batch overrides.
242
+ *
243
+ * Both streams must already be in `compare` order. A delta row that ties with
244
+ * a base row is the *same* row by primary key -- every fetch ordering includes
245
+ * the primary key -- so the base copy is suppressed and the delta's value
246
+ * takes its place at that position.
247
+ */
248
+ function* generateWithPendingDelta(baseRows, deltaRows, delta, compare) {
249
+ const deltaIter = deltaRows[Symbol.iterator]();
250
+ let pending = deltaIter.next();
251
+ try {
252
+ for (const row of baseRows) {
253
+ while (!pending.done && compare(pending.value, row) < 0) {
254
+ yield pending.value;
255
+ pending = deltaIter.next();
256
+ }
257
+ if (!delta.overrides(row)) yield row;
258
+ }
259
+ while (!pending.done) {
260
+ yield pending.value;
261
+ pending = deltaIter.next();
262
+ }
263
+ } finally {
264
+ if (!pending.done) deltaIter.return?.(void 0);
265
+ }
266
+ }
267
+ /**
268
+ * The unordered counterpart. Nothing downstream depends on position, so the
269
+ * surviving base rows are emitted first and the batch's rows follow.
270
+ */
271
+ function* generateWithPendingDeltaUnordered(baseRows, deltaRows, delta) {
272
+ for (const row of baseRows) if (!delta.overrides(row)) yield row;
273
+ yield* deltaRows;
274
+ }
275
+ //#endregion
276
+ export { NOT_OVERRIDDEN, PendingDelta, generateWithPendingDelta, generateWithPendingDeltaUnordered };
277
+
278
+ //# sourceMappingURL=pending-delta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-delta.js","names":["#primaryKey","#singleColumnKey","#firstKeyValues","#byKey","#indexes","#rowBytes","#indexBytes","#liveCount","#key","#track","#removeFromIndexes","#addToIndexes","#getOrCreateIndex"],"sources":["../../../../zqlite/src/pending-delta.ts"],"sourcesContent":["import {BTreeSet} from '../../shared/src/btree-set.ts';\nimport {hasOwn} from '../../shared/src/has-own.ts';\nimport type {Ordering, OrderPart} from '../../zero-protocol/src/ast.ts';\nimport type {Row, Value} from '../../zero-protocol/src/data.ts';\nimport type {PrimaryKey} from '../../zero-protocol/src/primary-key.ts';\nimport {\n constraintMatchesPrimaryKey,\n constraintMatchesRow,\n type Constraint,\n} from '../../zql/src/ivm/constraint.ts';\nimport type {Comparator} from '../../zql/src/ivm/data.ts';\nimport {\n generateRows,\n makeBoundComparator,\n maxValue,\n minValue,\n type RowBound,\n} from '../../zql/src/ivm/memory-source.ts';\nimport type {MultiConstraint} from '../../zql/src/ivm/operator.ts';\n\n/**\n * The batch overlay that lets a `TableSource` derive a transaction without\n * writing to the database it is reading from.\n *\n * A `TableSource` normally makes each change durable in its backing SQLite\n * connection as soon as it has been vended to every output, because change\n * *k+1* has to read a base that already includes changes *1..k* -- IVM's\n * joins fetch from the source mid-push. On the server that backing connection\n * is a `BEGIN CONCURRENT` snapshot that is never committed and always rolled\n * back, so those writes exist only to serve reads for the rest of the pass.\n *\n * A `PendingDelta` holds them in memory instead. The base stays pinned at the\n * state it had when the pass began and this object supplies *1..k*, merged\n * into every leaf scan. The durable copy comes from the replicator's own\n * commit, which the source picks up when it leapfrogs to the next snapshot.\n *\n * State is coalesced by primary key: whatever the batch did to a row, all that\n * matters downstream is the row's final value, or that it is gone. A key that\n * appears here overrides the base unconditionally -- an added row has no base\n * to override, an edited row's base copy is suppressed in favor of the new\n * value, and a removed row's base copy is suppressed with nothing to put in\n * its place.\n *\n * Live rows are additionally kept in lazily built sorted indexes, one per\n * `[...constraintKeys, ...sort]` shape a fetch has asked for, so that merging\n * the delta into a constrained scan costs `O(log D + matches)` rather than a\n * walk of the whole delta. This mirrors `MemorySource`'s index handling; see\n * its `#fetch` for the bound construction the scans here reproduce.\n */\nexport class PendingDelta {\n readonly #primaryKey: PrimaryKey;\n readonly #singleColumnKey: string | undefined;\n\n /**\n * For a composite primary key, the first key column's value of every key in\n * `#byKey`. {@link overrides} runs for every base row of a merged fetch, and\n * building a composite key allocates, so this rules most rows out first.\n */\n readonly #firstKeyValues = new Set<Value>();\n\n /**\n * The coalesced net state, keyed by primary key. A `undefined` value is a\n * tombstone: the batch removed the row, so the base copy is suppressed and\n * nothing replaces it. Membership alone is what suppresses a base row, so\n * this map is also the \"is this key overridden\" test.\n */\n readonly #byKey = new Map<unknown, Row | undefined>();\n\n /** Sorted views over the live (non-tombstone) rows, keyed by `JSON(sort)`. */\n readonly #indexes = new Map<string, BTreeSet<Row>>();\n\n #liveCount = 0;\n #rowBytes = 0;\n #indexBytes = 0;\n\n constructor(primaryKey: PrimaryKey) {\n this.#primaryKey = primaryKey;\n this.#singleColumnKey = primaryKey.length === 1 ? primaryKey[0] : undefined;\n }\n\n /**\n * True when the source can take its untouched fast path: no suppression\n * test per base row, no merge, no index maintenance.\n */\n get isEmpty(): boolean {\n return this.#byKey.size === 0;\n }\n\n /** The number of rows the batch has touched, tombstones included. */\n get size(): number {\n return this.#byKey.size;\n }\n\n /**\n * Estimated retained bytes, including keys, tombstones, and sorted indexes.\n * This is a budgeting heuristic, not a measurement of the JavaScript heap.\n */\n get estimatedBytes(): number {\n // Budget 32 bytes per live row per index for references and B-tree nodes.\n return (\n this.#rowBytes +\n this.#indexBytes +\n this.#liveCount * this.#indexes.size * 32\n );\n }\n\n #key(row: Row): unknown {\n const single = this.#singleColumnKey;\n if (single !== undefined) {\n return row[single];\n }\n const key = this.#primaryKey.map(k => row[k]);\n return JSON.stringify(key);\n }\n\n #track(row: Row, key: unknown): void {\n if (this.#byKey.has(key)) {\n return;\n }\n this.#rowBytes += 64 + estimateBytes(key as Value);\n if (this.#singleColumnKey === undefined) {\n this.#firstKeyValues.add(row[this.#primaryKey[0]]);\n }\n }\n\n /** Records that `row` is now present with this value. */\n set(row: Row): void {\n const key = this.#key(row);\n const existing = this.#byKey.get(key);\n this.#track(row, key);\n this.#rowBytes +=\n estimateBytes(row) -\n (existing === undefined ? 0 : estimateBytes(existing));\n if (existing !== undefined) {\n // A live row for this key is already indexed under its old value, which\n // may sort differently. Retract it before inserting the new one.\n this.#removeFromIndexes(existing);\n this.#liveCount--;\n }\n this.#byKey.set(key, row);\n this.#addToIndexes(row);\n this.#liveCount++;\n }\n\n /** Records that `row` is now absent. */\n delete(row: Row): void {\n const key = this.#key(row);\n const existing = this.#byKey.get(key);\n this.#track(row, key);\n if (existing !== undefined) {\n this.#rowBytes -= estimateBytes(existing);\n this.#removeFromIndexes(existing);\n this.#liveCount--;\n }\n this.#byKey.set(key, undefined);\n }\n\n /**\n * Whether the batch overrides this base row. Callers use it to suppress base\n * rows during a merge, so it must be answered by primary key -- the value\n * carried by the base row is exactly what the delta is replacing.\n */\n overrides(row: Row): boolean {\n if (\n this.#singleColumnKey === undefined &&\n !this.#firstKeyValues.has(row[this.#primaryKey[0]])\n ) {\n return false;\n }\n return this.#byKey.has(this.#key(row));\n }\n\n /**\n * The batch's value for a row, by primary key. Returns `NOT_OVERRIDDEN` when\n * the batch has not touched the key at all (so the caller should consult the\n * base) and `undefined` when the batch removed it.\n */\n get(row: Row): Row | undefined | typeof NOT_OVERRIDDEN {\n const key = this.#key(row);\n if (!this.#byKey.has(key)) {\n return NOT_OVERRIDDEN;\n }\n return this.#byKey.get(key);\n }\n\n /**\n * Looks a row up by an arbitrary (not necessarily primary) unique key. Only\n * reachable when the delta is non-empty, which is never the case for the one\n * caller that uses a non-primary key, so a walk is fine here.\n */\n getByColumns(keyCols: readonly string[], keyRow: Row): Row | undefined {\n for (const row of this.#byKey.values()) {\n if (row !== undefined && keyCols.every(c => row[c] === keyRow[c])) {\n return row;\n }\n }\n return undefined;\n }\n\n #addToIndexes(row: Row): void {\n for (const data of this.#indexes.values()) {\n data.add(row);\n }\n }\n\n #removeFromIndexes(row: Row): void {\n for (const data of this.#indexes.values()) {\n data.delete(row);\n }\n }\n\n #getOrCreateIndex(sort: Ordering): BTreeSet<Row> {\n const indexKey = JSON.stringify(sort);\n let data = this.#indexes.get(indexKey);\n if (data === undefined) {\n data = new BTreeSet<Row>(makeBoundComparator(sort) as Comparator);\n for (const row of this.#byKey.values()) {\n if (row !== undefined) {\n data.add(row);\n }\n }\n this.#indexes.set(indexKey, data);\n this.#indexBytes += 128 + estimateBytes(indexKey) + estimateBytes(sort);\n }\n return data;\n }\n\n /**\n * The live rows this batch contributes to a fetch, in the same order the\n * backing SQL query returns its rows, so the two streams can be merged.\n *\n * `constraint`, `multiConstraints` and `filterPredicate` are the ones SQL\n * already applied to the base rows; they are applied here so the delta rows\n * are subject to the same query. `start` is the fetch's start row: the scan\n * begins there rather than at the start of the constraint span. It is\n * inclusive, so the caller still applies an `after` basis.\n */\n *rowsFor(\n sort: Ordering,\n constraint: Constraint | undefined,\n reverse: boolean | undefined,\n filterPredicate: ((row: Row) => boolean | undefined) | undefined,\n multiConstraints: readonly MultiConstraint[] | undefined,\n start?: Row | undefined,\n ): Iterable<Row> {\n if (this.#liveCount === 0) {\n return;\n }\n\n // Lead the index with the constraint keys so a constrained fetch scans\n // only the matching span; within that span rows come out in `sort` order,\n // which is the order SQL returned the base rows in.\n const indexSort: OrderPart[] = [];\n if (constraint) {\n for (const key of Object.keys(constraint)) {\n indexSort.push([key, 'asc']);\n }\n }\n // Constraining by the whole primary key admits at most one row, so the\n // requested sort adds nothing.\n const sorted =\n !constraint || !constraintMatchesPrimaryKey(constraint, this.#primaryKey);\n if (sorted) {\n indexSort.push(...sort);\n }\n\n const data = this.#getOrCreateIndex(indexSort);\n\n let scanStart: RowBound | undefined;\n if (\n start !== undefined &&\n sorted &&\n (!constraint || constraintMatchesRow(constraint, start))\n ) {\n // `start` carries every index column, and within the constraint span it\n // agrees with the constraint, so it is the scan's bound as it stands.\n // A `start` outside the span bounds nothing the scan can use.\n scanStart = start;\n } else if (constraint) {\n // The first row matching the constraint is not simply the constraint\n // values with everything else absent: a `desc` part puts absent values\n // last. The min/max sentinels say \"the first row with these constraint\n // values\" regardless of direction. Same construction as `MemorySource`.\n scanStart = {};\n for (const [key, dir] of indexSort) {\n if (hasOwn(constraint, key)) {\n scanStart[key] = constraint[key] as Value;\n } else if (reverse) {\n scanStart[key] = dir === 'asc' ? maxValue : minValue;\n } else {\n scanStart[key] = dir === 'asc' ? minValue : maxValue;\n }\n }\n }\n\n for (const row of generateRows(data, scanStart, reverse)) {\n if (constraint && !constraintMatchesRow(constraint, row)) {\n // Rows are sorted by the constraint keys first, so matches are\n // contiguous and the first miss ends the span.\n break;\n }\n if (multiConstraints && !matchesMultiConstraints(row, multiConstraints)) {\n continue;\n }\n if (filterPredicate && !filterPredicate(row)) {\n continue;\n }\n yield row;\n }\n }\n\n /**\n * A row with the primary key of each row the batch has touched, removed\n * rows included. A live row is returned as is.\n */\n *touchedKeys(): Iterable<Row> {\n const single = this.#singleColumnKey;\n for (const [key, row] of this.#byKey) {\n if (row !== undefined) {\n yield row;\n } else if (single !== undefined) {\n yield {[single]: key as Value};\n } else {\n const values = JSON.parse(key as string) as Value[];\n yield Object.fromEntries(\n this.#primaryKey.map((k, i) => [k, values[i]]),\n );\n }\n }\n }\n\n /** The rows the batch has added or edited, and not since removed. */\n *liveRows(): Iterable<Row> {\n for (const row of this.#byKey.values()) {\n if (row !== undefined) {\n yield row;\n }\n }\n }\n\n /** Drops the batch. Called when the source moves to a snapshot that has it. */\n clear(): void {\n this.#byKey.clear();\n this.#firstKeyValues.clear();\n this.#indexes.clear();\n this.#liveCount = 0;\n this.#rowBytes = 0;\n this.#indexBytes = 0;\n }\n}\n\n// Allow for object/property storage and UTF-16 strings without allocating a\n// serialized copy. Shared values may be counted more than once intentionally.\nfunction estimateBytes(value: Value): number {\n if (typeof value === 'string') {\n return 24 + value.length * 2;\n }\n if (value === null || typeof value !== 'object') {\n return 8;\n }\n let bytes = 32;\n if (Array.isArray(value)) {\n for (const item of value) {\n bytes += 8 + estimateBytes(item);\n }\n } else {\n for (const key in value) {\n bytes += 16 + estimateBytes(key) + estimateBytes((value as Row)[key]);\n }\n }\n return bytes;\n}\n\nexport const NOT_OVERRIDDEN = Symbol('not-overridden');\n\nfunction matchesMultiConstraints(\n row: Row,\n multiConstraints: readonly MultiConstraint[],\n): boolean {\n for (const mc of multiConstraints) {\n if (mc.length === 0) {\n continue;\n }\n let any = false;\n for (const c of mc) {\n if (constraintMatchesRow(c, row)) {\n any = true;\n break;\n }\n }\n if (!any) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Merges a batch's rows into an ordered base stream and drops the base rows\n * the batch overrides.\n *\n * Both streams must already be in `compare` order. A delta row that ties with\n * a base row is the *same* row by primary key -- every fetch ordering includes\n * the primary key -- so the base copy is suppressed and the delta's value\n * takes its place at that position.\n */\nexport function* generateWithPendingDelta(\n baseRows: Iterable<Row>,\n deltaRows: Iterable<Row>,\n delta: PendingDelta,\n compare: Comparator,\n): IterableIterator<Row> {\n const deltaIter = deltaRows[Symbol.iterator]();\n let pending = deltaIter.next();\n try {\n for (const row of baseRows) {\n while (!pending.done && compare(pending.value, row) < 0) {\n yield pending.value;\n pending = deltaIter.next();\n }\n if (!delta.overrides(row)) {\n yield row;\n }\n }\n while (!pending.done) {\n yield pending.value;\n pending = deltaIter.next();\n }\n } finally {\n // Propagate early termination so the delta iterator can release the BTree\n // cursor it is walking, the same contract `mergeSortedStreams` documents.\n if (!pending.done) {\n deltaIter.return?.(undefined);\n }\n }\n}\n\n/**\n * The unordered counterpart. Nothing downstream depends on position, so the\n * surviving base rows are emitted first and the batch's rows follow.\n */\nexport function* generateWithPendingDeltaUnordered(\n baseRows: Iterable<Row>,\n deltaRows: Iterable<Row>,\n delta: PendingDelta,\n): IterableIterator<Row> {\n for (const row of baseRows) {\n if (!delta.overrides(row)) {\n yield row;\n }\n }\n yield* deltaRows;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"query-delegate.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/query-delegate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,gCAAgC,CAAC;AAC3D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAC,iBAAiB,EAAC,MAAM,4CAA4C,CAAC;AAC7E,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AAGtC,qBAAa,iBAAkB,SAAQ,iBAAiB;;IAMtD,QAAQ,CAAC,oBAAoB,QAAQ;IAGrC,YACE,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,SAAS,EAatB;IAED,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAYnC;IAED,mBAAmB,CAAC,EAAE,EAAE,cAAc,cAKrC;IACQ,gBAAgB,CAAC,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC,GAAG,CAAC,CAMzD;CACF"}
1
+ {"version":3,"file":"query-delegate.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/query-delegate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,gCAAgC,CAAC;AAC3D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAC,iBAAiB,EAAC,MAAM,4CAA4C,CAAC;AAC7E,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AAGtC,qBAAa,iBAAkB,SAAQ,iBAAiB;;IAMtD,QAAQ,CAAC,oBAAoB,QAAQ;IAGrC,YACE,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,SAAS,EAiBtB;IAED,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAYnC;IAED,mBAAmB,CAAC,EAAE,EAAE,cAAc,cAKrC;IACQ,gBAAgB,CAAC,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC,GAAG,CAAC,CAMzD;CACF"}
@@ -20,7 +20,11 @@ var QueryDelegateImpl = class extends QueryDelegateBase {
20
20
  ivmSampling: 0,
21
21
  level: "info",
22
22
  slowHydrateThreshold: 0,
23
- slowRowThreshold: 0
23
+ slowAdvanceThreshold: 0,
24
+ queryStatsIntervalSeconds: 0,
25
+ slowRowThreshold: 0,
26
+ planWarningRowThreshold: 0,
27
+ planWarningCostThreshold: 0
24
28
  };
25
29
  }
26
30
  getSource(tableName) {
@@ -1 +1 @@
1
- {"version":3,"file":"query-delegate.js","names":["#lc","#db","#schema","#sources","#logConfig","#commitObservers"],"sources":["../../../../zqlite/src/query-delegate.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {LogConfig} from '../../otel/src/log-options.ts';\nimport {getOrInsertComputed} from '../../shared/src/map.ts';\nimport type {Schema} from '../../zero-types/src/schema.ts';\nimport type {Source} from '../../zql/src/ivm/source.ts';\nimport {QueryDelegateBase} from '../../zql/src/query/query-delegate-base.ts';\nimport type {CommitListener} from '../../zql/src/query/query-delegate.ts';\nimport type {Database} from './db.ts';\nimport {TableSource} from './table-source.ts';\n\nexport class QueryDelegateImpl extends QueryDelegateBase {\n readonly #lc: LogContext;\n readonly #db: Database;\n readonly #schema: Schema;\n readonly #sources: Map<string, Source> = new Map();\n readonly #logConfig: LogConfig;\n readonly defaultQueryComplete = true;\n readonly #commitObservers = new Set<() => void>();\n\n constructor(\n lc: LogContext,\n db: Database,\n schema: Schema,\n logConfig?: LogConfig,\n ) {\n super();\n this.#lc = lc.withContext('class', 'QueryDelegateImpl');\n this.#db = db;\n this.#schema = schema;\n this.#logConfig = logConfig ?? {\n format: 'text',\n ivmSampling: 0,\n level: 'info',\n slowHydrateThreshold: 0,\n slowRowThreshold: 0,\n };\n }\n\n getSource(tableName: string): Source {\n return getOrInsertComputed(this.#sources, tableName, tableName => {\n const tableSchema = this.#schema.tables[tableName];\n return new TableSource(\n this.#lc,\n this.#logConfig,\n this.#db,\n tableName,\n tableSchema.columns,\n tableSchema.primaryKey,\n );\n });\n }\n\n onTransactionCommit(cb: CommitListener) {\n this.#commitObservers.add(cb);\n return () => {\n this.#commitObservers.delete(cb);\n };\n }\n override batchViewUpdates<T>(applyViewUpdates: () => T): T {\n const ret = applyViewUpdates();\n for (const observer of this.#commitObservers) {\n observer();\n }\n return ret;\n }\n}\n"],"mappings":";;;;AAUA,IAAa,oBAAb,cAAuC,kBAAkB;CACvD;CACA;CACA;CACA,2BAAyC,IAAI,IAAI;CACjD;CACA,uBAAgC;CAChC,mCAA4B,IAAI,IAAgB;CAEhD,YACE,IACA,IACA,QACA,WACA;EACA,MAAM;EACN,KAAKA,MAAM,GAAG,YAAY,SAAS,mBAAmB;EACtD,KAAKC,MAAM;EACX,KAAKC,UAAU;EACf,KAAKE,aAAa,aAAa;GAC7B,QAAQ;GACR,aAAa;GACb,OAAO;GACP,sBAAsB;GACtB,kBAAkB;EACpB;CACF;CAEA,UAAU,WAA2B;EACnC,OAAO,oBAAoB,KAAKD,UAAU,YAAW,cAAa;GAChE,MAAM,cAAc,KAAKD,QAAQ,OAAO;GACxC,OAAO,IAAI,YACT,KAAKF,KACL,KAAKI,YACL,KAAKH,KACL,WACA,YAAY,SACZ,YAAY,UACd;EACF,CAAC;CACH;CAEA,oBAAoB,IAAoB;EACtC,KAAKI,iBAAiB,IAAI,EAAE;EAC5B,aAAa;GACX,KAAKA,iBAAiB,OAAO,EAAE;EACjC;CACF;CACA,iBAA6B,kBAA8B;EACzD,MAAM,MAAM,iBAAiB;EAC7B,KAAK,MAAM,YAAY,KAAKA,kBAC1B,SAAS;EAEX,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"query-delegate.js","names":["#lc","#db","#schema","#sources","#logConfig","#commitObservers"],"sources":["../../../../zqlite/src/query-delegate.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {LogConfig} from '../../otel/src/log-options.ts';\nimport {getOrInsertComputed} from '../../shared/src/map.ts';\nimport type {Schema} from '../../zero-types/src/schema.ts';\nimport type {Source} from '../../zql/src/ivm/source.ts';\nimport {QueryDelegateBase} from '../../zql/src/query/query-delegate-base.ts';\nimport type {CommitListener} from '../../zql/src/query/query-delegate.ts';\nimport type {Database} from './db.ts';\nimport {TableSource} from './table-source.ts';\n\nexport class QueryDelegateImpl extends QueryDelegateBase {\n readonly #lc: LogContext;\n readonly #db: Database;\n readonly #schema: Schema;\n readonly #sources: Map<string, Source> = new Map();\n readonly #logConfig: LogConfig;\n readonly defaultQueryComplete = true;\n readonly #commitObservers = new Set<() => void>();\n\n constructor(\n lc: LogContext,\n db: Database,\n schema: Schema,\n logConfig?: LogConfig,\n ) {\n super();\n this.#lc = lc.withContext('class', 'QueryDelegateImpl');\n this.#db = db;\n this.#schema = schema;\n this.#logConfig = logConfig ?? {\n format: 'text',\n ivmSampling: 0,\n level: 'info',\n slowHydrateThreshold: 0,\n slowAdvanceThreshold: 0,\n queryStatsIntervalSeconds: 0,\n slowRowThreshold: 0,\n planWarningRowThreshold: 0,\n planWarningCostThreshold: 0,\n };\n }\n\n getSource(tableName: string): Source {\n return getOrInsertComputed(this.#sources, tableName, tableName => {\n const tableSchema = this.#schema.tables[tableName];\n return new TableSource(\n this.#lc,\n this.#logConfig,\n this.#db,\n tableName,\n tableSchema.columns,\n tableSchema.primaryKey,\n );\n });\n }\n\n onTransactionCommit(cb: CommitListener) {\n this.#commitObservers.add(cb);\n return () => {\n this.#commitObservers.delete(cb);\n };\n }\n override batchViewUpdates<T>(applyViewUpdates: () => T): T {\n const ret = applyViewUpdates();\n for (const observer of this.#commitObservers) {\n observer();\n }\n return ret;\n }\n}\n"],"mappings":";;;;AAUA,IAAa,oBAAb,cAAuC,kBAAkB;CACvD;CACA;CACA;CACA,2BAAyC,IAAI,IAAI;CACjD;CACA,uBAAgC;CAChC,mCAA4B,IAAI,IAAgB;CAEhD,YACE,IACA,IACA,QACA,WACA;EACA,MAAM;EACN,KAAKA,MAAM,GAAG,YAAY,SAAS,mBAAmB;EACtD,KAAKC,MAAM;EACX,KAAKC,UAAU;EACf,KAAKE,aAAa,aAAa;GAC7B,QAAQ;GACR,aAAa;GACb,OAAO;GACP,sBAAsB;GACtB,sBAAsB;GACtB,2BAA2B;GAC3B,kBAAkB;GAClB,yBAAyB;GACzB,0BAA0B;EAC5B;CACF;CAEA,UAAU,WAA2B;EACnC,OAAO,oBAAoB,KAAKD,UAAU,YAAW,cAAa;GAChE,MAAM,cAAc,KAAKD,QAAQ,OAAO;GACxC,OAAO,IAAI,YACT,KAAKF,KACL,KAAKI,YACL,KAAKH,KACL,WACA,YAAY,SACZ,YAAY,UACd;EACF,CAAC;CACH;CAEA,oBAAoB,IAAoB;EACtC,KAAKI,iBAAiB,IAAI,EAAE;EAC5B,aAAa;GACX,KAAKA,iBAAiB,OAAO,EAAE;EACjC;CACF;CACA,iBAA6B,kBAA8B;EACzD,MAAM,MAAM,iBAAiB;EAC7B,KAAK,MAAM,YAAY,KAAKA,kBAC1B,SAAS;EAEX,OAAO;CACT;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-cost-model.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,sCAAsC,CAAC;AAEtE,OAAO,KAAK,EACV,mBAAmB,EAEpB,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAmBjD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAC,CAAC,GAC9D,mBAAmB,CA+DrB;AAmFD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C"}
1
+ {"version":3,"file":"sqlite-cost-model.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,sCAAsC,CAAC;AAEtE,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAmBjD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAC,CAAC,GAC9D,mBAAmB,CAgErB;AAqJD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C"}
@@ -17,13 +17,64 @@ import SQLite3Database from "@rocicorp/zero-sqlite3";
17
17
  */
18
18
  function createSQLiteCostModel(db, tableSpecs) {
19
19
  const fanoutEstimator = new SQLiteStatFanout(db);
20
+ const tableRows = tableRowsReader(db);
20
21
  return (tableName, sort, filters, constraint) => {
21
22
  const noSubqueryFilters = transformFilters(filters).filters;
22
23
  const { zqlSpec } = must(tableSpecs.get(tableName));
23
24
  const sql = compileInline(buildSelectQuery(tableName, zqlSpec, constraint, noSubqueryFilters, sort, void 0, void 0));
24
25
  const loops = getScanstatusLoops(db.prepare(sql));
25
26
  assert(loops.length > 0, `Expected scanstatus to return at least one loop for query: ${sql}`);
26
- return estimateCost(loops, (columns) => fanoutEstimator.getFanout(tableName, columns));
27
+ return {
28
+ ...estimateCost(loops, (columns) => fanoutEstimator.getFanout(tableName, columns)),
29
+ plan: accessPlan(loops, tableRows(tableName))
30
+ };
31
+ };
32
+ }
33
+ /**
34
+ * Returns a function that reads the number of rows in a table from
35
+ * `sqlite_stat1`, or `undefined` if the table has not been analyzed.
36
+ *
37
+ * The counts are cached: they only feed plan warnings, for which the count
38
+ * as of the first plan is close enough.
39
+ */
40
+ function tableRowsReader(db) {
41
+ let stmt;
42
+ const read = (table) => {
43
+ try {
44
+ stmt ??= db.prepare("SELECT stat FROM sqlite_stat1 WHERE tbl = ? LIMIT 1");
45
+ const row = stmt.get(table);
46
+ const rows = row ? parseInt(row.stat, 10) : NaN;
47
+ return Number.isNaN(rows) ? void 0 : rows;
48
+ } catch {
49
+ return;
50
+ }
51
+ };
52
+ const cache = /* @__PURE__ */ new Map();
53
+ return (table) => {
54
+ if (!cache.has(table)) cache.set(table, read(table));
55
+ return cache.get(table);
56
+ };
57
+ }
58
+ /**
59
+ * Describes how SQLite reads the rows, from the EXPLAIN text of the
60
+ * statement's top-level loops, e.g.:
61
+ *
62
+ * - `SEARCH issue USING INDEX issue_project (projectID=?)`
63
+ * - `SCAN comment USING INDEX sqlite_autoindex_comment_1` (every row, in the
64
+ * order of the index)
65
+ * - `SCAN issue` (every row, in table order)
66
+ * - `USE TEMP B-TREE FOR ORDER BY` (a full sort)
67
+ * - `USE TEMP B-TREE FOR LAST TERM OF ORDER BY` or `... RIGHT PART OF ORDER
68
+ * BY` (sorting runs of rows that tie on the index)
69
+ */
70
+ function accessPlan(loops, tableRows) {
71
+ const [first, ...rest] = loops.filter((loop) => loop.parentId === 0);
72
+ let sort = "none";
73
+ for (const { explain } of rest) if (explain.startsWith("USE TEMP B-TREE FOR") && explain.includes("ORDER BY")) sort = explain === "USE TEMP B-TREE FOR ORDER BY" ? "full" : "partial";
74
+ return {
75
+ access: first?.explain.startsWith("SCAN ") ? "scan" : "search",
76
+ sort,
77
+ tableRows
27
78
  };
28
79
  }
29
80
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-cost-model.js","names":[],"sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"sourcesContent":["import SQLite3Database from '@rocicorp/zero-sqlite3';\nimport {assert} from '../../shared/src/asserts.ts';\nimport {must} from '../../shared/src/must.ts';\nimport type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {SchemaValue} from '../../zero-types/src/schema-value.ts';\nimport {transformFilters} from '../../zql/src/builder/filter.ts';\nimport type {\n ConnectionCostModel,\n CostModelCost,\n} from '../../zql/src/planner/planner-connection.ts';\nimport type {PlannerConstraint} from '../../zql/src/planner/planner-constraint.ts';\nimport type {Database, Statement} from './db.ts';\nimport {compileInline} from './internal/sql-inline.ts';\nimport {buildSelectQuery} from './query-builder.ts';\nimport {SQLiteStatFanout} from './sqlite-stat-fanout.ts';\n\n/**\n * Loop information returned by SQLite's scanstatus API.\n */\ninterface ScanstatusLoop {\n /** Unique identifier for this loop */\n selectId: number;\n /** Parent loop ID, or 0 for root loops */\n parentId: number;\n /** Estimated rows emitted per turn of parent loop */\n est: number;\n /** EXPLAIN text for this loop to determine: b-tree vs list subquery */\n explain: string;\n}\n\n/**\n * Creates a SQLite-based cost model for query planning.\n * Uses SQLite's scanstatus API to estimate query costs based on the actual\n * SQLite query planner's analysis.\n *\n * @param db Database instance for preparing statements\n * @param tableSpecs Map of table names to their table specs with ZQL schemas\n * @returns ConnectionCostModel function for use with the planner\n */\nexport function createSQLiteCostModel(\n db: Database,\n tableSpecs: Map<string, {zqlSpec: Record<string, SchemaValue>}>,\n): ConnectionCostModel {\n const fanoutEstimator = new SQLiteStatFanout(db);\n return (\n tableName: string,\n sort: Ordering,\n filters: Condition | undefined,\n constraint: PlannerConstraint | undefined,\n ): CostModelCost => {\n // Transform filters to remove correlated subqueries.\n //\n // We use `transformFilters` (the same function the runtime uses to\n // decide what to push to the source at connection time). It returns:\n // - For AND: the non-CSQ conjuncts (a *superset* of the original).\n // - For OR with any CSQ branch: `undefined` (the OR-with-CSQ is not\n // pushable, and matches a strict superset of any single branch).\n //\n // This matches what the runtime actually applies at the source. A\n // previous implementation here had wrong OR semantics — it dropped the\n // CSQ branch and returned the simple branch alone, which is a strict\n // *subset* of the original OR's match set, causing the cost model to\n // under-estimate scan rows for OR-with-CSQ shapes and biasing the\n // planner toward semi-join plans. See the comment in\n // `packages/zql/src/builder/filter.ts:transformFilters`.\n const noSubqueryFilters = transformFilters(filters).filters;\n\n // Build the SQL query using the same logic as actual queries\n const {zqlSpec} = must(tableSpecs.get(tableName));\n\n const query = buildSelectQuery(\n tableName,\n zqlSpec,\n constraint,\n noSubqueryFilters,\n sort,\n undefined, // reverse is undefined here\n undefined, // start is undefined here\n );\n\n // Use compileInline to inline actual values into the SQL for cost estimation.\n // This allows SQLite's query planner to see real values and make better decisions\n // about index usage and query plans. This is safe here because it's only used for\n // cost estimation, not for executing user-facing queries (which use parameterized\n // queries via the standard compile() function).\n const sql = compileInline(query);\n\n // Prepare statement to get scanstatus information\n const stmt = db.prepare(sql);\n\n // Get scanstatus loops from the prepared statement\n const loops = getScanstatusLoops(stmt);\n\n // Scanstatus should always be available - if we get no loops, something is wrong\n assert(\n loops.length > 0,\n `Expected scanstatus to return at least one loop for query: ${sql}`,\n );\n\n const ret = estimateCost(loops, (columns: string[]) =>\n fanoutEstimator.getFanout(tableName, columns),\n );\n\n return ret;\n };\n}\n\n/**\n * Gets scanstatus loop information from a prepared statement.\n * Iterates through all query elements and extracts loop statistics.\n *\n * Uses SQLITE_SCANSTAT_COMPLEX flag (1) to get all loops including sorting operations.\n *\n * @param stmt Prepared statement to get scanstatus from\n * @returns Array of loop information, or empty array if scanstatus unavailable\n */\nfunction getScanstatusLoops(stmt: Statement): ScanstatusLoop[] {\n const loops: ScanstatusLoop[] = [];\n\n // Iterate through query elements by incrementing idx until we get undefined\n // which indicates we've reached the end\n for (let idx = 0; ; idx++) {\n const selectId = stmt.scanStatus(\n idx,\n SQLite3Database.SQLITE_SCANSTAT_SELECTID,\n 1,\n );\n\n if (selectId === undefined) {\n break;\n }\n\n loops.push({\n selectId: must(selectId),\n parentId: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1),\n ),\n explain: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1),\n ),\n est: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EST, 1)),\n });\n }\n\n return loops.sort((a, b) => a.selectId - b.selectId);\n}\n\n/**\n * Estimates the cost of a query based on scanstats from sqlite3_stmt_scanstatus_v2\n */\nfunction estimateCost(\n scanstats: ScanstatusLoop[],\n fanout: CostModelCost['fanout'],\n): CostModelCost {\n // Sort by selectId to process in execution order\n const sorted = scanstats.toSorted((a, b) => a.selectId - b.selectId);\n\n let totalRows = 0;\n let totalCost = 0;\n\n // Identify if there are multiple top-level (parentId=0) operations\n // If so, the first is typically the scan, and subsequent ones are sorts\n const topLevelOps = sorted.filter(s => s.parentId === 0);\n\n // We only consider top level ops since ZQL queries are single-table when hitting SQLite.\n // We do have a nested op in the case of `WHERE x IN (:arg)` but it is negligible\n // assuming :arg is small.\n let firstLoop = true;\n for (const op of topLevelOps) {\n if (firstLoop) {\n // First top-level op is the main scan\n // and determines the total number of rows output.\n totalRows = op.est;\n firstLoop = false;\n } else {\n if (op.explain.includes('ORDER BY')) {\n totalCost += btreeCost(totalRows);\n }\n }\n }\n\n return {\n rows: totalRows,\n startupCost: totalCost,\n fanout,\n };\n}\n\nexport function btreeCost(rows: number): number {\n // B-Tree construction is ~O(n log n) so we estimate the cost as such.\n // We divide the cost by 10 because sorting in SQLite is ~10x faster\n // than bringing the data into JS and sorting there.\n return (rows * Math.log2(rows)) / 10;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAuCA,SAAgB,sBACd,IACA,YACqB;CACrB,MAAM,kBAAkB,IAAI,iBAAiB,EAAE;CAC/C,QACE,WACA,MACA,SACA,eACkB;EAgBlB,MAAM,oBAAoB,iBAAiB,OAAO,EAAE;EAGpD,MAAM,EAAC,YAAW,KAAK,WAAW,IAAI,SAAS,CAAC;EAiBhD,MAAM,MAAM,cAfE,iBACZ,WACA,SACA,YACA,mBACA,MACA,KAAA,GACA,KAAA,CAQwB,CAAK;EAM/B,MAAM,QAAQ,mBAHD,GAAG,QAAQ,GAGS,CAAI;EAGrC,OACE,MAAM,SAAS,GACf,8DAA8D,KAChE;EAMA,OAJY,aAAa,QAAQ,YAC/B,gBAAgB,UAAU,WAAW,OAAO,CAGvC;CACT;AACF;;;;;;;;;;AAWA,SAAS,mBAAmB,MAAmC;CAC7D,MAAM,QAA0B,CAAC;CAIjC,KAAK,IAAI,MAAM,IAAK,OAAO;EACzB,MAAM,WAAW,KAAK,WACpB,KACA,gBAAgB,0BAChB,CACF;EAEA,IAAI,aAAa,KAAA,GACf;EAGF,MAAM,KAAK;GACT,UAAU,KAAK,QAAQ;GACvB,UAAU,KACR,KAAK,WAAW,KAAK,gBAAgB,0BAA0B,CAAC,CAClE;GACA,SAAS,KACP,KAAK,WAAW,KAAK,gBAAgB,yBAAyB,CAAC,CACjE;GACA,KAAK,KAAK,KAAK,WAAW,KAAK,gBAAgB,qBAAqB,CAAC,CAAC;EACxE,CAAC;CACH;CAEA,OAAO,MAAM,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACrD;;;;AAKA,SAAS,aACP,WACA,QACe;CAEf,MAAM,SAAS,UAAU,UAAU,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;CAEnE,IAAI,YAAY;CAChB,IAAI,YAAY;CAIhB,MAAM,cAAc,OAAO,QAAO,MAAK,EAAE,aAAa,CAAC;CAKvD,IAAI,YAAY;CAChB,KAAK,MAAM,MAAM,aACf,IAAI,WAAW;EAGb,YAAY,GAAG;EACf,YAAY;CACd,OACE,IAAI,GAAG,QAAQ,SAAS,UAAU,GAChC,aAAa,UAAU,SAAS;CAKtC,OAAO;EACL,MAAM;EACN,aAAa;EACb;CACF;AACF;AAEA,SAAgB,UAAU,MAAsB;CAI9C,OAAQ,OAAO,KAAK,KAAK,IAAI,IAAK;AACpC"}
1
+ {"version":3,"file":"sqlite-cost-model.js","names":[],"sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"sourcesContent":["import SQLite3Database from '@rocicorp/zero-sqlite3';\nimport {assert} from '../../shared/src/asserts.ts';\nimport {must} from '../../shared/src/must.ts';\nimport type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {SchemaValue} from '../../zero-types/src/schema-value.ts';\nimport {transformFilters} from '../../zql/src/builder/filter.ts';\nimport type {\n AccessPlan,\n ConnectionCostModel,\n CostModelCost,\n} from '../../zql/src/planner/planner-connection.ts';\nimport type {PlannerConstraint} from '../../zql/src/planner/planner-constraint.ts';\nimport type {Database, Statement} from './db.ts';\nimport {compileInline} from './internal/sql-inline.ts';\nimport {buildSelectQuery} from './query-builder.ts';\nimport {SQLiteStatFanout} from './sqlite-stat-fanout.ts';\n\n/**\n * Loop information returned by SQLite's scanstatus API.\n */\ninterface ScanstatusLoop {\n /** Unique identifier for this loop */\n selectId: number;\n /** Parent loop ID, or 0 for root loops */\n parentId: number;\n /** Estimated rows emitted per turn of parent loop */\n est: number;\n /** EXPLAIN text for this loop to determine: b-tree vs list subquery */\n explain: string;\n}\n\n/**\n * Creates a SQLite-based cost model for query planning.\n * Uses SQLite's scanstatus API to estimate query costs based on the actual\n * SQLite query planner's analysis.\n *\n * @param db Database instance for preparing statements\n * @param tableSpecs Map of table names to their table specs with ZQL schemas\n * @returns ConnectionCostModel function for use with the planner\n */\nexport function createSQLiteCostModel(\n db: Database,\n tableSpecs: Map<string, {zqlSpec: Record<string, SchemaValue>}>,\n): ConnectionCostModel {\n const fanoutEstimator = new SQLiteStatFanout(db);\n const tableRows = tableRowsReader(db);\n return (\n tableName: string,\n sort: Ordering,\n filters: Condition | undefined,\n constraint: PlannerConstraint | undefined,\n ): CostModelCost => {\n // Transform filters to remove correlated subqueries.\n //\n // We use `transformFilters` (the same function the runtime uses to\n // decide what to push to the source at connection time). It returns:\n // - For AND: the non-CSQ conjuncts (a *superset* of the original).\n // - For OR with any CSQ branch: `undefined` (the OR-with-CSQ is not\n // pushable, and matches a strict superset of any single branch).\n //\n // This matches what the runtime actually applies at the source. A\n // previous implementation here had wrong OR semantics — it dropped the\n // CSQ branch and returned the simple branch alone, which is a strict\n // *subset* of the original OR's match set, causing the cost model to\n // under-estimate scan rows for OR-with-CSQ shapes and biasing the\n // planner toward semi-join plans. See the comment in\n // `packages/zql/src/builder/filter.ts:transformFilters`.\n const noSubqueryFilters = transformFilters(filters).filters;\n\n // Build the SQL query using the same logic as actual queries\n const {zqlSpec} = must(tableSpecs.get(tableName));\n\n const query = buildSelectQuery(\n tableName,\n zqlSpec,\n constraint,\n noSubqueryFilters,\n sort,\n undefined, // reverse is undefined here\n undefined, // start is undefined here\n );\n\n // Use compileInline to inline actual values into the SQL for cost estimation.\n // This allows SQLite's query planner to see real values and make better decisions\n // about index usage and query plans. This is safe here because it's only used for\n // cost estimation, not for executing user-facing queries (which use parameterized\n // queries via the standard compile() function).\n const sql = compileInline(query);\n\n // Prepare statement to get scanstatus information\n const stmt = db.prepare(sql);\n\n // Get scanstatus loops from the prepared statement\n const loops = getScanstatusLoops(stmt);\n\n // Scanstatus should always be available - if we get no loops, something is wrong\n assert(\n loops.length > 0,\n `Expected scanstatus to return at least one loop for query: ${sql}`,\n );\n\n const ret = estimateCost(loops, (columns: string[]) =>\n fanoutEstimator.getFanout(tableName, columns),\n );\n\n return {...ret, plan: accessPlan(loops, tableRows(tableName))};\n };\n}\n\n/**\n * Returns a function that reads the number of rows in a table from\n * `sqlite_stat1`, or `undefined` if the table has not been analyzed.\n *\n * The counts are cached: they only feed plan warnings, for which the count\n * as of the first plan is close enough.\n */\nfunction tableRowsReader(db: Database): (table: string) => number | undefined {\n let stmt: Statement | undefined;\n const read = (table: string): number | undefined => {\n try {\n stmt ??= db.prepare(\n 'SELECT stat FROM sqlite_stat1 WHERE tbl = ? LIMIT 1',\n );\n // Every row for a table starts with the number of rows in the table.\n const row = stmt.get<{stat: string} | undefined>(table);\n const rows = row ? parseInt(row.stat, 10) : NaN;\n return Number.isNaN(rows) ? undefined : rows;\n } catch {\n // sqlite_stat1 does not exist until the database is analyzed.\n return undefined;\n }\n };\n // Tables without statistics are cached too, as `undefined`.\n const cache = new Map<string, number | undefined>();\n return table => {\n if (!cache.has(table)) {\n cache.set(table, read(table));\n }\n return cache.get(table);\n };\n}\n\n/**\n * Describes how SQLite reads the rows, from the EXPLAIN text of the\n * statement's top-level loops, e.g.:\n *\n * - `SEARCH issue USING INDEX issue_project (projectID=?)`\n * - `SCAN comment USING INDEX sqlite_autoindex_comment_1` (every row, in the\n * order of the index)\n * - `SCAN issue` (every row, in table order)\n * - `USE TEMP B-TREE FOR ORDER BY` (a full sort)\n * - `USE TEMP B-TREE FOR LAST TERM OF ORDER BY` or `... RIGHT PART OF ORDER\n * BY` (sorting runs of rows that tie on the index)\n */\nfunction accessPlan(\n loops: ScanstatusLoop[],\n tableRows: number | undefined,\n): AccessPlan {\n const [first, ...rest] = loops.filter(loop => loop.parentId === 0);\n let sort: AccessPlan['sort'] = 'none';\n for (const {explain} of rest) {\n if (\n explain.startsWith('USE TEMP B-TREE FOR') &&\n explain.includes('ORDER BY')\n ) {\n sort = explain === 'USE TEMP B-TREE FOR ORDER BY' ? 'full' : 'partial';\n }\n }\n return {\n access: first?.explain.startsWith('SCAN ') ? 'scan' : 'search',\n sort,\n tableRows,\n };\n}\n\n/**\n * Gets scanstatus loop information from a prepared statement.\n * Iterates through all query elements and extracts loop statistics.\n *\n * Uses SQLITE_SCANSTAT_COMPLEX flag (1) to get all loops including sorting operations.\n *\n * @param stmt Prepared statement to get scanstatus from\n * @returns Array of loop information, or empty array if scanstatus unavailable\n */\nfunction getScanstatusLoops(stmt: Statement): ScanstatusLoop[] {\n const loops: ScanstatusLoop[] = [];\n\n // Iterate through query elements by incrementing idx until we get undefined\n // which indicates we've reached the end\n for (let idx = 0; ; idx++) {\n const selectId = stmt.scanStatus(\n idx,\n SQLite3Database.SQLITE_SCANSTAT_SELECTID,\n 1,\n );\n\n if (selectId === undefined) {\n break;\n }\n\n loops.push({\n selectId: must(selectId),\n parentId: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1),\n ),\n explain: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1),\n ),\n est: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EST, 1)),\n });\n }\n\n return loops.sort((a, b) => a.selectId - b.selectId);\n}\n\n/**\n * Estimates the cost of a query based on scanstats from sqlite3_stmt_scanstatus_v2\n */\nfunction estimateCost(\n scanstats: ScanstatusLoop[],\n fanout: CostModelCost['fanout'],\n): CostModelCost {\n // Sort by selectId to process in execution order\n const sorted = scanstats.toSorted((a, b) => a.selectId - b.selectId);\n\n let totalRows = 0;\n let totalCost = 0;\n\n // Identify if there are multiple top-level (parentId=0) operations\n // If so, the first is typically the scan, and subsequent ones are sorts\n const topLevelOps = sorted.filter(s => s.parentId === 0);\n\n // We only consider top level ops since ZQL queries are single-table when hitting SQLite.\n // We do have a nested op in the case of `WHERE x IN (:arg)` but it is negligible\n // assuming :arg is small.\n let firstLoop = true;\n for (const op of topLevelOps) {\n if (firstLoop) {\n // First top-level op is the main scan\n // and determines the total number of rows output.\n totalRows = op.est;\n firstLoop = false;\n } else {\n if (op.explain.includes('ORDER BY')) {\n totalCost += btreeCost(totalRows);\n }\n }\n }\n\n return {\n rows: totalRows,\n startupCost: totalCost,\n fanout,\n };\n}\n\nexport function btreeCost(rows: number): number {\n // B-Tree construction is ~O(n log n) so we estimate the cost as such.\n // We divide the cost by 10 because sorting in SQLite is ~10x faster\n // than bringing the data into JS and sorting there.\n return (rows * Math.log2(rows)) / 10;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-stat-fanout.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-stat-fanout.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AAEtC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;IAEpC;;;;;OAKG;IACH,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;CACvC;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,qBAAa,gBAAgB;;IAkB3B;;;;;;;;OAQG;IACH,YAAY,EAAE,EAAE,QAAQ,EAAE,aAAa,SAAI,EAyB1C;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,YAAY,CAgC5D;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;CAgQF"}
1
+ {"version":3,"file":"sqlite-stat-fanout.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-stat-fanout.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AAGtC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;IAEpC;;;;;OAKG;IACH,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;CACvC;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,qBAAa,gBAAgB;;IAkB3B;;;;;;;;OAQG;IACH,YAAY,EAAE,EAAE,QAAQ,EAAE,aAAa,SAAI,EAyB1C;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,YAAY,CAgC5D;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;CA0NF"}