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
@@ -6,27 +6,30 @@ import { SelectQueryNode, SetOperationKind } from '../core/ast/query.js';
6
6
 
7
7
  import { HydrationPlan } from '../core/hydration/types.js';
8
8
 
9
- import {
10
-
11
- ColumnNode,
12
-
13
- ExpressionNode,
14
-
15
- FunctionNode,
16
-
17
- LiteralNode,
18
-
19
- BinaryExpressionNode,
20
-
21
- CaseExpressionNode,
22
-
23
- WindowFunctionNode,
24
-
25
- exists,
26
-
27
- notExists
28
-
29
- } from '../core/ast/expression.js';
9
+ import {
10
+
11
+ ColumnNode,
12
+
13
+ ExpressionNode,
14
+
15
+ FunctionNode,
16
+
17
+ LiteralNode,
18
+
19
+ BinaryExpressionNode,
20
+
21
+ CaseExpressionNode,
22
+
23
+ WindowFunctionNode,
24
+
25
+ and,
26
+
27
+ exists,
28
+
29
+ notExists
30
+
31
+ } from '../core/ast/expression.js';
32
+ import { derivedTable } from '../core/ast/builders.js';
30
33
 
31
34
  import { CompiledQuery, Dialect } from '../core/dialect/abstract.js';
32
35
 
