metal-orm 1.0.16 → 1.0.18
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 +37 -40
- package/dist/decorators/index.cjs +344 -69
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -1
- package/dist/decorators/index.d.ts +1 -1
- package/dist/decorators/index.js +344 -69
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +567 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -30
- package/dist/index.d.ts +66 -30
- package/dist/index.js +559 -181
- package/dist/index.js.map +1 -1
- package/dist/{select-BKZrMRCQ.d.cts → select-BuMpVcVt.d.cts} +265 -74
- package/dist/{select-BKZrMRCQ.d.ts → select-BuMpVcVt.d.ts} +265 -74
- package/package.json +5 -1
- package/src/codegen/naming-strategy.ts +15 -10
- package/src/core/ast/aggregate-functions.ts +50 -4
- package/src/core/ast/builders.ts +23 -3
- package/src/core/ast/expression-builders.ts +36 -16
- package/src/core/ast/expression-nodes.ts +17 -9
- package/src/core/ast/join-node.ts +5 -3
- package/src/core/ast/join.ts +16 -16
- package/src/core/ast/query.ts +44 -29
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +18 -0
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +11 -0
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +9 -0
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +9 -0
- package/src/core/ddl/introspect/functions/postgres.ts +2 -6
- package/src/core/dialect/abstract.ts +12 -8
- package/src/core/dialect/base/sql-dialect.ts +58 -46
- package/src/core/dialect/mssql/functions.ts +24 -15
- package/src/core/dialect/mssql/index.ts +53 -28
- package/src/core/dialect/postgres/functions.ts +33 -24
- package/src/core/dialect/sqlite/functions.ts +19 -12
- package/src/core/dialect/sqlite/index.ts +22 -13
- package/src/core/functions/datetime.ts +2 -1
- package/src/core/functions/numeric.ts +2 -1
- package/src/core/functions/standard-strategy.ts +52 -12
- package/src/core/functions/text.ts +2 -1
- package/src/core/functions/types.ts +8 -8
- package/src/index.ts +5 -4
- package/src/orm/domain-event-bus.ts +43 -25
- package/src/orm/entity-meta.ts +40 -0
- package/src/orm/execution-context.ts +6 -0
- package/src/orm/hydration-context.ts +6 -4
- package/src/orm/orm-session.ts +35 -24
- package/src/orm/orm.ts +10 -10
- package/src/orm/query-logger.ts +15 -0
- package/src/orm/runtime-types.ts +60 -2
- package/src/orm/transaction-runner.ts +7 -0
- package/src/orm/unit-of-work.ts +1 -0
- package/src/query-builder/column-selector.ts +9 -7
- package/src/query-builder/insert-query-state.ts +13 -3
- package/src/query-builder/query-ast-service.ts +59 -38
- package/src/query-builder/relation-conditions.ts +38 -34
- package/src/query-builder/relation-manager.ts +8 -3
- package/src/query-builder/relation-service.ts +59 -46
- package/src/query-builder/select-helpers.ts +50 -0
- package/src/query-builder/select-query-state.ts +19 -7
- package/src/query-builder/select.ts +339 -167
- package/src/query-builder/update-query-state.ts +31 -9
- package/src/schema/column.ts +75 -39
- package/src/schema/types.ts +17 -6
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { IdentityMap } from './identity-map.js';
|
|
2
2
|
import { UnitOfWork } from './unit-of-work.js';
|
|
3
|
-
import { DomainEventBus } from './domain-event-bus.js';
|
|
3
|
+
import type { DomainEventBus } from './domain-event-bus.js';
|
|
4
4
|
import { RelationChangeProcessor } from './relation-change-processor.js';
|
|
5
|
-
import { EntityContext } from './entity-context.js';
|
|
5
|
+
import type { EntityContext } from './entity-context.js';
|
|
6
|
+
import type { AnyDomainEvent, DomainEvent } from './runtime-types.js';
|
|
7
|
+
import type { OrmSession } from './orm-session.js';
|
|
6
8
|
|
|
7
|
-
export interface HydrationContext {
|
|
9
|
+
export interface HydrationContext<E extends DomainEvent = AnyDomainEvent> {
|
|
8
10
|
identityMap: IdentityMap;
|
|
9
11
|
unitOfWork: UnitOfWork;
|
|
10
|
-
domainEvents: DomainEventBus<
|
|
12
|
+
domainEvents: DomainEventBus<E, OrmSession<E>>;
|
|
11
13
|
relationChanges: RelationChangeProcessor;
|
|
12
14
|
entityContext: EntityContext;
|
|
13
15
|
// maybe mapping registry, converters, etc.
|
package/src/orm/orm-session.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Dialect } from '../core/dialect/abstract.js';
|
|
2
2
|
import { eq } from '../core/ast/expression.js';
|
|
3
3
|
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
4
|
-
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
5
|
-
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
6
|
-
import type {
|
|
4
|
+
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
5
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
6
|
+
import type { ColumnDef } from '../schema/column.js';
|
|
7
|
+
import type { TableDef } from '../schema/table.js';
|
|
7
8
|
import { Entity } from '../schema/types.js';
|
|
8
9
|
import { RelationDef } from '../schema/relation.js';
|
|
9
10
|
|
|
@@ -12,13 +13,15 @@ import type { EntityConstructor } from './entity-metadata.js';
|
|
|
12
13
|
import { Orm } from './orm.js';
|
|
13
14
|
import { IdentityMap } from './identity-map.js';
|
|
14
15
|
import { UnitOfWork } from './unit-of-work.js';
|
|
15
|
-
import { DomainEventBus, DomainEventHandler } from './domain-event-bus.js';
|
|
16
|
+
import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
|
|
16
17
|
import { RelationChangeProcessor } from './relation-change-processor.js';
|
|
17
18
|
import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
|
|
18
19
|
import { ExecutionContext } from './execution-context.js';
|
|
19
|
-
import { HydrationContext } from './hydration-context.js';
|
|
20
|
-
import { EntityContext } from './entity-context.js';
|
|
20
|
+
import type { HydrationContext } from './hydration-context.js';
|
|
21
|
+
import type { EntityContext } from './entity-context.js';
|
|
21
22
|
import {
|
|
23
|
+
DomainEvent,
|
|
24
|
+
OrmDomainEvent,
|
|
22
25
|
RelationChange,
|
|
23
26
|
RelationChangeEntry,
|
|
24
27
|
RelationKey,
|
|
@@ -32,25 +35,25 @@ export interface OrmInterceptor {
|
|
|
32
35
|
afterFlush?(ctx: EntityContext): Promise<void> | void;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
export interface OrmSessionOptions {
|
|
36
|
-
orm: Orm
|
|
38
|
+
export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
|
|
39
|
+
orm: Orm<E>;
|
|
37
40
|
executor: DbExecutor;
|
|
38
41
|
queryLogger?: QueryLogger;
|
|
39
42
|
interceptors?: OrmInterceptor[];
|
|
40
|
-
domainEventHandlers?:
|
|
43
|
+
domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
export class OrmSession implements EntityContext {
|
|
44
|
-
readonly orm: Orm
|
|
46
|
+
export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
|
|
47
|
+
readonly orm: Orm<E>;
|
|
45
48
|
readonly executor: DbExecutor;
|
|
46
49
|
readonly identityMap: IdentityMap;
|
|
47
50
|
readonly unitOfWork: UnitOfWork;
|
|
48
|
-
readonly domainEvents: DomainEventBus<OrmSession
|
|
51
|
+
readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
|
|
49
52
|
readonly relationChanges: RelationChangeProcessor;
|
|
50
53
|
|
|
51
54
|
private readonly interceptors: OrmInterceptor[];
|
|
52
55
|
|
|
53
|
-
constructor(opts: OrmSessionOptions) {
|
|
56
|
+
constructor(opts: OrmSessionOptions<E>) {
|
|
54
57
|
this.orm = opts.orm;
|
|
55
58
|
this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
|
|
56
59
|
this.interceptors = [...(opts.interceptors ?? [])];
|
|
@@ -58,7 +61,7 @@ export class OrmSession implements EntityContext {
|
|
|
58
61
|
this.identityMap = new IdentityMap();
|
|
59
62
|
this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
|
|
60
63
|
this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
|
|
61
|
-
this.domainEvents = new DomainEventBus<OrmSession
|
|
64
|
+
this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
get dialect(): Dialect {
|
|
@@ -118,8 +121,11 @@ export class OrmSession implements EntityContext {
|
|
|
118
121
|
this.interceptors.push(interceptor);
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
registerDomainEventHandler
|
|
122
|
-
|
|
124
|
+
registerDomainEventHandler<TType extends E['type']>(
|
|
125
|
+
type: TType,
|
|
126
|
+
handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
|
|
127
|
+
): void {
|
|
128
|
+
this.domainEvents.on(type, handler);
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
async find<TTable extends TableDef>(entityClass: EntityConstructor, id: any): Promise<Entity<TTable> | null> {
|
|
@@ -128,13 +134,18 @@ export class OrmSession implements EntityContext {
|
|
|
128
134
|
throw new Error('Entity metadata has not been bootstrapped');
|
|
129
135
|
}
|
|
130
136
|
const primaryKey = findPrimaryKey(table);
|
|
131
|
-
const column = table.columns[primaryKey];
|
|
132
|
-
if (!column) {
|
|
133
|
-
throw new Error('Entity table does not expose a primary key');
|
|
134
|
-
}
|
|
135
|
-
const
|
|
136
|
-
.
|
|
137
|
-
|
|
137
|
+
const column = table.columns[primaryKey];
|
|
138
|
+
if (!column) {
|
|
139
|
+
throw new Error('Entity table does not expose a primary key');
|
|
140
|
+
}
|
|
141
|
+
const columnSelections = Object.values(table.columns).reduce<Record<string, ColumnDef>>((acc, col) => {
|
|
142
|
+
acc[col.name] = col;
|
|
143
|
+
return acc;
|
|
144
|
+
}, {});
|
|
145
|
+
const qb = selectFromEntity<TTable>(entityClass)
|
|
146
|
+
.select(columnSelections)
|
|
147
|
+
.where(eq(column, id))
|
|
148
|
+
.limit(1);
|
|
138
149
|
const rows = await executeHydrated(this, qb);
|
|
139
150
|
return rows[0] ?? null;
|
|
140
151
|
}
|
|
@@ -206,7 +217,7 @@ export class OrmSession implements EntityContext {
|
|
|
206
217
|
};
|
|
207
218
|
}
|
|
208
219
|
|
|
209
|
-
getHydrationContext(): HydrationContext {
|
|
220
|
+
getHydrationContext(): HydrationContext<E> {
|
|
210
221
|
return {
|
|
211
222
|
identityMap: this.identityMap,
|
|
212
223
|
unitOfWork: this.unitOfWork,
|
package/src/orm/orm.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { DomainEvent, OrmDomainEvent } from './runtime-types.js';
|
|
1
2
|
import type { Dialect } from '../core/dialect/abstract.js';
|
|
2
|
-
import type { DbExecutor
|
|
3
|
+
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
3
4
|
import type { NamingStrategy } from '../codegen/naming-strategy.js';
|
|
4
5
|
import { InterceptorPipeline } from './interceptor-pipeline.js';
|
|
5
6
|
import { DefaultNamingStrategy } from '../codegen/naming-strategy.js';
|
|
6
|
-
import { OrmSession } from './orm-session.
|
|
7
|
+
import { OrmSession } from './orm-session.js';
|
|
7
8
|
|
|
8
|
-
export interface OrmOptions {
|
|
9
|
+
export interface OrmOptions<E extends DomainEvent = OrmDomainEvent> {
|
|
9
10
|
dialect: Dialect;
|
|
10
11
|
executorFactory: DbExecutorFactory;
|
|
11
12
|
interceptors?: InterceptorPipeline;
|
|
@@ -22,28 +23,27 @@ export interface ExternalTransaction {
|
|
|
22
23
|
// Transaction-specific properties
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export class Orm {
|
|
26
|
+
export class Orm<E extends DomainEvent = OrmDomainEvent> {
|
|
26
27
|
readonly dialect: Dialect;
|
|
27
28
|
readonly interceptors: InterceptorPipeline;
|
|
28
29
|
readonly namingStrategy: NamingStrategy;
|
|
29
30
|
private readonly executorFactory: DbExecutorFactory;
|
|
30
31
|
|
|
31
|
-
constructor(opts: OrmOptions) {
|
|
32
|
+
constructor(opts: OrmOptions<E>) {
|
|
32
33
|
this.dialect = opts.dialect;
|
|
33
34
|
this.interceptors = opts.interceptors ?? new InterceptorPipeline();
|
|
34
35
|
this.namingStrategy = opts.namingStrategy ?? new DefaultNamingStrategy();
|
|
35
36
|
this.executorFactory = opts.executorFactory;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
createSession(options?: { tx?: ExternalTransaction }): OrmSession {
|
|
39
|
+
createSession(options?: { tx?: ExternalTransaction }): OrmSession<E> {
|
|
39
40
|
const executor = this.executorFactory.createExecutor(options?.tx);
|
|
40
|
-
return new OrmSession({ orm: this, executor });
|
|
41
|
+
return new OrmSession<E>({ orm: this, executor });
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
async transaction<T>(fn: (session: OrmSession) => Promise<T>): Promise<T> {
|
|
44
|
+
async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
|
|
45
45
|
const executor = this.executorFactory.createTransactionalExecutor();
|
|
46
|
-
const session = new OrmSession({ orm: this, executor });
|
|
46
|
+
const session = new OrmSession<E>({ orm: this, executor });
|
|
47
47
|
try {
|
|
48
48
|
const result = await fn(session);
|
|
49
49
|
await session.commit();
|
package/src/orm/query-logger.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Represents a single SQL query log entry
|
|
5
|
+
*/
|
|
3
6
|
export interface QueryLogEntry {
|
|
7
|
+
/** The SQL query that was executed */
|
|
4
8
|
sql: string;
|
|
9
|
+
/** Parameters used in the query */
|
|
5
10
|
params?: unknown[];
|
|
6
11
|
}
|
|
7
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Function type for query logging callbacks
|
|
15
|
+
* @param entry - The query log entry to process
|
|
16
|
+
*/
|
|
8
17
|
export type QueryLogger = (entry: QueryLogEntry) => void;
|
|
9
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Creates a wrapped database executor that logs all SQL queries
|
|
21
|
+
* @param executor - Original database executor to wrap
|
|
22
|
+
* @param logger - Optional logger function to receive query log entries
|
|
23
|
+
* @returns Wrapped executor that logs queries before execution
|
|
24
|
+
*/
|
|
10
25
|
export const createQueryLoggingExecutor = (
|
|
11
26
|
executor: DbExecutor,
|
|
12
27
|
logger?: QueryLogger
|
package/src/orm/runtime-types.ts
CHANGED
|
@@ -1,39 +1,97 @@
|
|
|
1
1
|
import { RelationDef } from '../schema/relation.js';
|
|
2
2
|
import { TableDef } from '../schema/table.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Entity status enum representing the lifecycle state of an entity
|
|
6
|
+
*/
|
|
4
7
|
export enum EntityStatus {
|
|
8
|
+
/** Entity is newly created and not yet persisted */
|
|
5
9
|
New = 'new',
|
|
10
|
+
/** Entity is managed by the ORM and synchronized with the database */
|
|
6
11
|
Managed = 'managed',
|
|
12
|
+
/** Entity has been modified but not yet persisted */
|
|
7
13
|
Dirty = 'dirty',
|
|
14
|
+
/** Entity has been marked for removal */
|
|
8
15
|
Removed = 'removed',
|
|
16
|
+
/** Entity is detached from the ORM context */
|
|
9
17
|
Detached = 'detached'
|
|
10
18
|
}
|
|
11
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Represents an entity being tracked by the ORM
|
|
22
|
+
*/
|
|
12
23
|
export interface TrackedEntity {
|
|
24
|
+
/** The table definition this entity belongs to */
|
|
13
25
|
table: TableDef;
|
|
26
|
+
/** The actual entity instance */
|
|
14
27
|
entity: any;
|
|
28
|
+
/** Primary key value of the entity */
|
|
15
29
|
pk: string | number | null;
|
|
30
|
+
/** Current status of the entity */
|
|
16
31
|
status: EntityStatus;
|
|
32
|
+
/** Original values of the entity when it was loaded */
|
|
17
33
|
original: Record<string, any> | null;
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Type representing a key for relation navigation
|
|
38
|
+
*/
|
|
20
39
|
export type RelationKey = string;
|
|
21
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Represents a change operation on a relation
|
|
43
|
+
* @typeParam T - Type of the related entity
|
|
44
|
+
*/
|
|
22
45
|
export type RelationChange<T> =
|
|
23
46
|
| { kind: 'add'; entity: T }
|
|
24
47
|
| { kind: 'attach'; entity: T }
|
|
25
48
|
| { kind: 'remove'; entity: T }
|
|
26
49
|
| { kind: 'detach'; entity: T };
|
|
27
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Represents a relation change entry in the unit of work
|
|
53
|
+
*/
|
|
28
54
|
export interface RelationChangeEntry {
|
|
55
|
+
/** Root entity that owns the relation */
|
|
29
56
|
root: any;
|
|
57
|
+
/** Key of the relation being changed */
|
|
30
58
|
relationKey: RelationKey;
|
|
59
|
+
/** Table definition of the root entity */
|
|
31
60
|
rootTable: TableDef;
|
|
61
|
+
/** Name of the relation */
|
|
32
62
|
relationName: string;
|
|
63
|
+
/** Relation definition */
|
|
33
64
|
relation: RelationDef;
|
|
65
|
+
/** The change being applied */
|
|
34
66
|
change: RelationChange<any>;
|
|
35
67
|
}
|
|
36
68
|
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Represents a domain event that can be emitted by entities
|
|
71
|
+
* @typeParam TType - Type of the event (string literal)
|
|
72
|
+
*/
|
|
73
|
+
export interface DomainEvent<TType extends string = string> {
|
|
74
|
+
/** Type identifier for the event */
|
|
75
|
+
readonly type: TType;
|
|
76
|
+
/** Timestamp when the event occurred */
|
|
77
|
+
readonly occurredAt?: Date;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Type representing any domain event
|
|
82
|
+
*/
|
|
83
|
+
export type AnyDomainEvent = DomainEvent<string>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Type representing ORM-specific domain events
|
|
87
|
+
*/
|
|
88
|
+
export type OrmDomainEvent = AnyDomainEvent;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Interface for entities that can emit domain events
|
|
92
|
+
* @typeParam E - Type of domain events this entity can emit
|
|
93
|
+
*/
|
|
94
|
+
export interface HasDomainEvents<E extends DomainEvent = AnyDomainEvent> {
|
|
95
|
+
/** Array of domain events emitted by this entity */
|
|
96
|
+
domainEvents?: E[];
|
|
39
97
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Executes a function within a database transaction
|
|
5
|
+
* @param executor - Database executor to use for transaction operations
|
|
6
|
+
* @param action - Function to execute within the transaction
|
|
7
|
+
* @returns Promise that resolves when the transaction is complete
|
|
8
|
+
* @throws Re-throws any errors that occur during the transaction (after rolling back)
|
|
9
|
+
*/
|
|
3
10
|
export const runInTransaction = async (executor: DbExecutor, action: () => Promise<void>): Promise<void> => {
|
|
4
11
|
if (!executor.beginTransaction) {
|
|
5
12
|
await action();
|
package/src/orm/unit-of-work.ts
CHANGED
|
@@ -215,6 +215,7 @@ export class UnitOfWork {
|
|
|
215
215
|
private extractColumns(table: TableDef, entity: any): Record<string, unknown> {
|
|
216
216
|
const payload: Record<string, unknown> = {};
|
|
217
217
|
for (const column of Object.keys(table.columns)) {
|
|
218
|
+
if (entity[column] === undefined) continue;
|
|
218
219
|
payload[column] = entity[column];
|
|
219
220
|
}
|
|
220
221
|
return payload;
|
|
@@ -69,10 +69,12 @@ export class ColumnSelector {
|
|
|
69
69
|
* @param columns - Columns to make distinct
|
|
70
70
|
* @returns Updated query context with DISTINCT clause
|
|
71
71
|
*/
|
|
72
|
-
distinct(context: SelectQueryBuilderContext, columns: (ColumnDef | ColumnNode)[]): SelectQueryBuilderContext {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
72
|
+
distinct(context: SelectQueryBuilderContext, columns: (ColumnDef | ColumnNode)[]): SelectQueryBuilderContext {
|
|
73
|
+
const from = context.state.ast.from;
|
|
74
|
+
const tableRef = from.type === 'Table' && from.alias ? { ...this.env.table, alias: from.alias } : this.env.table;
|
|
75
|
+
const nodes = columns.map(col => buildColumnNode(tableRef, col));
|
|
76
|
+
const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
|
|
77
|
+
const nextState = astService.withDistinct(nodes);
|
|
78
|
+
return { state: nextState, hydration: context.hydration };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { InsertQueryNode, TableNode } from '../core/ast/query.js';
|
|
3
|
-
import { ColumnNode, OperandNode, valueToOperand } from '../core/ast/expression.js';
|
|
3
|
+
import { ColumnNode, OperandNode, isValueOperandInput, valueToOperand } from '../core/ast/expression.js';
|
|
4
4
|
import { buildColumnNodes, createTableNode } from '../core/ast/builders.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -31,8 +31,18 @@ export class InsertQueryState {
|
|
|
31
31
|
? this.ast.columns
|
|
32
32
|
: buildColumnNodes(this.table, Object.keys(rows[0]));
|
|
33
33
|
|
|
34
|
-
const newRows: OperandNode[][] = rows.map(row =>
|
|
35
|
-
definedColumns.map(column =>
|
|
34
|
+
const newRows: OperandNode[][] = rows.map((row, rowIndex) =>
|
|
35
|
+
definedColumns.map(column => {
|
|
36
|
+
const rawValue = row[column.name];
|
|
37
|
+
|
|
38
|
+
if (!isValueOperandInput(rawValue)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Invalid insert value for column "${column.name}" in row ${rowIndex}: only primitives, null, or OperandNodes are allowed`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return valueToOperand(rawValue);
|
|
45
|
+
})
|
|
36
46
|
);
|
|
37
47
|
|
|
38
48
|
return this.clone({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { ColumnDef } from '../schema/column.js';
|
|
3
|
-
import { SelectQueryNode, CommonTableExpressionNode, SetOperationKind, SetOperationNode } from '../core/ast/query.js';
|
|
3
|
+
import { SelectQueryNode, CommonTableExpressionNode, SetOperationKind, SetOperationNode, TableSourceNode } from '../core/ast/query.js';
|
|
4
4
|
import { buildColumnNode } from '../core/ast/builders.js';
|
|
5
5
|
import {
|
|
6
6
|
ColumnNode,
|
|
@@ -47,30 +47,36 @@ export class QueryAstService {
|
|
|
47
47
|
* @param columns - Columns to select (key: alias, value: column definition or expression)
|
|
48
48
|
* @returns Column selection result with updated state and added columns
|
|
49
49
|
*/
|
|
50
|
-
select(
|
|
51
|
-
columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>
|
|
52
|
-
): ColumnSelectionResult {
|
|
53
|
-
const existingAliases = new Set(
|
|
54
|
-
this.state.ast.columns.map(c => (c as ColumnNode).alias || (c as ColumnNode).name)
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
select(
|
|
51
|
+
columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>
|
|
52
|
+
): ColumnSelectionResult {
|
|
53
|
+
const existingAliases = new Set(
|
|
54
|
+
this.state.ast.columns.map(c => (c as ColumnNode).alias || (c as ColumnNode).name)
|
|
55
|
+
);
|
|
56
|
+
const from = this.state.ast.from;
|
|
57
|
+
const rootTableName = from.type === 'Table' && from.alias ? from.alias : this.table.name;
|
|
58
|
+
|
|
59
|
+
const newCols = Object.entries(columns).reduce<ProjectionNode[]>((acc, [alias, val]) => {
|
|
60
|
+
if (existingAliases.has(alias)) return acc;
|
|
61
|
+
|
|
62
|
+
if (isExpressionSelectionNode(val)) {
|
|
63
|
+
acc.push({ ...(val as FunctionNode | CaseExpressionNode | WindowFunctionNode), alias } as ProjectionNode);
|
|
64
|
+
return acc;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const colDef = val as ColumnDef;
|
|
68
|
+
const resolvedTable =
|
|
69
|
+
colDef.table && colDef.table === this.table.name && from.type === 'Table' && from.alias
|
|
70
|
+
? from.alias
|
|
71
|
+
: colDef.table || rootTableName;
|
|
72
|
+
acc.push({
|
|
73
|
+
type: 'Column',
|
|
74
|
+
table: resolvedTable,
|
|
75
|
+
name: colDef.name,
|
|
76
|
+
alias
|
|
77
|
+
} as ColumnNode);
|
|
78
|
+
return acc;
|
|
79
|
+
}, []);
|
|
74
80
|
|
|
75
81
|
const nextState = this.state.withColumns(newCols);
|
|
76
82
|
return { state: nextState, addedColumns: newCols };
|
|
@@ -81,11 +87,13 @@ export class QueryAstService {
|
|
|
81
87
|
* @param cols - Raw column expressions
|
|
82
88
|
* @returns Column selection result with updated state and added columns
|
|
83
89
|
*/
|
|
84
|
-
selectRaw(cols: string[]): ColumnSelectionResult {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
selectRaw(cols: string[]): ColumnSelectionResult {
|
|
91
|
+
const from = this.state.ast.from;
|
|
92
|
+
const defaultTable = from.type === 'Table' && from.alias ? from.alias : this.table.name;
|
|
93
|
+
const newCols = cols.map(col => parseRawColumn(col, defaultTable, this.state.ast.ctes));
|
|
94
|
+
const nextState = this.state.withColumns(newCols);
|
|
95
|
+
return { state: nextState, addedColumns: newCols };
|
|
96
|
+
}
|
|
89
97
|
|
|
90
98
|
/**
|
|
91
99
|
* Adds a Common Table Expression (CTE) to the query
|
|
@@ -121,6 +129,15 @@ export class QueryAstService {
|
|
|
121
129
|
};
|
|
122
130
|
return this.state.withSetOperation(op);
|
|
123
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Replaces the FROM clause for the current query.
|
|
135
|
+
* @param from - Table source to use in the FROM clause
|
|
136
|
+
* @returns Updated query state with new FROM
|
|
137
|
+
*/
|
|
138
|
+
withFrom(from: TableSourceNode): SelectQueryState {
|
|
139
|
+
return this.state.withFrom(from);
|
|
140
|
+
}
|
|
124
141
|
|
|
125
142
|
/**
|
|
126
143
|
* Selects a subquery as a column
|
|
@@ -157,10 +174,12 @@ export class QueryAstService {
|
|
|
157
174
|
* @param col - Column to group by
|
|
158
175
|
* @returns Updated query state with GROUP BY clause
|
|
159
176
|
*/
|
|
160
|
-
withGroupBy(col: ColumnDef | ColumnNode): SelectQueryState {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
withGroupBy(col: ColumnDef | ColumnNode): SelectQueryState {
|
|
178
|
+
const from = this.state.ast.from;
|
|
179
|
+
const tableRef = from.type === 'Table' && from.alias ? { ...this.table, alias: from.alias } : this.table;
|
|
180
|
+
const node = buildColumnNode(tableRef, col);
|
|
181
|
+
return this.state.withGroupBy([node]);
|
|
182
|
+
}
|
|
164
183
|
|
|
165
184
|
/**
|
|
166
185
|
* Adds a HAVING clause to the query
|
|
@@ -178,10 +197,12 @@ export class QueryAstService {
|
|
|
178
197
|
* @param direction - Order direction (ASC/DESC)
|
|
179
198
|
* @returns Updated query state with ORDER BY clause
|
|
180
199
|
*/
|
|
181
|
-
withOrderBy(col: ColumnDef | ColumnNode, direction: OrderDirection): SelectQueryState {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
200
|
+
withOrderBy(col: ColumnDef | ColumnNode, direction: OrderDirection): SelectQueryState {
|
|
201
|
+
const from = this.state.ast.from;
|
|
202
|
+
const tableRef = from.type === 'Table' && from.alias ? { ...this.table, alias: from.alias } : this.table;
|
|
203
|
+
const node = buildColumnNode(tableRef, col);
|
|
204
|
+
return this.state.withOrderBy([{ type: 'OrderBy', column: node, direction }]);
|
|
205
|
+
}
|
|
185
206
|
|
|
186
207
|
/**
|
|
187
208
|
* Adds a DISTINCT clause to the query
|
|
@@ -21,25 +21,26 @@ const assertNever = (value: never): never => {
|
|
|
21
21
|
* @param relation - Relation definition
|
|
22
22
|
* @returns Expression node representing the join condition
|
|
23
23
|
*/
|
|
24
|
-
const baseRelationCondition = (root: TableDef, relation: RelationDef): ExpressionNode => {
|
|
25
|
-
const
|
|
24
|
+
const baseRelationCondition = (root: TableDef, relation: RelationDef, rootAlias?: string): ExpressionNode => {
|
|
25
|
+
const rootTable = rootAlias || root.name;
|
|
26
|
+
const defaultLocalKey =
|
|
26
27
|
relation.type === RelationKinds.HasMany || relation.type === RelationKinds.HasOne
|
|
27
28
|
? findPrimaryKey(root)
|
|
28
29
|
: findPrimaryKey(relation.target);
|
|
29
|
-
const localKey = relation.localKey || defaultLocalKey;
|
|
30
|
+
const localKey = relation.localKey || defaultLocalKey;
|
|
30
31
|
|
|
31
32
|
switch (relation.type) {
|
|
32
33
|
case RelationKinds.HasMany:
|
|
33
34
|
case RelationKinds.HasOne:
|
|
34
35
|
return eq(
|
|
35
36
|
{ type: 'Column', table: relation.target.name, name: relation.foreignKey },
|
|
36
|
-
{ type: 'Column', table:
|
|
37
|
+
{ type: 'Column', table: rootTable, name: localKey }
|
|
38
|
+
);
|
|
39
|
+
case RelationKinds.BelongsTo:
|
|
40
|
+
return eq(
|
|
41
|
+
{ type: 'Column', table: relation.target.name, name: localKey },
|
|
42
|
+
{ type: 'Column', table: rootTable, name: relation.foreignKey }
|
|
37
43
|
);
|
|
38
|
-
case RelationKinds.BelongsTo:
|
|
39
|
-
return eq(
|
|
40
|
-
{ type: 'Column', table: relation.target.name, name: localKey },
|
|
41
|
-
{ type: 'Column', table: root.name, name: relation.foreignKey }
|
|
42
|
-
);
|
|
43
44
|
case RelationKinds.BelongsToMany:
|
|
44
45
|
throw new Error('BelongsToMany relations do not support the standard join condition builder');
|
|
45
46
|
default:
|
|
@@ -50,20 +51,22 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef): Expressio
|
|
|
50
51
|
/**
|
|
51
52
|
* Builds the join nodes required to include a BelongsToMany relation.
|
|
52
53
|
*/
|
|
53
|
-
export const buildBelongsToManyJoins = (
|
|
54
|
-
root: TableDef,
|
|
55
|
-
relationName: string,
|
|
56
|
-
relation: BelongsToManyRelation,
|
|
57
|
-
joinKind: JoinKind,
|
|
58
|
-
extra?: ExpressionNode
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
export const buildBelongsToManyJoins = (
|
|
55
|
+
root: TableDef,
|
|
56
|
+
relationName: string,
|
|
57
|
+
relation: BelongsToManyRelation,
|
|
58
|
+
joinKind: JoinKind,
|
|
59
|
+
extra?: ExpressionNode,
|
|
60
|
+
rootAlias?: string
|
|
61
|
+
): JoinNode[] => {
|
|
62
|
+
const rootKey = relation.localKey || findPrimaryKey(root);
|
|
63
|
+
const targetKey = relation.targetKey || findPrimaryKey(relation.target);
|
|
64
|
+
const rootTable = rootAlias || root.name;
|
|
65
|
+
|
|
66
|
+
const pivotCondition = eq(
|
|
67
|
+
{ type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
|
|
68
|
+
{ type: 'Column', table: rootTable, name: rootKey }
|
|
69
|
+
);
|
|
67
70
|
|
|
68
71
|
const pivotJoin = createJoinNode(joinKind, relation.pivotTable.name, pivotCondition);
|
|
69
72
|
|
|
@@ -93,14 +96,15 @@ export const buildBelongsToManyJoins = (
|
|
|
93
96
|
* @param extra - Optional additional expression to combine with AND
|
|
94
97
|
* @returns Expression node representing the complete join condition
|
|
95
98
|
*/
|
|
96
|
-
export const buildRelationJoinCondition = (
|
|
97
|
-
root: TableDef,
|
|
98
|
-
relation: RelationDef,
|
|
99
|
-
extra?: ExpressionNode
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
export const buildRelationJoinCondition = (
|
|
100
|
+
root: TableDef,
|
|
101
|
+
relation: RelationDef,
|
|
102
|
+
extra?: ExpressionNode,
|
|
103
|
+
rootAlias?: string
|
|
104
|
+
): ExpressionNode => {
|
|
105
|
+
const base = baseRelationCondition(root, relation, rootAlias);
|
|
106
|
+
return extra ? and(base, extra) : base;
|
|
107
|
+
};
|
|
104
108
|
|
|
105
109
|
/**
|
|
106
110
|
* Builds a relation correlation condition for subqueries
|
|
@@ -108,6 +112,6 @@ export const buildRelationJoinCondition = (
|
|
|
108
112
|
* @param relation - Relation definition
|
|
109
113
|
* @returns Expression node representing the correlation condition
|
|
110
114
|
*/
|
|
111
|
-
export const buildRelationCorrelation = (root: TableDef, relation: RelationDef): ExpressionNode => {
|
|
112
|
-
return baseRelationCondition(root, relation);
|
|
113
|
-
};
|
|
115
|
+
export const buildRelationCorrelation = (root: TableDef, relation: RelationDef, rootAlias?: string): ExpressionNode => {
|
|
116
|
+
return baseRelationCondition(root, relation, rootAlias);
|
|
117
|
+
};
|
|
@@ -67,9 +67,14 @@ export class RelationManager {
|
|
|
67
67
|
* @param ast - Query AST to modify
|
|
68
68
|
* @returns Modified query AST with relation correlation
|
|
69
69
|
*/
|
|
70
|
-
applyRelationCorrelation(
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
applyRelationCorrelation(
|
|
71
|
+
context: SelectQueryBuilderContext,
|
|
72
|
+
relationName: string,
|
|
73
|
+
ast: SelectQueryNode,
|
|
74
|
+
additionalCorrelation?: ExpressionNode
|
|
75
|
+
): SelectQueryNode {
|
|
76
|
+
return this.createService(context).applyRelationCorrelation(relationName, ast, additionalCorrelation);
|
|
77
|
+
}
|
|
73
78
|
|
|
74
79
|
/**
|
|
75
80
|
* Creates a relation service instance
|