@rocicorp/zero 1.11.0-canary.1 → 1.11.0-canary.2

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 (99) hide show
  1. package/out/replicache/src/btree/read.d.ts +12 -1
  2. package/out/replicache/src/btree/read.d.ts.map +1 -1
  3. package/out/replicache/src/btree/read.js +24 -12
  4. package/out/replicache/src/btree/read.js.map +1 -1
  5. package/out/replicache/src/db/write.js.map +1 -1
  6. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  7. package/out/replicache/src/replicache-impl.js +10 -0
  8. package/out/replicache/src/replicache-impl.js.map +1 -1
  9. package/out/replicache/src/transactions.d.ts.map +1 -1
  10. package/out/replicache/src/transactions.js +1 -1
  11. package/out/replicache/src/transactions.js.map +1 -1
  12. package/out/zero/package.js +1 -1
  13. package/out/zero/package.js.map +1 -1
  14. package/out/zero-cache/src/config/zero-config.d.ts +2 -0
  15. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  16. package/out/zero-cache/src/config/zero-config.js +12 -4
  17. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  18. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  19. package/out/zero-cache/src/server/replicator.js +1 -1
  20. package/out/zero-cache/src/server/replicator.js.map +1 -1
  21. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  22. package/out/zero-cache/src/services/change-source/common/replica-schema.js +2 -39
  23. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  24. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +2 -2
  25. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  26. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +2 -2
  27. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  28. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +6 -6
  29. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  30. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  31. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  32. package/out/zero-cache/src/services/litestream/commands.js +3 -1
  33. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  34. package/out/zero-cache/src/services/litestream/config-v5.yml +1 -4
  35. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
  36. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  37. package/out/zero-cache/src/services/replicator/incremental-sync.js +22 -5
  38. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  39. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  40. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts +4 -4
  41. package/out/zero-cache/src/services/replicator/write-worker-client.d.ts.map +1 -1
  42. package/out/zero-cache/src/services/replicator/write-worker-client.js +2 -2
  43. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  44. package/out/zero-cache/src/services/replicator/write-worker.js +10 -4
  45. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  46. package/out/zero-cache/src/types/streams.d.ts +6 -6
  47. package/out/zero-cache/src/types/streams.d.ts.map +1 -1
  48. package/out/zero-cache/src/types/streams.js +6 -11
  49. package/out/zero-cache/src/types/streams.js.map +1 -1
  50. package/out/zero-client/src/client/context.d.ts +15 -0
  51. package/out/zero-client/src/client/context.d.ts.map +1 -1
  52. package/out/zero-client/src/client/context.js +44 -0
  53. package/out/zero-client/src/client/context.js.map +1 -1
  54. package/out/zero-client/src/client/inspector/lazy-inspector.js +2 -2
  55. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  56. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  57. package/out/zero-client/src/client/version.js +1 -1
  58. package/out/zero-client/src/client/zero-rep.d.ts.map +1 -1
  59. package/out/zero-client/src/client/zero-rep.js +2 -1
  60. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  61. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  62. package/out/zero-client/src/client/zero.js +48 -11
  63. package/out/zero-client/src/client/zero.js.map +1 -1
  64. package/out/zero-protocol/src/down.d.ts +29 -29
  65. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  66. package/out/zero-protocol/src/poke.d.ts +6 -6
  67. package/out/zero-react/src/use-query.d.ts.map +1 -1
  68. package/out/zero-react/src/use-query.js +24 -5
  69. package/out/zero-react/src/use-query.js.map +1 -1
  70. package/out/zero-solid/src/use-query.js +4 -1
  71. package/out/zero-solid/src/use-query.js.map +1 -1
  72. package/out/zql/src/ivm/array-view.d.ts.map +1 -1
  73. package/out/zql/src/ivm/array-view.js +5 -3
  74. package/out/zql/src/ivm/array-view.js.map +1 -1
  75. package/out/zql/src/ivm/deferred-input.d.ts +44 -0
  76. package/out/zql/src/ivm/deferred-input.d.ts.map +1 -0
  77. package/out/zql/src/ivm/deferred-input.js +88 -0
  78. package/out/zql/src/ivm/deferred-input.js.map +1 -0
  79. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  80. package/out/zql/src/ivm/memory-source.js +5 -10
  81. package/out/zql/src/ivm/memory-source.js.map +1 -1
  82. package/out/zql/src/query/query-delegate-base.d.ts +6 -0
  83. package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
  84. package/out/zql/src/query/query-delegate-base.js +73 -10
  85. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  86. package/out/zql/src/query/query-delegate.d.ts +22 -0
  87. package/out/zql/src/query/query-delegate.d.ts.map +1 -1
  88. package/out/zql/src/query/query-impl.d.ts +12 -11
  89. package/out/zql/src/query/query-impl.d.ts.map +1 -1
  90. package/out/zql/src/query/query-impl.js +383 -60
  91. package/out/zql/src/query/query-impl.js.map +1 -1
  92. package/out/zql/src/query/query-transitions.d.ts +171 -0
  93. package/out/zql/src/query/query-transitions.d.ts.map +1 -0
  94. package/out/zql/src/query/query-transitions.js +207 -0
  95. package/out/zql/src/query/query-transitions.js.map +1 -0
  96. package/out/zql/src/query/runnable-query-impl.d.ts.map +1 -1
  97. package/out/zql/src/query/runnable-query-impl.js +21 -1
  98. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  99. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  import { assert } from "../../../shared/src/asserts.js";