@@ -54,15 +57,17 @@ import {
54
57
 
55
58
  import { QueryAstService } from './query-ast-service.js';
56
59
 
57
- import { ColumnSelector } from './column-selector.js';
58
-
59
- import { RelationManager } from './relation-manager.js';
60
-
61
- import { RelationIncludeOptions } from './relation-types.js';
60
+ import { ColumnSelector } from './column-selector.js';
61
+
62
+ import { RelationManager } from './relation-manager.js';
63
+
64
+ import { RelationIncludeOptions } from './relation-types.js';
65
+
66
+ import type { RelationDef } from '../schema/relation.js';
67
+
68
+ import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
62
69
 
63
- import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
64
-
65
- import { Entity, RelationMap } from '../schema/types.js';
70
+ import { Entity, RelationMap, RelationTargetTable } from '../schema/types.js';
66
71
 
67
72
  import { OrmSession } from '../orm/orm-session.ts';
68
73
 
@@ -72,11 +77,29 @@ import { HydrationContext } from '../orm/hydration-context.js';
72
77
 
73
78
  import { executeHydrated, executeHydratedWithContexts } from '../orm/execute.js';
74
79
 
75
- import { createJoinNode } from '../core/ast/join-node.js';
76
-
77
-
78
-
79
- /**
80
+ import { createJoinNode } from '../core/ast/join-node.js';
81
+
82
+
83
+ type ColumnSelectionValue = ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode;
84
+
85
+ type DeepSelectConfig<TTable extends TableDef> = {
86
+ root?: (keyof TTable['columns'] & string)[];
87
+ } & {
88
+ [K in keyof TTable['relations'] & string]?: (
89
+ keyof RelationTargetTable<TTable['relations'][K]>['columns'] & string
90
+ )[];
91
+ };
92
+
93
+ type WhereHasOptions = {
94
+ correlate?: ExpressionNode;
95
+ };
96
+
97
+ type RelationCallback = <TChildTable extends TableDef>(
98
+ qb: SelectQueryBuilder<any, TChildTable>
99
+ ) => SelectQueryBuilder<any, TChildTable>;
100
+
101
+
102
+ /**
80
103
 
81
104
  * Main query builder class for constructing SQL SELECT queries
82
105
 
@@ -154,29 +177,52 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
154
177
 
155
178
 
156
179
 
157
- private clone(
158
-
159
- context: SelectQueryBuilderContext = this.context,
160
-
161
- lazyRelations = new Set(this.lazyRelations)
162
-
163
- ): SelectQueryBuilder<T, TTable> {
164
-
165
- return new SelectQueryBuilder(this.env.table as TTable, context.state, context.hydration, this.env.deps, lazyRelations);
166
-
167
- }
168
-
169
-
170
-
171
- private resolveQueryNode(query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryNode {
172
-
173
- return typeof (query as any).getAST === 'function'
174
-
175
- ? (query as SelectQueryBuilder<any, TableDef<any>>).getAST()
176
-
177
- : (query as SelectQueryNode);
178
-
179
- }
180
+ private clone(
181
+
182
+ context: SelectQueryBuilderContext = this.context,
183
+
184
+ lazyRelations = new Set(this.lazyRelations)
185
+
186
+ ): SelectQueryBuilder<T, TTable> {
187
+
188
+ return new SelectQueryBuilder(this.env.table as TTable, context.state, context.hydration, this.env.deps, lazyRelations);
189
+
190
+ }
191
+
192
+ /**
193
+ * Applies an alias to the root FROM table.
194
+ * @param alias - Alias to apply
195
+ */
196
+ as(alias: string): SelectQueryBuilder<T, TTable> {
197
+ const from = this.context.state.ast.from;
198
+ if (from.type !== 'Table') {
199
+ throw new Error('Cannot alias non-table FROM sources');
200
+ }
201
+ const nextFrom = { ...from, alias };
202
+ const nextContext = this.applyAst(this.context, service => service.withFrom(nextFrom));
203
+ return this.clone(nextContext);
204
+ }
205
+
206
+
207
+
208
+ private resolveQueryNode(query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryNode {
209
+
210
+ return typeof (query as any).getAST === 'function'
211
+
212
+ ? (query as SelectQueryBuilder<any, TableDef<any>>).getAST()
213
+
214
+ : (query as SelectQueryNode);
215
+
216
+ }
217
+
218
+ private applyCorrelation(ast: SelectQueryNode, correlation?: ExpressionNode): SelectQueryNode {
219
+ if (!correlation) return ast;
220
+ const combinedWhere = ast.where ? and(correlation, ast.where) : correlation;
221
+ return {
222
+ ...ast,
223
+ where: combinedWhere
224
+ };
225
+ }
180
226
 
181
227
 
182
228
 
@@ -252,12 +298,31 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
252
298
 
253
299
  */
254
300
 
255
- select(columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>): SelectQueryBuilder<T, TTable> {
256
-
257
- return this.clone(this.columnSelector.select(this.context, columns));
258
-
259
- }
260
-
301
+ select(columns: Record<string, ColumnSelectionValue>): SelectQueryBuilder<T, TTable> {
302
+
303
+ return this.clone(this.columnSelector.select(this.context, columns));
304
+
305
+ }
306
+
307
+
308
+ /**
309
+ * Selects columns from the root table by name (typed).
310
+ * @param cols - Column names on the root table
311
+ */
312
+ selectColumns<K extends keyof TTable['columns'] & string>(...cols: K[]): SelectQueryBuilder<T, TTable> {
313
+ const selection: Record<string, ColumnDef> = {};
314
+
315
+ for (const key of cols) {
316
+ const col = this.env.table.columns[key];
317
+ if (!col) {
318
+ throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
319
+ }
320
+ selection[key] = col;
321
+ }
322
+
323
+ return this.select(selection);
324
+ }
325
+
261
326
 
262
327
 
263
328
  /**
@@ -318,15 +383,34 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
318
383
 
319
384
  */
320
385
 
321
- withRecursive(name: string, query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
322
-
323
- const subAst = this.resolveQueryNode(query);
324
-
325
- const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, true));
326
-
327
- return this.clone(nextContext);
328
-
329
- }
386
+ withRecursive(name: string, query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
387
+
388
+ const subAst = this.resolveQueryNode(query);
389
+
390
+ const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, true));
391
+
392
+ return this.clone(nextContext);
393
+
394
+ }
395
+
396
+
397
+ /**
398
+ * Replaces the FROM clause with a derived table (subquery with alias)
399
+ * @param subquery - Subquery to use as the FROM source
400
+ * @param alias - Alias for the derived table
401
+ * @param columnAliases - Optional column alias list
402
+ * @returns New query builder instance with updated FROM
403
+ */
404
+ fromSubquery(
405
+ subquery: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode,
406
+ alias: string,
407
+ columnAliases?: string[]
408
+ ): SelectQueryBuilder<T, TTable> {
409
+ const subAst = this.resolveQueryNode(subquery);
410
+ const fromNode = derivedTable(subAst, alias, columnAliases);
411
+ const nextContext = this.applyAst(this.context, service => service.withFrom(fromNode));
412
+ return this.clone(nextContext);
413
+ }
330
414
 
