metal-orm 1.0.14 → 1.0.16

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 (129) hide show
  1. package/README.md +69 -67
  2. package/dist/decorators/index.cjs +1983 -224
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +6 -6
  5. package/dist/decorators/index.d.ts +6 -6
  6. package/dist/decorators/index.js +1982 -224
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +5284 -3751
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +524 -169
  11. package/dist/index.d.ts +524 -169
  12. package/dist/index.js +5197 -3736
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-CCp1oz9p.d.cts → select-BKZrMRCQ.d.cts} +555 -94
  15. package/dist/{select-CCp1oz9p.d.ts → select-BKZrMRCQ.d.ts} +555 -94
  16. package/package.json +1 -1
  17. package/src/codegen/naming-strategy.ts +64 -0
  18. package/src/codegen/typescript.ts +19 -21
  19. package/src/core/ast/adapters.ts +21 -0
  20. package/src/core/ast/aggregate-functions.ts +13 -13
  21. package/src/core/ast/builders.ts +56 -43
  22. package/src/core/ast/expression-builders.ts +34 -34
  23. package/src/core/ast/expression-nodes.ts +18 -16
  24. package/src/core/ast/expression-visitor.ts +122 -69
  25. package/src/core/ast/expression.ts +6 -4
  26. package/src/core/ast/join-metadata.ts +15 -0
  27. package/src/core/ast/join-node.ts +22 -20
  28. package/src/core/ast/join.ts +5 -5
  29. package/src/core/ast/query.ts +52 -88
  30. package/src/core/ast/types.ts +20 -0
  31. package/src/core/ast/window-functions.ts +55 -55
  32. package/src/core/ddl/dialects/base-schema-dialect.ts +20 -6
  33. package/src/core/ddl/dialects/mssql-schema-dialect.ts +32 -8
  34. package/src/core/ddl/dialects/mysql-schema-dialect.ts +21 -10
  35. package/src/core/ddl/dialects/postgres-schema-dialect.ts +52 -7
  36. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +23 -9
  37. package/src/core/ddl/introspect/catalogs/index.ts +1 -0
  38. package/src/core/ddl/introspect/catalogs/postgres.ts +143 -0
  39. package/src/core/ddl/introspect/context.ts +9 -0
  40. package/src/core/ddl/introspect/functions/postgres.ts +26 -0
  41. package/src/core/ddl/introspect/mssql.ts +149 -149
  42. package/src/core/ddl/introspect/mysql.ts +99 -99
  43. package/src/core/ddl/introspect/postgres.ts +245 -154
  44. package/src/core/ddl/introspect/registry.ts +26 -0
  45. package/src/core/ddl/introspect/run-select.ts +25 -0
  46. package/src/core/ddl/introspect/sqlite.ts +7 -7
  47. package/src/core/ddl/introspect/types.ts +23 -19
  48. package/src/core/ddl/introspect/utils.ts +1 -1
  49. package/src/core/ddl/naming-strategy.ts +10 -0
  50. package/src/core/ddl/schema-dialect.ts +41 -0
  51. package/src/core/ddl/schema-diff.ts +211 -179
  52. package/src/core/ddl/schema-generator.ts +17 -90
  53. package/src/core/ddl/schema-introspect.ts +25 -32
  54. package/src/core/ddl/schema-plan-executor.ts +17 -0
  55. package/src/core/ddl/schema-types.ts +46 -39
  56. package/src/core/ddl/sql-writing.ts +170 -0
  57. package/src/core/dialect/abstract.ts +172 -126
  58. package/src/core/dialect/base/cte-compiler.ts +33 -0
  59. package/src/core/dialect/base/function-table-formatter.ts +132 -0
  60. package/src/core/dialect/base/groupby-compiler.ts +21 -0
  61. package/src/core/dialect/base/join-compiler.ts +26 -0
  62. package/src/core/dialect/base/orderby-compiler.ts +21 -0
  63. package/src/core/dialect/base/pagination-strategy.ts +32 -0
  64. package/src/core/dialect/base/returning-strategy.ts +56 -0
  65. package/src/core/dialect/base/sql-dialect.ts +181 -204
  66. package/src/core/dialect/dialect-factory.ts +91 -0
  67. package/src/core/dialect/mssql/functions.ts +101 -0
  68. package/src/core/dialect/mssql/index.ts +128 -126
  69. package/src/core/dialect/mysql/functions.ts +101 -0
  70. package/src/core/dialect/mysql/index.ts +20 -18
  71. package/src/core/dialect/postgres/functions.ts +95 -0
  72. package/src/core/dialect/postgres/index.ts +30 -28
  73. package/src/core/dialect/sqlite/functions.ts +115 -0
  74. package/src/core/dialect/sqlite/index.ts +30 -28
  75. package/src/core/driver/database-driver.ts +11 -0
  76. package/src/core/driver/mssql-driver.ts +20 -0
  77. package/src/core/driver/mysql-driver.ts +20 -0
  78. package/src/core/driver/postgres-driver.ts +20 -0
  79. package/src/core/driver/sqlite-driver.ts +20 -0
  80. package/src/core/execution/db-executor.ts +63 -0
  81. package/src/core/execution/executors/mssql-executor.ts +39 -0
  82. package/src/core/execution/executors/mysql-executor.ts +47 -0
  83. package/src/core/execution/executors/postgres-executor.ts +32 -0
  84. package/src/core/execution/executors/sqlite-executor.ts +31 -0
  85. package/src/core/functions/datetime.ts +132 -0
  86. package/src/core/functions/numeric.ts +179 -0
  87. package/src/core/functions/standard-strategy.ts +47 -0
  88. package/src/core/functions/text.ts +147 -0
  89. package/src/core/functions/types.ts +18 -0
  90. package/src/core/hydration/types.ts +57 -0
  91. package/src/decorators/bootstrap.ts +10 -0
  92. package/src/decorators/column.ts +13 -4
  93. package/src/decorators/relations.ts +15 -0
  94. package/src/index.ts +37 -19
  95. package/src/orm/entity-context.ts +30 -0
  96. package/src/orm/entity-meta.ts +2 -2
  97. package/src/orm/entity-metadata.ts +8 -6
  98. package/src/orm/entity.ts +72 -41
  99. package/src/orm/execute.ts +42 -25
  100. package/src/orm/execution-context.ts +12 -0
  101. package/src/orm/hydration-context.ts +14 -0
  102. package/src/orm/hydration.ts +25 -17
  103. package/src/orm/identity-map.ts +4 -0
  104. package/src/orm/interceptor-pipeline.ts +29 -0
  105. package/src/orm/lazy-batch.ts +50 -6
  106. package/src/orm/orm-session.ts +234 -0
  107. package/src/orm/orm.ts +58 -0
  108. package/src/orm/query-logger.ts +1 -1
  109. package/src/orm/relation-change-processor.ts +48 -3
  110. package/src/orm/relations/belongs-to.ts +45 -44
  111. package/src/orm/relations/has-many.ts +44 -43
  112. package/src/orm/relations/has-one.ts +140 -0
  113. package/src/orm/relations/many-to-many.ts +46 -45
  114. package/src/orm/transaction-runner.ts +1 -1
  115. package/src/orm/unit-of-work.ts +66 -61
  116. package/src/query-builder/delete.ts +22 -5
  117. package/src/query-builder/hydration-manager.ts +2 -1
  118. package/src/query-builder/hydration-planner.ts +8 -7
  119. package/src/query-builder/insert.ts +22 -5
  120. package/src/query-builder/relation-conditions.ts +9 -8
  121. package/src/query-builder/relation-service.ts +3 -2
  122. package/src/query-builder/select.ts +575 -64
  123. package/src/query-builder/update.ts +22 -5
  124. package/src/schema/column.ts +246 -246
  125. package/src/schema/relation.ts +35 -1
  126. package/src/schema/table.ts +28 -28
  127. package/src/schema/types.ts +41 -31
  128. package/src/orm/db-executor.ts +0 -11
  129. package/src/orm/orm-context.ts +0 -159