2
+ import { deepEqual } from "../../../shared/src/json.js";
2
3
  import { SUBQ_PREFIX, insertRelated, normalizeAST, normalizeCondition, normalizedRelated, tableAST } from "../../../zero-protocol/src/ast.js";
3
4
  import { defaultFormat } from "../../../zero-types/src/format.js";
4
5
  import { hashOfQueryInternals } from "../../../zero-protocol/src/query-hash-visitor.js";
5
6
  import { NotImplementedError } from "../error.js";
6
7
  import { ExpressionBuilder, and, cmp, simplifyCondition } from "./expression.js";
7
8
  import { queryInternalsTag } from "./query-internals.js";
9
+ import { HashIndex, Transitions } from "./query-transitions.js";
8
10
  //#region ../zql/src/query/query-impl.ts
9
11
  function newQuery(schema, table) {
10
12
  return newQueryInternal(schema, table, tableAST(table), defaultFormat, "client");
@@ -12,9 +14,40 @@ function newQuery(schema, table) {
12
14
  function newQueryImpl(schema, tableName, ast, format, system) {
13
15
  return newQueryInternal(schema, tableName, normalizeAST(ast), format, system);
14
16
  }
17
+ /**
18
+ * Root queries are interned per schema so that two `createBuilder(schema)` calls
19
+ * hand out the same starting point, and therefore share the whole transition
20
+ * tree below it. Only cleared entries accumulate here, at most one per
21
+ * system/table pair, so no sweeping is needed.
22
+ */
23
+ var rootsBySchema = /* @__PURE__ */ new WeakMap();
24
+ /**
25
+ * Whether this is the AST `tableAST(tableName)` produces -- the whole table,
26
+ * nothing else. A normalized AST carries every field, so this cannot be a key
27
+ * count; each one has to be checked.
28
+ */
29
+ function isRootAST(ast, tableName) {
30
+ return ast.table === tableName && ast.schema === void 0 && ast.alias === void 0 && ast.where === void 0 && ast.related === void 0 && ast.start === void 0 && ast.limit === void 0 && ast.orderBy === void 0;
31
+ }
15
32
  function newQueryInternal(schema, tableName, ast, format, system) {
33
+ const interned = format === defaultFormat && isRootAST(ast, tableName);
34
+ const key = `${system}:${tableName}`;
35
+ let roots;
36
+ if (interned) {
37
+ roots = rootsBySchema.get(schema);
38
+ const existing = roots?.get(key)?.deref();
39
+ if (existing) return existing;
40
+ }
16
41
  const inner = (tableName, ast, format, customQueryID, currentJunction) => new QueryImpl(schema, tableName, ast, format, system, customQueryID, currentJunction, inner);
17
- return inner(tableName, ast, format, void 0, void 0);
42
+ const created = inner(tableName, ast, format, void 0, void 0);
43
+ if (interned) {
44
+ if (!roots) {
45
+ roots = /* @__PURE__ */ new Map();
46
+ rootsBySchema.set(schema, roots);
47
+ }
48
+ roots.set(key, new WeakRef(created));
49
+ }
50
+ return created;
18
51
  }
19
52
  /**
20
53
  * The AST of a QueryImpl is always normalized. Each step builds its AST in
@@ -36,6 +69,26 @@ var QueryImpl = class {
36
69
  #currentJunction;
37
70
  customQueryID;
38
71
  #newQuery;
72
+ /**
73
+ * The query this one was derived from, held *strongly*, and the queries
74
+ * derived from this one, held *weakly*. This is the arrangement V8 uses for
75
+ * hidden-class transitions, and it is what makes interning stable: as long as
76
+ * a query is reachable its whole ancestor spine is too, so re-deriving it from
77
+ * the root yields the same instance. See `query-transitions.ts`.
78
+ */
79
+ #parent;
80
+ #transitions;
81
+ /**
82
+ * The second level of interning, keyed by hash; see `HashIndex`. Set on the
83
+ * root only, which is what scopes it to one schema and one delegate.
84
+ */
85
+ #byHash;
86
+ /**
87
+ * Memoized because a query is interned: the same node serves every rebuild of
88
+ * a chain, so building this once rather than per `where(fn)` call takes the
89
+ * whole thing -- the builder and the bound `#exists` -- off the repeat path.
90
+ */
91
+ #expressionBuilder;
39
92
  constructor(schema, tableName, ast, format, system, customQueryID, currentJunction, newQuery) {
40
93
  this.#schema = schema;
41
94
  this.#tableName = tableName;
@@ -46,6 +99,44 @@ var QueryImpl = class {
46
99
  this.customQueryID = customQueryID;
47
100
  this.#newQuery = newQuery;
48
101
  }
102
+ /**
103
+ * Interns a transition whose key space is finite and schema-derived; see
104
+ * {@linkcode Transitions.storeBounded}. Used for the base sub-query handed to
105
+ * a `related`/`exists` callback, which is determined entirely by the
106
+ * relationship name.
107
+ */
108
+ #deriveBounded(key, build) {
109
+ const existing = this.#transitions?.lookupBounded(key);
110
+ if (existing !== void 0) return existing;
111
+ return this.#record(key, void 0, void 0, build(), true);
112
+ }
113
+ /**
114
+ * Interns the result of applying a builder operation to this query.
115
+ *
116
+ * `key` names the operation and `value` carries its varying argument, kept
117
+ * apart so the key stays a string V8 has already hashed. When the two do not
118
+ * settle it, `delta` carries the rest and is compared with `deepEqual`. Only
119
+ * the delta is compared, never the whole AST: this query is already canonical,
120
+ * so two of its children are equal exactly when the operations that produced
121
+ * them are.
122
+ *
123
+ * `T` is deliberately unconstrained so that the contextual return type at each
124
+ * call site still drives `#newQuery`'s inference, exactly as it did when the
125
+ * call sites returned `this.#newQuery(...)` directly.
126
+ */
127
+ #derive(key, value, delta, build) {
128
+ const existing = this.#transitions?.lookup(key, value, delta);
129
+ if (existing !== void 0) return existing;
130
+ return this.#record(key, value, delta, build(), false);
131
+ }
132
+ #record(key, value, delta, created, bounded) {
133
+ const q = created;
134
+ q.#parent = this;
135
+ const transitions = this.#transitions ??= new Transitions();
136
+ if (bounded) transitions.storeBounded(key, q);
137
+ else transitions.store(key, value, delta, q);
138
+ return created;
139
+ }
49
140
  run(_options) {
50
141
  throwQueryNotRunnable();
51
142
  }
