metal-orm 1.1.8 → 1.1.10

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 (75) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2352 -226
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +605 -40
  5. package/dist/index.d.ts +605 -40
  6. package/dist/index.js +2324 -226
  7. package/dist/index.js.map +1 -1
  8. package/package.json +22 -17
  9. package/src/bulk/bulk-context.ts +83 -0
  10. package/src/bulk/bulk-delete-executor.ts +89 -0
  11. package/src/bulk/bulk-executor.base.ts +73 -0
  12. package/src/bulk/bulk-insert-executor.ts +74 -0
  13. package/src/bulk/bulk-types.ts +70 -0
  14. package/src/bulk/bulk-update-executor.ts +192 -0
  15. package/src/bulk/bulk-upsert-executor.ts +95 -0
  16. package/src/bulk/bulk-utils.ts +91 -0
  17. package/src/bulk/index.ts +18 -0
  18. package/src/codegen/typescript.ts +30 -21
  19. package/src/core/ast/expression-builders.ts +107 -10
  20. package/src/core/ast/expression-nodes.ts +52 -22
  21. package/src/core/ast/expression-visitor.ts +23 -13
  22. package/src/core/dialect/abstract.ts +30 -17
  23. package/src/core/dialect/mysql/index.ts +20 -5
  24. package/src/core/execution/db-executor.ts +96 -64
  25. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  26. package/src/core/execution/executors/mssql-executor.ts +66 -34
  27. package/src/core/execution/executors/mysql-executor.ts +98 -66
  28. package/src/core/execution/executors/postgres-executor.ts +33 -11
  29. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  30. package/src/decorators/bootstrap.ts +482 -398
  31. package/src/decorators/column-decorator.ts +87 -96
  32. package/src/decorators/decorator-metadata.ts +100 -24
  33. package/src/decorators/entity.ts +27 -24
  34. package/src/decorators/relations.ts +231 -149
  35. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  36. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  37. package/src/dto/apply-filter.ts +568 -551
  38. package/src/index.ts +16 -9
  39. package/src/orm/entity-hydration.ts +116 -72
  40. package/src/orm/entity-metadata.ts +347 -301
  41. package/src/orm/entity-relations.ts +264 -207
  42. package/src/orm/entity.ts +199 -199
  43. package/src/orm/execute.ts +13 -13
  44. package/src/orm/lazy-batch/morph-many.ts +70 -0
  45. package/src/orm/lazy-batch/morph-one.ts +69 -0
  46. package/src/orm/lazy-batch/morph-to.ts +59 -0
  47. package/src/orm/lazy-batch.ts +4 -1
  48. package/src/orm/orm-session.ts +170 -104
  49. package/src/orm/pooled-executor-factory.ts +99 -58
  50. package/src/orm/query-logger.ts +49 -40
  51. package/src/orm/relation-change-processor.ts +198 -96
  52. package/src/orm/relations/belongs-to.ts +143 -143
  53. package/src/orm/relations/has-many.ts +204 -204
  54. package/src/orm/relations/has-one.ts +174 -174
  55. package/src/orm/relations/many-to-many.ts +288 -288
  56. package/src/orm/relations/morph-many.ts +156 -0
  57. package/src/orm/relations/morph-one.ts +151 -0
  58. package/src/orm/relations/morph-to.ts +162 -0
  59. package/src/orm/save-graph.ts +116 -1
  60. package/src/query-builder/expression-table-mapper.ts +5 -0
  61. package/src/query-builder/hydration-manager.ts +345 -345
  62. package/src/query-builder/hydration-planner.ts +178 -148
  63. package/src/query-builder/relation-conditions.ts +171 -151
  64. package/src/query-builder/relation-cte-builder.ts +5 -1
  65. package/src/query-builder/relation-filter-utils.ts +9 -6
  66. package/src/query-builder/relation-include-strategies.ts +44 -2
  67. package/src/query-builder/relation-join-strategies.ts +8 -1
  68. package/src/query-builder/relation-service.ts +250 -241
  69. package/src/query-builder/select/cursor-pagination.ts +323 -0
  70. package/src/query-builder/select/select-operations.ts +110 -105
  71. package/src/query-builder/select.ts +42 -1
  72. package/src/query-builder/update-include.ts +4 -0
  73. package/src/schema/relation.ts +296 -188
  74. package/src/schema/types.ts +138 -123
  75. package/src/tree/tree-decorator.ts +127 -137
