@tanstack/db 0.4.2 → 0.4.3

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 (32) hide show
  1. package/dist/cjs/collection/subscription.cjs +1 -1
  2. package/dist/cjs/collection/subscription.cjs.map +1 -1
  3. package/dist/cjs/query/compiler/index.cjs +2 -39
  4. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  5. package/dist/cjs/query/compiler/index.d.cts +1 -0
  6. package/dist/cjs/query/compiler/joins.cjs +9 -8
  7. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  8. package/dist/cjs/query/compiler/joins.d.cts +2 -1
  9. package/dist/cjs/query/compiler/order-by.cjs +4 -5
  10. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  11. package/dist/cjs/query/ir.cjs +38 -0
  12. package/dist/cjs/query/ir.cjs.map +1 -1
  13. package/dist/cjs/query/ir.d.cts +10 -1
  14. package/dist/esm/collection/subscription.js +1 -1
  15. package/dist/esm/collection/subscription.js.map +1 -1
  16. package/dist/esm/query/compiler/index.d.ts +1 -0
  17. package/dist/esm/query/compiler/index.js +4 -41
  18. package/dist/esm/query/compiler/index.js.map +1 -1
  19. package/dist/esm/query/compiler/joins.d.ts +2 -1
  20. package/dist/esm/query/compiler/joins.js +9 -8
  21. package/dist/esm/query/compiler/joins.js.map +1 -1
  22. package/dist/esm/query/compiler/order-by.js +1 -2
  23. package/dist/esm/query/compiler/order-by.js.map +1 -1
  24. package/dist/esm/query/ir.d.ts +10 -1
  25. package/dist/esm/query/ir.js +38 -0
  26. package/dist/esm/query/ir.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/collection/subscription.ts +1 -1
  29. package/src/query/compiler/index.ts +4 -1
  30. package/src/query/compiler/joins.ts +13 -8
  31. package/src/query/compiler/order-by.ts +1 -2
  32. package/src/query/ir.ts +67 -1
