metal-orm 1.0.42 → 1.0.44

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 (122) hide show
  1. package/README.md +195 -37
  2. package/dist/index.cjs +1014 -538
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1267 -371
  5. package/dist/index.d.ts +1267 -371
  6. package/dist/index.js +1012 -536
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -2
  9. package/scripts/run-eslint.mjs +34 -0
  10. package/src/codegen/typescript.ts +32 -15
  11. package/src/core/ast/adapters.ts +8 -2
  12. package/src/core/ast/builders.ts +105 -76
  13. package/src/core/ast/expression-builders.ts +430 -392
  14. package/src/core/ast/expression-nodes.ts +14 -5
  15. package/src/core/ast/expression-visitor.ts +56 -14
  16. package/src/core/ast/helpers.ts +23 -0
  17. package/src/core/ast/join-node.ts +18 -2
  18. package/src/core/ast/query.ts +6 -6
  19. package/src/core/ast/window-functions.ts +10 -2
  20. package/src/core/ddl/dialects/base-schema-dialect.ts +37 -4
  21. package/src/core/ddl/dialects/index.ts +1 -0
  22. package/src/core/ddl/dialects/mssql-schema-dialect.ts +5 -0
  23. package/src/core/ddl/dialects/mysql-schema-dialect.ts +3 -0
  24. package/src/core/ddl/dialects/postgres-schema-dialect.ts +14 -1
  25. package/src/core/ddl/dialects/render-reference.test.ts +69 -0
  26. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +10 -0
  27. package/src/core/ddl/introspect/catalogs/index.ts +1 -0
  28. package/src/core/ddl/introspect/catalogs/postgres.ts +2 -0
  29. package/src/core/ddl/introspect/context.ts +6 -0
  30. package/src/core/ddl/introspect/functions/postgres.ts +13 -0
  31. package/src/core/ddl/introspect/mssql.ts +53 -8
  32. package/src/core/ddl/introspect/mysql.ts +32 -6
  33. package/src/core/ddl/introspect/postgres.ts +102 -34
  34. package/src/core/ddl/introspect/registry.ts +14 -0
  35. package/src/core/ddl/introspect/run-select.ts +19 -4
  36. package/src/core/ddl/introspect/sqlite.ts +78 -11
  37. package/src/core/ddl/introspect/types.ts +0 -1
  38. package/src/core/ddl/introspect/utils.ts +21 -3
  39. package/src/core/ddl/naming-strategy.ts +6 -0
  40. package/src/core/ddl/schema-dialect.ts +20 -6
  41. package/src/core/ddl/schema-diff.ts +22 -0
  42. package/src/core/ddl/schema-generator.ts +26 -12
  43. package/src/core/ddl/schema-plan-executor.ts +6 -0
  44. package/src/core/ddl/schema-types.ts +6 -0
  45. package/src/core/ddl/sql-writing.ts +4 -4
  46. package/src/core/dialect/abstract.ts +19 -7
  47. package/src/core/dialect/base/function-table-formatter.ts +3 -2
  48. package/src/core/dialect/base/join-compiler.ts +5 -3
  49. package/src/core/dialect/base/returning-strategy.ts +1 -0
  50. package/src/core/dialect/base/sql-dialect.ts +3 -3
  51. package/src/core/dialect/mssql/functions.ts +24 -25
  52. package/src/core/dialect/mssql/index.ts +1 -4
  53. package/src/core/dialect/mysql/functions.ts +0 -1
  54. package/src/core/dialect/postgres/functions.ts +33 -34
  55. package/src/core/dialect/postgres/index.ts +1 -0
  56. package/src/core/dialect/sqlite/functions.ts +18 -19
  57. package/src/core/dialect/sqlite/index.ts +2 -0
  58. package/src/core/execution/db-executor.ts +1 -1
  59. package/src/core/execution/executors/mysql-executor.ts +2 -2
  60. package/src/core/execution/executors/postgres-executor.ts +1 -1
  61. package/src/core/execution/pooling/pool.ts +12 -5
  62. package/src/core/functions/datetime.ts +58 -34
  63. package/src/core/functions/numeric.ts +96 -31
  64. package/src/core/functions/standard-strategy.ts +35 -0
  65. package/src/core/functions/text.ts +84 -23
  66. package/src/core/functions/types.ts +23 -8
  67. package/src/decorators/bootstrap.ts +42 -11
  68. package/src/decorators/column.ts +20 -11
  69. package/src/decorators/decorator-metadata.ts +30 -9
  70. package/src/decorators/entity.ts +29 -5
  71. package/src/decorators/index.ts +3 -0
  72. package/src/decorators/relations.ts +34 -11
  73. package/src/orm/als.ts +34 -9
  74. package/src/orm/entity-context.ts +62 -8
  75. package/src/orm/entity-meta.ts +8 -8
  76. package/src/orm/entity-metadata.ts +131 -16
  77. package/src/orm/entity.ts +28 -29
  78. package/src/orm/execute.ts +19 -4
  79. package/src/orm/hydration.ts +42 -39
  80. package/src/orm/identity-map.ts +1 -1
  81. package/src/orm/lazy-batch.ts +74 -104
  82. package/src/orm/orm-session.ts +24 -23
  83. package/src/orm/orm.ts +2 -5
  84. package/src/orm/relation-change-processor.ts +12 -11
  85. package/src/orm/relations/belongs-to.ts +11 -11
  86. package/src/orm/relations/has-many.ts +54 -10
  87. package/src/orm/relations/has-one.ts +8 -7
  88. package/src/orm/relations/many-to-many.ts +13 -13
  89. package/src/orm/runtime-types.ts +4 -4
  90. package/src/orm/save-graph.ts +31 -25
  91. package/src/orm/unit-of-work.ts +17 -17
  92. package/src/query/index.ts +74 -0
  93. package/src/query/target.ts +46 -0
  94. package/src/query-builder/delete-query-state.ts +30 -0
  95. package/src/query-builder/delete.ts +64 -18
  96. package/src/query-builder/hydration-manager.ts +52 -5
  97. package/src/query-builder/insert-query-state.ts +30 -0
  98. package/src/query-builder/insert.ts +58 -10
  99. package/src/query-builder/query-ast-service.ts +7 -2
  100. package/src/query-builder/query-resolution.ts +78 -0
  101. package/src/query-builder/raw-column-parser.ts +7 -1
  102. package/src/query-builder/relation-alias.ts +7 -0
  103. package/src/query-builder/relation-conditions.ts +61 -48
  104. package/src/query-builder/relation-service.ts +68 -63
  105. package/src/query-builder/relation-utils.ts +3 -0
  106. package/src/query-builder/select/cte-facet.ts +40 -0
  107. package/src/query-builder/select/from-facet.ts +80 -0
  108. package/src/query-builder/select/join-facet.ts +62 -0
  109. package/src/query-builder/select/predicate-facet.ts +103 -0
  110. package/src/query-builder/select/projection-facet.ts +69 -0
  111. package/src/query-builder/select/relation-facet.ts +81 -0
  112. package/src/query-builder/select/setop-facet.ts +36 -0
  113. package/src/query-builder/select-helpers.ts +15 -2
  114. package/src/query-builder/select-query-builder-deps.ts +19 -1
  115. package/src/query-builder/select-query-state.ts +2 -1
  116. package/src/query-builder/select.ts +795 -1163
  117. package/src/query-builder/update-query-state.ts +52 -0
  118. package/src/query-builder/update.ts +69 -18
  119. package/src/schema/column.ts +26 -26
  120. package/src/schema/table-guards.ts +31 -0
  121. package/src/schema/table.ts +47 -18
  122. package/src/schema/types.ts +22 -22
