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.
Files changed (64) hide show
  1. package/README.md +37 -40
  2. package/dist/decorators/index.cjs +344 -69
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -1
  5. package/dist/decorators/index.d.ts +1 -1
  6. package/dist/decorators/index.js +344 -69
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +567 -181
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +66 -30
  11. package/dist/index.d.ts +66 -30
  12. package/dist/index.js +559 -181
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-BKZrMRCQ.d.cts → select-BuMpVcVt.d.cts} +265 -74
  15. package/dist/{select-BKZrMRCQ.d.ts → select-BuMpVcVt.d.ts} +265 -74
  16. package/package.json +5 -1
  17. package/src/codegen/naming-strategy.ts +15 -10
  18. package/src/core/ast/aggregate-functions.ts +50 -4
  19. package/src/core/ast/builders.ts +23 -3
  20. package/src/core/ast/expression-builders.ts +36 -16
  21. package/src/core/ast/expression-nodes.ts +17 -9
  22. package/src/core/ast/join-node.ts +5 -3
  23. package/src/core/ast/join.ts +16 -16
  24. package/src/core/ast/query.ts +44 -29
  25. package/src/core/ddl/dialects/mssql-schema-dialect.ts +18 -0
  26. package/src/core/ddl/dialects/mysql-schema-dialect.ts +11 -0
  27. package/src/core/ddl/dialects/postgres-schema-dialect.ts +9 -0
  28. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +9 -0
  29. package/src/core/ddl/introspect/functions/postgres.ts +2 -6
  30. package/src/core/dialect/abstract.ts +12 -8
  31. package/src/core/dialect/base/sql-dialect.ts +58 -46
  32. package/src/core/dialect/mssql/functions.ts +24 -15
  33. package/src/core/dialect/mssql/index.ts +53 -28
  34. package/src/core/dialect/postgres/functions.ts +33 -24
  35. package/src/core/dialect/sqlite/functions.ts +19 -12
  36. package/src/core/dialect/sqlite/index.ts +22 -13
  37. package/src/core/functions/datetime.ts +2 -1
  38. package/src/core/functions/numeric.ts +2 -1
  39. package/src/core/functions/standard-strategy.ts +52 -12
  40. package/src/core/functions/text.ts +2 -1
  41. package/src/core/functions/types.ts +8 -8
  42. package/src/index.ts +5 -4
  43. package/src/orm/domain-event-bus.ts +43 -25
  44. package/src/orm/entity-meta.ts +40 -0
  45. package/src/orm/execution-context.ts +6 -0
  46. package/src/orm/hydration-context.ts +6 -4
  47. package/src/orm/orm-session.ts +35 -24
  48. package/src/orm/orm.ts +10 -10
  49. package/src/orm/query-logger.ts +15 -0
  50. package/src/orm/runtime-types.ts +60 -2
  51. package/src/orm/transaction-runner.ts +7 -0
  52. package/src/orm/unit-of-work.ts +1 -0
  53. package/src/query-builder/column-selector.ts +9 -7
  54. package/src/query-builder/insert-query-state.ts +13 -3
  55. package/src/query-builder/query-ast-service.ts +59 -38
  56. package/src/query-builder/relation-conditions.ts +38 -34
  57. package/src/query-builder/relation-manager.ts +8 -3
  58. package/src/query-builder/relation-service.ts +59 -46
  59. package/src/query-builder/select-helpers.ts +50 -0
  60. package/src/query-builder/select-query-state.ts +19 -7
  61. package/src/query-builder/select.ts +339 -167
  62. package/src/query-builder/update-query-state.ts +31 -9
  63. package/src/schema/column.ts +75 -39
  64. 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<any>;
12
+ domainEvents: DomainEventBus<E, OrmSession<E>>;
11
13
  relationChanges: RelationChangeProcessor;
12
14
  entityContext: EntityContext;
13
15
  // maybe mapping registry, converters, etc.
@@ -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 { TableDef } from '../schema/table.js';
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?: Record<string, DomainEventHandler<OrmSession>[]>;
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>(opts.domainEventHandlers);
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(name: string, handler: DomainEventHandler<OrmSession>): void {
122
- this.domainEvents.register(name, handler);
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 qb = selectFromEntity<TTable>(entityClass)
136
- .where(eq(column, id))
137
- .limit(1);
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, QueryResult } from '../core/execution/db-executor.js';
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.ts';
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
- // Nice convenience:
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();
@@ -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
@@ -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
- export interface HasDomainEvents {
38
- domainEvents?: any[];
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();
@@ -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 nodes = columns.map(col => buildColumnNode(this.env.table, col));
74
- const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
75
- const nextState = astService.withDistinct(nodes);
76
- return { state: nextState, hydration: context.hydration };
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 => valueToOperand(row[column.name]))
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 newCols = Object.entries(columns).reduce<ProjectionNode[]>((acc, [alias, val]) => {
58
- if (existingAliases.has(alias)) return acc;
59
-
60
- if (isExpressionSelectionNode(val)) {
61
- acc.push({ ...(val as FunctionNode | CaseExpressionNode | WindowFunctionNode), alias } as ProjectionNode);
62
- return acc;
63
- }
64
-
65
- const colDef = val as ColumnDef;
66
- acc.push({
67
- type: 'Column',
68
- table: colDef.table || this.table.name,
69
- name: colDef.name,
70
- alias
71
- } as ColumnNode);
72
- return acc;
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 newCols = cols.map(col => parseRawColumn(col, this.table.name, this.state.ast.ctes));
86
- const nextState = this.state.withColumns(newCols);
87
- return { state: nextState, addedColumns: newCols };
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 node = buildColumnNode(this.table, col);
162
- return this.state.withGroupBy([node]);
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 node = buildColumnNode(this.table, col);
183
- return this.state.withOrderBy([{ type: 'OrderBy', column: node, direction }]);
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 defaultLocalKey =
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: root.name, name: localKey }
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
- ): JoinNode[] => {
60
- const rootKey = relation.localKey || findPrimaryKey(root);
61
- const targetKey = relation.targetKey || findPrimaryKey(relation.target);
62
-
63
- const pivotCondition = eq(
64
- { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
65
- { type: 'Column', table: root.name, name: rootKey }
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
- ): ExpressionNode => {
101
- const base = baseRelationCondition(root, relation);
102
- return extra ? and(base, extra) : base;
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(context: SelectQueryBuilderContext, relationName: string, ast: SelectQueryNode): SelectQueryNode {
71
- return this.createService(context).applyRelationCorrelation(relationName, ast);
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