331
415
 
332
416
 
@@ -342,14 +426,37 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
342
426
 
343
427
  */
344
428
 
345
- selectSubquery(alias: string, sub: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
346
-
347
- const query = this.resolveQueryNode(sub);
348
-
349
- return this.clone(this.columnSelector.selectSubquery(this.context, alias, query));
350
-
351
- }
352
-
429
+ selectSubquery(alias: string, sub: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
430
+
431
+ const query = this.resolveQueryNode(sub);
432
+
433
+ return this.clone(this.columnSelector.selectSubquery(this.context, alias, query));
434
+
435
+ }
436
+
437
+
438
+ /**
439
+ * Adds a JOIN against a derived table (subquery with alias)
440
+ * @param subquery - Subquery to join
441
+ * @param alias - Alias for the derived table
442
+ * @param condition - Join condition expression
443
+ * @param joinKind - Join kind (defaults to INNER)
444
+ * @param columnAliases - Optional column alias list for the derived table
445
+ * @returns New query builder instance with the derived-table join
446
+ */
447
+ joinSubquery(
448
+ subquery: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode,
449
+ alias: string,
450
+ condition: BinaryExpressionNode,
451
+ joinKind: JoinKind = JOIN_KINDS.INNER,
452
+ columnAliases?: string[]
453
+ ): SelectQueryBuilder<T, TTable> {
454
+ const subAst = this.resolveQueryNode(subquery);
455
+ const joinNode = createJoinNode(joinKind, derivedTable(subAst, alias, columnAliases), condition);
456
+ const nextContext = this.applyAst(this.context, service => service.withJoin(joinNode));
457
+ return this.clone(nextContext);
458
+ }
459
+
353
460
 
354
461
 
355
462
  /**
@@ -494,16 +601,77 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
494
601
 
495
602
 
496
603
 
497
- includeLazy<K extends keyof RelationMap<TTable>>(relationName: K): SelectQueryBuilder<T, TTable> {
498
-
499
- const nextLazy = new Set(this.lazyRelations);
500
-
501
- nextLazy.add(relationName as string);
502
-
503
- return this.clone(this.context, nextLazy);
504
-
505
- }
506
-
604
+ includeLazy<K extends keyof RelationMap<TTable>>(relationName: K): SelectQueryBuilder<T, TTable> {
605
+
606
+ const nextLazy = new Set(this.lazyRelations);
607
+
608
+ nextLazy.add(relationName as string);
609
+
610
+ return this.clone(this.context, nextLazy);
611
+
612
+ }
613
+
614
+ /**
615
+ * Selects columns for a related table in a single hop.
616
+ */
617
+ selectRelationColumns<
618
+ K extends keyof TTable['relations'] & string,
619
+ TRel extends RelationDef = TTable['relations'][K],
620
+ TTarget extends TableDef = RelationTargetTable<TRel>,
621
+ C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
622
+ >(relationName: K, ...cols: C[]): SelectQueryBuilder<T, TTable> {
623
+ const relation = this.env.table.relations[relationName] as RelationDef | undefined;
624
+ if (!relation) {
625
+ throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
626
+ }
627
+ const target = relation.target;
628
+
629
+ for (const col of cols) {
630
+ if (!target.columns[col]) {
631
+ throw new Error(
632
+ `Column '${col}' not found on related table '${target.name}' for relation '${relationName}'`
633
+ );
634
+ }
635
+ }
636
+
637
+ return this.include(relationName as string, { columns: cols as string[] });
638
+ }
639
+
640
+
641
+ /**
642
+ * Convenience alias for selecting specific columns from a relation.
643
+ */
644
+ includePick<
645
+ K extends keyof TTable['relations'] & string,
646
+ TRel extends RelationDef = TTable['relations'][K],
647
+ TTarget extends TableDef = RelationTargetTable<TRel>,
648
+ C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
649
+ >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
650
+ return this.selectRelationColumns(relationName, ...cols);
651
+ }
652
+
653
+
654
+ /**
655
+ * Selects columns for the root table and relations from a single config object.
656
+ */
657
+ selectColumnsDeep(config: DeepSelectConfig<TTable>): SelectQueryBuilder<T, TTable> {
658
+ let qb: SelectQueryBuilder<T, TTable> = this;
659
+
660
+ if (config.root?.length) {
661
+ qb = qb.selectColumns(...config.root);
662
+ }
663
+
664
+ for (const key of Object.keys(config) as (keyof typeof config)[]) {
665
+ if (key === 'root') continue;
666
+ const relName = key as keyof TTable['relations'] & string;
667
+ const cols = config[relName as keyof DeepSelectConfig<TTable>] as string[] | undefined;
668
+ if (!cols || !cols.length) continue;
669
+ qb = qb.selectRelationColumns(relName, ...(cols as string[]));
670
+ }
671
+
672
+ return qb;
673
+ }
674
+
507
675
 
