@danceroutine/tango-orm 1.6.0 → 1.8.0

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 (84) 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/ManagerLike.d.ts +19 -0
  22. package/dist/manager/ModelManager.d.ts +45 -2
  23. package/dist/manager/index.d.ts +6 -0
  24. package/dist/manager/index.js +8 -7
  25. package/dist/manager/internal/MutationCompiler.d.ts +14 -6
  26. package/dist/manager/relations/ManyToManyRelatedManager.d.ts +147 -0
  27. package/dist/manager/relations/ManyToManyRelatedQuerySet.d.ts +62 -0
  28. package/dist/manager/relations/MaterializedModelRecord.d.ts +28 -0
  29. package/dist/manager/relations/index.d.ts +9 -0
  30. package/dist/manager/relations/internal/ThroughTableManager.d.ts +79 -0
  31. package/dist/manager-DrDTiCAz.js +24 -0
  32. package/dist/manager-DrDTiCAz.js.map +1 -0
  33. package/dist/query/ModelQuerySet.d.ts +20 -0
  34. package/dist/query/QBuilder.d.ts +3 -3
  35. package/dist/query/QuerySet.d.ts +58 -18
  36. package/dist/query/compiler/QueryCompiler.d.ts +13 -4
  37. package/dist/query/domain/CompiledQuery.d.ts +169 -2
  38. package/dist/query/domain/FilterInput.d.ts +1 -1
  39. package/dist/query/domain/FilterKey.d.ts +4 -2
  40. package/dist/query/domain/QNode.d.ts +4 -4
  41. package/dist/query/domain/QuerySetState.d.ts +3 -3
  42. package/dist/query/domain/RelationMeta.d.ts +9 -0
  43. package/dist/query/domain/RelationTyping.d.ts +47 -0
  44. package/dist/query/domain/TableMetaFactory.d.ts +1 -14
  45. package/dist/query/domain/index.d.ts +1 -1
  46. package/dist/query/domain/internal/InternalPrefetchQueryKind.d.ts +20 -0
  47. package/dist/query/index.d.ts +1 -0
  48. package/dist/query/index.js +3 -2
  49. package/dist/query/internal/isQNodeLike.d.ts +3 -0
  50. package/dist/query/planning/QueryPlanner.d.ts +1 -1
  51. package/dist/{query-C6So1r6H.js → query-DUZnBFhf.js} +474 -156
  52. package/dist/query-DUZnBFhf.js.map +1 -0
  53. package/dist/registerModelObjects-DxlBfuUN.js +797 -0
  54. package/dist/registerModelObjects-DxlBfuUN.js.map +1 -0
  55. package/dist/runtime/TangoRuntime.d.ts +9 -0
  56. package/dist/runtime/index.d.ts +3 -2
  57. package/dist/runtime/index.js +7 -6
  58. package/dist/runtime/internal/SqliteDBClientProvider.d.ts +3 -0
  59. package/dist/{runtime-ByXbpVBS.js → runtime-1H88J3nN.js} +3 -3
  60. package/dist/runtime-1H88J3nN.js.map +1 -0
  61. package/dist/transaction/index.js +5 -4
  62. package/dist/{transaction-Cs0Z9tbW.js → transaction-ZhfDf-f8.js} +2 -2
  63. package/dist/{transaction-Cs0Z9tbW.js.map → transaction-ZhfDf-f8.js.map} +1 -1
  64. package/dist/validation/SQLValidationEngine.d.ts +22 -5
  65. package/dist/validation/SqlValidationPlan.d.ts +5 -4
  66. package/dist/validation/internal/InternalSqlValidationPlanKind.d.ts +25 -0
  67. package/dist/validation/internal/InternalValidatedFilterDescriptorKind.d.ts +4 -0
  68. package/package.json +6 -6
  69. package/dist/PostgresAdapter-BFdo_nIt.js +0 -4
  70. package/dist/PostgresAdapter-CMiEpHya.js +0 -49
  71. package/dist/PostgresAdapter-CMiEpHya.js.map +0 -1
  72. package/dist/PostgresClient-BQJZfEOT.js +0 -68
  73. package/dist/PostgresClient-BQJZfEOT.js.map +0 -1
  74. package/dist/SqliteAdapter-A-P9zUhP.js +0 -4
  75. package/dist/SqliteAdapter-CeqhyrPC.js +0 -44
  76. package/dist/SqliteAdapter-CeqhyrPC.js.map +0 -1
  77. package/dist/SqliteClient-CjOK9-ki.js.map +0 -1
  78. package/dist/defaultRuntime-BPK9kWEW.js.map +0 -1
  79. package/dist/manager-C6oJ2tAF.js +0 -13
  80. package/dist/manager-C6oJ2tAF.js.map +0 -1
  81. package/dist/query-C6So1r6H.js.map +0 -1
  82. package/dist/registerModelObjects-BKMpfc4Z.js +0 -263
  83. package/dist/registerModelObjects-BKMpfc4Z.js.map +0 -1
  84. package/dist/runtime-ByXbpVBS.js.map +0 -1