@@ -0,0 +1,140 @@
1
+ import { HasOneReference } from '../../schema/types.js';
2
+ import { EntityContext } from '../entity-context.js';
3
+ import { RelationKey } from '../runtime-types.js';
4
+ import { HasOneRelation } from '../../schema/relation.js';
5
+ import { TableDef } from '../../schema/table.js';
6
+ import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
7
+
8
+ type Row = Record<string, any>;
9
+
10
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
+
12
+ const hideInternal = (obj: any, keys: string[]): void => {
13
+ for (const key of keys) {
14
+ Object.defineProperty(obj, key, {
15
+ value: obj[key],
16
+ writable: false,
17
+ configurable: false,
18
+ enumerable: false
19
+ });
20
+ }
21
+ };
22
+
23
+ export class DefaultHasOneReference<TChild> implements HasOneReference<TChild> {
24
+ private loaded = false;
25
+ private current: TChild | null = null;
26
+
27
+ constructor(
28
+ private readonly ctx: EntityContext,
29
+ private readonly meta: EntityMeta<any>,
30
+ private readonly root: any,
31
+ private readonly relationName: string,
32
+ private readonly relation: HasOneRelation,
33
+ private readonly rootTable: TableDef,
34
+ private readonly loader: () => Promise<Map<string, Row>>,
35
+ private readonly createEntity: (row: Row) => TChild,
36
+ private readonly localKey: string
37
+ ) {
38
+ hideInternal(this, [
39
+ 'ctx',
40
+ 'meta',
41
+ 'root',
42
+ 'relationName',
43
+ 'relation',
44
+ 'rootTable',
45
+ 'loader',
46
+ 'createEntity',
47
+ 'localKey'
48
+ ]);
49
+ this.populateFromHydrationCache();
50
+ }
51
+
52
+ async load(): Promise<TChild | null> {
53
+ if (this.loaded) return this.current;
54
+ const map = await this.loader();
55
+ const keyValue = this.root[this.localKey];
56
+ if (keyValue === undefined || keyValue === null) {
57
+ this.loaded = true;
58
+ return this.current;
59
+ }
60
+ const row = map.get(toKey(keyValue));
61
+ this.current = row ? this.createEntity(row) : null;
62
+ this.loaded = true;
63
+ return this.current;
64
+ }
65
+
66
+ get(): TChild | null {
67
+ return this.current;
68
+ }
69
+
70
+ set(data: Partial<TChild> | TChild | null): TChild | null {
71
+ if (data === null) {
72
+ return this.detachCurrent();
73
+ }
74
+
75
+ const entity = hasEntityMeta(data) ? (data as TChild) : this.createEntity(data as Row);
76
+ if (this.current && this.current !== entity) {
77
+ this.ctx.registerRelationChange(
78
+ this.root,
79
+ this.relationKey,
80
+ this.rootTable,
81
+ this.relationName,
82
+ this.relation,
83
+ { kind: 'remove', entity: this.current }
84
+ );
85
+ }
86
+
87
+ this.assignForeignKey(entity);
88
+ this.current = entity;
89
+ this.loaded = true;
90
+
91
+ this.ctx.registerRelationChange(
92
+ this.root,
93
+ this.relationKey,
94
+ this.rootTable,
95
+ this.relationName,
96
+ this.relation,
97
+ { kind: 'attach', entity }
98
+ );
99
+
100
+ return entity;
101
+ }
102
+
103
+ toJSON(): TChild | null {
104
+ return this.current;
105
+ }
106
+
107
+ private detachCurrent(): TChild | null {
108
+ const previous = this.current;
109
+ if (!previous) return null;
110
+ this.current = null;
111
+ this.loaded = true;
112
+ this.ctx.registerRelationChange(
113
+ this.root,
114
+ this.relationKey,
115
+ this.rootTable,
116
+ this.relationName,
117
+ this.relation,
118
+ { kind: 'remove', entity: previous }
119
+ );
120
+ return null;
121
+ }
122
+
123
+ private assignForeignKey(entity: TChild): void {
124
+ const keyValue = this.root[this.localKey];
125
+ (entity as Row)[this.relation.foreignKey] = keyValue;
126
+ }
127
+
128
+ private get relationKey(): RelationKey {
129
+ return `${this.rootTable.name}.${this.relationName}`;
130
+ }
131
+
132
+ private populateFromHydrationCache(): void {
133
+ const keyValue = this.root[this.localKey];
134
+ if (keyValue === undefined || keyValue === null) return;
135
+ const row = getHydrationRecord(this.meta, this.relationName, keyValue);
136
+ if (!row) return;
137
+ this.current = this.createEntity(row);
138
+ this.loaded = true;
139
+ }
140
+ }
@@ -1,46 +1,47 @@
1
1
  import { ManyToManyCollection } from '../../schema/types.js';
