@danceroutine/tango-orm 1.7.0 → 1.8.1

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 (82) hide show
  1. package/dist/InternalDialect-ClSaUNso.js +10 -0
  2. package/dist/InternalDialect-ClSaUNso.js.map +1 -0
  3. package/dist/PostgresAdapter-CXKdKBG-.js +4 -0
  4. package/dist/PostgresAdapter-DySFW6vy.js +128 -0
  5. package/dist/PostgresAdapter-DySFW6vy.js.map +1 -0
  6. package/dist/{SqliteClient-CjOK9-ki.js → SqliteAdapter-CDdOjRmW.js} +57 -3
  7. package/dist/SqliteAdapter-CDdOjRmW.js.map +1 -0
  8. package/dist/SqliteAdapter-mjtXuVTg.js +4 -0
  9. package/dist/connection/adapters/Adapter.d.ts +32 -1
  10. package/dist/connection/adapters/dialects/PostgresAdapter.d.ts +5 -6
  11. package/dist/connection/adapters/dialects/SqliteAdapter.d.ts +4 -6
  12. package/dist/connection/adapters/index.d.ts +1 -1
  13. package/dist/connection/index.d.ts +1 -1
  14. package/dist/connection/index.js +4 -5
  15. package/dist/{connection-B_K2ZAf7.js → connection-Dmhgx31M.js} +5 -7
  16. package/dist/{connection-B_K2ZAf7.js.map → connection-Dmhgx31M.js.map} +1 -1
  17. package/dist/{defaultRuntime-BPK9kWEW.js → defaultRuntime-DzqBQ9Hb.js} +63 -16
  18. package/dist/defaultRuntime-DzqBQ9Hb.js.map +1 -0
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +11 -12
  21. package/dist/manager/ModelManager.d.ts +25 -5
  22. package/dist/manager/index.d.ts +6 -0
  23. package/dist/manager/index.js +8 -7
  24. package/dist/manager/internal/MutationCompiler.d.ts +14 -6
  25. package/dist/manager/relations/ManyToManyRelatedManager.d.ts +147 -0
  26. package/dist/manager/relations/ManyToManyRelatedQuerySet.d.ts +62 -0
  27. package/dist/manager/relations/MaterializedModelRecord.d.ts +28 -0
  28. package/dist/manager/relations/index.d.ts +9 -0
  29. package/dist/manager/relations/internal/ThroughTableManager.d.ts +79 -0
  30. package/dist/manager-DrDTiCAz.js +24 -0
  31. package/dist/manager-DrDTiCAz.js.map +1 -0
  32. package/dist/query/ModelQuerySet.d.ts +20 -0
  33. package/dist/query/QBuilder.d.ts +3 -3
  34. package/dist/query/QuerySet.d.ts +49 -21
  35. package/dist/query/compiler/QueryCompiler.d.ts +13 -4
  36. package/dist/query/domain/CompiledQuery.d.ts +169 -2
  37. package/dist/query/domain/FilterInput.d.ts +1 -1
  38. package/dist/query/domain/FilterKey.d.ts +4 -2
  39. package/dist/query/domain/QNode.d.ts +4 -4
  40. package/dist/query/domain/QuerySetState.d.ts +3 -3
  41. package/dist/query/domain/RelationMeta.d.ts +9 -0
  42. package/dist/query/domain/RelationTyping.d.ts +47 -0
  43. package/dist/query/domain/TableMetaFactory.d.ts +1 -14
  44. package/dist/query/domain/index.d.ts +1 -1
  45. package/dist/query/domain/internal/InternalPrefetchQueryKind.d.ts +20 -0
  46. package/dist/query/index.d.ts +1 -0
  47. package/dist/query/index.js +3 -2
  48. package/dist/query/planning/QueryPlanner.d.ts +1 -1
  49. package/dist/{query-FZJoSCg4.js → query-DUZnBFhf.js} +425 -166
  50. package/dist/query-DUZnBFhf.js.map +1 -0
  51. package/dist/registerModelObjects-DxlBfuUN.js +797 -0
  52. package/dist/registerModelObjects-DxlBfuUN.js.map +1 -0
  53. package/dist/runtime/TangoRuntime.d.ts +9 -0
  54. package/dist/runtime/index.d.ts +3 -2
  55. package/dist/runtime/index.js +7 -6
  56. package/dist/runtime/internal/SqliteDBClientProvider.d.ts +3 -0
  57. package/dist/{runtime-ByXbpVBS.js → runtime-1H88J3nN.js} +3 -3
  58. package/dist/runtime-1H88J3nN.js.map +1 -0
  59. package/dist/transaction/index.js +5 -4
  60. package/dist/{transaction-Cs0Z9tbW.js → transaction-ZhfDf-f8.js} +2 -2
  61. package/dist/{transaction-Cs0Z9tbW.js.map → transaction-ZhfDf-f8.js.map} +1 -1
  62. package/dist/validation/SQLValidationEngine.d.ts +22 -5
  63. package/dist/validation/SqlValidationPlan.d.ts +5 -4
  64. package/dist/validation/internal/InternalSqlValidationPlanKind.d.ts +25 -0
  65. package/dist/validation/internal/InternalValidatedFilterDescriptorKind.d.ts +4 -0
  66. package/package.json +6 -6
  67. package/dist/PostgresAdapter-BFdo_nIt.js +0 -4
  68. package/dist/PostgresAdapter-CMiEpHya.js +0 -49
  69. package/dist/PostgresAdapter-CMiEpHya.js.map +0 -1
  70. package/dist/PostgresClient-BQJZfEOT.js +0 -68
  71. package/dist/PostgresClient-BQJZfEOT.js.map +0 -1
  72. package/dist/SqliteAdapter-A-P9zUhP.js +0 -4
  73. package/dist/SqliteAdapter-CeqhyrPC.js +0 -44
  74. package/dist/SqliteAdapter-CeqhyrPC.js.map +0 -1
  75. package/dist/SqliteClient-CjOK9-ki.js.map +0 -1
  76. package/dist/defaultRuntime-BPK9kWEW.js.map +0 -1
  77. package/dist/manager-C6oJ2tAF.js +0 -13
  78. package/dist/manager-C6oJ2tAF.js.map +0 -1
  79. package/dist/query-FZJoSCg4.js.map +0 -1
  80. package/dist/registerModelObjects-C-1RbUHS.js +0 -385
  81. package/dist/registerModelObjects-C-1RbUHS.js.map +0 -1
  82. package/dist/runtime-ByXbpVBS.js.map +0 -1
@@ -1,84 +1,8 @@
1
1
  import { __export } from "./chunk-DLY2FNSh.js";
2
+ import { InternalDialect } from "./InternalDialect-ClSaUNso.js";
2
3
  import { MultipleObjectsReturned, NotFoundError, SqlSafetyEngine, getLogger, isError } from "@danceroutine/tango-core";
3
4
  import { ModelRegistry } from "@danceroutine/tango-schema";
4
5
 
