@dxos/echo-pipeline 0.8.3 → 0.8.4-main.05e74ebcff

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 (244) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/chunk-46DCRDA7.mjs +271 -0
  4. package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
  5. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-LR7UFVQX.mjs} +313 -709
  6. package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
  7. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  8. package/dist/lib/neutral/index.mjs +7332 -0
  9. package/dist/lib/neutral/index.mjs.map +7 -0
  10. package/dist/lib/neutral/meta.json +1 -0
  11. package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
  12. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  13. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  14. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  15. package/dist/types/src/automerge/automerge-host.d.ts +119 -54
  16. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  17. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  18. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  19. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  20. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  21. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  22. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  23. package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
  24. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  25. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
  27. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  28. package/dist/types/src/automerge/echo-replicator.d.ts +46 -15
  29. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  30. package/dist/types/src/automerge/errors.d.ts +35 -0
  31. package/dist/types/src/automerge/errors.d.ts.map +1 -0
  32. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  33. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  34. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  35. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  36. package/dist/types/src/automerge/index.d.ts +3 -1
  37. package/dist/types/src/automerge/index.d.ts.map +1 -1
  38. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +11 -5
  39. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  40. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  41. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  42. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  43. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  44. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  45. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  46. package/dist/types/src/common/codec.d.ts +1 -1
  47. package/dist/types/src/common/codec.d.ts.map +1 -1
  48. package/dist/types/src/common/feeds.d.ts.map +1 -1
  49. package/dist/types/src/common/space-id.d.ts.map +1 -1
  50. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  51. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  52. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  53. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  54. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  55. package/dist/types/src/db-host/data-service.d.ts +13 -7
  56. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  57. package/dist/types/src/db-host/database-root.d.ts +9 -3
  58. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  59. package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
  60. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  61. package/dist/types/src/db-host/echo-host.d.ts +74 -28
  62. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  63. package/dist/types/src/db-host/index.d.ts +2 -0
  64. package/dist/types/src/db-host/index.d.ts.map +1 -1
  65. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  66. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  67. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  68. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  69. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  70. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  71. package/dist/types/src/db-host/query-service.d.ts +19 -11
  72. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  73. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  74. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  75. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  76. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  77. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  78. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  79. package/dist/types/src/db-host/stub.d.ts +11 -0
  80. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  81. package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
  82. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  83. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  84. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  85. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  86. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  87. package/dist/types/src/edge/index.d.ts +1 -0
  88. package/dist/types/src/edge/index.d.ts.map +1 -1
  89. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  90. package/dist/types/src/filter/filter-match.d.ts +4 -1
  91. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  92. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  93. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  94. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  95. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  96. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  97. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  98. package/dist/types/src/query/errors.d.ts +22 -10
  99. package/dist/types/src/query/errors.d.ts.map +1 -1
  100. package/dist/types/src/query/plan.d.ts +66 -8
  101. package/dist/types/src/query/plan.d.ts.map +1 -1
  102. package/dist/types/src/query/query-executor.d.ts +66 -11
  103. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  104. package/dist/types/src/query/query-planner.d.ts +12 -1
  105. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  106. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  107. package/dist/types/src/space/auth.d.ts +3 -3
  108. package/dist/types/src/space/auth.d.ts.map +1 -1
  109. package/dist/types/src/space/control-pipeline.d.ts +5 -4
  110. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  111. package/dist/types/src/space/space-manager.d.ts +8 -8
  112. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  113. package/dist/types/src/space/space-protocol.d.ts +10 -9
  114. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  115. package/dist/types/src/space/space.d.ts +6 -6
  116. package/dist/types/src/space/space.d.ts.map +1 -1
  117. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  118. package/dist/types/src/testing/index.d.ts +0 -2
  119. package/dist/types/src/testing/index.d.ts.map +1 -1
  120. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  121. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  122. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  123. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  124. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  125. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  126. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  127. package/dist/types/src/util.d.ts +1 -1
  128. package/dist/types/src/util.d.ts.map +1 -1
  129. package/dist/types/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +59 -55
  131. package/src/automerge/automerge-host-subduction.test.ts +263 -0
  132. package/src/automerge/automerge-host.test.ts +162 -39
  133. package/src/automerge/automerge-host.ts +679 -201
  134. package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
  135. package/src/automerge/automerge-repo.test.ts +349 -91
  136. package/src/automerge/automerge-subduction.test.ts +330 -0
  137. package/src/automerge/collection-synchronizer.test.ts +98 -17
  138. package/src/automerge/collection-synchronizer.ts +141 -37
  139. package/src/automerge/echo-data-monitor.ts +1 -1
  140. package/src/automerge/echo-network-adapter.test.ts +5 -4
  141. package/src/automerge/echo-network-adapter.ts +76 -31
  142. package/src/automerge/echo-replicator.ts +50 -15
  143. package/src/automerge/errors.ts +10 -0
  144. package/src/automerge/handle-state.ts +62 -0
  145. package/src/automerge/heads-store.ts +42 -3
  146. package/src/automerge/index.ts +3 -1
  147. package/src/automerge/leveldb-storage-adapter.ts +77 -11
  148. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  149. package/src/automerge/mesh-echo-replicator.ts +15 -11
  150. package/src/automerge/storage-adapter.test.ts +1 -1
  151. package/src/common/space-id.ts +1 -1
  152. package/src/db-host/automerge-data-source.test.ts +299 -0
  153. package/src/db-host/automerge-data-source.ts +187 -0
  154. package/src/db-host/data-service.ts +35 -26
  155. package/src/db-host/database-root.ts +20 -8
  156. package/src/db-host/documents-synchronizer.test.ts +145 -14
  157. package/src/db-host/documents-synchronizer.ts +163 -51
  158. package/src/db-host/echo-host.ts +384 -99
  159. package/src/db-host/index.ts +2 -0
  160. package/src/db-host/invalidation-hint.ts +50 -0
  161. package/src/db-host/local-queue-service.ts +127 -0
  162. package/src/db-host/query-invalidation.test.ts +270 -0
  163. package/src/db-host/query-service.ts +111 -106
  164. package/src/db-host/queue-data-source.ts +160 -0
  165. package/src/db-host/queue-service.test.ts +145 -0
  166. package/src/db-host/space-state-manager.ts +12 -10
  167. package/src/db-host/stub.ts +33 -0
  168. package/src/edge/echo-edge-replicator.test.ts +12 -10
  169. package/src/edge/echo-edge-replicator.ts +144 -58
  170. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  171. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  172. package/src/edge/index.ts +1 -0
  173. package/src/filter/filter-match.test.ts +68 -32
  174. package/src/filter/filter-match.ts +230 -21
  175. package/src/metadata/metadata-store.ts +11 -8
  176. package/src/pipeline/pipeline-stress.test.ts +1 -1
  177. package/src/pipeline/pipeline.test.ts +2 -2
  178. package/src/pipeline/pipeline.ts +9 -7
  179. package/src/query/errors.ts +3 -1
  180. package/src/query/plan.ts +84 -9
  181. package/src/query/query-executor.ts +1197 -165
  182. package/src/query/query-planner.test.ts +993 -81
  183. package/src/query/query-planner.ts +503 -27
  184. package/src/space/admission-discovery-extension.ts +3 -3
  185. package/src/space/auth.ts +6 -6
  186. package/src/space/control-pipeline.test.ts +5 -4
  187. package/src/space/control-pipeline.ts +16 -13
  188. package/src/space/space-manager.browser.test.ts +1 -1
  189. package/src/space/space-manager.ts +16 -20
  190. package/src/space/space-protocol.browser.test.ts +15 -14
  191. package/src/space/space-protocol.test.ts +15 -14
  192. package/src/space/space-protocol.ts +22 -18
  193. package/src/space/space.test.ts +3 -3
  194. package/src/space/space.ts +11 -11
  195. package/src/testing/index.ts +0 -2
  196. package/src/testing/test-agent-builder.ts +3 -3
  197. package/src/testing/test-network-adapter.ts +24 -3
  198. package/src/testing/test-replicator.ts +19 -20
  199. package/src/util.ts +7 -3
  200. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  201. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  202. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  203. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  204. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  205. package/dist/lib/browser/index.mjs +0 -4743
  206. package/dist/lib/browser/index.mjs.map +0 -7
  207. package/dist/lib/browser/meta.json +0 -1
  208. package/dist/lib/browser/testing/index.mjs.map +0 -7
  209. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  210. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  211. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  212. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  213. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  214. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  215. package/dist/lib/node/filter/index.cjs +0 -32
  216. package/dist/lib/node/filter/index.cjs.map +0 -7
  217. package/dist/lib/node/index.cjs +0 -4699
  218. package/dist/lib/node/index.cjs.map +0 -7
  219. package/dist/lib/node/meta.json +0 -1
  220. package/dist/lib/node/testing/index.cjs +0 -753
  221. package/dist/lib/node/testing/index.cjs.map +0 -7
  222. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  223. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  224. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  225. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  226. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  227. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  228. package/dist/lib/node-esm/filter/index.mjs +0 -11
  229. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  230. package/dist/lib/node-esm/index.mjs +0 -4743
  231. package/dist/lib/node-esm/index.mjs.map +0 -7
  232. package/dist/lib/node-esm/meta.json +0 -1
  233. package/dist/lib/node-esm/testing/index.mjs +0 -742
  234. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  235. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  236. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  237. package/dist/types/src/testing/test-data.d.ts +0 -18
  238. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  239. package/dist/types/src/testing/test-schema.d.ts +0 -37
  240. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  241. package/src/db-host/documents-iterator.ts +0 -74
  242. package/src/testing/test-data.ts +0 -127
  243. package/src/testing/test-schema.ts +0 -53
  244. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -2,13 +2,31 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type QueryAST } from '@dxos/echo-protocol';