@@ -56,43 +147,76 @@ var QueryImpl = class {
56
147
  throwQueryNotRunnable();
57
148
  }
58
149
  nameAndArgs(name, args) {
59
- return this.#newQuery(this.#tableName, this.#ast, this.format, {
150
+ return this.#derive("nameAndArgs", name, args, () => this.#newQuery(this.#tableName, this.#ast, this.format, {
60
151
  name,
61
152
  args
62
- }, this.#currentJunction);
153
+ }, this.#currentJunction));
63
154
  }
64
155
  hash() {
65
- if (!this.#hash) this.#hash = hashOfQueryInternals(this.#ast, this.format, this.#system, this.#currentJunction, this.customQueryID?.name, this.customQueryID?.args);
156
+ if (!this.#hash) {
157
+ this.#hash = hashOfQueryInternals(this.#ast, this.format, this.#system, this.#currentJunction, this.customQueryID?.name, this.customQueryID?.args);
158
+ this.#canonicalize();
159
+ }
66
160
  return this.#hash;
67
161
  }
68
- one = () => this.#newQuery(this.#tableName, {
69
- ...this.#ast,
70
- limit: 1
71
- }, {
72
- ...this.format,
73
- singular: true
74
- }, this.customQueryID, this.#currentJunction);
75
- whereExists = (relationship, cbOrOptions, options) => {
162
+ /**
163
+ * Folds this query into its root's hash index, now that its hash is known.
164
+ *
165
+ * If an equal query is already indexed, the transition that produced this
166
+ * one is re-pointed at it, so the next rebuild along this path yields that
167
+ * instance rather than a second copy. This query itself is left alone: its
168
+ * identity has already been handed out. Content is verified with `#sameAs`
169
+ * before anything is redirected, so a hash collision costs nothing worse
170
+ * than a missed unification.
171
+ */
172
+ #canonicalize() {
173
+ const parent = this.#parent;
174
+ if (parent === void 0) return;
175
+ let root = parent;
176
+ while (root.#parent !== void 0) root = root.#parent;
177
+ const index = root.#byHash ??= new HashIndex();
178
+ const existing = index.get(this.#hash);
179
+ if (existing === void 0) index.set(this.#hash, this);
180
+ else if (existing !== this && this.#sameAs(existing)) parent.#transitions?.replace(this, existing);
181
+ }
182
+ /**
183
+ * Whether `other` is interchangeable with this query: everything `hash()`
184
+ * covers, compared structurally. Schema and delegate are not compared. Both
185
+ * are fixed by the root, and this only ever compares queries under one root.
186
+ */
187
+ #sameAs(other) {
188
+ return this.#system === other.#system && this.#currentJunction === other.#currentJunction && this.customQueryID?.name === other.customQueryID?.name && deepEqual(this.customQueryID?.args, other.customQueryID?.args) && deepEqual(this.format, other.format) && deepEqual(this.#ast, other.#ast);
189
+ }
190
+ one() {
191
+ return this.#derive("one", void 0, void 0, () => this.#newQuery(this.#tableName, {
192
+ ...this.#ast,
193
+ limit: 1
194
+ }, {
195
+ ...this.format,
196
+ singular: true
197
+ }, this.customQueryID, this.#currentJunction));
198
+ }
199
+ whereExists(relationship, cbOrOptions, options) {
76
200
  const cb = typeof cbOrOptions === "function" ? cbOrOptions : void 0;
77
201
  const opts = typeof cbOrOptions === "function" ? options : cbOrOptions;
78
202
  return this.where(({ exists }) => exists(relationship, cb, opts));
79
- };
80
- related = (relationship, cb) => {
203
+ }
204
+ related(relationship, cb) {
81
205
  if (relationship.startsWith("zsubq_")) throw new Error(`Relationship names may not start with "${SUBQ_PREFIX}". That is a reserved prefix.`);
82
206
  cb = cb ?? ((q) => q);
83
207
  const related = this.#schema.relationships[this.#tableName][relationship];
84
208
  assert(related, "Invalid relationship");
85
209
  if (isOneHop(related)) {
86
210
  const { destSchema, destField, sourceField, cardinality } = related[0];
87
- const q = this.#newQuery(destSchema, tableAST(destSchema, relationship), {
211
+ const q = this.#deriveBounded(`relatedBase:${relationship}`, () => this.#newQuery(destSchema, tableAST(destSchema, relationship), {
88
212
  relationships: {},
89
213
  singular: cardinality === "one"
90
- }, this.customQueryID, void 0);
214
+ }, this.customQueryID, void 0));
91
215
  const subQuery = asQueryImpl(cb(q));