5
- //#region src/query/domain/internal/InternalQNodeType.ts
6
- const InternalQNodeType = {
7
- ATOM: "atom",
8
- AND: "and",
9
- OR: "or",
10
- NOT: "not"
11
- };
12
-
13
- //#endregion
14
- //#region src/query/domain/internal/InternalRelationKind.ts
15
- const InternalRelationKind = {
16
- HAS_MANY: "hasMany",
17
- BELONGS_TO: "belongsTo",
18
- HAS_ONE: "hasOne",
19
- MANY_TO_MANY: "manyToMany"
20
- };
21
-
22
- //#endregion
23
- //#region src/query/domain/TableMetaFactory.ts
24
- var TableMetaFactory = class TableMetaFactory {
25
- static create(model) {
26
- const owner = model.metadata.key ? ModelRegistry.getOwner(model) : undefined;
27
- const cache = new Map();
28
- return TableMetaFactory.createWithCache(model, owner, cache);
29
- }
30
- static createWithCache(model, owner, cache) {
31
- if (model.metadata.key) {
32
- const cached = cache.get(model.metadata.key);
33
- if (cached) return cached;
34
- }
35
- const pkField = model.metadata.fields.find((field) => field.primaryKey);
36
- if (!pkField) throw new Error(`Model '${model.metadata.name}' cannot attach a manager without a primary key field.`);
37
- const tableMeta = {
38
- modelKey: model.metadata.key,
39
- table: model.metadata.table,
40
- pk: pkField.name,
41
- columns: Object.fromEntries(model.metadata.fields.map((field) => [field.name, field.type]))
42
- };
43
- if (model.metadata.key) cache.set(model.metadata.key, tableMeta);
44
- if (!model.metadata.key || !owner) return tableMeta;
45
- const relations = owner.getResolvedRelationGraph().byModel.get(model.metadata.key);
46
- if (!relations || relations.size === 0) return tableMeta;
47
- tableMeta.relations = Object.fromEntries(Array.from(relations.entries()).filter(([, relation]) => relation.capabilities.queryable && relation.capabilities.hydratable).map(([name, relation]) => {
48
- const targetModel = owner.getByKey(relation.targetModelKey);
49
- const targetMeta = TableMetaFactory.createWithCache(targetModel, owner, cache);
50
- const { queryable, hydratable } = relation.capabilities;
51
- const isSingleRelation = relation.kind === InternalRelationKind.BELONGS_TO || relation.kind === InternalRelationKind.HAS_ONE;
52
- const sourceKey = relation.kind === InternalRelationKind.BELONGS_TO ? relation.localFieldName : relation.targetFieldName;
53
- const targetKey = relation.kind === InternalRelationKind.BELONGS_TO ? relation.targetFieldName : relation.localFieldName;
54
- const targetColumns = Object.fromEntries(targetModel.metadata.fields.map((field) => [field.name, field.type]));
55
- const capabilities = {
56
- queryable,
57
- hydratable,
58
- joinable: isSingleRelation && queryable && hydratable,
59
- prefetchable: queryable && hydratable
60
- };
61
- return [name, {
62
- edgeId: relation.edgeId,
63
- sourceModelKey: relation.sourceModelKey,
64
- targetModelKey: relation.targetModelKey,
65
- kind: relation.kind,
66
- cardinality: isSingleRelation ? "single" : "many",
67
- capabilities,
68
- table: targetModel.metadata.table,
69
- sourceKey,
70
- targetKey,
71
- targetPrimaryKey: targetMeta.pk,
72
- targetColumns,
73
- alias: relation.alias,
74
- targetMeta
75
- }];
76
- }));
77
- return tableMeta;
78
- }
79
- };
80
-
81
- //#endregion
82
6
  //#region src/query/domain/RelationMeta.ts