@@ -1,75 +1,8 @@
1
1
  import { __export } from "./chunk-DLY2FNSh.js";
2
- import { SqlSafetyEngine, getLogger, isError } from "@danceroutine/tango-core";
2
+ import { InternalDialect } from "./InternalDialect-ClSaUNso.js";
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/InternalRelationKind.ts
6
- const InternalRelationKind = {
7
- HAS_MANY: "hasMany",
8
- BELONGS_TO: "belongsTo",
9
- HAS_ONE: "hasOne",
10
- MANY_TO_MANY: "manyToMany"
11
- };
12
-
13
- //#endregion
14
- //#region src/query/domain/TableMetaFactory.ts
15
- var TableMetaFactory = class TableMetaFactory {
16
- static create(model) {
17
- const owner = model.metadata.key ? ModelRegistry.getOwner(model) : undefined;
18
- const cache = new Map();
19
- return TableMetaFactory.createWithCache(model, owner, cache);
20
- }
21
- static createWithCache(model, owner, cache) {
22
- if (model.metadata.key) {
23
- const cached = cache.get(model.metadata.key);
24
- if (cached) return cached;
25
- }
26
- const pkField = model.metadata.fields.find((field) => field.primaryKey);
27
- if (!pkField) throw new Error(`Model '${model.metadata.name}' cannot attach a manager without a primary key field.`);
28
- const tableMeta = {
29
- modelKey: model.metadata.key,
30
- table: model.metadata.table,
31
- pk: pkField.name,
32
- columns: Object.fromEntries(model.metadata.fields.map((field) => [field.name, field.type]))
33
- };
34
- if (model.metadata.key) cache.set(model.metadata.key, tableMeta);
35
- if (!model.metadata.key || !owner) return tableMeta;
36
- const relations = owner.getResolvedRelationGraph().byModel.get(model.metadata.key);
37
- if (!relations || relations.size === 0) return tableMeta;
38
- tableMeta.relations = Object.fromEntries(Array.from(relations.entries()).filter(([, relation]) => relation.capabilities.queryable && relation.capabilities.hydratable).map(([name, relation]) => {
39
- const targetModel = owner.getByKey(relation.targetModelKey);
40
- const targetMeta = TableMetaFactory.createWithCache(targetModel, owner, cache);
41
- const { queryable, hydratable } = relation.capabilities;
42
- const isSingleRelation = relation.kind === InternalRelationKind.BELONGS_TO || relation.kind === InternalRelationKind.HAS_ONE;
43
- const sourceKey = relation.kind === InternalRelationKind.BELONGS_TO ? relation.localFieldName : relation.targetFieldName;
44
- const targetKey = relation.kind === InternalRelationKind.BELONGS_TO ? relation.targetFieldName : relation.localFieldName;
45
- const targetColumns = Object.fromEntries(targetModel.metadata.fields.map((field) => [field.name, field.type]));
46
- const capabilities = {
47
- queryable,
48
- hydratable,
49
- joinable: isSingleRelation && queryable && hydratable,
50
- prefetchable: queryable && hydratable
51
- };
52
- return [name, {
53
- edgeId: relation.edgeId,
54
- sourceModelKey: relation.sourceModelKey,
55
- targetModelKey: relation.targetModelKey,
56
- kind: relation.kind,
57
- cardinality: isSingleRelation ? "single" : "many",
58
- capabilities,
59
- table: targetModel.metadata.table,
60
- sourceKey,
61
- targetKey,
62
- targetPrimaryKey: targetMeta.pk,
63
- targetColumns,
64
- alias: relation.alias,
65
- targetMeta
66
- }];
67
- }));
68
- return tableMeta;
69
- }
70
- };
71
-
72
- //#endregion
73
6
  //#region src/query/domain/RelationMeta.ts