92
216
  assert(isCompoundKey(sourceField), "The source of a relationship must specify at last 1 field");
93
217
  assert(isCompoundKey(destField), "The destination of a relationship must specify at last 1 field");
94
218
  assert(sourceField.length === destField.length, "The source and destination of a relationship must have the same number of fields");
95
- return this.#newQuery(this.#tableName, {
219
+ return this.#derive(relatedKey(relationship), astID(subQuery.#ast), void 0, () => this.#newQuery(this.#tableName, {
96
220
  ...this.#ast,
97
221
  related: insertRelated(this.#ast.related, {
98
222
  correlation: {
@@ -108,34 +232,36 @@ var QueryImpl = class {
108
232
  ...this.format.relationships,
109
233
  [relationship]: subQuery.format
110
234
  }
111
- }, this.customQueryID, this.#currentJunction);
235
+ }, this.customQueryID, this.#currentJunction));
112
236
  }
113
237
  if (isTwoHop(related)) {
114
238
  const [firstRelation, secondRelation] = related;
115
239
  const { destSchema } = secondRelation;
116
240
  const junctionSchema = firstRelation.destSchema;
117
- const sq = asQueryImpl(cb(this.#newQuery(destSchema, tableAST(destSchema, relationship), {
241
+ const sq = asQueryImpl(cb(this.#deriveBounded(`relatedBase:${relationship}`, () => this.#newQuery(destSchema, tableAST(destSchema, relationship), {
118
242
  relationships: {},
119
243
  singular: secondRelation.cardinality === "one"
120
- }, this.customQueryID, relationship)));
121
- assert(isCompoundKey(firstRelation.sourceField), "Invalid relationship");
122
- assert(isCompoundKey(firstRelation.destField), "Invalid relationship");
123
- assert(isCompoundKey(secondRelation.sourceField), "Invalid relationship");
124
- assert(isCompoundKey(secondRelation.destField), "Invalid relationship");
125
- return this.#newQuery(this.#tableName, {
244
+ }, this.customQueryID, relationship))));
245
+ const { sourceField: firstSource, destField: firstDest } = firstRelation;
246
+ const { sourceField: secondSource, destField: secondDest } = secondRelation;
247
+ assert(isCompoundKey(firstSource), "Invalid relationship");
248
+ assert(isCompoundKey(firstDest), "Invalid relationship");
249
+ assert(isCompoundKey(secondSource), "Invalid relationship");
250
+ assert(isCompoundKey(secondDest), "Invalid relationship");
251
+ return this.#derive(relatedKey(relationship), astID(sq.#ast), void 0, () => this.#newQuery(this.#tableName, {
126
252
  ...this.#ast,