2
- import { OrmContext, RelationKey } from '../orm-context.js';
2
+ import { EntityContext } from '../entity-context.js';
3
+ import { RelationKey } from '../runtime-types.js';
3
4
  import { BelongsToManyRelation } from '../../schema/relation.js';
4
5
  import { TableDef } from '../../schema/table.js';
5
6
  import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
6
7
  import { EntityMeta, getHydrationRows } from '../entity-meta.js';
7
8
 
8
- type Rows = Record<string, any>[];
9
-
10
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
-
12
- const hideInternal = (obj: any, keys: string[]): void => {
13
- for (const key of keys) {
14
- Object.defineProperty(obj, key, {
15
- value: obj[key],
16
- writable: false,
17
- configurable: false,
18
- enumerable: false
19
- });
20
- }
21
- };
22
-
23
- export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
24
- private loaded = false;
25
- private items: TTarget[] = [];
26
-
27
- constructor(
28
- private readonly ctx: OrmContext,
9
+ type Rows = Record<string, any>[];
10
+
11
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
12
+
13
+ const hideInternal = (obj: any, keys: string[]): void => {
14
+ for (const key of keys) {
15
+ Object.defineProperty(obj, key, {
16
+ value: obj[key],
17
+ writable: false,
18
+ configurable: false,
19
+ enumerable: false
20
+ });
21
+ }
22
+ };
23
+
24
+ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
25
+ private loaded = false;
26
+ private items: TTarget[] = [];
27
+
28
+ constructor(
29
+ private readonly ctx: EntityContext,
29
30
  private readonly meta: EntityMeta<any>,
30
31
  private readonly root: any,
31
32
  private readonly relationName: string,
32
33
  private readonly relation: BelongsToManyRelation,
33
34
  private readonly rootTable: TableDef,
34
35
  private readonly loader: () => Promise<Map<string, Rows>>,
35
- private readonly createEntity: (row: Record<string, any>) => TTarget,
36
- private readonly localKey: string
37
- ) {
38
- hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
39
- this.hydrateFromCache();
40
- }
41
-
42
- async load(): Promise<TTarget[]> {
43
- if (this.loaded) return this.items;
36
+ private readonly createEntity: (row: Record<string, any>) => TTarget,
37
+ private readonly localKey: string
38
+ ) {
39
+ hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
40
+ this.hydrateFromCache();
41
+ }
42
+
43
+ async load(): Promise<TTarget[]> {
44
+ if (this.loaded) return this.items;
44
45
  const map = await this.loader();
45
46
  const key = toKey(this.root[this.localKey]);
46
47
  const rows = map.get(key) ?? [];
@@ -144,22 +145,22 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
144
145
  return this.relation.targetKey || findPrimaryKey(this.relation.target);
145
146
  }
146
147
 
147
- private hydrateFromCache(): void {
148
- const keyValue = this.root[this.localKey];
149
- if (keyValue === undefined || keyValue === null) return;
150
- const rows = getHydrationRows(this.meta, this.relationName, keyValue);
151
- if (!rows?.length) return;
148
+ private hydrateFromCache(): void {
149
+ const keyValue = this.root[this.localKey];
150
+ if (keyValue === undefined || keyValue === null) return;
151
+ const rows = getHydrationRows(this.meta, this.relationName, keyValue);
152
+ if (!rows?.length) return;
152
153
  this.items = rows.map(row => {
153
154
  const entity = this.createEntity(row);
154
155
  if ((row as any)._pivot) {
155
156
  (entity as any)._pivot = (row as any)._pivot;
156
157
  }
157
- return entity;
158
- });
159
- this.loaded = true;
160
- }
161
-
162
- toJSON(): TTarget[] {
163
- return this.items;
164
- }
165
- }
158
+ return entity;
159
+ });
160
+ this.loaded = true;
161
+ }
162
+
163
+ toJSON(): TTarget[] {
164
+ return this.items;
165
+ }
166
+ }
@@ -1,4 +1,4 @@
1
- import type { DbExecutor } from './db-executor.js';
1
+ import type { DbExecutor } from '../core/execution/db-executor.js';
2
2
 