74
7
  const InternalRelationHydrationLoadMode = {
75
8
  JOIN: "join",
@@ -77,10 +10,10 @@ const InternalRelationHydrationLoadMode = {
77
10
  };
78
11
 
79
12
  //#endregion
80
- //#region src/query/domain/internal/InternalDialect.ts
81
- const InternalDialect = {
82
- POSTGRES: "postgres",
83
- SQLITE: "sqlite"
13
+ //#region src/query/domain/internal/InternalPrefetchQueryKind.ts
14
+ const InternalPrefetchQueryKind = {
15
+ DIRECT: "direct",
16
+ MANY_TO_MANY: "manyToMany"
84
17
  };
85
18
 
86
19
  //#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 = {};
@@ -821,6 +982,19 @@ const InternalDirection = {
821
982
  DESC: "desc"
822
983
  };
823
984
 
985
+ //#endregion
986
+ //#region src/query/internal/isQNodeLike.ts
987
+ function isQNodeLike(value) {
988
+ if (typeof value !== "object" || value === null || "__tangoBrand" in value) return false;
989
+ switch (value.kind) {
990
+ case InternalQNodeType.ATOM: return "where" in value;
991
+ case InternalQNodeType.AND:
992
+ case InternalQNodeType.OR: return Array.isArray(value.nodes);
993
+ case InternalQNodeType.NOT: return "node" in value;
994
+ default: return false;
995
+ }
996
+ }
997
+
824
998
  //#endregion
825
999
  //#region src/query/QBuilder.ts
826
1000
  var QBuilder = class QBuilder {
@@ -860,7 +1034,7 @@ var QBuilder = class QBuilder {
860
1034
  };
861
1035
  }
862
1036
  static wrapNode(input) {
863
- if (input.kind) return input;
1037
+ if (isQNodeLike(input)) return input;
864
1038
  return {
865
1039
  kind: InternalQNodeType.ATOM,
866
1040
  where: input
@@ -885,17 +1059,45 @@ var QuerySet = class QuerySet {
885
1059
  return typeof value === "object" && value !== null && value.__tangoBrand === QuerySet.BRAND;
886
1060
  }
887
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
+ }
1082
+ static invertOrderSpec(order) {
1083
+ if (!order?.length) return [];
1084
+ return order.map((spec) => ({
1085
+ by: spec.by,
1086
+ dir: spec.dir === InternalDirection.ASC ? InternalDirection.DESC : InternalDirection.ASC
1087
+ }));
1088
+ }
1089
+ /**
888
1090
  * Add a filter expression to the query.
889
1091
  *
890
1092
  * Multiple `filter()` calls are composed with `AND`.
891
1093
  */
892
1094
  filter(q) {
893
- const wrapped = q.kind ? q : {
1095
+ const wrapped = isQNodeLike(q) ? q : {
894
1096
  kind: InternalQNodeType.ATOM,
895
1097
  where: q
896
1098
  };
897
1099
  const merged = this.state.q ? QBuilder.and(this.state.q, wrapped) : wrapped;
898
- return new QuerySet(this.executor, {
1100
+ return this.spawn({
899
1101
  ...this.state,
900
1102
  q: merged
901
1103
  });
@@ -906,12 +1108,12 @@ var QuerySet = class QuerySet {
906
1108
  * Exclusions are translated to `NOT (...)` predicates.
907
1109
  */
908
1110
  exclude(q) {
909
- const wrapped = q.kind ? q : {
1111
+ const wrapped = isQNodeLike(q) ? q : {
910
1112
  kind: InternalQNodeType.ATOM,
911
1113
  where: q
912
1114
  };
913
1115
  const excludes = [...this.state.excludes ?? [], wrapped];
914
- return new QuerySet(this.executor, {
1116
+ return this.spawn({
915
1117
  ...this.state,
916
1118
  excludes
917
1119
  });
@@ -920,27 +1122,16 @@ var QuerySet = class QuerySet {
920
1122
  * Apply ordering tokens such as `'name'` or `'-createdAt'`.
921
1123
  */
922
1124
  orderBy(...tokens) {
923
- const order = tokens.map((t) => {
924
- const str = String(t);
925
- if (str.startsWith("-")) return {
926
- by: str.slice(1),
927
- dir: InternalDirection.DESC
928
- };
929
- return {
930
- by: t,
931
- dir: InternalDirection.ASC
932
- };
933
- });
934
- return new QuerySet(this.executor, {
1125
+ return this.spawn({
935
1126
  ...this.state,
936
- order
1127
+ order: QuerySet.buildOrderSpecs(tokens)
937
1128
  });
938
1129
  }
939
1130
  /**
940
1131
  * Limit the maximum number of rows returned.
941
1132
  */
942
1133
  limit(n) {
943
- return new QuerySet(this.executor, {
1134
+ return this.spawn({
944
1135
  ...this.state,
945
1136
  limit: n
946
1137
  });
@@ -949,13 +1140,13 @@ var QuerySet = class QuerySet {
949
1140
  * Skip the first `n` rows.
950
1141
  */
951
1142
  offset(n) {
952
- return new QuerySet(this.executor, {
1143
+ return this.spawn({
953
1144
  ...this.state,
954
1145
  offset: n
955
1146
  });
956
1147
  }
957
1148
  select(fields) {
958
- return new QuerySet(this.executor, {
1149
+ return this.spawn({
959
1150
  ...this.state,
960
1151
  select: [...fields]
961
1152
  });
@@ -970,7 +1161,7 @@ var QuerySet = class QuerySet {
970
1161
  * path keys for applications that keep the app-local registry current.
971
1162
  */
972
1163
  selectRelated(...rels) {
973
- return new QuerySet(this.executor, {
1164
+ return this.spawn({
974
1165
  ...this.state,
975
1166
  selectRelated: [...rels]
976
1167
  });
@@ -984,11 +1175,14 @@ var QuerySet = class QuerySet {
984
1175
  * app-local registry current.
985
1176
  */
986
1177
  prefetchRelated(...rels) {
987
- return new QuerySet(this.executor, {
1178
+ return this.spawn({
988
1179
  ...this.state,
989
1180
  prefetchRelated: [...rels]
990
1181
  });
991
1182
  }
1183
+ all() {
1184
+ return this.spawn({ ...this.state });
1185
+ }
992
1186
  async fetch(shape) {
993
1187
  const baseResult = await this.getOrCreateEvaluationCache();
994
1188
  if (!shape) return baseResult;
@@ -998,9 +1192,10 @@ var QuerySet = class QuerySet {
998
1192
  /**
999
1193
  * Async iterable surface for `for await (... of queryset)`.
1000
1194
  *
1001
- * Evaluates this queryset on first use by awaiting {@link QuerySet.fetch} without arguments, then
1002
- * yields each element from that {@link QueryResult}. Later async iterations over the same queryset
1003
- * 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.
1004
1199
  */
1005
1200
  async *[Symbol.asyncIterator]() {
1006
1201
  const result = await this.fetch();
@@ -1012,11 +1207,40 @@ var QuerySet = class QuerySet {
1012
1207
  for (const row of result) return row;
1013
1208
  return null;
1014
1209
  }
1210
+ async first(shape) {
1211
+ return this.fetchOne(shape);
1212
+ }
1213
+ async last(shape) {
1214
+ if (this.state.limit !== undefined || this.state.offset !== undefined) {
1215
+ const page = await this.fetch();
1216
+ const row = page.at(-1);
1217
+ if (!row) return null;
1218
+ return this.shapeFetchedRow(row, shape);
1219
+ }
1220
+ const invertedOrder = QuerySet.invertOrderSpec(this.state.order);
1221
+ const effectiveOrder = invertedOrder.length > 0 ? invertedOrder : [{
1222
+ by: this.executor.meta.pk,
1223
+ dir: InternalDirection.DESC
1224
+ }];
1225
+ const qs = this.spawn({
1226
+ ...this.state,
1227
+ order: effectiveOrder
1228
+ });
1229
+ return qs.limit(1).fetchOne(shape);
1230
+ }
1231
+ async get(q, shape) {
1232
+ const limited = this.filter(q).limit(2);
1233
+ const page = await limited.fetch();
1234
+ const rows = page.items;
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`);
1237
+ return this.shapeFetchedRow(rows[0], shape);
1238
+ }
1015
1239
  /**
1016
1240
  * Execute a `COUNT(*)` query for the current filtered state.
1017
1241
  */
1018
1242
  async count() {
1019
- const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
1243
+ const compiler = new QueryCompiler(this.executor.meta, this.executor.adapter);
1020
1244
  const compiled = compiler.compile(this.withoutHydrationState());
1021
1245
  const countQuery = `SELECT COUNT(*) as count FROM (${compiled.sql}) AS tango_count_subquery`;
1022
1246
  const rows = await this.executor.client.query(countQuery, compiled.params);
@@ -1029,6 +1253,12 @@ var QuerySet = class QuerySet {
1029
1253
  const count = await this.count();
1030
1254
  return count > 0;
1031
1255
  }
1256
+ shapeFetchedRow(row, shape) {
1257
+ if (!shape) return row;
1258
+ if (typeof shape === "function") return shape(row);
1259
+ const normalizedRow = this.normalizeHydratedRowsForParserShape([row])[0] ?? row;
1260
+ return shape.parse(normalizedRow);
1261
+ }
1032
1262
  getOrCreateEvaluationCache() {
1033
1263
  if (!this.evaluationCache) this.evaluationCache = this.evaluateRows().catch((error) => {
1034
1264
  this.evaluationCache = undefined;
@@ -1037,15 +1267,23 @@ var QuerySet = class QuerySet {
1037
1267
  return this.evaluationCache;
1038
1268
  }
1039
1269
  async evaluateRows() {
1040
- const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
1270
+ const compiler = new QueryCompiler(this.executor.meta, this.executor.adapter);
1041
1271
  const compiled = compiler.compile(this.state);
1042
1272
  const rows = await this.executor.run(compiled);
1043
- 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);
1044
1276
  const projectedRows = hydratedRows;
1045
1277
  return new QueryResult(projectedRows);
1046
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
+ }
1047
1285
  normalizeHydratedRowsForParserShape(rows) {
1048
- if (this.executor.dialect !== InternalDialect.SQLITE) return [...rows];
1286
+ if (this.executor.adapter.dialect !== InternalDialect.SQLITE) return [...rows];
1049
1287
  const booleanColumns = Object.entries(this.executor.meta.columns).filter(([, value]) => this.isBooleanColumnType(value)).map(([column]) => column);
1050
1288
  if (booleanColumns.length === 0) return [...rows];
1051
1289
  return rows.map((row) => this.normalizeBooleanColumns(row, booleanColumns));
@@ -1053,15 +1291,29 @@ var QuerySet = class QuerySet {
1053
1291
  async hydrateRows(rows, compiled) {
1054
1292
  if (!compiled.hydrationPlan) return rows;
1055
1293
  const hydratedRows = rows.map((row) => ({ ...row }));
1294
+ this.attachRootRecordAccessors(hydratedRows);
1056
1295
  const canonicalEntities = new Map();
1057
1296
  const queuedJoinPrefetchOwners = new Map();
1058
- const compiler = new QueryCompiler(this.executor.meta, this.executor.dialect);
1297
+ const compiler = new QueryCompiler(this.executor.meta, this.executor.adapter);
1059
1298
  for (const row of hydratedRows) this.hydrateJoinNodesForOwner(row, row, compiled.hydrationPlan.joinNodes, canonicalEntities, queuedJoinPrefetchOwners);
1060
1299
  for (const node of compiled.hydrationPlan.prefetchNodes) await this.hydratePrefetchNode(node, hydratedRows, canonicalEntities, compiler);
1061
1300
  for (const [node, owners] of queuedJoinPrefetchOwners.entries()) await this.hydratePrefetchNode(node, [...owners], canonicalEntities, compiler);
1062
1301
  for (const row of hydratedRows) for (const alias of compiled.hydrationPlan.hiddenRootAliases) delete row[alias];
1063
1302
  return hydratedRows;
1064
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
+ }
1065
1317
  hydrateJoinNodesForOwner(owner, rawRow, nodes, canonicalEntities, queuedJoinPrefetchOwners) {
1066
1318
  for (const node of nodes) {
1067
1319
  if (!node.join) continue;
@@ -1094,28 +1346,81 @@ var QuerySet = class QuerySet {
1094
1346
  if (owners.length === 0) return;
1095
1347
  const groupedOwners = this.groupOwnersByAccessor(owners, node.ownerSourceAccessor);
1096
1348
  const sourceValues = [...groupedOwners.keys()];
1097
- 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;
1098
1351
  if (sourceValues.length === 0) return;
1099
- const compiledPrefetch = compiler.compilePrefetch(node, sourceValues);
1100
- const result = await this.executor.client.query(compiledPrefetch.sql, compiledPrefetch.params);
1101
- 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
+ }
1102
1398
  const canonicalChildren = new Map();
1103
- for (const rawResultRow of result.rows) {
1104
- const normalized = this.normalizeTargetRow(compiledPrefetch, rawResultRow);
1105
- const canonical = this.canonicalizeEntity(node, normalized, canonicalEntities);
1106
- this.hydrateJoinNodesForOwner(canonical, normalized, node.joinChildren, canonicalEntities);
1107
- const key = normalized[compiledPrefetch.targetKey];
1108
- if (typeof key !== "string" && typeof key !== "number") continue;
1109
- const bucket = groupedTargets.get(key) ?? [];
1110
- bucket.push(canonical);
1111
- groupedTargets.set(key, bucket);
1112
- const childPrimaryKey = canonical[node.targetPrimaryKey];
1113
- 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
+ }
1114
1413
  }
1115
- for (const [sourceValue, grouped] of groupedTargets.entries()) for (const owner of groupedOwners.get(sourceValue) ?? []) owner[node.relationName] = node.cardinality === InternalRelationHydrationCardinality.MANY ? grouped : grouped[0];
1116
1414
  const childOwners = [...canonicalChildren.values()];
1117
1415
  for (const childNode of node.prefetchChildren) await this.hydratePrefetchNode(childNode, childOwners, canonicalEntities, compiler);
1118
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
+ }
1119
1424
  groupOwnersByAccessor(owners, accessor) {
1120
1425
  const grouped = new Map();
1121
1426
  for (const owner of owners) {
@@ -1138,10 +1443,11 @@ var QuerySet = class QuerySet {
1138
1443
  }
1139
1444
  byModel.set(primaryKeyValue, row);
1140
1445
  canonicalEntities.set(node.targetModelKey, byModel);
1446
+ this.executor.attachPersistedRecordAccessors?.(row, node.targetModelKey);
1141
1447
  return row;
1142
1448
  }
1143
1449
  normalizeTargetRow(prefetch, row) {
1144
- if (this.executor.dialect !== InternalDialect.SQLITE) return row;
1450
+ if (this.executor.adapter.dialect !== InternalDialect.SQLITE) return row;
1145
1451
  let normalized = null;
1146
1452
  for (const [column, type] of Object.entries(prefetch.targetColumns)) {
1147
1453
  if (!this.isBooleanColumnType(type)) continue;
@@ -1153,7 +1459,7 @@ var QuerySet = class QuerySet {
1153
1459
  return normalized ?? row;
1154
1460
  }
1155
1461
  normalizeColumnValue(columnType, value) {
1156
- 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;
1157
1463
  }
1158
1464
  isBooleanColumnType(value) {
1159
1465
  return typeof value === "string" && ["bool", "boolean"].includes(value.trim().toLowerCase());
@@ -1180,10 +1486,22 @@ var QuerySet = class QuerySet {
1180
1486
  }
1181
1487
  };
1182
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
+
1183
1500
  //#endregion
1184
1501
  //#region src/query/index.ts
1185
1502
  var query_exports = {};
1186
1503
  __export(query_exports, {
1504
+ ModelQuerySet: () => ModelQuerySet,
1187
1505
  Q: () => QBuilder,
1188
1506
  QBuilder: () => QBuilder,
1189
1507
  QueryCompiler: () => QueryCompiler,
@@ -1194,5 +1512,5 @@ __export(query_exports, {
1194
1512
  });
1195
1513
 
1196
1514
  //#endregion
1197
- export { OrmSqlSafetyAdapter, QBuilder, QueryCompiler, QueryResult, QuerySet, TableMetaFactory, compiler_exports, domain_exports, query_exports };
1198
- //# sourceMappingURL=query-C6So1r6H.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