508
676
 
509
677
  getLazyRelations(): (keyof RelationMap<TTable>)[] {
@@ -760,13 +928,14 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
760
928
 
761
929
  */
762
930
 
763
- whereExists(subquery: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
764
-
765
- const subAst = this.resolveQueryNode(subquery);
766
-
767
- return this.where(exists(subAst));
768
-
769
- }
931
+ whereExists(
932
+ subquery: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode,
933
+ correlate?: ExpressionNode
934
+ ): SelectQueryBuilder<T, TTable> {
935
+ const subAst = this.resolveQueryNode(subquery);
936
+ const correlated = this.applyCorrelation(subAst, correlate);
937
+ return this.where(exists(correlated));
938
+ }
770
939
 
771
940
 
772
941
 
@@ -780,13 +949,14 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
780
949
 
781
950
  */
782
951
 
783
- whereNotExists(subquery: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
784
-
785
- const subAst = this.resolveQueryNode(subquery);
786
-
787
- return this.where(notExists(subAst));
788
-
789
- }
952
+ whereNotExists(
953
+ subquery: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode,
954
+ correlate?: ExpressionNode
955
+ ): SelectQueryBuilder<T, TTable> {
956
+ const subAst = this.resolveQueryNode(subquery);
957
+ const correlated = this.applyCorrelation(subAst, correlate);
958
+ return this.where(notExists(correlated));
959
+ }
790
960
 
791
961
 
792
962
 
@@ -802,45 +972,46 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
802
972
 
803
973
  */
804
974
 
805
- whereHas(
806
-
807
- relationName: string,
808
-
809
- callback?: <TChildTable extends TableDef>(
810
-
811
- qb: SelectQueryBuilder<any, TChildTable>
812
-
813
- ) => SelectQueryBuilder<any, TChildTable>
814
-
815
- ): SelectQueryBuilder<T, TTable> {
816
-
817
- const relation = this.env.table.relations[relationName];
818
-
819
- if (!relation) {
820
-
975
+ whereHas(
976
+
977
+ relationName: string,
978
+
979
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
980
+
981
+ maybeOptions?: WhereHasOptions
982
+
983
+ ): SelectQueryBuilder<T, TTable> {
984
+
985
+ const relation = this.env.table.relations[relationName];
986
+
987
+ if (!relation) {
988
+
821
989
  throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
822
990
 
823
991
  }
824
992
 
825
993
 
826
994
 
827
- let subQb = this.createChildBuilder<any, typeof relation.target>(relation.target);
828
-
829
- if (callback) {
830
-
831
- subQb = callback(subQb);
832
-
833
- }
995
+ const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions as RelationCallback : undefined;
996
+ const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as WhereHasOptions | undefined;
997
+
998
+ let subQb = this.createChildBuilder<any, typeof relation.target>(relation.target);
999
+
1000
+ if (callback) {
1001
+
1002
+ subQb = callback(subQb);
1003
+
1004
+ }
834
1005
 
835
1006
 
836
1007
 
837
- const subAst = subQb.getAST();
838
-
839
- const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst);
840
-
841
- return this.where(exists(finalSubAst));
842
-
843
- }
1008
+ const subAst = subQb.getAST();
1009
+
1010
+ const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst, options?.correlate);
1011
+
1012
+ return this.where(exists(finalSubAst));
1013
+
1014
+ }
844
1015
 