@@ -61,6 +61,43 @@ function isResidualWhere(where) {
61
61
  function createResidualWhere(expression) {
62
62
  return { expression, residual: true };
63
63
  }
64
+ function getRefFromAlias(query, alias) {
65
+ if (query.from.alias === alias) {
66
+ return query.from;
67
+ }
68
+ for (const join of query.join || []) {
69
+ if (join.from.alias === alias) {
70
+ return join.from;
71
+ }
72
+ }
73
+ }
74
+ function followRef(query, ref, collection) {
75
+ if (ref.path.length === 0) {
76
+ return;
77
+ }
78
+ if (ref.path.length === 1) {
79
+ const field = ref.path[0];
80
+ if (query.select) {
81
+ const selectedField = query.select[field];
82
+ if (selectedField && selectedField.type === `ref`) {
83
+ return followRef(query, selectedField, collection);
84
+ }
85
+ }
86
+ return { collection, path: [field] };
87
+ }
88
+ if (ref.path.length > 1) {
89
+ const [alias, ...rest] = ref.path;
90
+ const aliasRef = getRefFromAlias(query, alias);
91
+ if (!aliasRef) {
92
+ return;
93
+ }
94
+ if (aliasRef.type === `queryRef`) {
95
+ return followRef(aliasRef.query, new PropRef(rest), collection);
96
+ } else {
97
+ return { collection: aliasRef.collection, path: rest };
98
+ }
99
+ }
100
+ }
64
101
  export {
65
102
  Aggregate,
66
103
  CollectionRef,
@@ -69,6 +106,7 @@ export {
69
106
  QueryRef,
70
107
  Value,
71
108
  createResidualWhere,
109
+ followRef,
72
110
  getHavingExpression,
73
111
  getWhereExpression,
74
112
  isExpressionLike,
@@ -1 +1 @@
1
- {"version":3,"file":"ir.js","sources":["../../../src/query/ir.ts"],"sourcesContent":["/*\nThis is the intermediate representation of the query.\n*/\n\nimport type { CompareOptions } from \"./builder/types\"\nimport type { CollectionImpl } from \"../collection/index.js\"\nimport type { NamespacedRow } from \"../types\"\n\nexport interface QueryIR {\n from: From\n select?: Select\n join?: Join\n where?: Array<Where>\n groupBy?: GroupBy\n having?: Array<Having>\n orderBy?: OrderBy\n limit?: Limit\n offset?: Offset\n distinct?: true\n\n // Functional variants\n fnSelect?: (row: NamespacedRow) => any\n fnWhere?: Array<(row: NamespacedRow) => any>\n fnHaving?: Array<(row: NamespacedRow) => any>\n}\n\nexport type From = CollectionRef | QueryRef\n\nexport type Select = {\n [alias: string]: BasicExpression | Aggregate | Select\n}\n\nexport type Join = Array<JoinClause>\n\nexport interface JoinClause {\n from: CollectionRef | QueryRef\n type: `left` | `right` | `inner` | `outer` | `full` | `cross`\n left: BasicExpression\n right: BasicExpression\n}\n\nexport type Where =\n | BasicExpression<boolean>\n | { expression: BasicExpression<boolean>; residual?: boolean }\n\nexport type GroupBy = Array<BasicExpression>\n\nexport type Having = Where\n\nexport type OrderBy = Array<OrderByClause>\n\nexport type OrderByClause = {\n expression: BasicExpression\n compareOptions: CompareOptions\n}\n\nexport type OrderByDirection = `asc` | `desc`\n\nexport type Limit = number\n\nexport type Offset = number\n\n/* Expressions */\n\nabstract class BaseExpression<T = any> {\n public abstract type: string\n /** @internal - Type brand for TypeScript inference */\n declare readonly __returnType: T\n}\n\nexport class CollectionRef extends BaseExpression {\n public type = `collectionRef` as const\n constructor(\n public collection: CollectionImpl,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class QueryRef extends BaseExpression {\n public type = `queryRef` as const\n constructor(\n public query: QueryIR,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class PropRef<T = any> extends BaseExpression<T> {\n public type = `ref` as const\n constructor(\n public path: Array<string> // path to the property in the collection, with the alias as the first element\n ) {\n super()\n }\n}\n\nexport class Value<T = any> extends BaseExpression<T> {\n public type = `val` as const\n constructor(\n public value: T // any js value\n ) {\n super()\n }\n}\n\nexport class Func<T = any> extends BaseExpression<T> {\n public type = `func` as const\n constructor(\n public name: string, // such as eq, gt, lt, upper, lower, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n// This is the basic expression type that is used in the majority of expression\n// builder callbacks (select, where, groupBy, having, orderBy, etc.)\n// it doesn't include aggregate functions as those are only used in the select clause\nexport type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>\n\nexport class Aggregate<T = any> extends BaseExpression<T> {\n public type = `agg` as const\n constructor(\n public name: string, // such as count, avg, sum, min, max, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n/**\n * Runtime helper to detect IR expression-like objects.\n * Prefer this over ad-hoc local implementations to keep behavior consistent.\n */\nexport function isExpressionLike(value: any): boolean {\n return (\n value instanceof Aggregate ||\n value instanceof Func ||\n value instanceof PropRef ||\n value instanceof Value\n )\n}\n\n/**\n * Helper functions for working with Where clauses\n */\n\n/**\n * Extract the expression from a Where clause\n */\nexport function getWhereExpression(where: Where): BasicExpression<boolean> {\n return typeof where === `object` && `expression` in where\n ? where.expression\n : where\n}\n\n/**\n * Extract the expression from a HAVING clause\n * HAVING clauses can contain aggregates, unlike regular WHERE clauses\n */\nexport function getHavingExpression(\n having: Having\n): BasicExpression | Aggregate {\n return typeof having === `object` && `expression` in having\n ? having.expression\n : having\n}\n\n/**\n * Check if a Where clause is marked as residual\n */\nexport function isResidualWhere(where: Where): boolean {\n return (\n typeof where === `object` &&\n `expression` in where &&\n where.residual === true\n )\n}\n\n/**\n * Create a residual Where clause from an expression\n */\nexport function createResidualWhere(\n expression: BasicExpression<boolean>\n): Where {\n return { expression, residual: true }\n}\n"],"names":[],"mappings":"AAgEA,MAAe,eAAwB;AAIvC;AAEO,MAAM,sBAAsB,eAAe;AAAA,EAEhD,YACS,YACA,OACP;AACA,UAAA;AAHO,SAAA,aAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,iBAAiB,eAAe;AAAA,EAE3C,YACS,OACA,OACP;AACA,UAAA;AAHO,SAAA,QAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,gBAAyB,eAAkB;AAAA,EAEtD,YACS,MACP;AACA,UAAA;AAFO,SAAA,OAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,cAAuB,eAAkB;AAAA,EAEpD,YACS,OACP;AACA,UAAA;AAFO,SAAA,QAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,aAAsB,eAAkB;AAAA,EAEnD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAOO,MAAM,kBAA2B,eAAkB;AAAA,EAExD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAMO,SAAS,iBAAiB,OAAqB;AACpD,SACE,iBAAiB,aACjB,iBAAiB,QACjB,iBAAiB,WACjB,iBAAiB;AAErB;AASO,SAAS,mBAAmB,OAAwC;AACzE,SAAO,OAAO,UAAU,YAAY,gBAAgB,QAChD,MAAM,aACN;AACN;AAMO,SAAS,oBACd,QAC6B;AAC7B,SAAO,OAAO,WAAW,YAAY,gBAAgB,SACjD,OAAO,aACP;AACN;AAKO,SAAS,gBAAgB,OAAuB;AACrD,SACE,OAAO,UAAU,YACjB,gBAAgB,SAChB,MAAM,aAAa;AAEvB;AAKO,SAAS,oBACd,YACO;AACP,SAAO,EAAE,YAAY,UAAU,KAAA;AACjC;"}
1
+ {"version":3,"file":"ir.js","sources":["../../../src/query/ir.ts"],"sourcesContent":["/*\nThis is the intermediate representation of the query.\n*/\n\nimport type { CompareOptions } from \"./builder/types\"\nimport type { Collection, CollectionImpl } from \"../collection/index.js\"\nimport type { NamespacedRow } from \"../types\"\n\nexport interface QueryIR {\n from: From\n select?: Select\n join?: Join\n where?: Array<Where>\n groupBy?: GroupBy\n having?: Array<Having>\n orderBy?: OrderBy\n limit?: Limit\n offset?: Offset\n distinct?: true\n\n // Functional variants\n fnSelect?: (row: NamespacedRow) => any\n fnWhere?: Array<(row: NamespacedRow) => any>\n fnHaving?: Array<(row: NamespacedRow) => any>\n}\n\nexport type From = CollectionRef | QueryRef\n\nexport type Select = {\n [alias: string]: BasicExpression | Aggregate | Select\n}\n\nexport type Join = Array<JoinClause>\n\nexport interface JoinClause {\n from: CollectionRef | QueryRef\n type: `left` | `right` | `inner` | `outer` | `full` | `cross`\n left: BasicExpression\n right: BasicExpression\n}\n\nexport type Where =\n | BasicExpression<boolean>\n | { expression: BasicExpression<boolean>; residual?: boolean }\n\nexport type GroupBy = Array<BasicExpression>\n\nexport type Having = Where\n\nexport type OrderBy = Array<OrderByClause>\n\nexport type OrderByClause = {\n expression: BasicExpression\n compareOptions: CompareOptions\n}\n\nexport type OrderByDirection = `asc` | `desc`\n\nexport type Limit = number\n\nexport type Offset = number\n\n/* Expressions */\n\nabstract class BaseExpression<T = any> {\n public abstract type: string\n /** @internal - Type brand for TypeScript inference */\n declare readonly __returnType: T\n}\n\nexport class CollectionRef extends BaseExpression {\n public type = `collectionRef` as const\n constructor(\n public collection: CollectionImpl,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class QueryRef extends BaseExpression {\n public type = `queryRef` as const\n constructor(\n public query: QueryIR,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class PropRef<T = any> extends BaseExpression<T> {\n public type = `ref` as const\n constructor(\n public path: Array<string> // path to the property in the collection, with the alias as the first element\n ) {\n super()\n }\n}\n\nexport class Value<T = any> extends BaseExpression<T> {\n public type = `val` as const\n constructor(\n public value: T // any js value\n ) {\n super()\n }\n}\n\nexport class Func<T = any> extends BaseExpression<T> {\n public type = `func` as const\n constructor(\n public name: string, // such as eq, gt, lt, upper, lower, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n// This is the basic expression type that is used in the majority of expression\n// builder callbacks (select, where, groupBy, having, orderBy, etc.)\n// it doesn't include aggregate functions as those are only used in the select clause\nexport type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>\n\nexport class Aggregate<T = any> extends BaseExpression<T> {\n public type = `agg` as const\n constructor(\n public name: string, // such as count, avg, sum, min, max, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n/**\n * Runtime helper to detect IR expression-like objects.\n * Prefer this over ad-hoc local implementations to keep behavior consistent.\n */\nexport function isExpressionLike(value: any): boolean {\n return (\n value instanceof Aggregate ||\n value instanceof Func ||\n value instanceof PropRef ||\n value instanceof Value\n )\n}\n\n/**\n * Helper functions for working with Where clauses\n */\n\n/**\n * Extract the expression from a Where clause\n */\nexport function getWhereExpression(where: Where): BasicExpression<boolean> {\n return typeof where === `object` && `expression` in where\n ? where.expression\n : where\n}\n\n/**\n * Extract the expression from a HAVING clause\n * HAVING clauses can contain aggregates, unlike regular WHERE clauses\n */\nexport function getHavingExpression(\n having: Having\n): BasicExpression | Aggregate {\n return typeof having === `object` && `expression` in having\n ? having.expression\n : having\n}\n\n/**\n * Check if a Where clause is marked as residual\n */\nexport function isResidualWhere(where: Where): boolean {\n return (\n typeof where === `object` &&\n `expression` in where &&\n where.residual === true\n )\n}\n\n/**\n * Create a residual Where clause from an expression\n */\nexport function createResidualWhere(\n expression: BasicExpression<boolean>\n): Where {\n return { expression, residual: true }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n"],"names":[],"mappings":"AAgEA,MAAe,eAAwB;AAIvC;AAEO,MAAM,sBAAsB,eAAe;AAAA,EAEhD,YACS,YACA,OACP;AACA,UAAA;AAHO,SAAA,aAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,iBAAiB,eAAe;AAAA,EAE3C,YACS,OACA,OACP;AACA,UAAA;AAHO,SAAA,QAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,gBAAyB,eAAkB;AAAA,EAEtD,YACS,MACP;AACA,UAAA;AAFO,SAAA,OAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,cAAuB,eAAkB;AAAA,EAEpD,YACS,OACP;AACA,UAAA;AAFO,SAAA,QAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,aAAsB,eAAkB;AAAA,EAEnD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAOO,MAAM,kBAA2B,eAAkB;AAAA,EAExD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAMO,SAAS,iBAAiB,OAAqB;AACpD,SACE,iBAAiB,aACjB,iBAAiB,QACjB,iBAAiB,WACjB,iBAAiB;AAErB;AASO,SAAS,mBAAmB,OAAwC;AACzE,SAAO,OAAO,UAAU,YAAY,gBAAgB,QAChD,MAAM,aACN;AACN;AAMO,SAAS,oBACd,QAC6B;AAC7B,SAAO,OAAO,WAAW,YAAY,gBAAgB,SACjD,OAAO,aACP;AACN;AAKO,SAAS,gBAAgB,OAAuB;AACrD,SACE,OAAO,UAAU,YACjB,gBAAgB,SAChB,MAAM,aAAa;AAEvB;AAKO,SAAS,oBACd,YACO;AACP,SAAO,EAAE,YAAY,UAAU,KAAA;AACjC;AAEA,SAAS,gBACP,OACA,OACiC;AACjC,MAAI,MAAM,KAAK,UAAU,OAAO;AAC9B,WAAO,MAAM;AAAA,EACf;AAEA,aAAW,QAAQ,MAAM,QAAQ,CAAA,GAAI;AACnC,QAAI,KAAK,KAAK,UAAU,OAAO;AAC7B,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAOO,SAAS,UACd,OACA,KACA,YACwD;AACxD,MAAI,IAAI,KAAK,WAAW,GAAG;AACzB;AAAA,EACF;AAEA,MAAI,IAAI,KAAK,WAAW,GAAG;AAEzB,UAAM,QAAQ,IAAI,KAAK,CAAC;AAExB,QAAI,MAAM,QAAQ;AAChB,YAAM,gBAAgB,MAAM,OAAO,KAAK;AACxC,UAAI,iBAAiB,cAAc,SAAS,OAAO;AACjD,eAAO,UAAU,OAAO,eAAe,UAAU;AAAA,MACnD;AAAA,IACF;AAMA,WAAO,EAAE,YAAY,MAAM,CAAC,KAAK,EAAA;AAAA,EACnC;AAEA,MAAI,IAAI,KAAK,SAAS,GAAG;AAEvB,UAAM,CAAC,OAAO,GAAG,IAAI,IAAI,IAAI;AAC7B,UAAM,WAAW,gBAAgB,OAAO,KAAM;AAC9C,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AAEA,QAAI,SAAS,SAAS,YAAY;AAChC,aAAO,UAAU,SAAS,OAAO,IAAI,QAAQ,IAAI,GAAG,UAAU;AAAA,IAChE,OAAO;AAIL,aAAO,EAAE,YAAY,SAAS,YAAY,MAAM,KAAA;AAAA,IAClD;AAAA,EACF;AACF;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.4.2",
4
+ "version": "0.4.3",
5
5
  "dependencies": {
6
6
  "@standard-schema/spec": "^1.0.0",
7
7
  "@tanstack/db-ivm": "0.1.9"
@@ -1,5 +1,5 @@
1
1
  import { ensureIndexForExpression } from "../indexes/auto-index.js"
2
- import { and } from "../query/index.js"
2
+ import { and } from "../query/builder/functions.js"
3
3
  import {
4
4
  createFilterFunctionFromExpression,
5
5
  createFilteredCallback,
@@ -127,7 +127,8 @@ export function compileQuery(
127
127
  callbacks,
128
128
  lazyCollections,
129
129
  optimizableOrderByCollections,
130
- rawQuery
130
+ rawQuery,
131
+ compileQuery
131
132
  )
132
133
  }
133
134
 
@@ -512,3 +513,5 @@ export function followRef(
512
513
  }
513
514
  }
514
515
  }
516
+
517
+ export type CompileQueryFn = typeof compileQuery
@@ -17,10 +17,10 @@ import {
17
17
  UnsupportedJoinTypeError,
18
18
  } from "../../errors.js"
19
19
  import { ensureIndexForField } from "../../indexes/auto-index.js"
20
- import { PropRef } from "../ir.js"
20
+ import { PropRef, followRef } from "../ir.js"
21
21
  import { inArray } from "../builder/functions.js"
22
22
  import { compileExpression } from "./evaluators.js"
23
- import { compileQuery, followRef } from "./index.js"
23
+ import type { CompileQueryFn } from "./index.js"
24
24
  import type { OrderByOptimizationInfo } from "./order-by.js"
25
25
  import type {
26
26
  BasicExpression,
@@ -62,7 +62,8 @@ export function processJoins(
62
62
  callbacks: Record<string, LazyCollectionCallbacks>,
63
63
  lazyCollections: Set<string>,
64
64
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
65
- rawQuery: QueryIR
65
+ rawQuery: QueryIR,
66
+ onCompileSubquery: CompileQueryFn
66
67
  ): NamespacedAndKeyedStream {
67
68
  let resultPipeline = pipeline
68
69
 
@@ -81,7 +82,8 @@ export function processJoins(
81
82
  callbacks,
82
83
  lazyCollections,
83
84
  optimizableOrderByCollections,
84
- rawQuery
85
+ rawQuery,
86
+ onCompileSubquery
85
87
  )
86
88
  }
87
89
 
@@ -105,7 +107,8 @@ function processJoin(
105
107
  callbacks: Record<string, LazyCollectionCallbacks>,
106
108
  lazyCollections: Set<string>,
107
109
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
108
- rawQuery: QueryIR
110
+ rawQuery: QueryIR,
111
+ onCompileSubquery: CompileQueryFn
109
112
  ): NamespacedAndKeyedStream {
110
113
  // Get the joined table alias and input stream
111
114
  const {
@@ -121,7 +124,8 @@ function processJoin(
121
124
  lazyCollections,
122
125
  optimizableOrderByCollections,
123
126
  cache,
124
- queryMapping
127
+ queryMapping,
128
+ onCompileSubquery
125
129
  )
126
130
 
127
131
  // Add the joined table to the tables map
@@ -392,7 +396,8 @@ function processJoinSource(
392
396
  lazyCollections: Set<string>,
393
397
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
394
398
  cache: QueryCache,
395
- queryMapping: QueryMapping
399
+ queryMapping: QueryMapping,
400
+ onCompileSubquery: CompileQueryFn
396
401
  ): { alias: string; input: KeyedStream; collectionId: string } {
397
402
  switch (from.type) {
398
403
  case `collectionRef`: {
@@ -407,7 +412,7 @@ function processJoinSource(
407
412
  const originalQuery = queryMapping.get(from.query) || from.query
408
413
 
409
414
  // Recursively compile the sub-query with cache
410
- const subQueryResult = compileQuery(
415
+ const subQueryResult = onCompileSubquery(
411
416
  originalQuery,
412
417
  allInputs,
413
418
  collections,
@@ -1,11 +1,10 @@
1
1
  import { orderByWithFractionalIndex } from "@tanstack/db-ivm"
2
2
  import { defaultComparator, makeComparator } from "../../utils/comparison.js"
3
- import { PropRef } from "../ir.js"
3
+ import { PropRef, followRef } from "../ir.js"
4
4
  import { ensureIndexForField } from "../../indexes/auto-index.js"
5
5
  import { findIndexForField } from "../../utils/index-optimization.js"
6
6
  import { compileExpression } from "./evaluators.js"
7
7
  import { replaceAggregatesByRefs } from "./group-by.js"
8
- import { followRef } from "./index.js"
9
8
  import type { CompiledSingleRowExpression } from "./evaluators.js"
10
9
  import type { OrderByClause, QueryIR, Select } from "../ir.js"
11
10
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
package/src/query/ir.ts CHANGED
@@ -3,7 +3,7 @@ This is the intermediate representation of the query.
3
3
  */
4
4
 
5
5
  import type { CompareOptions } from "./builder/types"
6
- import type { CollectionImpl } from "../collection/index.js"
6
+ import type { Collection, CollectionImpl } from "../collection/index.js"
7
7
  import type { NamespacedRow } from "../types"
8
8
 
9
9
  export interface QueryIR {
@@ -188,3 +188,69 @@ export function createResidualWhere(
188
188
  ): Where {
189
189
  return { expression, residual: true }
190
190
  }
191
+
192
+ function getRefFromAlias(
193
+ query: QueryIR,
194
+ alias: string
195
+ ): CollectionRef | QueryRef | void {
196
+ if (query.from.alias === alias) {
197
+ return query.from
198
+ }
199
+
200
+ for (const join of query.join || []) {
201
+ if (join.from.alias === alias) {
202
+ return join.from
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Follows the given reference in a query
209
+ * until its finds the root field the reference points to.
210
+ * @returns The collection, its alias, and the path to the root field in this collection
211
+ */
212
+ export function followRef(
213
+ query: QueryIR,
214
+ ref: PropRef<any>,
215
+ collection: Collection
216
+ ): { collection: Collection; path: Array<string> } | void {
217
+ if (ref.path.length === 0) {
218
+ return
219
+ }
220
+
221
+ if (ref.path.length === 1) {
222
+ // This field should be part of this collection
223
+ const field = ref.path[0]!
224
+ // is it part of the select clause?
225
+ if (query.select) {
226
+ const selectedField = query.select[field]
227
+ if (selectedField && selectedField.type === `ref`) {
228
+ return followRef(query, selectedField, collection)
229
+ }
230
+ }
231
+
232
+ // Either this field is not part of the select clause
233
+ // and thus it must be part of the collection itself
234
+ // or it is part of the select but is not a reference
235
+ // so we can stop here and don't have to follow it
236
+ return { collection, path: [field] }
237
+ }
238
+
239
+ if (ref.path.length > 1) {
240
+ // This is a nested field
241
+ const [alias, ...rest] = ref.path
242
+ const aliasRef = getRefFromAlias(query, alias!)
243
+ if (!aliasRef) {
244
+ return
245
+ }
246
+
247
+ if (aliasRef.type === `queryRef`) {
248
+ return followRef(aliasRef.query, new PropRef(rest), collection)
249
+ } else {
250
+ // This is a reference to a collection
251
+ // we can't follow it further
252
+ // so the field must be on the collection itself
253
+ return { collection: aliasRef.collection, path: rest }
254
+ }
255
+ }
256
+ }