127
253
  related: insertRelated(this.#ast.related, {
128
254
  correlation: {
129
- parentField: firstRelation.sourceField,
130
- childField: firstRelation.destField
255
+ parentField: firstSource,
256
+ childField: firstDest
131
257
  },
132
258
  hidden: true,
133
259
  subquery: {
134
260
  ...tableAST(junctionSchema, relationship),
135
261
  related: [normalizedRelated({
136
262
  correlation: {
137
- parentField: secondRelation.sourceField,
138
- childField: secondRelation.destField
263
+ parentField: secondSource,
264
+ childField: secondDest
139
265
  },
140
266
  subquery: sq.#ast,
141
267
  system: this.#system
@@ -149,49 +275,76 @@ var QueryImpl = class {
149
275
  ...this.format.relationships,
150
276
  [relationship]: sq.format
151
277
  }
152
- }, this.customQueryID, this.#currentJunction);
278
+ }, this.customQueryID, this.#currentJunction));
153
279
  }
154
280
  throw new Error(`Invalid relationship ${relationship}`);
155
- };
156
- where = function(fieldOrExpressionFactory, opOrValue, value) {
281
+ }
282
+ where(fieldOrExpressionFactory, opOrValue, value) {
157
283
  let cond;
158
284
  if (typeof fieldOrExpressionFactory === "function") cond = fieldOrExpressionFactory(this.expressionBuilder());
159
285
  else {
160
286
  assert(arguments.length >= 2, "Invalid condition. Too few arguments.");
161
- if (arguments.length === 2) cond = cmp(fieldOrExpressionFactory, opOrValue);
162
- else cond = cmp(fieldOrExpressionFactory, opOrValue, value);
287
+ const twoArg = arguments.length === 2;
288
+ const op = twoArg ? "=" : opOrValue;
289
+ const raw = twoArg ? opOrValue : value;
290
+ if (raw === null || typeof raw !== "object") {
291
+ const hit = this.#transitions?.lookup(whereKey(fieldOrExpressionFactory, op), raw ?? null, void 0);
292
+ if (hit !== void 0) return hit;
293
+ }
294
+ cond = twoArg ? cmp(fieldOrExpressionFactory, opOrValue) : cmp(fieldOrExpressionFactory, opOrValue, value);
163
295
  }
164
- const existingWhere = this.#ast.where;
165
- if (existingWhere) cond = and(existingWhere, cond);
166
- return this.#newQuery(this.#tableName, {
167
- ...this.#ast,
168
- where: normalizeCondition(simplifyCondition(cond))
169
- }, this.format, this.customQueryID, this.#currentJunction);
170
- }.bind(this);
171
- start = (row, opts) => this.#newQuery(this.#tableName, {
172
- ...this.#ast,
173
- start: {
174
- row,
175
- exclusive: !opts?.inclusive
296
+ let key;
297
+ let tValue;
298
+ let delta = cond;
299
+ if (cond.type === "simple" && cond.left.type === "column") {
300
+ key = whereKey(cond.left.name, cond.op);
301
+ const right = cond.right;
302
+ tValue = right.type === "literal" && isPrimitive(right.value) ? right.value : rightTag(right);
303
+ delta = void 0;
304
+ } else {
305
+ const exact = condKey(cond);
306
+ if (exact === void 0) key = treeKey(cond);
307
+ else {
308
+ key = "where:tree";
309
+ tValue = exact;
310
+ delta = void 0;
311
+ }
176
312
  }
177
- }, this.format, this.customQueryID, this.#currentJunction);
178
- limit = (limit) => {
313
+ return this.#derive(key, tValue, delta, () => {
314
+ const existingWhere = this.#ast.where;
315
+ const merged = existingWhere ? and(existingWhere, cond) : cond;
316
+ return this.#newQuery(this.#tableName, {
317
+ ...this.#ast,
318
+ where: normalizeCondition(simplifyCondition(merged))
319
+ }, this.format, this.customQueryID, this.#currentJunction);
320
+ });
321
+ }
322
+ start(row, opts) {
323
+ return this.#derive(opts?.inclusive ? "start:inclusive" : "start:exclusive", valueTag(row), void 0, () => this.#newQuery(this.#tableName, {
324
+ ...this.#ast,
325
+ start: {
326
+ row,
327
+ exclusive: !opts?.inclusive
328
+ }
329
+ }, this.format, this.customQueryID, this.#currentJunction));
330
+ }
331
+ limit(limit) {
179
332
  if (limit < 0) throw new Error("Limit must be non-negative");
180
333
  if ((limit | 0) !== limit) throw new Error("Limit must be an integer");
181
334
  if (this.#currentJunction) throw new NotImplementedError("Limit is not supported in junction relationships yet. Junction relationship being limited: " + this.#currentJunction);
182
- return this.#newQuery(this.#tableName, {
335
+ return this.#derive("limit", limit, void 0, () => this.#newQuery(this.#tableName, {
183
336
  ...this.#ast,
184
337
  limit
185
- }, this.format, this.customQueryID, this.#currentJunction);
186
- };
187
- orderBy = (field, direction) => {
338
+ }, this.format, this.customQueryID, this.#currentJunction));
339
+ }
340
+ orderBy(field, direction) {
188
341
  if (this.#currentJunction) throw new NotImplementedError("Order by is not supported in junction relationships yet. Junction relationship being ordered: " + this.#currentJunction);
189
- return this.#newQuery(this.#tableName, {
342
+ return this.#derive(direction === "asc" ? "orderBy:asc" : "orderBy:desc", field, void 0, () => this.#newQuery(this.#tableName, {
190
343
  ...this.#ast,
191
344
  orderBy: [...this.#ast.orderBy ?? [], [field, direction]]
192
- }, this.format, this.customQueryID, this.#currentJunction);
193
- };
194
- #exists = (relationship, cb, options) => {
345
+ }, this.format, this.customQueryID, this.#currentJunction));
346
+ }
347
+ #exists(relationship, cb, options) {
195
348
  cb = cb ?? ((q) => q);