3
3
  export const runInTransaction = async (executor: DbExecutor, action: () => Promise<void>): Promise<void> => {
4
4
  if (!executor.beginTransaction) {
@@ -1,11 +1,11 @@
1
- import { ColumnNode, eq } from '../core/ast/expression.js';
2
- import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
1
+ import { ColumnNode, eq } from '../core/ast/expression.js';
2
+ import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
3
3
  import { InsertQueryBuilder } from '../query-builder/insert.js';
4
4
  import { UpdateQueryBuilder } from '../query-builder/update.js';
5
5
  import { DeleteQueryBuilder } from '../query-builder/delete.js';
6
6
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
7
7
  import type { TableDef, TableHooks } from '../schema/table.js';
8
- import type { DbExecutor, QueryResult } from './db-executor.js';
8
+ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
9
9
  import { IdentityMap } from './identity-map.js';
10
10
  import { EntityStatus } from './runtime-types.js';
11
11
  import type { TrackedEntity } from './runtime-types.js';
@@ -18,7 +18,7 @@ export class UnitOfWork {
18
18
  private readonly executor: DbExecutor,
19
19
  private readonly identityMap: IdentityMap,
20
20
  private readonly hookContext: () => unknown
21
- ) {}
21
+ ) { }
22
22
 
23
23
  get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
24
24
  return this.identityMap.bucketsMap;
@@ -117,17 +117,22 @@ export class UnitOfWork {
117
117
  }
118
118
  }