package/src/orm/entity.ts CHANGED
@@ -1,199 +1,199 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { EntityInstance } from '../schema/types.js';
3
- import type { EntityContext, PrimaryKey } from './entity-context.js';
4
- import { ENTITY_META, EntityMeta, RelationKey } from './entity-meta.js';
5
- import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
- import { RelationIncludeOptions } from '../query-builder/relation-types.js';
7
- import { populateHydrationCache } from './entity-hydration.js';
8
- import { getRelationWrapper, RelationEntityFactory } from './entity-relations.js';
9
- import { RelationKinds } from '../schema/relation.js';
10
-
11
- export { relationLoaderCache } from './entity-relation-cache.js';
12
-
13
- /**
14
- * Options for toJSON serialization.
15
- */
16
- export interface ToJsonOptions {
17
- /**
18
- * If true (default), includes all relations defined in the schema (empty arrays/null for unloaded).
19
- * If false, only includes relations that were loaded.
20
- */
21
- includeAllRelations?: boolean;
22
- }
23
-
24
- const isRelationWrapperLoaded = (value: unknown): boolean => {
25
- if (!value || typeof value !== 'object') return false;
26
- return Boolean((value as { loaded?: boolean }).loaded);
27
- };
28
-
29
- type JsonSource<TTable extends TableDef> = EntityInstance<TTable> & Record<string, unknown>;
30
-
31
- /**
32
- * Creates an entity proxy with lazy loading capabilities.
33
- * @template TTable - The table type
34
- * @template TLazy - The lazy relation keys
35
- * @param ctx - The entity context
36
- * @param table - The table definition
37
- * @param row - The database row
38
- * @param lazyRelations - Optional lazy relations
39
- * @returns The entity instance
40
- */
41
- export const createEntityProxy = <
42
- TTable extends TableDef,
43
- TLazy extends RelationKey<TTable> = RelationKey<TTable>
44
- >(
45
- ctx: EntityContext,
46
- table: TTable,
47
- row: Record<string, unknown>,
48
- lazyRelations: TLazy[] = [] as TLazy[],
49
- lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
50
- ): EntityInstance<TTable> => {
51
- const target: Record<string, unknown> = { ...row };
52
- const meta: EntityMeta<TTable> = {
53
- ctx,
54
- table,
55
- lazyRelations: [...lazyRelations],
56
- lazyRelationOptions: new Map(lazyRelationOptions),
57
- relationCache: new Map(),
58
- relationHydration: new Map(),
59
- relationWrappers: new Map()
60
- };
61
- const createRelationEntity: RelationEntityFactory = (relationTable, relationRow) =>
62
- createEntityFromRow(meta.ctx, relationTable, relationRow);
63
-
64
- const isCollectionRelation = (relationName: string): boolean => {
65
- const rel = table.relations[relationName];
66
- if (!rel) return false;
67
- return rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany;
68
- };
69
-
70
- const buildJson = (self: JsonSource<TTable>, options?: ToJsonOptions): Record<string, unknown> => {
71
- const json: Record<string, unknown> = {};
72
- const includeAll = options?.includeAllRelations ?? true;
73
-
74
- // Add non-relation columns
75
- for (const key of Object.keys(target)) {
76
- if (!table.relations[key]) {
77
- json[key] = self[key];
78
- }
79
- }
80
-
81
- // Add relations
82
- if (includeAll) {
83
- // Include ALL relations from schema
84
- for (const relationName of Object.keys(table.relations)) {
85
- const wrapper = self[relationName];
86
- if (wrapper && isRelationWrapperLoaded(wrapper)) {
87
- const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
88
- json[relationName] = typeof wrapperWithToJSON.toJSON === 'function'
89
- ? wrapperWithToJSON.toJSON()
90
- : wrapper;
91
- } else {
92
- // Unloaded: use empty array for collections, null for single relations
93
- json[relationName] = isCollectionRelation(relationName) ? [] : null;
94
- }
95
- }
96
- } else {
97
- // Only include loaded relations that exist in target
98
- for (const key of Object.keys(target)) {
99
- if (table.relations[key]) {
100
- const wrapper = self[key];
101
- if (wrapper && isRelationWrapperLoaded(wrapper)) {
102
- const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
103
- json[key] = typeof wrapperWithToJSON.toJSON === 'function'
104
- ? wrapperWithToJSON.toJSON()
105
- : wrapper;
106
- }
107
- }
108
- }
109
- }
110
-
111
- return json;
112
- };
113
-
114
- Object.defineProperty(target, ENTITY_META, {
115
- value: meta,
116
- enumerable: false,
117
- writable: false
118
- });
119
-
120
- const handler: ProxyHandler<object> = {
121
- get(targetObj, prop, receiver) {
122
- if (prop === ENTITY_META) {
123
- return meta;
124
- }
125
-
126
- if (prop === '$load') {
127
- return async (relationName: RelationKey<TTable>) => {
128
- const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
129
- if (wrapper && typeof wrapper.load === 'function') {
130
- return wrapper.load();
131
- }
132
- return undefined;
133
- };
134
- }
135
-
136
- if (prop === 'toJSON') {
137
- if (prop in targetObj) {
138
- return Reflect.get(targetObj, prop, receiver);
139
- }
140
- return (options?: ToJsonOptions) => buildJson(receiver as JsonSource<TTable>, options);
141
- }
142
-
143
- if (typeof prop === 'string' && table.relations[prop]) {
144
- return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
145
- }
146
-
147
- return Reflect.get(targetObj, prop, receiver);
148
- },
149
-
150
- set(targetObj, prop, value, receiver) {
151
- const result = Reflect.set(targetObj, prop, value, receiver);
152
- if (typeof prop === 'string' && table.columns[prop]) {
153
- ctx.markDirty(receiver);
154
- }
155
- return result;
156
- }
157
- };
158
-
159
- const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
160
- populateHydrationCache(proxy, row, meta);
161
- return proxy;
162
- };
163
-
164
- /**
165
- * Creates an entity instance from a database row.
166
- * @template TTable - The table type
167
- * @template TResult - The result type
168
- * @param ctx - The entity context
169
- * @param table - The table definition
170
- * @param row - The database row
171
- * @param lazyRelations - Optional lazy relations
172
- * @returns The entity instance
173
- */
174
- export const createEntityFromRow = <
175
- TTable extends TableDef,
176
- TResult extends EntityInstance<TTable> = EntityInstance<TTable>
177
- >(
178
- ctx: EntityContext,
179
- table: TTable,
180
- row: Record<string, unknown>,
181
- lazyRelations: RelationKey<TTable>[] = [],
182
- lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
183
- ): TResult => {
184
- const pkName = findPrimaryKey(table);
185
- const pkValue = row[pkName];
186
- if (pkValue !== undefined && pkValue !== null) {
187
- const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
188
- if (tracked) return tracked as TResult;
189
- }
190
-
191
- const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
192
- if (pkValue !== undefined && pkValue !== null) {
193
- ctx.trackManaged(table, pkValue as PrimaryKey, entity);
194
- } else {
195
- ctx.trackNew(table, entity);
196
- }
197
-
198
- return entity as TResult;
199
- };
1
+ import { TableDef } from '../schema/table.js';
2
+ import { EntityInstance } from '../schema/types.js';
3
+ import type { EntityContext, PrimaryKey } from './entity-context.js';
4
+ import { ENTITY_META, EntityMeta, RelationKey } from './entity-meta.js';
5
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
+ import { RelationIncludeOptions } from '../query-builder/relation-types.js';
7
+ import { populateHydrationCache } from './entity-hydration.js';
8
+ import { getRelationWrapper, RelationEntityFactory } from './entity-relations.js';
9
+ import { RelationKinds } from '../schema/relation.js';
10
+
11
+ export { relationLoaderCache } from './entity-relation-cache.js';
12
+
13
+ /**
14
+ * Options for toJSON serialization.
15
+ */
16
+ export interface ToJsonOptions {
17
+ /**
18
+ * If true (default), includes all relations defined in the schema (empty arrays/null for unloaded).
19
+ * If false, only includes relations that were loaded.
20
+ */
21
+ includeAllRelations?: boolean;
22
+ }
23
+
24
+ const isRelationWrapperLoaded = (value: unknown): boolean => {
25
+ if (!value || typeof value !== 'object') return false;
26
+ return Boolean((value as { loaded?: boolean }).loaded);
27
+ };
28
+
29
+ type JsonSource<TTable extends TableDef> = EntityInstance<TTable> & Record<string, unknown>;
30
+
31
+ /**
32
+ * Creates an entity proxy with lazy loading capabilities.
33
+ * @template TTable - The table type
34
+ * @template TLazy - The lazy relation keys
35
+ * @param ctx - The entity context
36
+ * @param table - The table definition
37
+ * @param row - The database row
38
+ * @param lazyRelations - Optional lazy relations
39
+ * @returns The entity instance
40
+ */
41
+ export const createEntityProxy = <
42
+ TTable extends TableDef,
43
+ TLazy extends RelationKey<TTable> = RelationKey<TTable>
44
+ >(
45
+ ctx: EntityContext,
46
+ table: TTable,
47
+ row: Record<string, unknown>,
48
+ lazyRelations: TLazy[] = [] as TLazy[],
49
+ lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
50
+ ): EntityInstance<TTable> => {
51
+ const target: Record<string, unknown> = { ...row };
52
+ const meta: EntityMeta<TTable> = {
53
+ ctx,
54
+ table,
55
+ lazyRelations: [...lazyRelations],
56
+ lazyRelationOptions: new Map(lazyRelationOptions),
57
+ relationCache: new Map(),
58
+ relationHydration: new Map(),
59
+ relationWrappers: new Map()
60
+ };
61
+ const createRelationEntity: RelationEntityFactory = (relationTable, relationRow) =>
62
+ createEntityFromRow(meta.ctx, relationTable, relationRow);
63
+
64
+ const isCollectionRelation = (relationName: string): boolean => {
65
+ const rel = table.relations[relationName];
66
+ if (!rel) return false;
67
+ return rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany || rel.type === RelationKinds.MorphMany;
68
+ };
69
+
70
+ const buildJson = (self: JsonSource<TTable>, options?: ToJsonOptions): Record<string, unknown> => {
71
+ const json: Record<string, unknown> = {};
72
+ const includeAll = options?.includeAllRelations ?? true;
73
+
74
+ // Add non-relation columns
75
+ for (const key of Object.keys(target)) {
76
+ if (!table.relations[key]) {
77
+ json[key] = self[key];
78
+ }
79
+ }
80
+
81
+ // Add relations
82
+ if (includeAll) {
83
+ // Include ALL relations from schema
84
+ for (const relationName of Object.keys(table.relations)) {
85
+ const wrapper = self[relationName];
86
+ if (wrapper && isRelationWrapperLoaded(wrapper)) {
87
+ const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
88
+ json[relationName] = typeof wrapperWithToJSON.toJSON === 'function'
89
+ ? wrapperWithToJSON.toJSON()
90
+ : wrapper;
91
+ } else {
92
+ // Unloaded: use empty array for collections, null for single relations
93
+ json[relationName] = isCollectionRelation(relationName) ? [] : null;
94
+ }
95
+ }
96
+ } else {
97
+ // Only include loaded relations that exist in target
98
+ for (const key of Object.keys(target)) {
99
+ if (table.relations[key]) {
100
+ const wrapper = self[key];
101
+ if (wrapper && isRelationWrapperLoaded(wrapper)) {
102
+ const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
103
+ json[key] = typeof wrapperWithToJSON.toJSON === 'function'
104
+ ? wrapperWithToJSON.toJSON()
105
+ : wrapper;
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ return json;
112
+ };
113
+
114
+ Object.defineProperty(target, ENTITY_META, {
115
+ value: meta,
116
+ enumerable: false,
117
+ writable: false
118
+ });
119
+
120
+ const handler: ProxyHandler<object> = {
121
+ get(targetObj, prop, receiver) {
122
+ if (prop === ENTITY_META) {
123
+ return meta;
124
+ }
125
+
126
+ if (prop === '$load') {
127
+ return async (relationName: RelationKey<TTable>) => {
128
+ const wrapper = getRelationWrapper(meta, relationName, receiver, createRelationEntity);
129
+ if (wrapper && typeof wrapper.load === 'function') {
130
+ return wrapper.load();
131
+ }
132
+ return undefined;
133
+ };
134
+ }
135
+
136
+ if (prop === 'toJSON') {
137
+ if (prop in targetObj) {
138
+ return Reflect.get(targetObj, prop, receiver);
139
+ }
140
+ return (options?: ToJsonOptions) => buildJson(receiver as JsonSource<TTable>, options);
141
+ }
142
+
143
+ if (typeof prop === 'string' && table.relations[prop]) {
144
+ return getRelationWrapper(meta, prop as RelationKey<TTable>, receiver, createRelationEntity);
145
+ }
146
+
147
+ return Reflect.get(targetObj, prop, receiver);
148
+ },
149
+
150
+ set(targetObj, prop, value, receiver) {
151
+ const result = Reflect.set(targetObj, prop, value, receiver);
152
+ if (typeof prop === 'string' && table.columns[prop]) {
153
+ ctx.markDirty(receiver);
154
+ }
155
+ return result;
156
+ }
157
+ };
158
+
159
+ const proxy = new Proxy(target, handler) as EntityInstance<TTable>;
160
+ populateHydrationCache(proxy, row, meta);
161
+ return proxy;
162
+ };
163
+
164
+ /**
165
+ * Creates an entity instance from a database row.
166
+ * @template TTable - The table type
167
+ * @template TResult - The result type
168
+ * @param ctx - The entity context
169
+ * @param table - The table definition
170
+ * @param row - The database row
171
+ * @param lazyRelations - Optional lazy relations
172
+ * @returns The entity instance
173
+ */
174
+ export const createEntityFromRow = <
175
+ TTable extends TableDef,
176
+ TResult extends EntityInstance<TTable> = EntityInstance<TTable>
177
+ >(
178
+ ctx: EntityContext,
179
+ table: TTable,
180
+ row: Record<string, unknown>,
181
+ lazyRelations: RelationKey<TTable>[] = [],
182
+ lazyRelationOptions: Map<string, RelationIncludeOptions> = new Map()
183
+ ): TResult => {
184
+ const pkName = findPrimaryKey(table);
185
+ const pkValue = row[pkName];
186
+ if (pkValue !== undefined && pkValue !== null) {
187
+ const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
188
+ if (tracked) return tracked as TResult;
189
+ }
190
+
191
+ const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
192
+ if (pkValue !== undefined && pkValue !== null) {
193
+ ctx.trackManaged(table, pkValue as PrimaryKey, entity);
194
+ } else {
195
+ ctx.trackNew(table, entity);
196
+ }
197
+
198
+ return entity as TResult;
199
+ };
@@ -15,13 +15,13 @@ import { HydrationContext } from './hydration-context.js';
15
15
  import { RelationIncludeOptions } from '../query-builder/relation-types.js';
16
16
  import { getEntityMeta, RelationKey } from './entity-meta.js';
17
17
  import { preloadRelationIncludes } from './relation-preload.js';
18
- import {
19
- loadHasManyRelation,
20
- loadHasOneRelation,
21
- loadBelongsToRelation,
22
- loadBelongsToManyRelation
23
- } from './lazy-batch.js';
24
- import { payloadResultSets } from '../core/execution/db-executor.js';
18
+ import {
19
+ loadHasManyRelation,
20
+ loadHasOneRelation,
21
+ loadBelongsToRelation,
22
+ loadBelongsToManyRelation
23
+ } from './lazy-batch.js';
24
+ import { payloadResultSets } from '../core/execution/db-executor.js';
25
25
 
26
26
  type Row = Record<string, unknown>;
27
27
 
@@ -46,9 +46,9 @@ const executeWithContexts = async <TTable extends TableDef>(
46
46
  qb: SelectQueryBuilder<unknown, TTable>
47
47
  ): Promise<EntityInstance<TTable>[]> => {
48
48
  const ast = qb.getAST();
49
- const compiled = execCtx.dialect.compileSelect(ast);
50
- const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
51
- const rows = flattenResults(payloadResultSets(executed));
49
+ const compiled = execCtx.dialect.compileSelect(ast);
50
+ const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
51
+ const rows = flattenResults(payloadResultSets(executed));
52
52
  const lazyRelations = qb.getLazyRelations() as RelationKey<TTable>[];
53
53
  const lazyRelationOptions = qb.getLazyRelationOptions();
54
54
  const includeTree = qb.getIncludeTree();
@@ -72,9 +72,9 @@ const executePlainWithContexts = async <TTable extends TableDef>(
72
72
  qb: SelectQueryBuilder<unknown, TTable>
73
73
  ): Promise<Record<string, unknown>[]> => {
74
74
  const ast = qb.getAST();
75
- const compiled = execCtx.dialect.compileSelect(ast);
76
- const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
77
- const rows = flattenResults(payloadResultSets(executed));
75
+ const compiled = execCtx.dialect.compileSelect(ast);
76
+ const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
77
+ const rows = flattenResults(payloadResultSets(executed));
78
78
 
79
79
  if (ast.setOps && ast.setOps.length > 0) {
80
80
  return rows;
@@ -0,0 +1,70 @@
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { MorphManyRelation } from '../../schema/relation.js';
3
+ import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
4
+ import { RelationIncludeOptions } from '../../query-builder/relation-types.js';
5
+ import { EntityContext } from '../entity-context.js';
6
+ import { eq, and, ExpressionNode } from '../../core/ast/expression.js';
7
+ import {
8
+ buildColumnSelection,
9
+ collectKeysFromRoots,
10
+ fetchRowsForKeys,
11
+ filterRows,
12
+ groupRowsByMany,
13
+ hasColumns,
14
+ Rows
15
+ } from './shared.js';
16
+
17
+ export const loadMorphManyRelation = async (
18
+ ctx: EntityContext,
19
+ rootTable: TableDef,
20
+ relationName: string,
21
+ relation: MorphManyRelation,
22
+ options?: RelationIncludeOptions
23
+ ): Promise<Map<string, Rows>> => {
24
+ const localKey = relation.localKey || findPrimaryKey(rootTable);
25
+ const roots = ctx.getEntitiesForTable(rootTable);
26
+ const keys = collectKeysFromRoots(roots, localKey);
27
+
28
+ if (!keys.size) {
29
+ return new Map();
30
+ }
31
+
32
+ const fkColumn = relation.target.columns[relation.idField];
33
+ if (!fkColumn) return new Map();
34
+
35
+ const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
36
+ const targetPrimaryKey = findPrimaryKey(relation.target);
37
+ const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
38
+ if (!selectedColumns.includes(targetPrimaryKey)) {
39
+ selectedColumns.push(targetPrimaryKey);
40
+ }
41
+
42
+ const queryColumns = new Set(selectedColumns);
43
+ queryColumns.add(relation.idField);
44
+
45
+ const selection = buildColumnSelection(
46
+ relation.target,
47
+ Array.from(queryColumns),
48
+ column => `Column '${column}' not found on relation '${relationName}'`
49
+ );
50
+
51
+ // Add discriminator filter
52
+ const typeColumn = relation.target.columns[relation.typeField];
53
+ const discriminatorFilter: ExpressionNode = eq(
54
+ typeColumn ?? { type: 'Column', table: relation.target.name, name: relation.typeField },
55
+ { type: 'Literal', value: relation.typeValue }
56
+ );
57
+ const combinedFilter = options?.filter ? and(options.filter, discriminatorFilter) : discriminatorFilter;
58
+
59
+ const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys, selection, combinedFilter);
60
+ const grouped = groupRowsByMany(rows, relation.idField);
61
+
62
+ if (!requestedColumns) return grouped;
63
+
64
+ const visibleColumns = new Set(selectedColumns);
65
+ const filtered = new Map<string, Rows>();
66
+ for (const [key, bucket] of grouped.entries()) {
67
+ filtered.set(key, filterRows(bucket, visibleColumns));
68
+ }
69
+ return filtered;
70
+ };
@@ -0,0 +1,69 @@
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { MorphOneRelation } from '../../schema/relation.js';
3
+ import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
4
+ import { RelationIncludeOptions } from '../../query-builder/relation-types.js';
5
+ import { EntityContext } from '../entity-context.js';
6
+ import { eq, and, ExpressionNode } from '../../core/ast/expression.js';
7
+ import {
8
+ buildColumnSelection,
9
+ collectKeysFromRoots,
10
+ fetchRowsForKeys,
11
+ filterRow,
12
+ groupRowsByUnique,
13
+ hasColumns
14
+ } from './shared.js';
15
+
16
+ export const loadMorphOneRelation = async (
17
+ ctx: EntityContext,
18
+ rootTable: TableDef,
19
+ relationName: string,
20
+ relation: MorphOneRelation,
21
+ options?: RelationIncludeOptions
22
+ ): Promise<Map<string, Record<string, unknown>>> => {
23
+ const localKey = relation.localKey || findPrimaryKey(rootTable);
24
+ const roots = ctx.getEntitiesForTable(rootTable);
25
+ const keys = collectKeysFromRoots(roots, localKey);
26
+
27
+ if (!keys.size) {
28
+ return new Map();
29
+ }
30
+
31
+ const fkColumn = relation.target.columns[relation.idField];
32
+ if (!fkColumn) return new Map();
33
+
34
+ const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
35
+ const targetPrimaryKey = findPrimaryKey(relation.target);
36
+ const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
37
+ if (!selectedColumns.includes(targetPrimaryKey)) {
38
+ selectedColumns.push(targetPrimaryKey);
39
+ }
40
+
41
+ const queryColumns = new Set(selectedColumns);
42
+ queryColumns.add(relation.idField);
43
+
44
+ const selection = buildColumnSelection(
45
+ relation.target,
46
+ Array.from(queryColumns),
47
+ column => `Column '${column}' not found on relation '${relationName}'`
48
+ );
49
+
50
+ // Add discriminator filter
51
+ const typeColumn = relation.target.columns[relation.typeField];
52
+ const discriminatorFilter: ExpressionNode = eq(
53
+ typeColumn ?? { type: 'Column', table: relation.target.name, name: relation.typeField },
54
+ { type: 'Literal', value: relation.typeValue }
55
+ );
56
+ const combinedFilter = options?.filter ? and(options.filter, discriminatorFilter) : discriminatorFilter;
57
+
58
+ const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys, selection, combinedFilter);
59
+ const grouped = groupRowsByUnique(rows, relation.idField);
60
+
61
+ if (!requestedColumns) return grouped;
62
+
63
+ const visibleColumns = new Set(selectedColumns);
64
+ const filtered = new Map<string, Record<string, unknown>>();
65
+ for (const [key, row] of grouped.entries()) {
66
+ filtered.set(key, filterRow(row, visibleColumns));
67
+ }
68
+ return filtered;
69
+ };
@@ -0,0 +1,59 @@
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { MorphToRelation } from '../../schema/relation.js';
3
+ import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
4
+ import { EntityContext } from '../entity-context.js';
5
+ import {
6
+ buildColumnSelection,
7
+ fetchRowsForKeys,
8
+ toKey
9
+ } from './shared.js';
10
+
11
+ export const loadMorphToRelation = async (
12
+ ctx: EntityContext,
13
+ rootTable: TableDef,
14
+ _relationName: string,
15
+ relation: MorphToRelation
16
+ ): Promise<Map<string, Record<string, unknown>>> => {
17
+ const roots = ctx.getEntitiesForTable(rootTable);
18
+ const result = new Map<string, Record<string, unknown>>();
19
+
20
+ // Group root entities by type value
21
+ const grouped = new Map<string, Set<unknown>>();
22
+ for (const tracked of roots) {
23
+ const entity = tracked.entity as Record<string, unknown>;
24
+ const typeValue = entity[relation.typeField];
25
+ const idValue = entity[relation.idField];
26
+ if (!typeValue || idValue === undefined || idValue === null) continue;
27
+ const typeKey = toKey(typeValue);
28
+ const ids = grouped.get(typeKey) ?? new Set();
29
+ ids.add(idValue);
30
+ grouped.set(typeKey, ids);
31
+ }
32
+
33
+ // For each type, load from the corresponding target table
34
+ for (const [typeKey, ids] of grouped.entries()) {
35
+ const targetTable = relation.targets[typeKey];
36
+ if (!targetTable) continue;
37
+
38
+ const targetPk = relation.targetKey || findPrimaryKey(targetTable);
39
+ const pkColumn = targetTable.columns[targetPk];
40
+ if (!pkColumn) continue;
41
+
42
+ const selection = buildColumnSelection(
43
+ targetTable,
44
+ Object.keys(targetTable.columns),
45
+ column => `Column '${column}' not found on target '${targetTable.name}'`
46
+ );
47
+
48
+ const rows = await fetchRowsForKeys(ctx, targetTable, pkColumn, ids, selection);
49
+
50
+ for (const row of rows) {
51
+ const pkValue = row[targetPk];
52
+ if (pkValue === undefined || pkValue === null) continue;
53
+ const compositeKey = `${typeKey}:${toKey(pkValue)}`;
54
+ result.set(compositeKey, row);
55
+ }
56
+ }
57
+
58
+ return result;
59
+ };
@@ -2,4 +2,7 @@ export { loadHasManyRelation } from './lazy-batch/has-many.js';
2
2
  export { loadHasOneRelation } from './lazy-batch/has-one.js';
3
3
  export { loadBelongsToRelation } from './lazy-batch/belongs-to.js';
4
4
  export { loadBelongsToManyRelation } from './lazy-batch/belongs-to-many.js';
5
-
5
+ export { loadMorphOneRelation } from './lazy-batch/morph-one.js';
6
+ export { loadMorphManyRelation } from './lazy-batch/morph-many.js';
7
+ export { loadMorphToRelation } from './lazy-batch/morph-to.js';
8
+