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.
- package/README.md +195 -37
- package/dist/index.cjs +1014 -538
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1267 -371
- package/dist/index.d.ts +1267 -371
- package/dist/index.js +1012 -536
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/scripts/run-eslint.mjs +34 -0
- package/src/codegen/typescript.ts +32 -15
- package/src/core/ast/adapters.ts +8 -2
- package/src/core/ast/builders.ts +105 -76
- package/src/core/ast/expression-builders.ts +430 -392
- package/src/core/ast/expression-nodes.ts +14 -5
- package/src/core/ast/expression-visitor.ts +56 -14
- package/src/core/ast/helpers.ts +23 -0
- package/src/core/ast/join-node.ts +18 -2
- package/src/core/ast/query.ts +6 -6
- package/src/core/ast/window-functions.ts +10 -2
- package/src/core/ddl/dialects/base-schema-dialect.ts +37 -4
- package/src/core/ddl/dialects/index.ts +1 -0
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +5 -0
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +3 -0
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +14 -1
- package/src/core/ddl/dialects/render-reference.test.ts +69 -0
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +10 -0
- package/src/core/ddl/introspect/catalogs/index.ts +1 -0
- package/src/core/ddl/introspect/catalogs/postgres.ts +2 -0
- package/src/core/ddl/introspect/context.ts +6 -0
- package/src/core/ddl/introspect/functions/postgres.ts +13 -0
- package/src/core/ddl/introspect/mssql.ts +53 -8
- package/src/core/ddl/introspect/mysql.ts +32 -6
- package/src/core/ddl/introspect/postgres.ts +102 -34
- package/src/core/ddl/introspect/registry.ts +14 -0
- package/src/core/ddl/introspect/run-select.ts +19 -4
- package/src/core/ddl/introspect/sqlite.ts +78 -11
- package/src/core/ddl/introspect/types.ts +0 -1
- package/src/core/ddl/introspect/utils.ts +21 -3
- package/src/core/ddl/naming-strategy.ts +6 -0
- package/src/core/ddl/schema-dialect.ts +20 -6
- package/src/core/ddl/schema-diff.ts +22 -0
- package/src/core/ddl/schema-generator.ts +26 -12
- package/src/core/ddl/schema-plan-executor.ts +6 -0
- package/src/core/ddl/schema-types.ts +6 -0
- package/src/core/ddl/sql-writing.ts +4 -4
- package/src/core/dialect/abstract.ts +19 -7
- package/src/core/dialect/base/function-table-formatter.ts +3 -2
- package/src/core/dialect/base/join-compiler.ts +5 -3
- package/src/core/dialect/base/returning-strategy.ts +1 -0
- package/src/core/dialect/base/sql-dialect.ts +3 -3
- package/src/core/dialect/mssql/functions.ts +24 -25
- package/src/core/dialect/mssql/index.ts +1 -4
- package/src/core/dialect/mysql/functions.ts +0 -1
- package/src/core/dialect/postgres/functions.ts +33 -34
- package/src/core/dialect/postgres/index.ts +1 -0
- package/src/core/dialect/sqlite/functions.ts +18 -19
- package/src/core/dialect/sqlite/index.ts +2 -0
- package/src/core/execution/db-executor.ts +1 -1
- package/src/core/execution/executors/mysql-executor.ts +2 -2
- package/src/core/execution/executors/postgres-executor.ts +1 -1
- package/src/core/execution/pooling/pool.ts +12 -5
- package/src/core/functions/datetime.ts +58 -34
- package/src/core/functions/numeric.ts +96 -31
- package/src/core/functions/standard-strategy.ts +35 -0
- package/src/core/functions/text.ts +84 -23
- package/src/core/functions/types.ts +23 -8
- package/src/decorators/bootstrap.ts +42 -11
- package/src/decorators/column.ts +20 -11
- package/src/decorators/decorator-metadata.ts +30 -9
- package/src/decorators/entity.ts +29 -5
- package/src/decorators/index.ts +3 -0
- package/src/decorators/relations.ts +34 -11
- package/src/orm/als.ts +34 -9
- package/src/orm/entity-context.ts +62 -8
- package/src/orm/entity-meta.ts +8 -8
- package/src/orm/entity-metadata.ts +131 -16
- package/src/orm/entity.ts +28 -29
- package/src/orm/execute.ts +19 -4
- package/src/orm/hydration.ts +42 -39
- package/src/orm/identity-map.ts +1 -1
- package/src/orm/lazy-batch.ts +74 -104
- package/src/orm/orm-session.ts +24 -23
- package/src/orm/orm.ts +2 -5
- package/src/orm/relation-change-processor.ts +12 -11
- package/src/orm/relations/belongs-to.ts +11 -11
- package/src/orm/relations/has-many.ts +54 -10
- package/src/orm/relations/has-one.ts +8 -7
- package/src/orm/relations/many-to-many.ts +13 -13
- package/src/orm/runtime-types.ts +4 -4
- package/src/orm/save-graph.ts +31 -25
- package/src/orm/unit-of-work.ts +17 -17
- package/src/query/index.ts +74 -0
- package/src/query/target.ts +46 -0
- package/src/query-builder/delete-query-state.ts +30 -0
- package/src/query-builder/delete.ts +64 -18
- package/src/query-builder/hydration-manager.ts +52 -5
- package/src/query-builder/insert-query-state.ts +30 -0
- package/src/query-builder/insert.ts +58 -10
- package/src/query-builder/query-ast-service.ts +7 -2
- package/src/query-builder/query-resolution.ts +78 -0
- package/src/query-builder/raw-column-parser.ts +7 -1
- package/src/query-builder/relation-alias.ts +7 -0
- package/src/query-builder/relation-conditions.ts +61 -48
- package/src/query-builder/relation-service.ts +68 -63
- package/src/query-builder/relation-utils.ts +3 -0
- package/src/query-builder/select/cte-facet.ts +40 -0
- package/src/query-builder/select/from-facet.ts +80 -0
- package/src/query-builder/select/join-facet.ts +62 -0
- package/src/query-builder/select/predicate-facet.ts +103 -0
- package/src/query-builder/select/projection-facet.ts +69 -0
- package/src/query-builder/select/relation-facet.ts +81 -0
- package/src/query-builder/select/setop-facet.ts +36 -0
- package/src/query-builder/select-helpers.ts +15 -2
- package/src/query-builder/select-query-builder-deps.ts +19 -1
- package/src/query-builder/select-query-state.ts +2 -1
- package/src/query-builder/select.ts +795 -1163
- package/src/query-builder/update-query-state.ts +52 -0
- package/src/query-builder/update.ts +69 -18
- package/src/schema/column.ts +26 -26
- package/src/schema/table-guards.ts +31 -0
- package/src/schema/table.ts +47 -18
- 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,
|
|
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,
|
|
27
|
-
meta: EntityMeta<
|
|
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,
|
|
71
|
+
row: Record<string, unknown>,
|
|
72
72
|
lazyRelations: TLazy[] = [] as TLazy[]
|
|
73
73
|
): EntityInstance<TTable> => {
|
|
74
|
-
const target: Record<string,
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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:
|
|
180
|
-
row: Record<string,
|
|
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,
|
|
192
|
-
cache.set(toKey(rootValue), data as Record<string,
|
|
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,
|
|
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<
|
|
234
|
+
meta: EntityMeta<TableDef>,
|
|
236
235
|
relationName: string,
|
|
237
|
-
owner:
|
|
238
|
-
): HasManyCollection<
|
|
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<
|
|
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
|
|
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<
|
|
262
|
+
meta: EntityMeta<TableDef>,
|
|
264
263
|
relationName: string,
|
|
265
264
|
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
266
|
-
owner:
|
|
267
|
-
): HasManyCollection<
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
336
|
+
(row: Record<string, unknown>) => createEntityFromRow(meta.ctx, relation.target, row),
|
|
338
337
|
localKey
|
|
339
338
|
);
|
|
340
339
|
}
|
package/src/orm/execute.ts
CHANGED
|
@@ -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,
|
|
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<
|
|
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<
|
|
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<
|
|
70
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
56
71
|
): Promise<EntityInstance<TTable>[]> {
|
|
57
72
|
const entityCtx = hydCtx.entityContext;
|
|
58
73
|
if (!entityCtx) {
|
package/src/orm/hydration.ts
CHANGED
|
@@ -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,
|
|
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<
|
|
15
|
-
const relationIndex = new Map<
|
|
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,
|
|
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:
|
|
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<
|
|
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
|
|
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,
|
|
76
|
-
const base: Record<string,
|
|
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,
|
|
93
|
-
const child: Record<string,
|
|
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
|
|
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,
|
|
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,
|
|
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];
|
package/src/orm/identity-map.ts
CHANGED
|
@@ -8,7 +8,7 @@ export class IdentityMap {
|
|
|
8
8
|
return this.buckets;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
getEntity(table: TableDef, pk: string | number):
|
|
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
|
}
|
package/src/orm/lazy-batch.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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<
|
|
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
|
-
|
|
42
|
-
|
|
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[
|
|
46
|
+
const value = tracked.entity[key];
|
|
53
47
|
if (value !== null && value !== undefined) {
|
|
54
|
-
|
|
48
|
+
collected.add(value);
|
|
55
49
|
}
|
|
56
50
|
}
|
|
51
|
+
return collected;
|
|
52
|
+
};
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
54
|
+
const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
|
|
55
|
+
Array.from(keys) as (string | number | LiteralNode)[];
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
if (
|
|
75
|
-
const key = toKey(
|
|
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
|
-
|
|
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:
|
|
89
|
-
): Promise<Map<string,
|
|
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 =
|
|
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
|
-
|
|
111
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
|
|
112
|
+
return groupRowsByMany(rows, relation.foreignKey);
|
|
113
|
+
};
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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,
|
|
141
|
+
): Promise<Map<string, Record<string, unknown>>> => {
|
|
133
142
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
134
|
-
const foreignKeys =
|
|
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
|
-
|
|
154
|
-
|
|
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 =
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
const
|
|
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
|
|
223
|
-
|
|
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()) {
|