5
+ import { Order, Query } from '@dxos/echo';
6
+ import { QueryAST } from '@dxos/echo-protocol';
6
7
  import { invariant } from '@dxos/invariant';
7
- import type { DXN, SpaceId } from '@dxos/keys';
8
+ import type { DXN } from '@dxos/keys';
8
9
 
9
10
  import { QueryError } from './errors';
10
11
  import { QueryPlan } from './plan';
11
12
 
13
+ /**
14
+ * Creates a QueryError with "Query too complex" message and includes the prettified query in the context.
15
+ */
16
+ const queryTooComplexError = (query: QueryAST.Query | null): QueryError => {
17
+ if (query === null) {
18
+ return new QueryError({
19
+ message: 'Query too complex',
20
+ context: { query: null },
21
+ });
22
+ }
23
+ const prettyQuery = Query.pretty(Query.fromAst(query));
24
+ return new QueryError({
25
+ message: `Query too complex: ${prettyQuery}`,
26
+ context: { query, prettyQuery },
27
+ });
28
+ };
29
+
12
30
  export type QueryPlannerOptions = {
13
31
  defaultTextSearchKind: QueryPlan.TextSearchKind;
14
32
  };
@@ -32,9 +50,12 @@ export class QueryPlanner {
32
50
  }
33
51
 