196
349
  const flip = options?.flip;
197
350
  const scalar = options?.scalar;
@@ -201,7 +354,8 @@ var QueryImpl = class {
201
354
  const { destSchema: destTableName, sourceField, destField } = related[0];
202
355
  assert(isCompoundKey(sourceField), "Invalid relationship");
203
356
  assert(isCompoundKey(destField), "Invalid relationship");
204
- const subQuery = asQueryImpl(cb(this.#newQuery(destTableName, tableAST(destTableName, `${SUBQ_PREFIX}${relationship}`), defaultFormat, this.customQueryID, void 0)));
357
+ const subQuery = asQueryImpl(cb(this.#deriveBounded(`existsBase:${relationship}`, () => this.#newQuery(destTableName, tableAST(destTableName, `${SUBQ_PREFIX}${relationship}`), defaultFormat, this.customQueryID, void 0))));
358
+ astID(subQuery.#ast);
205
359
  return {
206
360
  type: "correlatedSubquery",
207
361
  related: {
@@ -225,7 +379,7 @@ var QueryImpl = class {
225
379
  assert(isCompoundKey(secondRelation.destField), "Invalid relationship");
226
380
  const { destSchema } = secondRelation;
227
381
  const junctionSchema = firstRelation.destSchema;
228
- const queryToDest = cb(this.#newQuery(destSchema, tableAST(destSchema, `${SUBQ_PREFIX}zhidden_${relationship}`), defaultFormat, this.customQueryID, relationship));
382
+ const queryToDest = cb(this.#deriveBounded(`existsJunctionBase:${relationship}`, () => this.#newQuery(destSchema, tableAST(destSchema, `${SUBQ_PREFIX}zhidden_${relationship}`), defaultFormat, this.customQueryID, relationship)));
229
383
  return {
230
384
  type: "correlatedSubquery",
231
385
  related: {
@@ -257,18 +411,187 @@ var QueryImpl = class {
257
411
  };
258
412
  }
259
413
  throw new Error(`Invalid relationship ${relationship}`);
260
- };
414
+ }
261
415
  get ast() {
262
416
  return this.#ast;
263
417
  }
264
418
  expressionBuilder() {
265
- return new ExpressionBuilder(this.#exists);
419
+ return this.#expressionBuilder ??= new ExpressionBuilder(this.#exists.bind(this));
266
420
  }
267
421
  };
268
422
  function asQueryImpl(q) {
269
423
  assert(q instanceof QueryImpl, "Expected QueryImpl instance");
270
424
  return q;
271
425
  }
426
+ /**
427
+ * A stable id per query AST.
428
+ *
429
+ * Every `QueryImpl` allocates its own AST object, so these are one-to-one with
430
+ * queries, and interning makes the object identity stable across rebuilds. That
431
+ * lets a `related` or `exists` transition key on *which* sub-query it points at
432
+ * rather than comparing the sub-tree.
433
+ *
434
+ * Assigned only where it is needed. A `WeakMap.set` costs on the order of a
435
+ * hundred nanoseconds, so doing this for every query rather than for the few
436
+ * that are sub-queries was one of the largest single costs of building a query
437
+ * from cold. ASTs that never pass through here -- including the junction AST a
438
+ * two-hop `exists` builds inline -- are absent, and fall back to a delta.
439
+ */
440
+ var astIDs = /* @__PURE__ */ new WeakMap();
441
+ var nextASTID = 0;
442
+ function astID(ast) {
443
+ let id = astIDs.get(ast);
444
+ if (id === void 0) {
445
+ id = ++nextASTID;
446
+ astIDs.set(ast, id);
447
+ }
448
+ return id;
449
+ }
450
+ /**
451
+ * Memoized transition keys.
452
+ *
453
+ * A transition key must be a string V8 has already hashed, or the `Map` lookup
454
+ * has to hash it from scratch -- measured at ~148ns against ~10ns, which dwarfs
455
+ * everything else a lookup does. Both tables are keyed by schema-derived names,
456
+ * so they are bounded by the schema.
457
+ */
458
+ var whereKeys = /* @__PURE__ */ new Map();
459
+ function whereKey(column, op) {
460
+ let byOp = whereKeys.get(column);
461
+ if (byOp === void 0) {
462
+ byOp = /* @__PURE__ */ new Map();
463
+ whereKeys.set(column, byOp);
464
+ }
465
+ let key = byOp.get(op);
466
+ if (key === void 0) {
467
+ key = `where:${column}:${op}`;
468
+ byOp.set(op, key);
469
+ }
470
+ return key;
471
+ }
472
+ var relatedKeys = /* @__PURE__ */ new Map();
473
+ function relatedKey(relationship) {
474
+ let key = relatedKeys.get(relationship);
475
+ if (key === void 0) {
476
+ key = `related:${relationship}`;
477
+ relatedKeys.set(relationship, key);
478
+ }
479
+ return key;
480
+ }
481
+ function isPrimitive(v) {
482
+ return v === null || typeof v !== "object";
483
+ }
484
+ /**
485
+ * Folds a JSON value into a self-delimiting token: a string is
486
+ * length-prefixed, a number is terminated, an array or object is
487
+ * count-prefixed and encodes its members recursively, and the rest are fixed
488
+ * width. Tokens can therefore be concatenated into a key with no way for one
489
+ * to run into the next, and the leading tag keeps `'1'` and `1` apart.
490
+ *
491
+ * Not `JSON.stringify`: it writes `Infinity`, `NaN` and `null` all as `null`,
492
+ * and this token is trusted as exact, so a nested value that serialized like
493
+ * another's would hand back a query with the wrong value in its AST. Numbers
494
+ * are written the way `String` writes them, which keeps those apart. An
495
+ * `undefined` property is skipped, as `deepEqual` would skip it; property
496
+ * order is part of the encoding, so an object spelled in another order
497
+ * interns separately.
498
+ */
499
+ function valueTag(v) {
500
+ switch (typeof v) {
501
+ case "string": return `:s${enc(v)}`;
502
+ case "number": return `:n${v};`;
503
+ case "boolean": return v ? ":t" : ":f";
504
+ default: {
505
+ if (v === null) return ":z";
506
+ if (Array.isArray(v)) {
507
+ let out = `:a${v.length}:`;
508
+ for (const e of v) out += valueTag(e);
509
+ return out;
510
+ }
511
+ const o = v;
512
+ let body = "";
513
+ let n = 0;
514
+ for (const key in o) {
515
+ const e = o[key];
516
+ if (e !== void 0 && Object.hasOwn(o, key)) {
517
+ body += enc(key) + valueTag(e);
518
+ n++;
519
+ }
520
+ }
521
+ return `:o${n}:${body}`;
522
+ }
523
+ }
524
+ }
525
+ /**
526
+ * Folds the right-hand side of a simple condition into a transition value, so
527
+ * that the transition is exact and the lookup needs no comparison at all.
528
+ *
529
+ * Arrays (`IN`) and parameter references are encoded in full: that is one
530
+ * pass over a small value, where leaving them in the delta would instead cost
531
+ * a `deepEqual` over the whole condition for every sibling a lookup walks
532
+ * past.
533
+ */
534
+ function rightTag(right) {
535
+ if (right.type !== "literal") return `:p${valueTag(right)}`;
536
+ return valueTag(right.value);
537
+ }
538
+ /**
539
+ * Length-prefixes a free-form string so it can be concatenated into a key
540
+ * without a separator being ambiguous with the content.
541
+ */
542
+ function enc(s) {
543
+ return `${s.length}:${s}`;
544
+ }
545
+ /** Encodes an optional boolean without collapsing `undefined` into `false`. */
546
+ function tri(b) {
547
+ return b === void 0 ? "-" : b ? "t" : "f";
548
+ }
549
+ /**
550
+ * An exact key for a whole condition tree, or `undefined` for a shape we cannot
551
+ * encode (which falls back to {@linkcode treeKey} plus a delta).
552
+ *
553
+ * Everything variable-length is length-prefixed, so distinct trees cannot
554
+ * produce the same string and the key needs no `deepEqual` to back it up. That
555
+ * matters because the alternative -- one coarse bucket per shape -- turns N
556
+ * sibling conditions off the same parent into an N-deep scan of tree
557
+ * comparisons, which measured far slower than not interning at all.
558
+ */
559
+ function condKey(cond) {
560
+ switch (cond.type) {
561
+ case "simple":
562
+ if (cond.left.type !== "column") return;
563
+ return `S${enc(cond.left.name)}${enc(cond.op)}${rightTag(cond.right)}`;
564
+ case "correlatedSubquery": {
565
+ const id = astIDs.get(cond.related.subquery);
566
+ return id === void 0 ? void 0 : `E${id}:${enc(cond.op)}${tri(cond.flip)}${tri(cond.scalar)}`;
567
+ }
568
+ case "and":
569
+ case "or": {
570
+ let out = `${cond.type === "and" ? "A" : "O"}${cond.conditions.length}:`;
571
+ for (const c of cond.conditions) {
572
+ const k = condKey(c);
573
+ if (k === void 0) return;
574
+ out += k;
575
+ }
576
+ return out;
577
+ }
578
+ default: return;
579
+ }
580
+ }
581
+ /**
582
+ * A coarse bucket key for the conditions {@linkcode condKey} cannot encode --
583
+ * a two-hop `exists`, whose junction AST is built inline and so has no identity
584
+ * of its own. Reads only the top of the tree, so it is cheap; the delta tells
585
+ * apart what shares it.
586
+ */
587
+ function treeKey(cond) {
588
+ switch (cond.type) {
589
+ case "correlatedSubquery": return `where:e:${cond.related.subquery.alias}:${cond.op}`;
590
+ case "and":
591
+ case "or": return `where:${cond.type}:${cond.conditions.length}`;
592
+ default: return "where:*";
593
+ }
594
+ }
272
595
  function throwQueryNotRunnable() {
273
596
  throw new Error("Query is not runnable");
274
597
  }