845
1016
 
846
1017
 
@@ -856,45 +1027,46 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
856
1027
 
857
1028
  */
858
1029
 
859
- whereHasNot(
860
-
861
- relationName: string,
862
-
863
- callback?: <TChildTable extends TableDef>(
864
-
865
- qb: SelectQueryBuilder<any, TChildTable>
866
-
867
- ) => SelectQueryBuilder<any, TChildTable>
868
-
869
- ): SelectQueryBuilder<T, TTable> {
870
-
871
- const relation = this.env.table.relations[relationName];
872
-
873
- if (!relation) {
874
-
1030
+ whereHasNot(
1031
+
1032
+ relationName: string,
1033
+
1034
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
1035
+
1036
+ maybeOptions?: WhereHasOptions
1037
+
1038
+ ): SelectQueryBuilder<T, TTable> {
1039
+
1040
+ const relation = this.env.table.relations[relationName];
1041
+
1042
+ if (!relation) {
1043
+
875
1044
  throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
876
1045
 
877
1046
  }
878
1047
 
879
1048
 
880
1049
 
881
- let subQb = this.createChildBuilder<any, typeof relation.target>(relation.target);
882
-
883
- if (callback) {
884
-
885
- subQb = callback(subQb);
886
-
887
- }
888
-
889
-
890
-
891
- const subAst = subQb.getAST();
892
-
893
- const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst);
894
-
895
- return this.where(notExists(finalSubAst));
896
-
897
- }
1050
+ const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions as RelationCallback : undefined;
1051
+ const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as WhereHasOptions | undefined;
1052
+
1053
+ let subQb = this.createChildBuilder<any, typeof relation.target>(relation.target);
1054
+
1055
+ if (callback) {
1056
+
1057
+ subQb = callback(subQb);
1058
+
1059
+ }
1060
+
1061
+
1062
+
1063
+ const subAst = subQb.getAST();
1064
+
1065
+ const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst, options?.correlate);
1066
+
1067
+ return this.where(notExists(finalSubAst));
1068
+
1069
+ }
898
1070
 
899
1071
 
900
1072
 
@@ -1,7 +1,21 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { ColumnNode, ExpressionNode, valueToOperand } from '../core/ast/expression.js';
2
+ import { ColumnNode, ExpressionNode, OperandNode, isOperandNode, valueToOperand } from '../core/ast/expression.js';
3
3
  import { TableNode, UpdateQueryNode, UpdateAssignmentNode } from '../core/ast/query.js';
4
4
  import { createTableNode } from '../core/ast/builders.js';
5
+ type LiteralValue = string | number | boolean | null;
6
+ type UpdateValue = OperandNode | LiteralValue;
7
+
8
+ const isUpdateValue = (value: unknown): value is UpdateValue => {
9
+ if (value === null) return true;
10
+ switch (typeof value) {
11
+ case 'string':
12
+ case 'number':
13
+ case 'boolean':
14
+ return true;
15
+ default:
16
+ return isOperandNode(value);
17
+ }
18
+ };
5
19
 
6
20
  /**
7
21
  * Immutable state for UPDATE queries
@@ -24,14 +38,22 @@ export class UpdateQueryState {
24
38
  }
25
39
 
26
40
  withSet(values: Record<string, unknown>): UpdateQueryState {
27
- const assignments: UpdateAssignmentNode[] = Object.entries(values).map(([column, value]) => ({
28
- column: {
29
- type: 'Column',
30
- table: this.table.name,
31
- name: column
32
- },
33
- value: valueToOperand(value)
34
- }));
41
+ const assignments: UpdateAssignmentNode[] = Object.entries(values).map(([column, rawValue]) => {
42
+ if (!isUpdateValue(rawValue)) {
43
+ throw new Error(
44
+ `Invalid update value for column "${column}": only primitives, null, or OperandNodes are allowed`
45
+ );
46
+ }
47
+
48
+ return {
49
+ column: {
50
+ type: 'Column',
51
+ table: this.table.name,
52
+ name: column
53
+ },
54
+ value: valueToOperand(rawValue)
55
+ };
56
+ });
35
57
 
36
58
  return this.clone({
37
59
  ...this.ast,