119
119
 
120
+ reset(): void {
121
+ this.trackedEntities.clear();
122
+ this.identityMap.clear();
123
+ }
124
+
120
125
  private async flushInsert(tracked: TrackedEntity): Promise<void> {
121
126
  await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
122
127
 
123
- const payload = this.extractColumns(tracked.table, tracked.entity);
124
- let builder = new InsertQueryBuilder(tracked.table).values(payload);
125
- if (this.dialect.supportsReturning()) {
126
- builder = builder.returning(...this.getReturningColumns(tracked.table));
127
- }
128
- const compiled = builder.compile(this.dialect);
129
- const results = await this.executeCompiled(compiled);
130
- this.applyReturningResults(tracked, results);
128
+ const payload = this.extractColumns(tracked.table, tracked.entity);
129
+ let builder = new InsertQueryBuilder(tracked.table).values(payload);
130
+ if (this.dialect.supportsReturning()) {
131
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
132
+ }
133
+ const compiled = builder.compile(this.dialect);
134
+ const results = await this.executeCompiled(compiled);
135
+ this.applyReturningResults(tracked, results);
131
136
 
132
137
  tracked.status = EntityStatus.Managed;
133
138
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
@@ -150,17 +155,17 @@ export class UnitOfWork {
150
155
  const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
151
156
  if (!pkColumn) return;
152
157
 
153
- let builder = new UpdateQueryBuilder(tracked.table)
154
- .set(changes)
155
- .where(eq(pkColumn, tracked.pk));
156
-
157
- if (this.dialect.supportsReturning()) {
158
- builder = builder.returning(...this.getReturningColumns(tracked.table));
159
- }
160
-
161
- const compiled = builder.compile(this.dialect);
162
- const results = await this.executeCompiled(compiled);
163
- this.applyReturningResults(tracked, results);
158
+ let builder = new UpdateQueryBuilder(tracked.table)
159
+ .set(changes)
160
+ .where(eq(pkColumn, tracked.pk));
161
+
162
+ if (this.dialect.supportsReturning()) {
163
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
164
+ }
165
+
166
+ const compiled = builder.compile(this.dialect);
167
+ const results = await this.executeCompiled(compiled);
168
+ this.applyReturningResults(tracked, results);
164
169
 
165
170
  tracked.status = EntityStatus.Managed;
166
171
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
@@ -215,44 +220,44 @@ export class UnitOfWork {
215
220
  return payload;
216
221
  }
217
222
 
218
- private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
219
- return this.executor.executeSql(compiled.sql, compiled.params);
220
- }
221
-
222
- private getReturningColumns(table: TableDef): ColumnNode[] {
223
- return Object.values(table.columns).map(column => ({
224
- type: 'Column',
225
- table: table.name,
226
- name: column.name,
227
- alias: column.name
228
- }));
229
- }
230
-
231
- private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
232
- if (!this.dialect.supportsReturning()) return;
233
- const first = results[0];
234
- if (!first || first.values.length === 0) return;
235
-
236
- const row = first.values[0];
237
- for (let i = 0; i < first.columns.length; i++) {
238
- const columnName = this.normalizeColumnName(first.columns[i]);
239
- if (!(columnName in tracked.table.columns)) continue;
240
- tracked.entity[columnName] = row[i];
241
- }
242
- }
243
-
244
- private normalizeColumnName(column: string): string {
245
- const parts = column.split('.');
246
- const candidate = parts[parts.length - 1];
247
- return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
248
- }
249
-
250
- private registerIdentity(tracked: TrackedEntity): void {
251
- if (tracked.pk == null) return;
252
- this.identityMap.register(tracked);
253
- }
254
-
255
- private createSnapshot(table: TableDef, entity: any): Record<string, any> {
223
+ private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
224
+ return this.executor.executeSql(compiled.sql, compiled.params);
225
+ }
226
+
227
+ private getReturningColumns(table: TableDef): ColumnNode[] {
228
+ return Object.values(table.columns).map(column => ({
229
+ type: 'Column',
230
+ table: table.name,
231
+ name: column.name,
232
+ alias: column.name
233
+ }));
234
+ }
235
+
236
+ private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
237
+ if (!this.dialect.supportsReturning()) return;
238
+ const first = results[0];
239
+ if (!first || first.values.length === 0) return;
240
+
241
+ const row = first.values[0];
242
+ for (let i = 0; i < first.columns.length; i++) {
243
+ const columnName = this.normalizeColumnName(first.columns[i]);
244
+ if (!(columnName in tracked.table.columns)) continue;
245
+ tracked.entity[columnName] = row[i];
246
+ }
247
+ }
248
+
249
+ private normalizeColumnName(column: string): string {
250
+ const parts = column.split('.');
251
+ const candidate = parts[parts.length - 1];
252
+ return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
253
+ }
254
+
255
+ private registerIdentity(tracked: TrackedEntity): void {
256
+ if (tracked.pk == null) return;
257
+ this.identityMap.register(tracked);
258
+ }
259
+
260
+ private createSnapshot(table: TableDef, entity: any): Record<string, any> {
256
261
  const snapshot: Record<string, any> = {};
257
262
  for (const column of Object.keys(table.columns)) {
258
263
  snapshot[column] = entity[column];
@@ -1,11 +1,14 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { ColumnDef } from '../schema/column.js';
3
3
  import { ColumnNode, ExpressionNode } from '../core/ast/expression.js';
4
- import { CompiledQuery, DeleteCompiler } from '../core/dialect/abstract.js';
4
+ import { CompiledQuery, DeleteCompiler, Dialect } from '../core/dialect/abstract.js';
5
+ import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
5
6
  import { DeleteQueryNode } from '../core/ast/query.js';
6
7
  import { DeleteQueryState } from './delete-query-state.js';
7
8
  import { buildColumnNode } from '../core/ast/builders.js';
8
9
 
10
+ type DeleteDialectInput = Dialect | DialectKey;
11
+
9
12
  /**
10
13
  * Builder for DELETE queries
11
14
  */
@@ -32,12 +35,26 @@ export class DeleteQueryBuilder<T> {
32
35
  return this.clone(this.state.withReturning(nodes));
33
36
  }
34
37
 
35
- compile(compiler: DeleteCompiler): CompiledQuery {
36
- return compiler.compileDelete(this.state.ast);
38
+ // Existing compiler-based compile stays, but we add a new overload.
39
+
40
+ // 1) Keep the old behavior (used internally / tests, if any):
41
+ compile(compiler: DeleteCompiler): CompiledQuery;
42
+ // 2) New ergonomic overload:
43
+ compile(dialect: DeleteDialectInput): CompiledQuery;
44
+
45
+ compile(arg: DeleteCompiler | DeleteDialectInput): CompiledQuery {
46
+ if (typeof (arg as any).compileDelete === 'function') {
47
+ // DeleteCompiler path – old behavior
48
+ return (arg as DeleteCompiler).compileDelete(this.state.ast);
49
+ }
50
+
51
+ // Dialect | string path – new behavior
52
+ const dialect = resolveDialectInput(arg as DeleteDialectInput);
53
+ return dialect.compileDelete(this.state.ast);
37
54
  }
38
55
 
39
- toSql(compiler: DeleteCompiler): string {
40
- return this.compile(compiler).sql;
56
+ toSql(arg: DeleteCompiler | DeleteDialectInput): string {
57
+ return this.compile(arg as any).sql;
41
58
  }
42
59
 
43
60
  getAST(): DeleteQueryNode {
@@ -1,6 +1,7 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { RelationDef, RelationKinds } from '../schema/relation.js';
3
- import { CommonTableExpressionNode, HydrationPlan, OrderByNode, SelectQueryNode } from '../core/ast/query.js';
3
+ import { CommonTableExpressionNode, OrderByNode, SelectQueryNode } from '../core/ast/query.js';
4
+ import { HydrationPlan } from '../core/hydration/types.js';
4
5
  import { HydrationPlanner } from './hydration-planner.js';
5
6
  import { ProjectionNode, SelectQueryState } from './select-query-state.js';
6
7
  import { ColumnNode, eq } from '../core/ast/expression.js';
@@ -1,7 +1,7 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation.js';
3
3
  import { ProjectionNode } from './select-query-state.js';
4
- import { HydrationPlan, HydrationRelationPlan } from '../core/ast/query.js';
4
+ import { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
5
5
  import { isRelationAlias } from './relation-alias.js';
6
6
  import { buildDefaultPivotColumns } from './relation-utils.js';
7
7
 
@@ -111,12 +111,13 @@ export class HydrationPlanner {
111
111
  pivot?: { aliasPrefix: string; columns: string[] }
112
112
  ): HydrationRelationPlan {
113
113
  switch (rel.type) {
114
- case RelationKinds.HasMany: {
115
- const localKey = rel.localKey || findPrimaryKey(this.table);
116
- return {
117
- name: relationName,
118
- aliasPrefix,
119
- type: rel.type,
114
+ case RelationKinds.HasMany:
115
+ case RelationKinds.HasOne: {
116
+ const localKey = rel.localKey || findPrimaryKey(this.table);
117
+ return {
118
+ name: relationName,
119
+ aliasPrefix,
120
+ type: rel.type,
120
121
  targetTable: rel.target.name,
121
122
  targetPrimaryKey: findPrimaryKey(rel.target),
122
123
  foreignKey: rel.foreignKey,
@@ -1,11 +1,14 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { ColumnDef } from '../schema/column.js';
3
3
  import { ColumnNode } from '../core/ast/expression.js';
4
- import { CompiledQuery, InsertCompiler } from '../core/dialect/abstract.js';
4
+ import { CompiledQuery, InsertCompiler, Dialect } from '../core/dialect/abstract.js';
5
+ import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
5
6
  import { InsertQueryNode } from '../core/ast/query.js';
6
7
  import { InsertQueryState } from './insert-query-state.js';
7
8
  import { buildColumnNode } from '../core/ast/builders.js';
8
9
 
10
+ type InsertDialectInput = Dialect | DialectKey;
11
+
9
12
  /**
10
13
  * Builder for INSERT queries
11
14
  */
@@ -34,12 +37,26 @@ export class InsertQueryBuilder<T> {
34
37
  return this.clone(this.state.withReturning(nodes));
35
38
  }
36
39
 
37
- compile(compiler: InsertCompiler): CompiledQuery {
38
- return compiler.compileInsert(this.state.ast);
40
+ // Existing compiler-based compile stays, but we add a new overload.
41
+
42
+ // 1) Keep the old behavior (used internally / tests, if any):
43
+ compile(compiler: InsertCompiler): CompiledQuery;
44
+ // 2) New ergonomic overload:
45
+ compile(dialect: InsertDialectInput): CompiledQuery;
46
+
47
+ compile(arg: InsertCompiler | InsertDialectInput): CompiledQuery {
48
+ if (typeof (arg as any).compileInsert === 'function') {
49
+ // InsertCompiler path – old behavior
50
+ return (arg as InsertCompiler).compileInsert(this.state.ast);
51
+ }
52
+
53
+ // Dialect | string path – new behavior
54
+ const dialect = resolveDialectInput(arg as InsertDialectInput);
55
+ return dialect.compileInsert(this.state.ast);
39
56
  }
40
57
 
41
- toSql(compiler: InsertCompiler): string {
42
- return this.compile(compiler).sql;
58
+ toSql(arg: InsertCompiler | InsertDialectInput): string {
59
+ return this.compile(arg as any).sql;
43
60
  }
44
61
 
45
62
  getAST(): InsertQueryNode {
@@ -23,17 +23,18 @@ const assertNever = (value: never): never => {
23
23
  */
24
24
  const baseRelationCondition = (root: TableDef, relation: RelationDef): ExpressionNode => {
25
25
  const defaultLocalKey =
26
- relation.type === RelationKinds.HasMany
27
- ? findPrimaryKey(root)
28
- : findPrimaryKey(relation.target);
26
+ relation.type === RelationKinds.HasMany || relation.type === RelationKinds.HasOne
27
+ ? findPrimaryKey(root)
28
+ : findPrimaryKey(relation.target);
29
29
  const localKey = relation.localKey || defaultLocalKey;
30
30
 
31
31
  switch (relation.type) {
32
- case RelationKinds.HasMany:
33
- return eq(
34
- { type: 'Column', table: relation.target.name, name: relation.foreignKey },
35
- { type: 'Column', table: root.name, name: localKey }
36
- );
32
+ case RelationKinds.HasMany:
33
+ case RelationKinds.HasOne:
34
+ return eq(
35
+ { type: 'Column', table: relation.target.name, name: relation.foreignKey },
36
+ { type: 'Column', table: root.name, name: localKey }
37
+ );
37
38
  case RelationKinds.BelongsTo:
38
39
  return eq(
39
40
  { type: 'Column', table: relation.target.name, name: localKey },
@@ -20,7 +20,8 @@ import {
20
20
  } from './relation-conditions.js';
21
21
  import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
22
22
  import { RelationIncludeOptions } from './relation-types.js';
23
- import { createJoinNode } from '../core/ast/join-node.js';
23
+ import { createJoinNode } from '../core/ast/join-node.js';
24
+ import { getJoinRelationName } from '../core/ast/join-metadata.js';
24
25
  import { makeRelationAlias } from './relation-alias.js';
25
26
  import { buildDefaultPivotColumns } from './relation-utils.js';
26
27
 
@@ -93,7 +94,7 @@ export class RelationService {
93
94
 
94
95
  const relation = this.getRelation(relationName);
95
96
  const aliasPrefix = options?.aliasPrefix ?? relationName;
96
- const alreadyJoined = state.ast.joins.some(j => j.relationName === relationName);
97
+ const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
97
98
 
98
99
  if (!alreadyJoined) {
99
100
  const joined = this.joinRelation(relationName, options?.joinKind ?? JOIN_KINDS.LEFT, options?.filter);