34
52
  createPlan(query: QueryAST.Query): QueryPlan.Plan {
53
+ this._validateQueryScoped(query);
35
54
  let plan = this._generate(query, { ...DEFAULT_CONTEXT, originalQuery: query });
36
55
  plan = this._optimizeEmptyFilters(plan);
37
56
  plan = this._optimizeSoloUnions(plan);
57
+ plan = this._ensureOrderStep(plan);
58
+ plan = this._optimizeLimits(plan);
38
59
  return plan;
39
60
  }
40
61
 
@@ -54,12 +75,21 @@ export class QueryPlanner {
54
75
  return this._generateRelationTraversalClause(query, context);
55
76
  case 'reference-traversal':
56
77
  return this._generateReferenceTraversalClause(query, context);
78
+ case 'hierarchy-traversal':
79
+ return this._generateHierarchyTraversalClause(query, context);
57
80
  case 'union':
58
81
  return this._generateUnionClause(query, context);
59
82
  case 'set-difference':
60
83
  return this._generateSetDifferenceClause(query, context);
84
+ case 'order':
85
+ return this._generateOrderClause(query, context);
86
+ case 'limit':
87
+ return this._generateLimitClause(query, context);
88
+ case 'from':
89
+ return this._generateFromClause(query, context);
61
90
  default:
62
- throw new QueryError(`Unsupported query type: ${(query as any).type}`, {
91
+ throw new QueryError({
92
+ message: `Unsupported query type: ${(query as any).type}`,
63
93
  context: { query: context.originalQuery },
64
94
  });
65
95
  }
@@ -69,15 +99,25 @@ export class QueryPlanner {
69
99
  const newContext = {
70
100
  ...context,
71
101
  };
72
- if (query.options.spaceIds) {
73
- newContext.selectionSpaces = query.options.spaceIds as readonly SpaceId[];
74
- }
75
102
  if (query.options.deleted) {
76
103
  newContext.deletedHandling = query.options.deleted;
77
104
  }
78
105
  return this._generate(query.query, newContext);
79
106
  }
80
107
 
108
+ private _generateFromClause(query: QueryAST.QueryFromClause, context: GenerationContext): QueryPlan.Plan {
109
+ if (query.from._tag === 'scope') {
110
+ return this._generate(query.query, { ...context, scope: query.from.scope });
111
+ }
112
+
113
+ // Subquery from: flatten by rewriting the outer query's leaf select into a filter on the subquery.
114
+ const subquery = query.from.query;
115
+ const rewritten = QueryAST.map(query.query, (node) =>
116
+ node.type === 'select' ? { type: 'filter', selection: subquery, filter: node.filter } : node,
117
+ );
118
+ return this._generate(rewritten, context);
119
+ }
120
+
81
121
  private _generateSelectClause(query: QueryAST.QuerySelectClause, context: GenerationContext): QueryPlan.Plan {
82
122
  return this._generateSelectionFromFilter(query.filter, context);
83
123
  }
@@ -86,6 +126,7 @@ export class QueryPlanner {
86
126
  // TODO(dmaretskyi): If the tip of the query ast is a [select, ...filter] shape we can reorder the filters so the query is most efficient.
87
127
  private _generateSelectionFromFilter(filter: QueryAST.Filter, context: GenerationContext): QueryPlan.Plan {
88
128
  switch (filter.type) {
129
+ // Props
89
130
  case 'object': {
90
131
  if (
91
132
  context.selectionInverted &&
@@ -102,7 +143,7 @@ export class QueryPlanner {
102
143
  ]);
103
144
  }
104
145
  if (context.selectionInverted) {
105
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
146
+ throw queryTooComplexError(context.originalQuery);
106
147
  }
107
148
 
108
149
  // Try to utilize indexes during selection, prioritizing selecting by id, then by typename.
@@ -111,7 +152,7 @@ export class QueryPlanner {
111
152
  return QueryPlan.Plan.make([
112
153
  {
113
154
  _tag: 'SelectStep',
114
- spaces: context.selectionSpaces,
155
+ scope: context.scope,
115
156
  selector: {
116
157
  _tag: 'IdSelector',
117
158
  objectIds: filter.id,
@@ -127,7 +168,7 @@ export class QueryPlanner {
127
168
  return QueryPlan.Plan.make([
128
169
  {
129
170
  _tag: 'SelectStep',
130
- spaces: context.selectionSpaces,
171
+ scope: context.scope,
131
172
  selector: {
132
173
  _tag: 'TypeSelector',
133
174
  typename: [filter.typename as DXN.String],
@@ -135,16 +176,17 @@ export class QueryPlanner {
135
176
  },
136
177
  },
137
178
  ...this._generateDeletedHandlingSteps(context),
179
+ // TODO(dmaretskyi): Normally we could skip filtering by typename here, but since the index does not separate schema versions, we need to do additional filter to only select the correct version.
138
180
  {
139
181
  _tag: 'FilterStep',
140
- filter: { ...filter, typename: null },
182
+ filter: { ...filter },
141
183
  },
142
184
  ]);
143
185
  } else {
144
186
  return QueryPlan.Plan.make([
145
187
  {
146
188
  _tag: 'SelectStep',
147
- spaces: context.selectionSpaces,
189
+ scope: context.scope,
148
190
  selector: {
149
191
  _tag: 'WildcardSelector',
150
192
  },
@@ -157,44 +199,223 @@ export class QueryPlanner {
157
199
  ]);
158
200
  }
159
201
  }
202
+
203
+ // Tag
204
+ case 'tag': {
205
+ return QueryPlan.Plan.make([
206
+ {
207
+ _tag: 'SelectStep',
208
+ scope: context.scope,
209
+ selector: {
210
+ _tag: 'WildcardSelector',
211
+ },
212
+ },
213
+ ...this._generateDeletedHandlingSteps(context),
214
+ {
215
+ _tag: 'FilterStep',
216
+ filter: { ...filter },
217
+ },
218
+ ]);
219
+ }
220
+
221
+ // Text
160
222
  case 'text-search': {
161
223
  return QueryPlan.Plan.make([
162
224
  {
163
225
  _tag: 'SelectStep',
164
- spaces: context.selectionSpaces,
226
+ scope: context.scope,
165
227
  selector: {
166
228
  _tag: 'TextSelector',
167
229
  text: filter.text,
168
230
  searchKind: filter.searchKind ?? this._options.defaultTextSearchKind,
231
+ typename: null,
169
232
  },
170
233
  },
171
234
  ...this._generateDeletedHandlingSteps(context),
172
235
  ]);
173
236
  }
237
+
238
+ // Timestamp
239
+ case 'timestamp': {
240
+ if (context.selectionInverted) {
241
+ throw new QueryError({
242
+ message:
243
+ 'Negated timestamp filters are not supported. Use Filter.updated/Filter.created with the inverted range instead.',
244
+ context: { query: context.originalQuery },
245
+ });
246
+ }
247
+ return QueryPlan.Plan.make([
248
+ {
249
+ _tag: 'SelectStep',
250
+ scope: context.scope,
251
+ selector: _timestampFilterToSelector(filter),
252
+ },
253
+ ...this._generateDeletedHandlingSteps(context),
254
+ ]);
255
+ }
256
+
257
+ // ChildOf
258
+ case 'child-of': {
259
+ return QueryPlan.Plan.make([
260
+ {
261
+ _tag: 'SelectStep',
262
+ scope: context.scope,
263
+ selector: {
264
+ _tag: 'WildcardSelector',
265
+ },
266
+ },
267
+ ...this._generateDeletedHandlingSteps(context),
268
+ {
269
+ _tag: 'FilterStep',
270
+ filter: { ...filter },
271
+ },
272
+ ]);
273
+ }
274
+
275
+ // Compare
174
276
  case 'compare':
175
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
277
+ throw queryTooComplexError(context.originalQuery);
176
278
  case 'in':
177
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
279
+ throw queryTooComplexError(context.originalQuery);
178
280
  case 'range':
179
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
281
+ throw queryTooComplexError(context.originalQuery);
282
+
283
+ // Boolean
180
284
  case 'not':
181
285
  return this._generateSelectionFromFilter(filter.filter, {
182
286
  ...context,
183
287
  selectionInverted: !context.selectionInverted,
184
288
  });
185
- case 'and':
186
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
289
+ case 'and': {
290
+ const flatFilters = _flattenAnd(filter.filters);
291
+ const timestampFilters = flatFilters.filter((f): f is QueryAST.FilterTimestamp => f.type === 'timestamp');
292
+ const childOfFilters = flatFilters.filter((f): f is QueryAST.FilterChildOf => f.type === 'child-of');
293
+ const otherFilters = flatFilters.filter((f) => f.type !== 'timestamp' && f.type !== 'child-of');
294
+
295
+ if (timestampFilters.length > 0 && context.selectionInverted) {
296
+ throw new QueryError({
297
+ message:
298
+ 'Negated timestamp filters are not supported. Use Filter.updated/Filter.created with the inverted range instead.',
299
+ context: { query: context.originalQuery },
300
+ });
301
+ }
302
+
303
+ if (timestampFilters.length > 0 && otherFilters.length <= 1 && childOfFilters.length === 0) {
304
+ const innerFilter = otherFilters[0];
305
+ const innerPlan = innerFilter
306
+ ? this._generateSelectionFromFilter(innerFilter, context)
307
+ : QueryPlan.Plan.make([
308
+ {
309
+ _tag: 'SelectStep',
310
+ scope: context.scope,
311
+ selector: { _tag: 'WildcardSelector' },
312
+ },
313
+ ...this._generateDeletedHandlingSteps(context),
314
+ ]);
315
+
316
+ const selectIdx = innerPlan.steps.findIndex((s) => s._tag === 'SelectStep');
317
+ if (selectIdx !== -1) {
318
+ const newSteps = [...innerPlan.steps];
319
+ newSteps.splice(selectIdx + 1, 0, {
320
+ _tag: 'FilterStep',
321
+ filter: { type: 'and', filters: timestampFilters },
322
+ });
323
+ return QueryPlan.Plan.make(newSteps);
324
+ }
325
+
326
+ const selector = _mergeTimestampSelectors(timestampFilters.map(_timestampFilterToSelector));
327
+ return QueryPlan.Plan.make([
328
+ {
329
+ _tag: 'SelectStep',
330
+ scope: context.scope,
331
+ selector,
332
+ },
333
+ ...this._generateDeletedHandlingSteps(context),
334
+ ]);
335
+ }
336
+
337
+ if (timestampFilters.length > 0 && childOfFilters.length === 0) {
338
+ throw new QueryError({
339
+ message:
340
+ 'Timestamp filters can only be combined with a single type or property filter via AND. Split complex filters into a subquery.',
341
+ context: { query: context.originalQuery },
342
+ });
343
+ }
344
+
345
+ if (childOfFilters.length > 0) {
346
+ const remainingFilters = flatFilters.filter((f) => f.type !== 'child-of');
347
+ const innerPlan =
348
+ remainingFilters.length === 1
349
+ ? this._generateSelectionFromFilter(remainingFilters[0], context)
350
+ : remainingFilters.length > 1
351
+ ? this._generateSelectionFromFilter({ type: 'and', filters: remainingFilters }, context)
352
+ : QueryPlan.Plan.make([
353
+ {
354
+ _tag: 'SelectStep',
355
+ scope: context.scope,
356
+ selector: { _tag: 'WildcardSelector' },
357
+ },
358
+ ...this._generateDeletedHandlingSteps(context),
359
+ ]);
360
+
361
+ const childOfSteps: QueryPlan.Step[] = childOfFilters.map((f) => ({
362
+ _tag: 'FilterStep' as const,
363
+ filter: f,
364
+ }));
365
+
366
+ return QueryPlan.Plan.make([...innerPlan.steps, ...childOfSteps]);
367
+ }
368
+
369
+ // Simple AND: all filters are root-executable against in-memory objects after a wildcard select.
370
+ // Supports combining true root selectors (`object`, `tag`) plus negations / unions of those.
371
+ // Property-level filters (`compare`, `in`, `range`, `contains`) are intentionally excluded because
372
+ // `filterMatchObject` only handles them as nested predicates inside an `object` filter, not at the root.
373
+ const isRootExecutable = (filter: QueryAST.Filter): boolean => {
374
+ switch (filter.type) {
375
+ case 'object':
376
+ case 'tag':
377
+ return true;
378
+ case 'not':
379
+ return isRootExecutable(filter.filter);
380
+ case 'or':
381
+ return filter.filters.every(isRootExecutable);
382
+ default:
383
+ return false;
384
+ }
385
+ };
386
+ if (flatFilters.every(isRootExecutable)) {
387
+ const innerFilter: QueryAST.Filter = { type: 'and', filters: flatFilters };
388
+ const plannedFilter: QueryAST.Filter = context.selectionInverted
389
+ ? { type: 'not', filter: innerFilter }
390
+ : innerFilter;
391
+ return QueryPlan.Plan.make([
392
+ {
393
+ _tag: 'SelectStep',
394
+ scope: context.scope,
395
+ selector: { _tag: 'WildcardSelector' },
396
+ },
397
+ ...this._generateDeletedHandlingSteps(context),
398
+ {
399
+ _tag: 'FilterStep',
400
+ filter: plannedFilter,
401
+ },
402
+ ]);
403
+ }
404
+
405
+ throw queryTooComplexError(context.originalQuery);
406
+ }
187
407
  case 'or':
188
408
  // Optimized case
189
409
  if (filter.filters.every(isTrivialTypenameFilter)) {
190
- const typenames = filter.filters.map((f) => {
191
- invariant(f.type === 'object' && f.typename !== null);
192
- return f.typename;
410
+ const typenames = filter.filters.map((filter) => {
411
+ invariant(filter.type === 'object' && filter.typename !== null);
412
+ return filter.typename;
193
413
  });
414
+
194
415
  return QueryPlan.Plan.make([
195
416
  {
196
417
  _tag: 'SelectStep',
197
- spaces: context.selectionSpaces,
418
+ scope: context.scope,
198
419
  selector: {
199
420
  _tag: 'TypeSelector',
200
421
  typename: typenames as DXN.String[],
@@ -202,13 +423,18 @@ export class QueryPlanner {
202
423
  },
203
424
  },
204
425
  ...this._generateDeletedHandlingSteps(context),
426
+ {
427
+ _tag: 'FilterStep',
428
+ filter: { ...filter },
429
+ },
205
430
  ]);
206
431
  } else {
207
- throw new QueryError('Query too complex', { context: { query: context.originalQuery } });
432
+ throw queryTooComplexError(context.originalQuery);
208
433
  }
209
434
 
210
435
  default:
211
- throw new QueryError(`Unsupported filter type: ${(filter as any).type}`, {
436
+ throw new QueryError({
437
+ message: `Unsupported filter type: ${(filter as any).type}`,
212
438
  context: { query: context.originalQuery },
213
439
  });
214
440
  }
@@ -286,7 +512,7 @@ export class QueryPlanner {
286
512
  traversal: {
287
513
  _tag: 'ReferenceTraversal',
288
514
  direction: 'incoming',
289
- property: query.property,
515
+ property: query.property ?? null,
290
516
  },
291
517
  },
292
518
  ...this._generateDeletedHandlingSteps(context),
@@ -392,6 +618,38 @@ export class QueryPlanner {
392
618
  ]);
393
619
  }
394
620
 
621
+ private _generateHierarchyTraversalClause(
622
+ query: QueryAST.QueryHierarchyTraversalClause,
623
+ context: GenerationContext,
624
+ ): QueryPlan.Plan {
625
+ return QueryPlan.Plan.make([
626
+ ...this._generate(query.anchor, context).steps,
627
+ {
628
+ _tag: 'TraverseStep',
629
+ traversal: {
630
+ _tag: 'HierarchyTraversal',
631
+ direction: query.direction,
632
+ },
633
+ },
634
+ ...this._generateDeletedHandlingSteps(context),
635
+ ]);
636
+ }
637
+
638
+ private _validateQueryScoped(query: QueryAST.Query): void {
639
+ let hasScope = false;
640
+ QueryAST.visit(query, (node) => {
641
+ if (node.type === 'from') {
642
+ hasScope = true;
643
+ }
644
+ });
645
+ if (!hasScope) {
646
+ throw new QueryError({
647
+ message: 'Query must be scoped with a from() clause',
648
+ context: { query },
649
+ });
650
+ }
651
+ }
652
+
395
653
  /**
396
654
  * Removes filter steps that have no predicates.
397
655
  */
@@ -425,6 +683,176 @@ export class QueryPlanner {
425
683
  // TODO(dmaretskyi): Implement this.
426
684
  return plan;
427
685
  }
686
+
687
+ private _generateOrderClause(query: QueryAST.QueryOrderClause, context: GenerationContext): QueryPlan.Plan {
688
+ return QueryPlan.Plan.make([
689
+ ...this._generate(query.query, context).steps,
690
+ {
691
+ _tag: 'OrderStep',
692
+ order: query.order,
693
+ },
694
+ ]);
695
+ }
696
+
697
+ private _generateLimitClause(query: QueryAST.QueryLimitClause, context: GenerationContext): QueryPlan.Plan {
698
+ return QueryPlan.Plan.make([
699
+ ...this._generate(query.query, context).steps,
700
+ {
701
+ _tag: 'LimitStep',
702
+ limit: query.limit,
703
+ },
704
+ ]);
705
+ }
706
+
707
+ // After complete plan is built, inspect it from the end:
708
+ // - Walk backwards until hitting an object set changer.
709
+ // - If an order step is found, skip.
710
+ // - Otherwise insert natural order before any limit steps (since limit should be applied after ordering).
711
+ // This method is recursive and also processes sub-plans in unions and set differences.
712
+ private _ensureOrderStep(plan: QueryPlan.Plan): QueryPlan.Plan {
713
+ // First, recursively process any sub-plans.
714
+ const processedSteps = plan.steps.map((step): QueryPlan.Step => {
715
+ if (step._tag === 'UnionStep') {
716
+ return {
717
+ _tag: 'UnionStep',
718
+ plans: step.plans.map((subPlan) => this._ensureOrderStep(subPlan)),
719
+ };
720
+ } else if (step._tag === 'SetDifferenceStep') {
721
+ return {
722
+ _tag: 'SetDifferenceStep',
723
+ source: this._ensureOrderStep(step.source),
724
+ exclude: this._ensureOrderStep(step.exclude),
725
+ };
726
+ }
727
+ return step;
728
+ });
729
+
730
+ const processedPlan = QueryPlan.Plan.make(processedSteps);
731
+
732
+ const OBJECT_SET_CHANGERS = new Set(['SelectStep', 'TraverseStep', 'UnionStep', 'SetDifferenceStep']);
733
+ for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
734
+ const step = processedPlan.steps[i];
735
+ if (step._tag === 'OrderStep') {
736
+ return processedPlan;
737
+ }
738
+ if (OBJECT_SET_CHANGERS.has(step._tag)) {
739
+ break;
740
+ }
741
+ }
742
+
743
+ // Find the position to insert the order step (before any limit steps).
744
+ let insertIndex = processedPlan.steps.length;
745
+ for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
746
+ if (processedPlan.steps[i]._tag === 'LimitStep') {
747
+ insertIndex = i;
748
+ } else {
749
+ break;
750
+ }
751
+ }
752
+
753
+ const newSteps = [...processedPlan.steps];
754
+ newSteps.splice(insertIndex, 0, {
755
+ _tag: 'OrderStep',
756
+ order: [Order.natural.ast],
757
+ });
758
+
759
+ return QueryPlan.Plan.make(newSteps);
760
+ }
761
+
762
+ /**
763
+ * Propagates limits from LimitStep to SelectStep and OrderStep when possible.
764
+ * Limits can only be propagated if there are no unions or traversals between the LimitStep and SelectStep/OrderStep.
765
+ */
766
+ private _optimizeLimits(plan: QueryPlan.Plan): QueryPlan.Plan {
767
+ // First, recursively process any sub-plans.
768
+ const processedSteps = plan.steps.map((step): QueryPlan.Step => {
769
+ if (step._tag === 'UnionStep') {
770
+ return {
771
+ _tag: 'UnionStep',
772
+ plans: step.plans.map((subPlan) => this._optimizeLimits(subPlan)),
773
+ };
774
+ } else if (step._tag === 'SetDifferenceStep') {
775
+ return {
776
+ _tag: 'SetDifferenceStep',
777
+ source: this._optimizeLimits(step.source),
778
+ exclude: this._optimizeLimits(step.exclude),
779
+ };
780
+ }
781
+ return step;
782
+ });
783
+
784
+ // Find if there's a LimitStep at the end (possibly after OrderStep).
785
+ let limitStepIndex = -1;
786
+ let limitValue: number | undefined;
787
+
788
+ for (let i = processedSteps.length - 1; i >= 0; i--) {
789
+ const step = processedSteps[i];
790
+ if (step._tag === 'LimitStep') {
791
+ limitStepIndex = i;
792
+ limitValue = step.limit;
793
+ break;
794
+ }
795
+ // Only allow OrderStep after LimitStep when looking backwards.
796
+ if (step._tag !== 'OrderStep') {
797
+ break;
798
+ }
799
+ }
800
+
801
+ if (limitStepIndex === -1 || limitValue === undefined) {
802
+ return QueryPlan.Plan.make(processedSteps);
803
+ }
804
+
805
+ // Check if we can propagate the limit to a SelectStep and/or OrderStep.
806
+ // We can only do this if there are no unions, traversals, or set differences between them.
807
+ const BLOCKERS = new Set(['UnionStep', 'TraverseStep', 'SetDifferenceStep']);
808
+
809
+ let selectStepIndex = -1;
810
+ let orderStepIndex = -1;
811
+ for (let i = 0; i < limitStepIndex; i++) {
812
+ const step = processedSteps[i];
813
+ if (step._tag === 'SelectStep') {
814
+ selectStepIndex = i;
815
+ }
816
+ if (step._tag === 'OrderStep') {
817
+ orderStepIndex = i;
818
+ }
819
+ if (BLOCKERS.has(step._tag)) {
820
+ // Found a blocker after the select/order step - can't propagate.
821
+ selectStepIndex = -1;
822
+ orderStepIndex = -1;
823
+ }
824
+ }
825
+
826
+ if (selectStepIndex === -1 && orderStepIndex === -1) {
827
+ return QueryPlan.Plan.make(processedSteps);
828
+ }
829
+
830
+ // Create a mutable copy of steps to modify.
831
+ const newSteps = [...processedSteps];
832
+
833
+ // Propagate the limit to the SelectStep if found.
834
+ if (selectStepIndex !== -1) {
835
+ const selectStep = newSteps[selectStepIndex] as QueryPlan.SelectStep;
836
+ newSteps[selectStepIndex] = {
837
+ ...selectStep,
838
+ limit: limitValue,
839
+ };
840
+ }
841
+
842
+ // Propagate the limit to the OrderStep if found.
843
+ if (orderStepIndex !== -1) {
844
+ const orderStep = newSteps[orderStepIndex] as QueryPlan.OrderStep;
845
+ newSteps[orderStepIndex] = {
846
+ ...orderStep,
847
+ limit: limitValue,
848
+ };
849
+ }
850
+
851
+ // Remove the LimitStep since limit is now propagated.
852
+ newSteps.splice(limitStepIndex, 1);
853
+
854
+ return QueryPlan.Plan.make(newSteps);
855
+ }
428
856
  }
429
857
 
430
858
  /**
@@ -437,9 +865,9 @@ type GenerationContext = {
437
865
  originalQuery: QueryAST.Query | null;
438
866
 
439
867
  /**
440
- * Which spaces to select from.
868
+ * The scope to select from (space IDs, queues, etc.).
441
869
  */
442
- selectionSpaces: readonly SpaceId[];
870
+ scope: QueryAST.Scope;
443
871
 
444
872
  /**
445
873
  * How to handle deleted objects.
@@ -454,7 +882,7 @@ type GenerationContext = {
454
882
 
455
883
  const DEFAULT_CONTEXT: GenerationContext = {
456
884
  originalQuery: null,
457
- selectionSpaces: [],
885
+ scope: {},
458
886
  deletedHandling: 'exclude',
459
887
  selectionInverted: false,
460
888
  };
@@ -474,6 +902,54 @@ const createRelationTraversalStep = (direction: QueryPlan.RelationTraversal['dir
474
902
  },
475
903
  });
476
904
 
905
+ /**
906
+ * Recursively flattens nested `and` filters into a single list.
907
+ */
908
+ const _flattenAnd = (filters: readonly QueryAST.Filter[]): QueryAST.Filter[] => {
909
+ const result: QueryAST.Filter[] = [];
910
+ for (const f of filters) {
911
+ if (f.type === 'and') {
912
+ result.push(..._flattenAnd(f.filters));
913
+ } else {
914
+ result.push(f);
915
+ }
916
+ }
917
+ return result;
918
+ };
919
+
920
+ const _timestampFilterToSelector = (filter: QueryAST.FilterTimestamp): QueryPlan.TimestampSelector => {
921
+ const selector: QueryPlan.TimestampSelector = { _tag: 'TimestampSelector' };
922
+ const key =
923
+ filter.field === 'updatedAt'
924
+ ? filter.operator === 'gte' || filter.operator === 'gt'
925
+ ? 'updatedAfter'
926
+ : 'updatedBefore'
927
+ : filter.operator === 'gte' || filter.operator === 'gt'
928
+ ? 'createdAfter'
929
+ : 'createdBefore';
930
+ selector[key] = filter.value;
931
+ return selector;
932
+ };
933
+
934
+ const _mergeTimestampSelectors = (selectors: QueryPlan.TimestampSelector[]): QueryPlan.TimestampSelector => {
935
+ const merged: QueryPlan.TimestampSelector = { _tag: 'TimestampSelector' };
936
+ for (const s of selectors) {
937
+ if (s.updatedAfter != null) {
938
+ merged.updatedAfter = Math.max(merged.updatedAfter ?? 0, s.updatedAfter);
939
+ }
940
+ if (s.updatedBefore != null) {
941
+ merged.updatedBefore = Math.min(merged.updatedBefore ?? Infinity, s.updatedBefore);
942
+ }
943
+ if (s.createdAfter != null) {
944
+ merged.createdAfter = Math.max(merged.createdAfter ?? 0, s.createdAfter);
945
+ }
946
+ if (s.createdBefore != null) {
947
+ merged.createdBefore = Math.min(merged.createdBefore ?? Infinity, s.createdBefore);
948
+ }
949
+ }
950
+ return merged;
951
+ };
952
+
477
953
  const isTrivialTypenameFilter = (filter: QueryAST.Filter): boolean => {
478
954
  return (
479
955
  filter.type === 'object' &&