package/src/orm/entity.ts CHANGED
@@ -13,7 +13,7 @@ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
13
13
  /**
14
14
  * Type representing an array of database rows.
15
15
  */
16
- type Rows = Record<string, any>[];
16
+ type Rows = Record<string, unknown>[];
17
17
 
18
18
  /**
19
19
  * Caches relation loader results across entities of the same type.
@@ -23,8 +23,8 @@ type Rows = Record<string, any>[];
23
23
  * @param factory - The factory function to create the cache
24
24
  * @returns Promise with the cached relation data
25
25
  */
26
- const relationLoaderCache = <T extends Map<string, any>>(
27
- meta: EntityMeta<any>,
26
+ const relationLoaderCache = <T extends Map<string, unknown>>(
27
+ meta: EntityMeta<TableDef>,
28
28
  relationName: string,
29
29
  factory: () => Promise<T>
30
30
  ): Promise<T> => {
@@ -68,10 +68,10 @@ export const createEntityProxy = <
68
68
  >(
69
69
  ctx: EntityContext,
70
70
  table: TTable,
71
- row: Record<string, any>,
71
+ row: Record<string, unknown>,
72
72
  lazyRelations: TLazy[] = [] as TLazy[]
73
73
  ): EntityInstance<TTable> => {
74
- const target: Record<string, any> = { ...row };
74
+ const target: Record<string, unknown> = { ...row };
75
75
  const meta: EntityMeta<TTable> = {
76
76
  ctx,
77
77
  table,
@@ -87,8 +87,7 @@ export const createEntityProxy = <
87
87
  writable: false
88
88
  });
89
89
 
90
- let proxy: EntityInstance<TTable>;
91
- const handler: ProxyHandler<any> = {
90
+ const handler: ProxyHandler<object> = {
92
91
  get(targetObj, prop, receiver) {
93
92
  if (prop === ENTITY_META) {
94
93
  return meta;
@@ -96,7 +95,7 @@ export const createEntityProxy = <
96
95
 
97
96
  if (prop === '$load') {
98
97
  return async (relationName: keyof RelationMap<TTable>) => {
99
- const wrapper = getRelationWrapper(meta, relationName as string, proxy);
98
+ const wrapper = getRelationWrapper(meta as unknown as EntityMeta<TableDef>, relationName as string, receiver);
100
99
  if (wrapper && typeof wrapper.load === 'function') {
101
100
  return wrapper.load();
102
101
  }
@@ -105,7 +104,7 @@ export const createEntityProxy = <
105
104
  }
106
105
 
107
106
  if (typeof prop === 'string' && table.relations[prop]) {
108
- return getRelationWrapper(meta, prop, proxy);
107
+ return getRelationWrapper(meta as unknown as EntityMeta<TableDef>, prop, receiver);
109
108
  }
110
109
 
111
110
  return Reflect.get(targetObj, prop, receiver);
@@ -114,13 +113,13 @@ export const createEntityProxy = <
114
113
  set(targetObj, prop, value, receiver) {
115
114
  const result = Reflect.set(targetObj, prop, value, receiver);
116
115
  if (typeof prop === 'string' && table.columns[prop]) {
117
- ctx.markDirty(proxy);
116
+ ctx.markDirty(receiver);
118
117
  }
119
118
  return result;
120
119
  }
121
120
  };
122
121
 
123
- proxy = new Proxy(target, handler) as EntityInstance<TTable>;
122
+ const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
124
123
  populateHydrationCache(proxy, row, meta);
125
124
  return proxy;
126
125
  };
@@ -141,7 +140,7 @@ export const createEntityFromRow = <
141
140
  >(
142
141
  ctx: EntityContext,
143
142
  table: TTable,
144
- row: Record<string, any>,
143
+ row: Record<string, unknown>,
145
144
  lazyRelations: (keyof RelationMap<TTable>)[] = []
146
145
  ): TResult => {
147
146
  const pkName = findPrimaryKey(table);
@@ -176,8 +175,8 @@ const toKey = (value: unknown): string => (value === null || value === undefined
176
175
  * @param meta - The entity metadata
177
176
  */
178
177
  const populateHydrationCache = <TTable extends TableDef>(
179
- entity: any,
180
- row: Record<string, any>,
178
+ entity: Record<string, unknown>,
179
+ row: Record<string, unknown>,
181
180
  meta: EntityMeta<TTable>
182
181
  ): void => {
183
182
  for (const relationName of Object.keys(meta.table.relations)) {
@@ -188,8 +187,8 @@ const populateHydrationCache = <TTable extends TableDef>(
188
187
  const rootValue = entity[localKey];
189
188
  if (rootValue === undefined || rootValue === null) continue;
190
189
  if (!data || typeof data !== 'object') continue;
191
- const cache = new Map<string, Record<string, any>>();
192
- cache.set(toKey(rootValue), data as Record<string, any>);
190
+ const cache = new Map<string, Record<string, unknown>>();
191
+ cache.set(toKey(rootValue), data as Record<string, unknown>);
193
192
  meta.relationHydration.set(relationName, cache);
194
193
  meta.relationCache.set(relationName, Promise.resolve(cache));
195
194
  continue;
@@ -210,7 +209,7 @@ const populateHydrationCache = <TTable extends TableDef>(
210
209
 
211
210
  if (relation.type === RelationKinds.BelongsTo) {
212
211
  const targetKey = relation.localKey || findPrimaryKey(relation.target);
213
- const cache = new Map<string, Record<string, any>>();
212
+ const cache = new Map<string, Record<string, unknown>>();
214
213
  for (const item of data) {
215
214
  const pkValue = item[targetKey];
216
215
  if (pkValue === undefined || pkValue === null) continue;
@@ -232,18 +231,18 @@ const populateHydrationCache = <TTable extends TableDef>(
232
231
  * @returns The relation wrapper or undefined
233
232
  */
234
233
  const getRelationWrapper = (
235
- meta: EntityMeta<any>,
234
+ meta: EntityMeta<TableDef>,
236
235
  relationName: string,
237
- owner: any
238
- ): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
236
+ owner: unknown
237
+ ): HasManyCollection<unknown> | HasOneReference<unknown> | BelongsToReference<unknown> | ManyToManyCollection<unknown> | undefined => {
239
238
  if (meta.relationWrappers.has(relationName)) {
240
- return meta.relationWrappers.get(relationName) as HasManyCollection<any>;
239
+ return meta.relationWrappers.get(relationName) as HasManyCollection<unknown>;
241
240
  }
242
241
 
243
242
  const relation = meta.table.relations[relationName];
244
243
  if (!relation) return undefined;
245
244
 
246
- const wrapper = instantiateWrapper(meta, relationName, relation as any, owner);
245
+ const wrapper = instantiateWrapper(meta, relationName, relation, owner);
247
246
  if (wrapper) {
248
247
  meta.relationWrappers.set(relationName, wrapper);
249
248
  }
@@ -260,11 +259,11 @@ const getRelationWrapper = (
260
259
  * @returns The relation wrapper or undefined
261
260
  */
262
261
  const instantiateWrapper = (
263
- meta: EntityMeta<any>,
262
+ meta: EntityMeta<TableDef>,
264
263
  relationName: string,
265
264
  relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
266
- owner: any
267
- ): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
265
+ owner: unknown
266
+ ): HasManyCollection<unknown> | HasOneReference<unknown> | BelongsToReference<unknown> | ManyToManyCollection<unknown> | undefined => {
268
267
  switch (relation.type) {
269
268
  case RelationKinds.HasOne: {
270
269
  const hasOne = relation as HasOneRelation;
@@ -280,7 +279,7 @@ const instantiateWrapper = (
280
279
  hasOne,
281
280
  meta.table,
282
281
  loader,
283
- (row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
282
+ (row: Record<string, unknown>) => createEntityFromRow(meta.ctx, hasOne.target, row),
284
283
  localKey
285
284
  );
286
285
  }
@@ -298,7 +297,7 @@ const instantiateWrapper = (
298
297
  hasMany,
299
298
  meta.table,
300
299
  loader,
301
- (row: Record<string, any>) => createEntityFromRow(meta.ctx, relation.target, row),
300
+ (row: Record<string, unknown>) => createEntityFromRow(meta.ctx, relation.target, row),
302
301
  localKey
303
302
  );
304
303
  }
@@ -316,7 +315,7 @@ const instantiateWrapper = (
316
315
  belongsTo,
317
316
  meta.table,
318
317
  loader,
319
- (row: Record<string, any>) => createEntityFromRow(meta.ctx, relation.target, row),
318
+ (row: Record<string, unknown>) => createEntityFromRow(meta.ctx, relation.target, row),
320
319
  targetKey
321
320
  );
322
321
  }
@@ -334,7 +333,7 @@ const instantiateWrapper = (
334
333
  many,
335
334
  meta.table,
336
335
  loader,
337
- (row: Record<string, any>) => createEntityFromRow(meta.ctx, relation.target, row),
336
+ (row: Record<string, unknown>) => createEntityFromRow(meta.ctx, relation.target, row),
338
337
  localKey
339
338
  );
340
339
  }
@@ -8,7 +8,7 @@ import { EntityContext } from './entity-context.js';
8
8
  import { ExecutionContext } from './execution-context.js';
9
9
  import { HydrationContext } from './hydration-context.js';
10
10
 
11
- type Row = Record<string, any>;
11
+ type Row = Record<string, unknown>;
12
12
 
13
13
  const flattenResults = (results: { columns: string[]; values: unknown[][] }[]): Row[] => {
14
14
  const rows: Row[] = [];
@@ -27,7 +27,7 @@ const flattenResults = (results: { columns: string[]; values: unknown[][] }[]):
27
27
 
28
28
  const executeWithEntityContext = async <TTable extends TableDef>(
29
29
  entityCtx: EntityContext,
30
- qb: SelectQueryBuilder<any, TTable>
30
+ qb: SelectQueryBuilder<unknown, TTable>
31
31
  ): Promise<EntityInstance<TTable>[]> => {
32
32
  const ast = qb.getAST();
33
33
  const compiled = entityCtx.dialect.compileSelect(ast);
@@ -42,17 +42,32 @@ const executeWithEntityContext = async <TTable extends TableDef>(
42
42
  return hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
43
43
  };
44
44
 
45
+ /**
46
+ * Executes a hydrated query using the ORM session.
47
+ * @template TTable - The table type
48
+ * @param session - The ORM session
49
+ * @param qb - The select query builder
50
+ * @returns Promise resolving to array of entity instances
51
+ */
45
52
  export async function executeHydrated<TTable extends TableDef>(
46
53
  session: OrmSession,
47
- qb: SelectQueryBuilder<any, TTable>
54
+ qb: SelectQueryBuilder<unknown, TTable>
48
55
  ): Promise<EntityInstance<TTable>[]> {
49
56
  return executeWithEntityContext(session, qb);
50
57
  }
51
58
 
59
+ /**
60
+ * Executes a hydrated query using execution and hydration contexts.
61
+ * @template TTable - The table type
62
+ * @param _execCtx - The execution context (unused)
63
+ * @param hydCtx - The hydration context
64
+ * @param qb - The select query builder
65
+ * @returns Promise resolving to array of entity instances
66
+ */
52
67
  export async function executeHydratedWithContexts<TTable extends TableDef>(
53
68
  _execCtx: ExecutionContext,
54
69
  hydCtx: HydrationContext,
55
- qb: SelectQueryBuilder<any, TTable>
70
+ qb: SelectQueryBuilder<unknown, TTable>
56
71
  ): Promise<EntityInstance<TTable>[]> {
57
72
  const entityCtx = hydCtx.entityContext;
58
73
  if (!entityCtx) {
@@ -1,6 +1,10 @@
1
- import { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
2
- import { RelationKinds } from '../schema/relation.js';
3
- import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
1
+ import { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
2
+ import { RelationKinds } from '../schema/relation.js';
3
+ import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
4
+
5
+ /**
6
+ * Hydrates query results according to a hydration plan
7
+ * @param rows - Raw database rows
4
8
 
5
9
  /**
6
10
  * Hydrates query results according to a hydration plan
@@ -8,13 +12,13 @@ import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-al
8
12
  * @param plan - Hydration plan
9
13
  * @returns Hydrated result objects with nested relations
10
14
  */
11
- export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan): Record<string, any>[] => {
15
+ export const hydrateRows = (rows: Record<string, unknown>[], plan?: HydrationPlan): Record<string, unknown>[] => {
12
16
  if (!plan || !rows.length) return rows;
13
17
 
14
- const rootMap = new Map<any, Record<string, any>>();
15
- const relationIndex = new Map<any, Record<string, Set<any>>>();
18
+ const rootMap = new Map<unknown, Record<string, unknown>>();
19
+ const relationIndex = new Map<unknown, Record<string, Set<unknown>>>();
16
20
 
17
- const getOrCreateParent = (row: Record<string, any>) => {
21
+ const getOrCreateParent = (row: Record<string, unknown>) => {
18
22
  const rootId = row[plan.rootPrimaryKey];
19
23
  if (rootId === undefined) return undefined;
20
24
 
@@ -25,7 +29,7 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
25
29
  return rootMap.get(rootId);
26
30
  };
27
31
 
28
- const getRelationSeenSet = (rootId: any, relationName: string): Set<any> => {
32
+ const getRelationSeenSet = (rootId: unknown, relationName: string): Set<unknown> => {
29
33
  let byRelation = relationIndex.get(rootId);
30
34
  if (!byRelation) {
31
35
  byRelation = {};
@@ -34,7 +38,7 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
34
38
 
35
39
  let seen = byRelation[relationName];
36
40
  if (!seen) {
37
- seen = new Set<any>();
41
+ seen = new Set<unknown>();
38
42
  byRelation[relationName] = seen;
39
43
  }
40
44
 
@@ -48,32 +52,32 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
48
52
  const parent = getOrCreateParent(row);
49
53
  if (!parent) continue;
50
54
 
51
- for (const rel of plan.relations) {
52
- const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
53
- const childPk = row[childPkKey];
54
- if (childPk === null || childPk === undefined) continue;
55
-
56
- const seen = getRelationSeenSet(rootId, rel.name);
57
- if (seen.has(childPk)) continue;
58
- seen.add(childPk);
59
-
60
- if (rel.type === RelationKinds.HasOne) {
61
- if (!parent[rel.name]) {
62
- parent[rel.name] = buildChild(row, rel);
63
- }
64
- continue;
65
- }
66
-
67
- const bucket = parent[rel.name] as any[];
68
- bucket.push(buildChild(row, rel));
69
- }
55
+ for (const rel of plan.relations) {
56
+ const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
57
+ const childPk = row[childPkKey];
58
+ if (childPk === null || childPk === undefined) continue;
59
+
60
+ const seen = getRelationSeenSet(rootId, rel.name);
61
+ if (seen.has(childPk)) continue;
62
+ seen.add(childPk);
63
+
64
+ if (rel.type === RelationKinds.HasOne) {
65
+ if (!parent[rel.name]) {
66
+ parent[rel.name] = buildChild(row, rel);
67
+ }
68
+ continue;
69
+ }
70
+
71
+ const bucket = parent[rel.name] as unknown[];
72
+ bucket.push(buildChild(row, rel));
73
+ }
70
74
  }
71
75
 
72
76
  return Array.from(rootMap.values());
73
77
  };
74
78
 
75
- const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<string, any> => {
76
- const base: Record<string, any> = {};
79
+ const createBaseRow = (row: Record<string, unknown>, plan: HydrationPlan): Record<string, unknown> => {
80
+ const base: Record<string, unknown> = {};
77
81
  const baseKeys = plan.rootColumns.length
78
82
  ? plan.rootColumns
79
83
  : Object.keys(row).filter(k => !isRelationAlias(k));
@@ -82,15 +86,14 @@ const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<st
82
86
  base[key] = row[key];
83
87
  }
84
88
 
85
- for (const rel of plan.relations) {
86
- base[rel.name] = rel.type === RelationKinds.HasOne ? null : [];
87
- }
88
-
89
+ for (const rel of plan.relations) {
90
+ base[rel.name] = rel.type === RelationKinds.HasOne ? null : [];
91
+ }
89
92
  return base;
90
93
  };
91
94
 
92
- const buildChild = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> => {
93
- const child: Record<string, any> = {};
95
+ const buildChild = (row: Record<string, unknown>, rel: HydrationRelationPlan): Record<string, unknown> => {
96
+ const child: Record<string, unknown> = {};
94
97
  for (const col of rel.columns) {
95
98
  const key = makeRelationAlias(rel.aliasPrefix, col);
96
99
  child[col] = row[key];
@@ -98,16 +101,16 @@ const buildChild = (row: Record<string, any>, rel: HydrationRelationPlan): Recor
98
101
 
99
102
  const pivot = buildPivot(row, rel);
100
103
  if (pivot) {
101
- (child as any)._pivot = pivot;
104
+ (child as { _pivot: unknown })._pivot = pivot;
102
105
  }
103
106
 
104
107
  return child;
105
108
  };
106
109
 
107
- const buildPivot = (row: Record<string, any>, rel: HydrationRelationPlan): Record<string, any> | undefined => {
110
+ const buildPivot = (row: Record<string, unknown>, rel: HydrationRelationPlan): Record<string, unknown> | undefined => {
108
111
  if (!rel.pivot) return undefined;
109
112
 
110
- const pivot: Record<string, any> = {};
113
+ const pivot: Record<string, unknown> = {};
111
114
  for (const col of rel.pivot.columns) {
112
115
  const key = makeRelationAlias(rel.pivot.aliasPrefix, col);
113
116
  pivot[col] = row[key];
@@ -8,7 +8,7 @@ export class IdentityMap {
8
8
  return this.buckets;
9
9
  }
10
10
 
11
- getEntity(table: TableDef, pk: string | number): any | undefined {
11
+ getEntity(table: TableDef, pk: string | number): unknown | undefined {
12
12
  const bucket = this.buckets.get(table.name);
13
13
  return bucket?.get(this.toIdentityKey(pk))?.entity;
14
14
  }
@@ -7,7 +7,9 @@ import type { QueryResult } from '../core/execution/db-executor.js';
7
7
  import { ColumnDef } from '../schema/column.js';
8
8
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
9
9
 
10
- type Rows = Record<string, any>[];
10
+ type Rows = Record<string, unknown>[];
11
+
12
+ type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
11
13
 
12
14
  const selectAllColumns = (table: TableDef): Record<string, ColumnDef> =>
13
15
  Object.entries(table.columns).reduce((acc, [name, def]) => {
@@ -20,7 +22,7 @@ const rowsFromResults = (results: QueryResult[]): Rows => {
20
22
  for (const result of results) {
21
23
  const { columns, values } = result;
22
24
  for (const valueRow of values) {
23
- const row: Record<string, any> = {};
25
+ const row: Record<string, unknown> = {};
24
26
  columns.forEach((column, idx) => {
25
27
  row[column] = valueRow[idx];
26
28
  });
@@ -30,7 +32,7 @@ const rowsFromResults = (results: QueryResult[]): Rows => {
30
32
  return rows;
31
33
  };
32
34
 
33
- const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<any, TableDef<any>>): Promise<Rows> => {
35
+ const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
34
36
  const compiled = ctx.dialect.compileSelect(qb.getAST());
35
37
  const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
36
38
  return rowsFromResults(results);
@@ -38,90 +40,97 @@ const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<any, Tabl
38
40
 
39
41
  const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
40
42
 
41
- export const loadHasManyRelation = async (
42
- ctx: EntityContext,
43
- rootTable: TableDef,
44
- _relationName: string,
45
- relation: HasManyRelation
46
- ): Promise<Map<string, Rows>> => {
47
- const localKey = relation.localKey || findPrimaryKey(rootTable);
48
- const roots = ctx.getEntitiesForTable(rootTable);
49
- const keys = new Set<unknown>();
50
-
43
+ const collectKeysFromRoots = (roots: EntityTracker[], key: string): Set<unknown> => {
44
+ const collected = new Set<unknown>();
51
45
  for (const tracked of roots) {
52
- const value = tracked.entity[localKey];
46
+ const value = tracked.entity[key];
53
47
  if (value !== null && value !== undefined) {
54
- keys.add(value);
48
+ collected.add(value);
55
49
  }
56
50
  }
51
+ return collected;
52
+ };
57
53
 
58
- if (!keys.size) {
59
- return new Map();
60
- }
54
+ const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
55
+ Array.from(keys) as (string | number | LiteralNode)[];
61
56
 
62
- const selectMap = selectAllColumns(relation.target);
63
- const fb = new SelectQueryBuilder(relation.target).select(selectMap);
64
- const fkColumn = relation.target.columns[relation.foreignKey];
65
- if (!fkColumn) return new Map();
66
-
67
- fb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
57
+ const fetchRowsForKeys = async (
58
+ ctx: EntityContext,
59
+ table: TableDef,
60
+ column: ColumnDef,
61
+ keys: Set<unknown>
62
+ ): Promise<Rows> => {
63
+ const qb = new SelectQueryBuilder(table).select(selectAllColumns(table));
64
+ qb.where(inList(column, buildInListValues(keys)));
65
+ return executeQuery(ctx, qb);
66
+ };
68
67
 
69
- const rows = await executeQuery(ctx, fb);
68
+ const groupRowsByMany = (rows: Rows, keyColumn: string): Map<string, Rows> => {
70
69
  const grouped = new Map<string, Rows>();
71
-
72
70
  for (const row of rows) {
73
- const fkValue = row[relation.foreignKey];
74
- if (fkValue === null || fkValue === undefined) continue;
75
- const key = toKey(fkValue);
71
+ const value = row[keyColumn];
72
+ if (value === null || value === undefined) continue;
73
+ const key = toKey(value);
76
74
  const bucket = grouped.get(key) ?? [];
77
75
  bucket.push(row);
78
76
  grouped.set(key, bucket);
79
77
  }
80
-
81
78
  return grouped;
82
79
  };
83
80
 
84
- export const loadHasOneRelation = async (
81
+ const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<string, unknown>> => {
82
+ const lookup = new Map<string, Record<string, unknown>>();
83
+ for (const row of rows) {
84
+ const value = row[keyColumn];
85
+ if (value === null || value === undefined) continue;
86
+ const key = toKey(value);
87
+ if (!lookup.has(key)) {
88
+ lookup.set(key, row);
89
+ }
90
+ }
91
+ return lookup;
92
+ };
93
+
94
+ export const loadHasManyRelation = async (
85
95
  ctx: EntityContext,
86
96
  rootTable: TableDef,
87
97
  _relationName: string,
88
- relation: HasOneRelation
89
- ): Promise<Map<string, Record<string, any>>> => {
98
+ relation: HasManyRelation
99
+ ): Promise<Map<string, Rows>> => {
90
100
  const localKey = relation.localKey || findPrimaryKey(rootTable);
91
101
  const roots = ctx.getEntitiesForTable(rootTable);
92
- const keys = new Set<unknown>();
93
-
94
- for (const tracked of roots) {
95
- const value = tracked.entity[localKey];
96
- if (value !== null && value !== undefined) {
97
- keys.add(value);
98
- }
99
- }
102
+ const keys = collectKeysFromRoots(roots, localKey);
100
103
 
101
104
  if (!keys.size) {
102
105
  return new Map();
103
106
  }
104
107
 
105
- const selectMap = selectAllColumns(relation.target);
106
- const qb = new SelectQueryBuilder(relation.target).select(selectMap);
107
108
  const fkColumn = relation.target.columns[relation.foreignKey];
108
109
  if (!fkColumn) return new Map();
109
110
 
110
- qb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
111
+ const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
112
+ return groupRowsByMany(rows, relation.foreignKey);
113
+ };
111
114
 
112
- const rows = await executeQuery(ctx, qb);
113
- const lookup = new Map<string, Record<string, any>>();
115
+ export const loadHasOneRelation = async (
116
+ ctx: EntityContext,
117
+ rootTable: TableDef,
118
+ _relationName: string,
119
+ relation: HasOneRelation
120
+ ): Promise<Map<string, Record<string, unknown>>> => {
121
+ const localKey = relation.localKey || findPrimaryKey(rootTable);
122
+ const roots = ctx.getEntitiesForTable(rootTable);
123
+ const keys = collectKeysFromRoots(roots, localKey);
114
124
 
115
- for (const row of rows) {
116
- const fkValue = row[relation.foreignKey];
117
- if (fkValue === null || fkValue === undefined) continue;
118
- const key = toKey(fkValue);
119
- if (!lookup.has(key)) {
120
- lookup.set(key, row);
121
- }
125
+ if (!keys.size) {
126
+ return new Map();
122
127
  }
123
128
 
124
- return lookup;
129
+ const fkColumn = relation.target.columns[relation.foreignKey];
130
+ if (!fkColumn) return new Map();
131
+
132
+ const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
133
+ return groupRowsByUnique(rows, relation.foreignKey);
125
134
  };
126
135
 
127
136
  export const loadBelongsToRelation = async (
@@ -129,38 +138,20 @@ export const loadBelongsToRelation = async (
129
138
  rootTable: TableDef,
130
139
  _relationName: string,
131
140
  relation: BelongsToRelation
132
- ): Promise<Map<string, Record<string, any>>> => {
141
+ ): Promise<Map<string, Record<string, unknown>>> => {
133
142
  const roots = ctx.getEntitiesForTable(rootTable);
134
- const foreignKeys = new Set<unknown>();
135
-
136
- for (const tracked of roots) {
137
- const value = tracked.entity[relation.foreignKey];
138
- if (value !== null && value !== undefined) {
139
- foreignKeys.add(value);
140
- }
141
- }
143
+ const foreignKeys = collectKeysFromRoots(roots, relation.foreignKey);
142
144
 
143
145
  if (!foreignKeys.size) {
144
146
  return new Map();
145
147
  }
146
148
 
147
- const selectMap = selectAllColumns(relation.target);
148
- const qb = new SelectQueryBuilder(relation.target).select(selectMap);
149
149
  const targetKey = relation.localKey || findPrimaryKey(relation.target);
150
150
  const pkColumn = relation.target.columns[targetKey];
151
151
  if (!pkColumn) return new Map();
152
152
 
153
- qb.where(inList(pkColumn, Array.from(foreignKeys) as (string | number | LiteralNode)[]));
154
- const rows = await executeQuery(ctx, qb);
155
- const map = new Map<string, Record<string, any>>();
156
-
157
- for (const row of rows) {
158
- const keyValue = row[targetKey];
159
- if (keyValue === null || keyValue === undefined) continue;
160
- map.set(toKey(keyValue), row);
161
- }
162
-
163
- return map;
153
+ const rows = await fetchRowsForKeys(ctx, relation.target, pkColumn, foreignKeys);
154
+ return groupRowsByUnique(rows, targetKey);
164
155
  };
165
156
 
166
157
  export const loadBelongsToManyRelation = async (
@@ -171,28 +162,17 @@ export const loadBelongsToManyRelation = async (
171
162
  ): Promise<Map<string, Rows>> => {
172
163
  const rootKey = relation.localKey || findPrimaryKey(rootTable);
173
164
  const roots = ctx.getEntitiesForTable(rootTable);
174
- const rootIds = new Set<unknown>();
175
-
176
- for (const tracked of roots) {
177
- const value = tracked.entity[rootKey];
178
- if (value !== null && value !== undefined) {
179
- rootIds.add(value);
180
- }
181
- }
165
+ const rootIds = collectKeysFromRoots(roots, rootKey);
182
166
 
183
167
  if (!rootIds.size) {
184
168
  return new Map();
185
169
  }
186
170
 
187
- const pivotSelect = selectAllColumns(relation.pivotTable);
188
- const pivotQb = new SelectQueryBuilder(relation.pivotTable).select(pivotSelect);
189
- const pivotFkCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
190
- if (!pivotFkCol) return new Map();
171
+ const pivotColumn = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
172
+ if (!pivotColumn) return new Map();
191
173
 
192
- pivotQb.where(inList(pivotFkCol, Array.from(rootIds) as (string | number | LiteralNode)[]));
193
- const pivotRows = await executeQuery(ctx, pivotQb);
194
-
195
- const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, any> }[]>();
174
+ const pivotRows = await fetchRowsForKeys(ctx, relation.pivotTable, pivotColumn, rootIds);
175
+ const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, unknown> }[]>();
196
176
  const targetIds = new Set<unknown>();
197
177
 
198
178
  for (const pivot of pivotRows) {
@@ -214,22 +194,12 @@ export const loadBelongsToManyRelation = async (
214
194
  return new Map();
215
195
  }
216
196
 
217
- const targetSelect = selectAllColumns(relation.target);
218
197
  const targetKey = relation.targetKey || findPrimaryKey(relation.target);
219
198
  const targetPkColumn = relation.target.columns[targetKey];
220
199
  if (!targetPkColumn) return new Map();
221
200
 
222
- const targetQb = new SelectQueryBuilder(relation.target).select(targetSelect);
223
- targetQb.where(inList(targetPkColumn, Array.from(targetIds) as (string | number | LiteralNode)[]));
224
- const targetRows = await executeQuery(ctx, targetQb);
225
- const targetMap = new Map<string, Record<string, any>>();
226
-
227
- for (const row of targetRows) {
228
- const pkValue = row[targetKey];
229
- if (pkValue === null || pkValue === undefined) continue;
230
- targetMap.set(toKey(pkValue), row);
231
- }
232
-
201
+ const targetRows = await fetchRowsForKeys(ctx, relation.target, targetPkColumn, targetIds);
202
+ const targetMap = groupRowsByUnique(targetRows, targetKey);
233
203
  const result = new Map<string, Rows>();
234
204
 
235
205
  for (const [rootId, entries] of rootLookup.entries()) {