83
7
  const InternalRelationHydrationLoadMode = {
84
8
  JOIN: "join",
@@ -86,10 +10,19 @@ const InternalRelationHydrationLoadMode = {
86
10
  };
87
11
 
88
12
  //#endregion
89
- //#region src/query/domain/internal/InternalDialect.ts
90
- const InternalDialect = {
91
- POSTGRES: "postgres",
92
- SQLITE: "sqlite"
13
+ //#region src/query/domain/internal/InternalPrefetchQueryKind.ts
14
+ const InternalPrefetchQueryKind = {
15
+ DIRECT: "direct",
16
+ MANY_TO_MANY: "manyToMany"
17
+ };
18
+
19
+ //#endregion
20
+ //#region src/query/domain/internal/InternalQNodeType.ts
21
+ const InternalQNodeType = {
22
+ ATOM: "atom",
23
+ AND: "and",
24
+ OR: "or",
25
+ NOT: "not"
93
26
  };
94
27
 
95
28
  //#endregion
@@ -110,6 +43,22 @@ const InternalLookupType = {
110
43
  IENDSWITH: "iendswith"
111
44
  };
112
45
 
46
+ //#endregion
47
+ //#region src/validation/internal/InternalSqlValidationPlanKind.ts
48
+ const InternalSqlValidationPlanKind = {
49
+ SELECT: "select",
50
+ INSERT: "insert",
51
+ UPDATE: "update",
52
+ DELETE: "delete"
53
+ };
54
+
55
+ //#endregion
56
+ //#region src/validation/internal/InternalValidatedFilterDescriptorKind.ts
57
+ const InternalValidatedFilterDescriptorKind = {
58
+ COLUMN: "column",
59
+ RELATION: "relation"
60
+ };
61
+
113
62
  //#endregion
114
63
  //#region src/validation/OrmSqlSafetyAdapter.ts
115
64
  const ALLOWED_LOOKUPS = Object.values(InternalLookupType);
@@ -121,35 +70,35 @@ var OrmSqlSafetyAdapter = class OrmSqlSafetyAdapter {
121
70
  }
122
71
  validate(plan) {
123
72
  switch (plan.kind) {
124
- case "select": {
73
+ case InternalSqlValidationPlanKind.SELECT: {
125
74
  const meta = this.validateTableMeta(plan.meta, plan.relationNames ?? []);
126
75
  return {
127
- kind: "select",
76
+ kind: InternalSqlValidationPlanKind.SELECT,
128
77
  meta,
129
78
  selectFields: Object.fromEntries((plan.selectFields ?? []).map((field) => [field, `${meta.table}.${this.resolveColumn(meta, field)}`])),
130
- filterKeys: Object.fromEntries((plan.filterKeys ?? []).map((rawKey) => [rawKey, this.validateFilterKey(meta, rawKey)])),
79
+ filterKeys: Object.fromEntries((plan.filterKeys ?? []).map((rawKey) => [rawKey, this.validateFilterKey(meta, plan.meta, rawKey)])),
131
80
  orderFields: Object.fromEntries((plan.orderFields ?? []).map((field) => [field, `${meta.table}.${this.resolveColumn(meta, field)}`])),
132
81
  relations: Object.fromEntries((plan.relationNames ?? []).map((relationName) => [relationName, this.resolveRelation(meta, relationName)]))
133
82
  };
134
83
  }
135
- case "insert": {
84
+ case InternalSqlValidationPlanKind.INSERT: {
136
85
  const meta = this.validateTableMeta(plan.meta);
137
86
  return {
138
- kind: "insert",
87
+ kind: InternalSqlValidationPlanKind.INSERT,
139
88
  meta,
140
89
  writeKeys: plan.writeKeys.map((key) => this.resolveColumn(meta, key))
141
90
  };
142
91
  }
143
- case "update": {
92
+ case InternalSqlValidationPlanKind.UPDATE: {
144
93
  const meta = this.validateTableMeta(plan.meta);
145
94
  return {
146
- kind: "update",
95
+ kind: InternalSqlValidationPlanKind.UPDATE,
147
96
  meta,
148
97
  writeKeys: plan.writeKeys.map((key) => this.resolveColumn(meta, key))
149
98
  };
150
99
  }
151
- case "delete": return {
152
- kind: "delete",
100
+ case InternalSqlValidationPlanKind.DELETE: return {
101
+ kind: InternalSqlValidationPlanKind.DELETE,
153
102
  meta: this.validateTableMeta(plan.meta)
154
103
  };
155
104
  }
@@ -203,6 +152,26 @@ var OrmSqlSafetyAdapter = class OrmSqlSafetyAdapter {
203
152
  role: "relationTargetPrimaryKey",
204
153
  value: relation.targetKey
205
154
  },
155
+ {
156
+ key: "targetPrimaryKey",
157
+ role: "relationTargetPrimaryKey",
158
+ value: relation.targetPrimaryKey
159
+ },
160
+ ...relation.throughTable ? [{
161
+ key: "throughTable",
162
+ role: "table",
163
+ value: relation.throughTable
164
+ }] : [],
165
+ ...relation.throughSourceKey ? [{
166
+ key: "throughSourceKey",
167
+ role: "column",
168
+ value: relation.throughSourceKey
169
+ }] : [],
170
+ ...relation.throughTargetKey ? [{
171
+ key: "throughTargetKey",
172
+ role: "column",
173
+ value: relation.throughTargetKey
174
+ }] : [],
206
175
  ...Object.keys(relation.targetColumns).map((column) => ({
207
176
  key: `targetColumn:${column}`,
208
177
  role: "column",
@@ -215,24 +184,62 @@ var OrmSqlSafetyAdapter = class OrmSqlSafetyAdapter {
215
184
  alias: validated.identifiers.alias.value,
216
185
  sourceKey: this.resolveColumn(meta, relation.sourceKey),
217
186
  targetKey: validated.identifiers.targetKey.value,
218
- targetColumns: Object.fromEntries(Object.keys(relation.targetColumns).map((column) => [validated.identifiers[`targetColumn:${column}`].value, relation.targetColumns[column]]))
187
+ targetPrimaryKey: validated.identifiers.targetPrimaryKey.value,
188
+ targetColumns: Object.fromEntries(Object.keys(relation.targetColumns).map((column) => [validated.identifiers[`targetColumn:${column}`].value, relation.targetColumns[column]])),
189
+ throughTable: validated.identifiers.throughTable?.value,
190
+ throughSourceKey: validated.identifiers.throughSourceKey?.value,
191
+ throughTargetKey: validated.identifiers.throughTargetKey?.value
219
192
  };
220
193
  }
221
- validateFilterKey(meta, rawKey) {
194
+ validateFilterKey(meta, rawMeta, rawKey) {
222
195
  const segments = rawKey.split("__");
223
- if (segments.length > 2) throw new Error(`Invalid SQL lookup key: '${rawKey}'.`);
224
- const field = segments[0];
225
- const lookup = segments[1] ?? InternalLookupType.EXACT;
196
+ if (segments.length === 0 || segments.some((segment) => segment.length === 0)) throw new Error(`Invalid SQL lookup key: '${rawKey}'.`);
197
+ const lookupToken = segments.at(-1);
198
+ const hasExplicitLookup = ALLOWED_LOOKUPS.includes(lookupToken);
199
+ const lookup = hasExplicitLookup ? lookupToken : InternalLookupType.EXACT;
200
+ const pathSegments = hasExplicitLookup ? segments.slice(0, -1) : segments;
201
+ if (pathSegments.length === 0) throw new Error(`Invalid SQL lookup key: '${rawKey}'.`);
226
202
  const validated = this.engine.validate({ lookupTokens: [{
227
203
  key: rawKey,
228
204
  lookup,
229
205
  allowed: ALLOWED_LOOKUPS
230
206
  }] });
207
+ if (pathSegments.length === 1) {
208
+ const field$1 = pathSegments[0];
209
+ return {
210
+ kind: InternalValidatedFilterDescriptorKind.COLUMN,
211
+ rawKey,
212
+ field: field$1,
213
+ lookup: validated.lookupTokens[rawKey].lookup,
214
+ qualifiedColumn: `${meta.table}.${this.resolveColumn(meta, field$1)}`
215
+ };
216
+ }
217
+ const rootSegment = pathSegments[0];
218
+ const hasRootColumn = rootSegment in rawMeta.columns;
219
+ const hasRootRelation = rootSegment in (rawMeta.relations ?? {});
220
+ if (!hasExplicitLookup && hasRootColumn && !hasRootRelation) throw new Error(`Invalid SQL lookup key: '${rawKey}'.`);
221
+ const field = pathSegments.at(-1);
222
+ const relationSegments = pathSegments.slice(0, -1);
223
+ const relationChain = [];
224
+ let currentValidatedMeta = meta;
225
+ let currentRawMeta = rawMeta;
226
+ for (const relationName of relationSegments) {
227
+ const relation = currentRawMeta.relations?.[relationName];
228
+ if (!relation) throw new Error(`Unknown relation '${relationName}' for table '${currentValidatedMeta.table}'.`);
229
+ if (!relation.targetMeta) throw new Error(`Relation '${relationName}' for table '${currentValidatedMeta.table}' is missing target metadata.`);
230
+ const validatedRelation = this.validateRelationMeta(currentValidatedMeta, relationName, currentRawMeta.relations);
231
+ relationChain.push(validatedRelation);
232
+ currentRawMeta = relation.targetMeta;
233
+ currentValidatedMeta = this.validateTableMeta(currentRawMeta);
234
+ }
231
235
  return {
236
+ kind: InternalValidatedFilterDescriptorKind.RELATION,
232
237
  rawKey,
233
238
  field,
234
239
  lookup: validated.lookupTokens[rawKey].lookup,
235
- qualifiedColumn: `${meta.table}.${this.resolveColumn(meta, field)}`
240
+ relationPath: relationSegments.join("__"),
241
+ relationChain,
242
+ terminalColumn: this.resolveColumn(currentValidatedMeta, field)
236
243
  };
237
244
  }
238
245
  resolveColumn(meta, field) {
@@ -287,18 +294,15 @@ var QueryPlanner = class QueryPlanner {
287
294
  let currentMeta = this.meta;
288
295
  let currentChildren = rootChildren;
289
296
  let builtPath = "";
290
- let containsCollection = false;
291
297
  for (const segment of segments) {
292
298
  const relation = currentMeta.relations?.[segment];
293
299
  if (!relation) throw new Error(`Unknown relation path '${relationPath}' for table '${currentMeta.table}'.`);
294
300
  if (segment in currentMeta.columns && relation.sourceKey !== segment) throw new Error(`Relation path '${relationPath}' collides with an existing field on table '${currentMeta.table}'.`);
295
- if (relation.kind === InternalRelationKind.MANY_TO_MANY) throw new Error(`Relation path '${relationPath}' uses unsupported many-to-many hydration.`);
296
301
  if (!relation.capabilities.queryable || !relation.capabilities.hydratable) throw new Error(`Relation path '${relationPath}' cannot be hydrated.`);
297
302
  if (mode === "select") {
298
303
  if (relation.cardinality !== InternalRelationHydrationCardinality.SINGLE || !relation.capabilities.joinable) throw new Error(`Relation path '${relationPath}' cannot be loaded with selectRelated(...).`);
299
304
  } else if (relation.cardinality === InternalRelationHydrationCardinality.MANY) {
300
305
  if (!relation.capabilities.prefetchable) throw new Error(`Relation path '${relationPath}' cannot be loaded with prefetchRelated(...).`);
301
- containsCollection = true;
302
306
  } else if (!relation.capabilities.joinable) throw new Error(`Relation path '${relationPath}' cannot be loaded with prefetchRelated(...).`);
303
307
  const targetMeta = relation.targetMeta;
304
308
  if (!targetMeta) throw new Error(`Relation path '${relationPath}' is missing target metadata.`);
@@ -317,7 +321,6 @@ var QueryPlanner = class QueryPlanner {
317
321
  currentChildren = nextNode.children;
318
322
  currentMeta = targetMeta;
319
323
  }
320
- if (mode === "prefetch" && !containsCollection) throw new Error(`Relation path '${relationPath}' cannot be loaded with prefetchRelated(...).`);
321
324
  }
322
325
  buildPlannedChildren(children) {
323
326
  const joinNodes = [];
@@ -353,9 +356,11 @@ const sqlSafetyAdapter = new OrmSqlSafetyAdapter();
353
356
  var QueryCompiler = class QueryCompiler {
354
357
  static BRAND = "tango.orm.query_compiler";
355
358
  __tangoBrand = QueryCompiler.BRAND;
356
- constructor(meta, dialect = InternalDialect.POSTGRES) {
359
+ placeholders;
360
+ constructor(meta, adapter) {
357
361
  this.meta = meta;
358
- this.dialect = dialect;
362
+ this.adapter = adapter;
363
+ this.placeholders = adapter.placeholders;
359
364
  }
360
365
  static isQueryCompiler(value) {
361
366
  return typeof value === "object" && value !== null && value.__tangoBrand === QueryCompiler.BRAND;
@@ -363,7 +368,7 @@ var QueryCompiler = class QueryCompiler {
363
368
  compile(state) {
364
369
  const hydrationPlan = new QueryPlanner(this.meta).plan(state);
365
370
  const validatedPlan = sqlSafetyAdapter.validate({
366
- kind: "select",
371
+ kind: InternalSqlValidationPlanKind.SELECT,
367
372
  meta: this.meta,
368
373
  selectFields: state.select?.map(String),
369
374
  filterKeys: this.collectStateFilterKeys(state),
@@ -437,7 +442,8 @@ var QueryCompiler = class QueryCompiler {
437
442
  };
438
443
  }
439
444
  compilePrefetch(node, sourceValues) {
440
- const placeholders = this.dialect === InternalDialect.POSTGRES ? sourceValues.map((_, index) => `$${index + 1}`).join(", ") : sourceValues.map(() => "?").join(", ");
445
+ if (node.throughTable && node.throughSourceKey && node.throughTargetKey) return this.compileManyToManyPrefetch(node, sourceValues);
446
+ const placeholders = this.placeholders.list(sourceValues.length);
441
447
  const validatedTarget = this.validatePrefetchTarget(node);
442
448
  const baseAlias = this.buildPrefetchBaseAlias(node.relationPath);
443
449
  const joinCollection = {
@@ -447,12 +453,58 @@ var QueryCompiler = class QueryCompiler {
447
453
  for (const joinChild of node.joinChildren) this.collectNestedJoinSql(joinChild, baseAlias, validatedTarget.columns, joinCollection);
448
454
  const baseSelects = Object.keys(validatedTarget.columns).map((column) => `${baseAlias}.${column} AS ${column}`);
449
455
  return {
456
+ kind: InternalPrefetchQueryKind.DIRECT,
450
457
  sql: `SELECT ${[...baseSelects, ...joinCollection.selects].join(", ")} FROM ${validatedTarget.table} ${baseAlias}${joinCollection.joins.length ? ` ${joinCollection.joins.join(" ")}` : ""} WHERE ${baseAlias}.${validatedTarget.targetKey} IN (${placeholders}) ORDER BY ${baseAlias}.${validatedTarget.targetKey} ASC, ${baseAlias}.${validatedTarget.primaryKey} ASC`,
451
458
  params: sourceValues,
452
459
  targetKey: validatedTarget.targetKey,
453
460
  targetColumns: validatedTarget.columns
454
461
  };
455
462
  }
463
+ compileManyToManyTargets(node, targetIds) {
464
+ const placeholders = this.placeholders.list(targetIds.length);
465
+ const validatedTarget = this.validatePrefetchTarget(node);
466
+ const baseAlias = this.buildPrefetchBaseAlias(node.relationPath);
467
+ const joinCollection = {
468
+ selects: [],
469
+ joins: []
470
+ };
471
+ for (const joinChild of node.joinChildren) this.collectNestedJoinSql(joinChild, baseAlias, validatedTarget.columns, joinCollection);
472
+ const baseSelects = Object.keys(validatedTarget.columns).map((column) => `${baseAlias}.${column} AS ${column}`);
473
+ return {
474
+ sql: `SELECT ${[...baseSelects, ...joinCollection.selects].join(", ")} FROM ${validatedTarget.table} ${baseAlias}${joinCollection.joins.length ? ` ${joinCollection.joins.join(" ")}` : ""} WHERE ${baseAlias}.${validatedTarget.primaryKey} IN (${placeholders}) ORDER BY ${baseAlias}.${validatedTarget.primaryKey} ASC`,
475
+ params: targetIds
476
+ };
477
+ }
478
+ compileManyToManyPrefetch(node, sourceValues) {
479
+ const placeholders = this.placeholders.list(sourceValues.length);
480
+ const throughValidated = sqlSafetyAdapter.validate({
481
+ kind: InternalSqlValidationPlanKind.SELECT,
482
+ meta: {
483
+ table: node.throughTable,
484
+ pk: node.throughSourceKey,
485
+ columns: {
486
+ [node.throughSourceKey]: node.throughSourceColumnType ?? "int",
487
+ [node.throughTargetKey]: node.throughTargetColumnType ?? "int"
488
+ }
489
+ },
490
+ filterKeys: [node.throughSourceKey, node.throughTargetKey],
491
+ relationNames: []
492
+ });
493
+ const ownerAlias = this.validateInternalAlias("__tango_m2m_owner");
494
+ const targetAlias = this.validateInternalAlias("__tango_m2m_target");
495
+ const throughSourceColumn = throughValidated.filterKeys[node.throughSourceKey].field;
496
+ const throughTargetColumn = throughValidated.filterKeys[node.throughTargetKey].field;
497
+ return {
498
+ kind: InternalPrefetchQueryKind.MANY_TO_MANY,
499
+ throughSql: `SELECT ${throughValidated.meta.table}.${throughSourceColumn} AS ${ownerAlias}, ${throughValidated.meta.table}.${throughTargetColumn} AS ${targetAlias} FROM ${throughValidated.meta.table} WHERE ${throughValidated.meta.table}.${throughSourceColumn} IN (${placeholders}) ORDER BY ${throughValidated.meta.table}.${throughSourceColumn} ASC, ${throughValidated.meta.table}.${throughTargetColumn} ASC`,
500
+ throughParams: sourceValues,
501
+ ownerAlias,
502
+ targetAlias,
503
+ targetTable: node.targetTable,
504
+ targetPrimaryKey: node.targetPrimaryKey,
505
+ targetColumns: node.targetColumns
506
+ };
507
+ }
456
508
  compileHydrationNode(node, context) {
457
509
  const validatedRelation = this.validateHydrationRelation(context.ownerMeta, node.relationName);
458
510
  const targetColumns = validatedRelation.targetColumns;
@@ -496,6 +548,11 @@ var QueryCompiler = class QueryCompiler {
496
548
  targetKey: validatedRelation.targetKey,
497
549
  targetTable: validatedRelation.table,
498
550
  targetPrimaryKey: node.relationEdge.targetPrimaryKey,
551
+ throughTable: node.relationEdge.throughTable,
552
+ throughSourceKey: node.relationEdge.throughSourceKey,
553
+ throughTargetKey: node.relationEdge.throughTargetKey,
554
+ throughSourceColumnType: node.relationEdge.throughSourceColumnType,
555
+ throughTargetColumnType: node.relationEdge.throughTargetColumnType,
499
556
  targetColumns,
500
557
  provenance: node.provenance,
501
558
  joinChildren: compiledJoinChildren,
@@ -505,7 +562,7 @@ var QueryCompiler = class QueryCompiler {
505
562
  }
506
563
  validateHydrationRelation(ownerMeta, relationName) {
507
564
  return sqlSafetyAdapter.validate({
508
- kind: "select",
565
+ kind: InternalSqlValidationPlanKind.SELECT,
509
566
  meta: ownerMeta,
510
567
  relationNames: [relationName]
511
568
  }).relations[relationName];
@@ -519,7 +576,7 @@ var QueryCompiler = class QueryCompiler {
519
576
  validatePrefetchTarget(node) {
520
577
  try {
521
578
  const validated = sqlSafetyAdapter.validate({
522
- kind: "select",
579
+ kind: InternalSqlValidationPlanKind.SELECT,
523
580
  meta: {
524
581
  table: node.targetTable,
525
582
  pk: node.targetPrimaryKey,
@@ -570,6 +627,9 @@ var QueryCompiler = class QueryCompiler {
570
627
  buildPrefetchSourceAlias(relationPath, sourceKey) {
571
628
  return this.assertInternalAliasDoesNotCollide(`__tango_prefetch_${this.sanitizeRelationPath(relationPath)}_${sourceKey}`);
572
629
  }
630
+ buildFilterAlias(relationPath, suffix) {
631
+ return this.assertInternalAliasDoesNotCollide(`__tango_filter_${this.sanitizeRelationPath(relationPath)}_${suffix}`);
632
+ }
573
633
  sanitizeRelationPath(relationPath) {
574
634
  return relationPath.replace(/[^a-zA-Z0-9]+/g, "_");
575
635
  }
@@ -594,7 +654,7 @@ var QueryCompiler = class QueryCompiler {
594
654
  const { parts, params } = entries.reduce((accumulator, [key, value]) => {
595
655
  const descriptor = filterKeys[String(key)];
596
656
  const idx = paramIndex + accumulator.params.length;
597
- const clause = this.lookupToSQL(descriptor.qualifiedColumn, descriptor.lookup, value, idx);
657
+ const clause = descriptor.kind === InternalValidatedFilterDescriptorKind.COLUMN ? this.lookupToSQL(descriptor.qualifiedColumn, descriptor.lookup, value, idx) : this.compileRelationFilter(descriptor, value, idx);
598
658
  accumulator.parts.push(clause.sql);
599
659
  accumulator.params.push(...clause.params);
600
660
  return accumulator;
@@ -652,8 +712,28 @@ var QueryCompiler = class QueryCompiler {
652
712
  params: result.params
653
713
  };
654
714
  }
715
+ compileRelationFilter(descriptor, value, paramIndex) {
716
+ return this.buildRelationFilterExists(this.meta.table, descriptor.relationChain, descriptor.terminalColumn, descriptor.lookup, value, paramIndex, descriptor.relationPath);
717
+ }
718
+ buildRelationFilterExists(ownerAlias, relationChain, terminalColumn, lookup, value, paramIndex, relationPath) {
719
+ const [relation, ...rest] = relationChain;
720
+ if (!relation) throw new Error(`Cannot compile empty relation filter path '${relationPath}'.`);
721
+ const targetAlias = this.buildFilterAlias(relationPath, `target_${relation.alias}_${rest.length}`);
722
+ const targetPredicate = rest.length === 0 ? this.lookupToSQL(`${targetAlias}.${terminalColumn}`, lookup, value, paramIndex) : this.buildRelationFilterExists(targetAlias, rest, terminalColumn, lookup, value, paramIndex, relationPath);
723
+ if (relation.throughTable && relation.throughSourceKey && relation.throughTargetKey) {
724
+ const throughAlias = this.buildFilterAlias(relationPath, `through_${relation.alias}_${rest.length}`);
725
+ return {
726
+ sql: `EXISTS (SELECT 1 FROM ${relation.throughTable} ${throughAlias} INNER JOIN ${relation.table} ${targetAlias} ON ${targetAlias}.${relation.targetKey} = ${throughAlias}.${relation.throughTargetKey} WHERE ${throughAlias}.${relation.throughSourceKey} = ${ownerAlias}.${relation.sourceKey} AND ${targetPredicate.sql})`,
727
+ params: targetPredicate.params
728
+ };
729
+ }
730
+ return {
731
+ sql: `EXISTS (SELECT 1 FROM ${relation.table} ${targetAlias} WHERE ${targetAlias}.${relation.targetKey} = ${ownerAlias}.${relation.sourceKey} AND ${targetPredicate.sql})`,
732
+ params: targetPredicate.params
733
+ };
734
+ }
655
735
  lookupToSQL(col, lookup, value, paramIndex) {
656
- const placeholder = this.dialect === InternalDialect.POSTGRES ? `$${paramIndex}` : "?";
736
+ const placeholder = this.placeholders.at(paramIndex);
657
737
  const normalized = this.normalizeParam(value);
658
738
  switch (lookup) {
659
739
  case InternalLookupType.EXACT:
@@ -687,7 +767,7 @@ var QueryCompiler = class QueryCompiler {
687
767
  sql: "1=0",
688
768
  params: []
689
769
  };
690
- const placeholders = this.dialect === InternalDialect.POSTGRES ? entries.map((_, index) => `$${paramIndex + index}`).join(", ") : entries.map(() => "?").join(", ");
770
+ const placeholders = this.placeholders.listFromOffset(entries.length, paramIndex - 1);
691
771
  return {
692
772
  sql: `${col} IN (${placeholders})`,
693
773
  params: entries
@@ -702,7 +782,7 @@ var QueryCompiler = class QueryCompiler {
702
782
  params: [`%${value}%`]
703
783
  };
704
784
  case InternalLookupType.ICONTAINS: {
705
- const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
785
+ const lowerCol = this.adapter.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
706
786
  return {
707
787
  sql: `${lowerCol} LIKE ${placeholder}`,
708
788
  params: [`%${String(value).toLowerCase()}%`]
@@ -713,7 +793,7 @@ var QueryCompiler = class QueryCompiler {
713
793
  params: [`${value}%`]
714
794
  };
715
795
  case InternalLookupType.ISTARTSWITH: {
716
- const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
796
+ const lowerCol = this.adapter.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
717
797
  return {
718
798
  sql: `${lowerCol} LIKE ${placeholder}`,
719
799
  params: [`${String(value).toLowerCase()}%`]
@@ -724,7 +804,7 @@ var QueryCompiler = class QueryCompiler {
724
804
  params: [`%${value}`]
725
805
  };
726
806
  case InternalLookupType.IENDSWITH: {
727
- const lowerCol = this.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
807
+ const lowerCol = this.adapter.dialect === InternalDialect.POSTGRES ? `LOWER(${col})` : `${col}`;
728
808
  return {
729
809
  sql: `${lowerCol} LIKE ${placeholder}`,
730
810
  params: [`%${String(value).toLowerCase()}`]
@@ -734,7 +814,7 @@ var QueryCompiler = class QueryCompiler {
734
814
  }
735
815
  }
736
816
  normalizeParam(value) {
737
- if (this.dialect === InternalDialect.SQLITE && typeof value === "boolean") return value ? 1 : 0;
817
+ if (this.adapter.dialect === InternalDialect.SQLITE && typeof value === "boolean") return value ? 1 : 0;
738
818
  return value;
739
819
  }
740
820
  collectStateFilterKeys(state) {
@@ -805,6 +885,87 @@ var QueryResult = class QueryResult {
805
885
  }
806
886
  };
807
887
 
888
+ //#endregion
889
+ //#region src/query/domain/internal/InternalRelationKind.ts
890
+ const InternalRelationKind = {
891
+ HAS_MANY: "hasMany",
892
+ BELONGS_TO: "belongsTo",
893
+ HAS_ONE: "hasOne",
894
+ MANY_TO_MANY: "manyToMany"
895
+ };
896
+
897
+ //#endregion
898
+ //#region src/query/domain/TableMetaFactory.ts
899
+ var TableMetaFactory = class TableMetaFactory {
900
+ static create(model) {
901
+ const owner = model.metadata.key ? ModelRegistry.getOwner(model) : undefined;
902
+ const cache = new Map();
903
+ return TableMetaFactory.createWithCache(model, owner, cache);
904
+ }
905
+ static createWithCache(model, owner, cache) {
906
+ if (model.metadata.key) {
907
+ const cached = cache.get(model.metadata.key);
908
+ if (cached) return cached;
909
+ }
910
+ const pkField = model.metadata.fields.find((field) => field.primaryKey);
911
+ if (!pkField) throw new Error(`Model '${model.metadata.name}' cannot attach a manager without a primary key field.`);
912
+ const tableMeta = {
913
+ modelKey: model.metadata.key,
914
+ table: model.metadata.table,
915
+ pk: pkField.name,
916
+ columns: Object.fromEntries(model.metadata.fields.map((field) => [field.name, field.type]))
917
+ };
918
+ if (model.metadata.key) cache.set(model.metadata.key, tableMeta);
919
+ if (!model.metadata.key || !owner) return tableMeta;
920
+ const relations = owner.getResolvedRelationGraph().byModel.get(model.metadata.key);
921
+ if (!relations || relations.size === 0) return tableMeta;
922
+ tableMeta.relations = Object.fromEntries(Array.from(relations.entries()).filter(([, relation]) => relation.capabilities.queryable && relation.capabilities.hydratable).map(([name, relation]) => {
923
+ const targetModel = owner.getByKey(relation.targetModelKey);
924
+ const targetMeta = TableMetaFactory.createWithCache(targetModel, owner, cache);
925
+ const { queryable, hydratable } = relation.capabilities;
926
+ const isSingleRelation = relation.kind === InternalRelationKind.BELONGS_TO || relation.kind === InternalRelationKind.HAS_ONE;
927
+ const targetColumns = Object.fromEntries(targetModel.metadata.fields.map((field) => [field.name, field.type]));
928
+ let throughSourceColumnType;
929
+ let throughTargetColumnType;
930
+ if (relation.kind === InternalRelationKind.MANY_TO_MANY && relation.throughModelKey && relation.throughSourceKey && relation.throughTargetKey) {
931
+ const throughModel = owner.getByKey(relation.throughModelKey);
932
+ throughSourceColumnType = throughModel?.metadata.fields.find((field) => field.name === relation.throughSourceKey)?.type;
933
+ throughTargetColumnType = throughModel?.metadata.fields.find((field) => field.name === relation.throughTargetKey)?.type;
934
+ }
935
+ const capabilities = {
936
+ queryable,
937
+ hydratable,
938
+ joinable: isSingleRelation && queryable && hydratable,
939
+ prefetchable: queryable && hydratable
940
+ };
941
+ const sourceKey = relation.kind === InternalRelationKind.MANY_TO_MANY ? tableMeta.pk : relation.kind === InternalRelationKind.BELONGS_TO ? relation.localFieldName : relation.targetFieldName;
942
+ const targetKey = relation.kind === InternalRelationKind.MANY_TO_MANY ? targetMeta.pk : relation.kind === InternalRelationKind.BELONGS_TO ? relation.targetFieldName : relation.localFieldName;
943
+ return [name, {
944
+ edgeId: relation.edgeId,
945
+ sourceModelKey: relation.sourceModelKey,
946
+ targetModelKey: relation.targetModelKey,
947
+ kind: relation.kind,
948
+ cardinality: isSingleRelation ? "single" : "many",
949
+ capabilities,
950
+ table: targetModel.metadata.table,
951
+ sourceKey,
952
+ targetKey,
953
+ throughTable: relation.throughTable,
954
+ throughModelKey: relation.throughModelKey,
955
+ throughSourceKey: relation.throughSourceKey,
956
+ throughTargetKey: relation.throughTargetKey,
957
+ throughSourceColumnType,
958
+ throughTargetColumnType,
959
+ targetPrimaryKey: targetMeta.pk,
960
+ targetColumns,
961
+ alias: relation.alias,
962
+ targetMeta
963
+ }];
964
+ }));
965
+ return tableMeta;
966
+ }
967
+ };
968
+
808
969
  //#endregion
809
970
  //#region src/query/domain/index.ts
810
971
  var domain_exports = {};
@@ -897,6 +1058,27 @@ var QuerySet = class QuerySet {
897
1058
  static isQuerySet(value) {
898
1059
  return typeof value === "object" && value !== null && value.__tangoBrand === QuerySet.BRAND;
899
1060
  }
1061
+ /**
1062
+ * Translate user-facing order tokens like `'name'` or `'-createdAt'` into
1063
+ * the internal `OrderSpec` array used by `QuerySetState`.
1064
+ *
1065
+ * Exposed as `protected` so subclasses can compose the same parse logic
1066
+ * when they need to return their own concrete type from `orderBy` without
1067
+ * reaching into a base-class instance's protected state.
1068
+ */
1069
+ static buildOrderSpecs(tokens) {
1070
+ return tokens.map((t) => {
1071
+ const str = String(t);
1072
+ if (str.startsWith("-")) return {
1073
+ by: str.slice(1),
1074
+ dir: InternalDirection.DESC
1075
+ };
1076
+ return {
1077
+ by: t,
1078
+ dir: InternalDirection.ASC
1079
+ };
1080
+ });
1081
+ }
900
1082
  static invertOrderSpec(order) {
901
1083
  if (!order?.length) return [];
902
1084
  return order.map((spec) => ({
@@ -915,7 +1097,7 @@ var QuerySet = class QuerySet {
915
1097
  where: q
916
1098
  };
917
1099
  const merged = this.state.q ? QBuilder.and(this.state.q, wrapped) : wrapped;
918
- return new QuerySet(this.executor, {
1100
+ return this.spawn({
919
1101
  ...this.state,
920
1102
  q: merged
921
1103
  });
@@ -931,7 +1113,7 @@ var QuerySet = class QuerySet {
931
1113
  where: q
932
1114
  };
933
1115
  const excludes = [...this.state.excludes ?? [], wrapped];
934
- return new QuerySet(this.executor, {
1116
+ return this.spawn({
935
1117
  ...this.state,
936
1118
  excludes
937
1119
  });
@@ -940,27 +1122,16 @@ var QuerySet = class QuerySet {
940
1122
  * Apply ordering tokens such as `'name'` or `'-createdAt'`.
941
1123
  */
942
1124
  orderBy(...tokens) {
943
- const order = tokens.map((t) => {
944
- const str = String(t);
945
- if (str.startsWith("-")) return {
946
- by: str.slice(1),
947
- dir: InternalDirection.DESC
948
- };
949
- return {
950
- by: t,
951
- dir: InternalDirection.ASC
952
- };
953
- });
954
- return new QuerySet(this.executor, {
1125
+ return this.spawn({
955
1126
  ...this.state,
956
- order
1127
+ order: QuerySet.buildOrderSpecs(tokens)
957
1128
  });
958
1129
  }
959
1130
  /**
960
1131
  * Limit the maximum number of rows returned.
961
1132
  */
962
1133
  limit(n) {
963
- return new QuerySet(this.executor, {
1134
+ return this.spawn({
964
1135
  ...this.state,
965
1136
  limit: n
966
1137
  });
@@ -969,13 +1140,13 @@ var QuerySet = class QuerySet {
969
1140
  * Skip the first `n` rows.
970
1141
  */
971
1142
  offset(n) {
972
- return new QuerySet(this.executor, {
1143
+ return this.spawn({
973
1144
  ...this.state,
974
1145
  offset: n
975
1146
  });
976
1147
  }
977
1148
  select(fields) {
978
- return new QuerySet(this.executor, {
1149
+ return this.spawn({
979
1150
  ...this.state,
980
1151
  select: [...fields]
981
1152
  });
@@ -990,7 +1161,7 @@ var QuerySet = class QuerySet {
990
1161
  * path keys for applications that keep the app-local registry current.
991
1162
  */
992
1163
  selectRelated(...rels) {
993
- return new QuerySet(this.executor, {
1164
+ return this.spawn({
994
1165
  ...this.state,
995
1166
  selectRelated: [...rels]
996
1167
  });
@@ -1004,13 +1175,13 @@ var QuerySet = class QuerySet {
1004
1175
  * app-local registry current.
1005
1176
  */
1006
1177
  prefetchRelated(...rels) {
1007
- return new QuerySet(this.executor, {
1178
+ return this.spawn({
1008
1179
  ...this.state,
1009
1180
  prefetchRelated: [...rels]
1010
1181
  });
1011
1182
  }
1012
1183
  all() {
1013
- return new QuerySet(this.executor, { ...this.state });
1184
+ return this.spawn({ ...this.state });
1014
1185
  }
1015
1186
  async fetch(shape) {
1016
1187
  const baseResult = await this.getOrCreateEvaluationCache();
@@ -1021,9 +1192,10 @@ var QuerySet = class QuerySet {
1021
1192
  /**
1022
1193
  * Async iterable surface for `for await (... of queryset)`.
1023
1194
  *
1024
- * Evaluates this queryset on first use by awaiting {@link QuerySet.fetch} without arguments, then
1025
- * yields each element from that {@link QueryResult}. Later async iterations over the same queryset
1026
- * instance reuse the cached materialized result instead of issuing another database round-trip.
1195
+ * Evaluates this queryset on first use by awaiting `fetch()` without
1196
+ * arguments, then yields each element from that materialized result.
1197
+ * Later async iterations over the same queryset instance reuse the cached
1198
+ * materialized result instead of issuing another database round-trip.
1027
1199
  */
1028
1200
  async *[Symbol.asyncIterator]() {
1029
1201
  const result = await this.fetch();
@@ -1050,7 +1222,7 @@ var QuerySet = class QuerySet {
1050
1222
  by: this.executor.meta.pk,
1051
1223
  dir: InternalDirection.DESC
1052
1224
  }];
1053
- const qs = new QuerySet(this.executor, {
1225
+ const qs = this.spawn({
1054
1226
  ...this.state,
1055
1227
  order: effectiveOrder
1056
1228
  });
@@ -1060,16 +1232,15 @@ var QuerySet = class QuerySet {
1060
1232
  const limited = this.filter(q).limit(2);
1061
1233
  const page = await limited.fetch();
1062
1234
  const rows = page.items;
1063
- const count = rows.length;
1064
- if (count === 0) throw new NotFoundError(`${this.executor.meta.table}: no matching record`);
1065
- if (count > 1) throw new MultipleObjectsReturned(`${this.executor.meta.table}: more than one matching record`);
1235
+ if (rows.length === 0) throw new NotFoundError(`${this.executor.meta.table}: no matching record`);
1236
+ if (rows.length > 1) throw new MultipleObjectsReturned(`${this.executor.meta.table}: more than one matching record`);
1066
1237
  return this.shapeFetchedRow(rows[0], shape);
1067
1238
  }
1068
1239
  /**
1069
1240
  * Execute a `COUNT(*)` query for the current filtered state.
1070
1241
  */
1071
1242
  async count() {
1072
- const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
1243
+ const compiler = new QueryCompiler(this.executor.meta, this.executor.adapter);
1073
1244
  const compiled = compiler.compile(this.withoutHydrationState());
1074
1245
  const countQuery = `SELECT COUNT(*) as count FROM (${compiled.sql}) AS tango_count_subquery`;
1075
1246
  const rows = await this.executor.client.query(countQuery, compiled.params);
@@ -1096,15 +1267,23 @@ var QuerySet = class QuerySet {
1096
1267
  return this.evaluationCache;
1097
1268
  }
1098
1269
  async evaluateRows() {
1099
- const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
1270
+ const compiler = new QueryCompiler(this.executor.meta, this.executor.adapter);
1100
1271
  const compiled = compiler.compile(this.state);
1101
1272
  const rows = await this.executor.run(compiled);
1102
- const hydratedRows = await this.hydrateRows(rows, compiled);
1273
+ const normalizedRows = this.normalizeRowsForSchemaParsing(rows);
1274
+ const hydratedRows = await this.hydrateRows(normalizedRows, compiled);
1275
+ this.attachRootRecordAccessors(hydratedRows);
1103
1276
  const projectedRows = hydratedRows;
1104
1277
  return new QueryResult(projectedRows);
1105
1278
  }
1279
+ normalizeRowsForSchemaParsing(rows) {
1280
+ if (this.executor.adapter.dialect !== InternalDialect.SQLITE) return [...rows];
1281
+ const booleanColumns = Object.entries(this.executor.meta.columns).filter(([, value]) => this.isBooleanColumnType(value)).map(([column]) => column);
1282
+ if (booleanColumns.length === 0) return [...rows];
1283
+ return rows.map((row) => this.normalizeBooleanColumns(row, booleanColumns));
1284
+ }
1106
1285
  normalizeHydratedRowsForParserShape(rows) {
1107
- if (this.executor.dialect !== InternalDialect.SQLITE) return [...rows];
1286
+ if (this.executor.adapter.dialect !== InternalDialect.SQLITE) return [...rows];
1108
1287
  const booleanColumns = Object.entries(this.executor.meta.columns).filter(([, value]) => this.isBooleanColumnType(value)).map(([column]) => column);
1109
1288
  if (booleanColumns.length === 0) return [...rows];
1110
1289
  return rows.map((row) => this.normalizeBooleanColumns(row, booleanColumns));
@@ -1112,15 +1291,29 @@ var QuerySet = class QuerySet {
1112
1291
  async hydrateRows(rows, compiled) {
1113
1292
  if (!compiled.hydrationPlan) return rows;
1114
1293
  const hydratedRows = rows.map((row) => ({ ...row }));
1294
+ this.attachRootRecordAccessors(hydratedRows);
1115
1295
  const canonicalEntities = new Map();
1116
1296
  const queuedJoinPrefetchOwners = new Map();
1117
- const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
1297
+ const compiler = new QueryCompiler(this.executor.meta, this.executor.adapter);
1118
1298
  for (const row of hydratedRows) this.hydrateJoinNodesForOwner(row, row, compiled.hydrationPlan.joinNodes, canonicalEntities, queuedJoinPrefetchOwners);
1119
1299
  for (const node of compiled.hydrationPlan.prefetchNodes) await this.hydratePrefetchNode(node, hydratedRows, canonicalEntities, compiler);
1120
1300
  for (const [node, owners] of queuedJoinPrefetchOwners.entries()) await this.hydratePrefetchNode(node, [...owners], canonicalEntities, compiler);
1121
1301
  for (const row of hydratedRows) for (const alias of compiled.hydrationPlan.hiddenRootAliases) delete row[alias];
1122
1302
  return hydratedRows;
1123
1303
  }
1304
+ primeManyToManyOwnerCache(owner, relationName, bucket) {
1305
+ const existing = owner[relationName];
1306
+ if (existing && typeof existing.primePrefetchCache === "function") {
1307
+ existing.primePrefetchCache(bucket);
1308
+ return;
1309
+ }
1310
+ owner[relationName] = bucket.slice();
1311
+ }
1312
+ attachRootRecordAccessors(rows) {
1313
+ if (!this.executor.attachPersistedRecordAccessors) return;
1314
+ const sourceModelKey = this.executor.meta.modelKey;
1315
+ for (const row of rows) this.executor.attachPersistedRecordAccessors(row, sourceModelKey);
1316
+ }
1124
1317
  hydrateJoinNodesForOwner(owner, rawRow, nodes, canonicalEntities, queuedJoinPrefetchOwners) {
1125
1318
  for (const node of nodes) {
1126
1319
  if (!node.join) continue;
@@ -1153,28 +1346,81 @@ var QuerySet = class QuerySet {
1153
1346
  if (owners.length === 0) return;
1154
1347
  const groupedOwners = this.groupOwnersByAccessor(owners, node.ownerSourceAccessor);
1155
1348
  const sourceValues = [...groupedOwners.keys()];
1156
- for (const owner of owners) owner[node.relationName] = node.cardinality === InternalRelationHydrationCardinality.MANY ? [] : null;
1349
+ const isManyToMany = !!node.throughTable;
1350
+ if (!isManyToMany) for (const owner of owners) owner[node.relationName] = node.cardinality === InternalRelationHydrationCardinality.MANY ? [] : null;
1157
1351
  if (sourceValues.length === 0) return;
1158
- const compiledPrefetch = compiler.compilePrefetch(node, sourceValues);
1159
- const result = await this.executor.client.query(compiledPrefetch.sql, compiledPrefetch.params);
1160
- const groupedTargets = new Map();
1352
+ const sourceChunks = this.chunkValues(sourceValues, 500);
1353
+ const compiledPrefetch = compiler.compilePrefetch(node, sourceChunks[0]);
1354
+ if (compiledPrefetch.kind === InternalPrefetchQueryKind.MANY_TO_MANY) {
1355
+ const idsByOwner = new Map();
1356
+ const uniqueTargetIds = new Set();
1357
+ for (const chunk of sourceChunks) {
1358
+ const chunkCompiled = compiler.compilePrefetch(node, chunk);
1359
+ const throughResult = await this.executor.client.query(chunkCompiled.throughSql, chunkCompiled.throughParams);
1360
+ for (const row of throughResult.rows) {
1361
+ const ownerId = row[chunkCompiled.ownerAlias];
1362
+ const targetId = row[chunkCompiled.targetAlias];
1363
+ if (typeof ownerId !== "string" && typeof ownerId !== "number" || typeof targetId !== "string" && typeof targetId !== "number") continue;
1364
+ const bucket = idsByOwner.get(ownerId) ?? [];
1365
+ bucket.push(targetId);
1366
+ idsByOwner.set(ownerId, bucket);
1367
+ uniqueTargetIds.add(targetId);
1368
+ }
1369
+ }
1370
+ const targets = {};
1371
+ const targetIds = [...uniqueTargetIds.values()];
1372
+ if (targetIds.length > 0) for (const targetChunk of this.chunkValues(targetIds, 500)) {
1373
+ const targetQuery = compiler.compileManyToManyTargets(node, targetChunk);
1374
+ const targetResult = await this.executor.client.query(targetQuery.sql, targetQuery.params);
1375
+ for (const rawTargetRow of targetResult.rows) {
1376
+ const normalized = this.normalizeTargetRow({ targetColumns: compiledPrefetch.targetColumns }, rawTargetRow);
1377
+ const canonical = this.canonicalizeEntity(node, normalized, canonicalEntities);
1378
+ this.hydrateJoinNodesForOwner(canonical, normalized, node.joinChildren, canonicalEntities);
1379
+ const primaryKey = canonical[node.targetPrimaryKey];
1380
+ if (typeof primaryKey === "string" || typeof primaryKey === "number") targets[primaryKey] = canonical;
1381
+ }
1382
+ }
1383
+ const canonicalChildren$1 = new Map();
1384
+ const handledOwners = new Set();
1385
+ for (const [ownerId, ids] of idsByOwner.entries()) {
1386
+ const bucket = ids.map((id) => targets[id]).filter((value) => !!value);
1387
+ for (const owner of groupedOwners.get(ownerId) ?? []) {
1388
+ this.primeManyToManyOwnerCache(owner, node.relationName, bucket);
1389
+ handledOwners.add(owner);
1390
+ }
1391
+ for (const child of bucket) canonicalChildren$1.set(child[node.targetPrimaryKey], child);
1392
+ }
1393
+ for (const owner of owners) if (!handledOwners.has(owner)) this.primeManyToManyOwnerCache(owner, node.relationName, []);
1394
+ const childOwners$1 = [...canonicalChildren$1.values()];
1395
+ for (const childNode of node.prefetchChildren) await this.hydratePrefetchNode(childNode, childOwners$1, canonicalEntities, compiler);
1396
+ return;
1397
+ }
1161
1398
  const canonicalChildren = new Map();
1162
- for (const rawResultRow of result.rows) {
1163
- const normalized = this.normalizeTargetRow(compiledPrefetch, rawResultRow);
1164
- const canonical = this.canonicalizeEntity(node, normalized, canonicalEntities);
1165
- this.hydrateJoinNodesForOwner(canonical, normalized, node.joinChildren, canonicalEntities);
1166
- const key = normalized[compiledPrefetch.targetKey];
1167
- if (typeof key !== "string" && typeof key !== "number") continue;
1168
- const bucket = groupedTargets.get(key) ?? [];
1169
- bucket.push(canonical);
1170
- groupedTargets.set(key, bucket);
1171
- const childPrimaryKey = canonical[node.targetPrimaryKey];
1172
- if (typeof childPrimaryKey === "string" || typeof childPrimaryKey === "number") canonicalChildren.set(childPrimaryKey, canonical);
1399
+ for (const chunk of sourceChunks) {
1400
+ const chunkCompiled = compiler.compilePrefetch(node, chunk);
1401
+ const result = await this.executor.client.query(chunkCompiled.sql, chunkCompiled.params);
1402
+ for (const rawResultRow of result.rows) {
1403
+ const normalized = this.normalizeTargetRow(chunkCompiled, rawResultRow);
1404
+ const canonical = this.canonicalizeEntity(node, normalized, canonicalEntities);
1405
+ this.hydrateJoinNodesForOwner(canonical, normalized, node.joinChildren, canonicalEntities);
1406
+ const key = normalized[chunkCompiled.targetKey];
1407
+ if (typeof key !== "string" && typeof key !== "number") continue;
1408
+ for (const owner of groupedOwners.get(key) ?? []) if (node.cardinality === InternalRelationHydrationCardinality.MANY) owner[node.relationName].push(canonical);
1409
+ else if (owner[node.relationName] === null) owner[node.relationName] = canonical;
1410
+ const childPrimaryKey = canonical[node.targetPrimaryKey];
1411
+ if (typeof childPrimaryKey === "string" || typeof childPrimaryKey === "number") canonicalChildren.set(childPrimaryKey, canonical);
1412
+ }
1173
1413
  }
1174
- for (const [sourceValue, grouped] of groupedTargets.entries()) for (const owner of groupedOwners.get(sourceValue) ?? []) owner[node.relationName] = node.cardinality === InternalRelationHydrationCardinality.MANY ? grouped : grouped[0];
1175
1414
  const childOwners = [...canonicalChildren.values()];
1176
1415
  for (const childNode of node.prefetchChildren) await this.hydratePrefetchNode(childNode, childOwners, canonicalEntities, compiler);
1177
1416
  }
1417
+ chunkValues(values, size) {
1418
+ if (values.length === 0) return [];
1419
+ if (values.length <= size) return [Array.from(values)];
1420
+ const chunks = [];
1421
+ for (let i = 0; i < values.length; i += size) chunks.push(values.slice(i, i + size));
1422
+ return chunks;
1423
+ }
1178
1424
  groupOwnersByAccessor(owners, accessor) {
1179
1425
  const grouped = new Map();
1180
1426
  for (const owner of owners) {
@@ -1197,10 +1443,11 @@ var QuerySet = class QuerySet {
1197
1443
  }
1198
1444
  byModel.set(primaryKeyValue, row);
1199
1445
  canonicalEntities.set(node.targetModelKey, byModel);
1446
+ this.executor.attachPersistedRecordAccessors?.(row, node.targetModelKey);
1200
1447
  return row;
1201
1448
  }
1202
1449
  normalizeTargetRow(prefetch, row) {
1203
- if (this.executor.dialect !== InternalDialect.SQLITE) return row;
1450
+ if (this.executor.adapter.dialect !== InternalDialect.SQLITE) return row;
1204
1451
  let normalized = null;
1205
1452
  for (const [column, type] of Object.entries(prefetch.targetColumns)) {
1206
1453
  if (!this.isBooleanColumnType(type)) continue;
@@ -1212,7 +1459,7 @@ var QuerySet = class QuerySet {
1212
1459
  return normalized ?? row;
1213
1460
  }
1214
1461
  normalizeColumnValue(columnType, value) {
1215
- return this.executor.dialect === InternalDialect.SQLITE && this.isBooleanColumnType(columnType) ? this.normalizeSqliteBoolean(value) : value;
1462
+ return this.executor.adapter.dialect === InternalDialect.SQLITE && this.isBooleanColumnType(columnType) ? this.normalizeSqliteBoolean(value) : value;
1216
1463
  }
1217
1464
  isBooleanColumnType(value) {
1218
1465
  return typeof value === "string" && ["bool", "boolean"].includes(value.trim().toLowerCase());
@@ -1239,10 +1486,22 @@ var QuerySet = class QuerySet {
1239
1486
  }
1240
1487
  };
1241
1488
 
1489
+ //#endregion
1490
+ //#region src/query/ModelQuerySet.ts
1491
+ var ModelQuerySet = class ModelQuerySet extends QuerySet {
1492
+ constructor(executor, state = {}) {
1493
+ super(executor, state);
1494
+ }
1495
+ spawn(state) {
1496
+ return new ModelQuerySet(this.executor, state);
1497
+ }
1498
+ };
1499
+
1242
1500
  //#endregion
1243
1501
  //#region src/query/index.ts
1244
1502
  var query_exports = {};
1245
1503
  __export(query_exports, {
1504
+ ModelQuerySet: () => ModelQuerySet,
1246
1505
  Q: () => QBuilder,
1247
1506
  QBuilder: () => QBuilder,
1248
1507
  QueryCompiler: () => QueryCompiler,
@@ -1253,5 +1512,5 @@ __export(query_exports, {
1253
1512
  });
1254
1513
 
1255
1514
  //#endregion
1256
- export { InternalQNodeType, OrmSqlSafetyAdapter, QBuilder, QueryCompiler, QueryResult, QuerySet, TableMetaFactory, compiler_exports, domain_exports, isQNodeLike, query_exports };
1257
- //# sourceMappingURL=query-FZJoSCg4.js.map
1515
+ export { InternalQNodeType, InternalRelationKind, InternalSqlValidationPlanKind, ModelQuerySet, OrmSqlSafetyAdapter, QBuilder, QueryCompiler, QueryResult, QuerySet, TableMetaFactory, compiler_exports, domain_exports, isQNodeLike, query_exports };
1516
+ //# sourceMappingURL=query-DUZnBFhf.js.map