metal-orm 1.0.12 → 1.0.13

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/dist/index.js CHANGED
@@ -550,6 +550,82 @@ var SelectQueryState = class _SelectQueryState {
550
550
  ctes: [...this.ast.ctes ?? [], cte]
551
551
  });
552
552
  }
553
+ /**
554
+ * Adds a set operation (UNION/INTERSECT/EXCEPT) to the query
555
+ * @param op - Set operation node to add
556
+ * @returns New SelectQueryState with set operation
557
+ */
558
+ withSetOperation(op) {
559
+ return this.clone({
560
+ ...this.ast,
561
+ setOps: [...this.ast.setOps ?? [], op]
562
+ });
563
+ }
564
+ };
565
+
566
+ // src/core/ast/join-node.ts
567
+ var createJoinNode = (kind, tableName, condition, relationName) => ({
568
+ type: "Join",
569
+ kind,
570
+ table: { type: "Table", name: tableName },
571
+ condition,
572
+ relationName
573
+ });
574
+
575
+ // src/core/sql/sql.ts
576
+ var SQL_OPERATORS = {
577
+ /** Equality operator */
578
+ EQUALS: "=",
579
+ /** Not equals operator */
580
+ NOT_EQUALS: "!=",
581
+ /** Greater than operator */
582
+ GREATER_THAN: ">",
583
+ /** Greater than or equal operator */
584
+ GREATER_OR_EQUAL: ">=",
585
+ /** Less than operator */
586
+ LESS_THAN: "<",
587
+ /** Less than or equal operator */
588
+ LESS_OR_EQUAL: "<=",
589
+ /** LIKE pattern matching operator */
590
+ LIKE: "LIKE",
591
+ /** NOT LIKE pattern matching operator */
592
+ NOT_LIKE: "NOT LIKE",
593
+ /** IN membership operator */
594
+ IN: "IN",
595
+ /** NOT IN membership operator */
596
+ NOT_IN: "NOT IN",
597
+ /** BETWEEN range operator */
598
+ BETWEEN: "BETWEEN",
599
+ /** NOT BETWEEN range operator */
600
+ NOT_BETWEEN: "NOT BETWEEN",
601
+ /** IS NULL null check operator */
602
+ IS_NULL: "IS NULL",
603
+ /** IS NOT NULL null check operator */
604
+ IS_NOT_NULL: "IS NOT NULL",
605
+ /** Logical AND operator */
606
+ AND: "AND",
607
+ /** Logical OR operator */
608
+ OR: "OR",
609
+ /** EXISTS operator */
610
+ EXISTS: "EXISTS",
611
+ /** NOT EXISTS operator */
612
+ NOT_EXISTS: "NOT EXISTS"
613
+ };
614
+ var JOIN_KINDS = {
615
+ /** INNER JOIN type */
616
+ INNER: "INNER",
617
+ /** LEFT JOIN type */
618
+ LEFT: "LEFT",
619
+ /** RIGHT JOIN type */
620
+ RIGHT: "RIGHT",
621
+ /** CROSS JOIN type */
622
+ CROSS: "CROSS"
623
+ };
624
+ var ORDER_DIRECTIONS = {
625
+ /** Ascending order */
626
+ ASC: "ASC",
627
+ /** Descending order */
628
+ DESC: "DESC"
553
629
  };
554
630
 
555
631
  // src/query-builder/hydration-manager.ts
@@ -601,8 +677,26 @@ var HydrationManager = class _HydrationManager {
601
677
  * @returns AST with hydration metadata
602
678
  */
603
679
  applyToAst(ast) {
680
+ if (ast.setOps && ast.setOps.length > 0) {
681
+ return ast;
682
+ }
604
683
  const plan = this.planner.getPlan();
605
684
  if (!plan) return ast;
685
+ const needsPaginationGuard = this.requiresParentPagination(ast, plan);
686
+ const rewritten = needsPaginationGuard ? this.wrapForParentPagination(ast, plan) : ast;
687
+ return this.attachHydrationMeta(rewritten, plan);
688
+ }
689
+ /**
690
+ * Gets the current hydration plan
691
+ * @returns Hydration plan or undefined if none exists
692
+ */
693
+ getPlan() {
694
+ return this.planner.getPlan();
695
+ }
696
+ /**
697
+ * Attaches hydration metadata to a query AST node.
698
+ */
699
+ attachHydrationMeta(ast, plan) {
606
700
  return {
607
701
  ...ast,
608
702
  meta: {
@@ -612,11 +706,154 @@ var HydrationManager = class _HydrationManager {
612
706
  };
613
707
  }
614
708
  /**
615
- * Gets the current hydration plan
616
- * @returns Hydration plan or undefined if none exists
709
+ * Determines whether the query needs pagination rewriting to keep LIMIT/OFFSET
710
+ * applied to parent rows when eager-loading multiplicative relations.
617
711
  */
618
- getPlan() {
619
- return this.planner.getPlan();
712
+ requiresParentPagination(ast, plan) {
713
+ const hasPagination = ast.limit !== void 0 || ast.offset !== void 0;
714
+ return hasPagination && this.hasMultiplyingRelations(plan);
715
+ }
716
+ hasMultiplyingRelations(plan) {
717
+ return plan.relations.some(
718
+ (rel) => rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany
719
+ );
720
+ }
721
+ /**
722
+ * Rewrites the query using CTEs so LIMIT/OFFSET target distinct parent rows
723
+ * instead of the joined result set.
724
+ *
725
+ * The strategy:
726
+ * - Hoist the original query (minus limit/offset) into a base CTE.
727
+ * - Select distinct parent ids from that base CTE with the original ordering and pagination.
728
+ * - Join the base CTE against the paged ids to retrieve the joined rows for just that page.
729
+ */
730
+ wrapForParentPagination(ast, plan) {
731
+ const projectionNames = this.getProjectionNames(ast.columns);
732
+ if (!projectionNames) {
733
+ return ast;
734
+ }
735
+ const projectionAliases = this.buildProjectionAliasMap(ast.columns);
736
+ const projectionSet = new Set(projectionNames);
737
+ const rootPkAlias = projectionAliases.get(`${plan.rootTable}.${plan.rootPrimaryKey}`) ?? plan.rootPrimaryKey;
738
+ const baseCteName = this.nextCteName(ast.ctes, "__metal_pagination_base");
739
+ const baseQuery = {
740
+ ...ast,
741
+ ctes: void 0,
742
+ limit: void 0,
743
+ offset: void 0,
744
+ orderBy: void 0,
745
+ meta: void 0
746
+ };
747
+ const baseCte = {
748
+ type: "CommonTableExpression",
749
+ name: baseCteName,
750
+ query: baseQuery,
751
+ recursive: false
752
+ };
753
+ const orderBy = this.mapOrderBy(ast.orderBy, plan, projectionAliases, baseCteName, projectionSet);
754
+ if (orderBy === null) {
755
+ return ast;
756
+ }
757
+ const pageCteName = this.nextCteName([...ast.ctes ?? [], baseCte], "__metal_pagination_page");
758
+ const pagingColumns = this.buildPagingColumns(rootPkAlias, orderBy, baseCteName);
759
+ const pageCte = {
760
+ type: "CommonTableExpression",
761
+ name: pageCteName,
762
+ query: {
763
+ type: "SelectQuery",
764
+ from: { type: "Table", name: baseCteName },
765
+ columns: pagingColumns,
766
+ joins: [],
767
+ distinct: [{ type: "Column", table: baseCteName, name: rootPkAlias }],
768
+ orderBy,
769
+ limit: ast.limit,
770
+ offset: ast.offset
771
+ },
772
+ recursive: false
773
+ };
774
+ const joinCondition = eq(
775
+ { type: "Column", table: baseCteName, name: rootPkAlias },
776
+ { type: "Column", table: pageCteName, name: rootPkAlias }
777
+ );
778
+ const outerColumns = projectionNames.map((name) => ({
779
+ type: "Column",
780
+ table: baseCteName,
781
+ name,
782
+ alias: name
783
+ }));
784
+ return {
785
+ type: "SelectQuery",
786
+ from: { type: "Table", name: baseCteName },
787
+ columns: outerColumns,
788
+ joins: [createJoinNode(JOIN_KINDS.INNER, pageCteName, joinCondition)],
789
+ orderBy,
790
+ ctes: [...ast.ctes ?? [], baseCte, pageCte]
791
+ };
792
+ }
793
+ nextCteName(existing, baseName) {
794
+ const names = new Set((existing ?? []).map((cte) => cte.name));
795
+ let candidate = baseName;
796
+ let suffix = 1;
797
+ while (names.has(candidate)) {
798
+ suffix += 1;
799
+ candidate = `${baseName}_${suffix}`;
800
+ }
801
+ return candidate;
802
+ }
803
+ getProjectionNames(columns) {
804
+ const names = [];
805
+ for (const col2 of columns) {
806
+ const alias = col2.alias ?? col2.name;
807
+ if (!alias) return void 0;
808
+ names.push(alias);
809
+ }
810
+ return names;
811
+ }
812
+ buildProjectionAliasMap(columns) {
813
+ const map = /* @__PURE__ */ new Map();
814
+ for (const col2 of columns) {
815
+ if (col2.type !== "Column") continue;
816
+ const node = col2;
817
+ const key = `${node.table}.${node.name}`;
818
+ map.set(key, node.alias ?? node.name);
819
+ }
820
+ return map;
821
+ }
822
+ mapOrderBy(orderBy, plan, projectionAliases, baseAlias, availableColumns) {
823
+ if (!orderBy || orderBy.length === 0) {
824
+ return void 0;
825
+ }
826
+ const mapped = [];
827
+ for (const ob of orderBy) {
828
+ if (ob.column.table !== plan.rootTable) {
829
+ return null;
830
+ }
831
+ const alias = projectionAliases.get(`${ob.column.table}.${ob.column.name}`) ?? ob.column.name;
832
+ if (!availableColumns.has(alias)) {
833
+ return null;
834
+ }
835
+ mapped.push({
836
+ type: "OrderBy",
837
+ column: { type: "Column", table: baseAlias, name: alias },
838
+ direction: ob.direction
839
+ });
840
+ }
841
+ return mapped;
842
+ }
843
+ buildPagingColumns(primaryKey, orderBy, tableAlias) {
844
+ const columns = [{ type: "Column", table: tableAlias, name: primaryKey, alias: primaryKey }];
845
+ if (!orderBy) return columns;
846
+ for (const ob of orderBy) {
847
+ if (!columns.some((col2) => col2.name === ob.column.name)) {
848
+ columns.push({
849
+ type: "Column",
850
+ table: tableAlias,
851
+ name: ob.column.name,
852
+ alias: ob.column.name
853
+ });
854
+ }
855
+ }
856
+ return columns;
620
857
  }
621
858
  };
622
859
 
@@ -879,6 +1116,20 @@ var QueryAstService = class {
879
1116
  };
880
1117
  return this.state.withCte(cte);
881
1118
  }
1119
+ /**
1120
+ * Adds a set operation (UNION/UNION ALL/INTERSECT/EXCEPT) to the query
1121
+ * @param operator - Set operator
1122
+ * @param query - Right-hand side query
1123
+ * @returns Updated query state with set operation
1124
+ */
1125
+ withSetOperation(operator, query) {
1126
+ const op = {
1127
+ type: "SetOperation",
1128
+ operator,
1129
+ query
1130
+ };
1131
+ return this.state.withSetOperation(op);
1132
+ }
882
1133
  /**
883
1134
  * Selects a subquery as a column
884
1135
  * @param alias - Alias for the subquery
@@ -1031,15 +1282,6 @@ var RelationProjectionHelper = class {
1031
1282
  }
1032
1283
  };
1033
1284
 
1034
- // src/core/ast/join-node.ts
1035
- var createJoinNode = (kind, tableName, condition, relationName) => ({
1036
- type: "Join",
1037
- kind,
1038
- table: { type: "Table", name: tableName },
1039
- condition,
1040
- relationName
1041
- });
1042
-
1043
1285
  // src/query-builder/relation-conditions.ts
1044
1286
  var assertNever = (value) => {
1045
1287
  throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
@@ -1095,62 +1337,6 @@ var buildRelationCorrelation = (root, relation) => {
1095
1337
  return baseRelationCondition(root, relation);
1096
1338
  };
1097
1339
 
1098
- // src/core/sql/sql.ts
1099
- var SQL_OPERATORS = {
1100
- /** Equality operator */
1101
- EQUALS: "=",
1102
- /** Not equals operator */
1103
- NOT_EQUALS: "!=",
1104
- /** Greater than operator */
1105
- GREATER_THAN: ">",
1106
- /** Greater than or equal operator */
1107
- GREATER_OR_EQUAL: ">=",
1108
- /** Less than operator */
1109
- LESS_THAN: "<",
1110
- /** Less than or equal operator */
1111
- LESS_OR_EQUAL: "<=",
1112
- /** LIKE pattern matching operator */
1113
- LIKE: "LIKE",
1114
- /** NOT LIKE pattern matching operator */
1115
- NOT_LIKE: "NOT LIKE",
1116
- /** IN membership operator */
1117
- IN: "IN",
1118
- /** NOT IN membership operator */
1119
- NOT_IN: "NOT IN",
1120
- /** BETWEEN range operator */
1121
- BETWEEN: "BETWEEN",
1122
- /** NOT BETWEEN range operator */
1123
- NOT_BETWEEN: "NOT BETWEEN",
1124
- /** IS NULL null check operator */
1125
- IS_NULL: "IS NULL",
1126
- /** IS NOT NULL null check operator */
1127
- IS_NOT_NULL: "IS NOT NULL",
1128
- /** Logical AND operator */
1129
- AND: "AND",
1130
- /** Logical OR operator */
1131
- OR: "OR",
1132
- /** EXISTS operator */
1133
- EXISTS: "EXISTS",
1134
- /** NOT EXISTS operator */
1135
- NOT_EXISTS: "NOT EXISTS"
1136
- };
1137
- var JOIN_KINDS = {
1138
- /** INNER JOIN type */
1139
- INNER: "INNER",
1140
- /** LEFT JOIN type */
1141
- LEFT: "LEFT",
1142
- /** RIGHT JOIN type */
1143
- RIGHT: "RIGHT",
1144
- /** CROSS JOIN type */
1145
- CROSS: "CROSS"
1146
- };
1147
- var ORDER_DIRECTIONS = {
1148
- /** Ascending order */
1149
- ASC: "ASC",
1150
- /** Descending order */
1151
- DESC: "DESC"
1152
- };
1153
-
1154
1340
  // src/query-builder/relation-service.ts
1155
1341
  var RelationService = class {
1156
1342
  /**
@@ -1596,6 +1782,16 @@ var hasEntityMeta = (entity) => {
1596
1782
 
1597
1783
  // src/orm/relations/has-many.ts
1598
1784
  var toKey2 = (value) => value === null || value === void 0 ? "" : String(value);
1785
+ var hideInternal = (obj, keys) => {
1786
+ for (const key of keys) {
1787
+ Object.defineProperty(obj, key, {
1788
+ value: obj[key],
1789
+ writable: false,
1790
+ configurable: false,
1791
+ enumerable: false
1792
+ });
1793
+ }
1794
+ };
1599
1795
  var DefaultHasManyCollection = class {
1600
1796
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1601
1797
  this.ctx = ctx;
@@ -1611,6 +1807,7 @@ var DefaultHasManyCollection = class {
1611
1807
  this.items = [];
1612
1808
  this.added = /* @__PURE__ */ new Set();
1613
1809
  this.removed = /* @__PURE__ */ new Set();
1810
+ hideInternal(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1614
1811
  this.hydrateFromCache();
1615
1812
  }
1616
1813
  async load() {
@@ -1686,10 +1883,23 @@ var DefaultHasManyCollection = class {
1686
1883
  this.items = rows.map((row) => this.createEntity(row));
1687
1884
  this.loaded = true;
1688
1885
  }
1886
+ toJSON() {
1887
+ return this.items;
1888
+ }
1689
1889
  };
1690
1890
 
1691
1891
  // src/orm/relations/belongs-to.ts
1692
1892
  var toKey3 = (value) => value === null || value === void 0 ? "" : String(value);
1893
+ var hideInternal2 = (obj, keys) => {
1894
+ for (const key of keys) {
1895
+ Object.defineProperty(obj, key, {
1896
+ value: obj[key],
1897
+ writable: false,
1898
+ configurable: false,
1899
+ enumerable: false
1900
+ });
1901
+ }
1902
+ };
1693
1903
  var DefaultBelongsToReference = class {
1694
1904
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, targetKey) {
1695
1905
  this.ctx = ctx;
@@ -1703,6 +1913,7 @@ var DefaultBelongsToReference = class {
1703
1913
  this.targetKey = targetKey;
1704
1914
  this.loaded = false;
1705
1915
  this.current = null;
1916
+ hideInternal2(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "targetKey"]);
1706
1917
  this.populateFromHydrationCache();
1707
1918
  }
1708
1919
  async load() {
@@ -1763,10 +1974,23 @@ var DefaultBelongsToReference = class {
1763
1974
  this.current = this.createEntity(row);
1764
1975
  this.loaded = true;
1765
1976
  }
1977
+ toJSON() {
1978
+ return this.current;
1979
+ }
1766
1980
  };
1767
1981
 
1768
1982
  // src/orm/relations/many-to-many.ts
1769
1983
  var toKey4 = (value) => value === null || value === void 0 ? "" : String(value);
1984
+ var hideInternal3 = (obj, keys) => {
1985
+ for (const key of keys) {
1986
+ Object.defineProperty(obj, key, {
1987
+ value: obj[key],
1988
+ writable: false,
1989
+ configurable: false,
1990
+ enumerable: false
1991
+ });
1992
+ }
1993
+ };
1770
1994
  var DefaultManyToManyCollection = class {
1771
1995
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1772
1996
  this.ctx = ctx;
@@ -1780,6 +2004,7 @@ var DefaultManyToManyCollection = class {
1780
2004
  this.localKey = localKey;
1781
2005
  this.loaded = false;
1782
2006
  this.items = [];
2007
+ hideInternal3(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1783
2008
  this.hydrateFromCache();
1784
2009
  }
1785
2010
  async load() {
@@ -1885,6 +2110,9 @@ var DefaultManyToManyCollection = class {
1885
2110
  });
1886
2111
  this.loaded = true;
1887
2112
  }
2113
+ toJSON() {
2114
+ return this.items;
2115
+ }
1888
2116
  };
1889
2117
 
1890
2118
  // src/orm/lazy-batch.ts
@@ -2245,9 +2473,15 @@ var flattenResults = (results) => {
2245
2473
  return rows;
2246
2474
  };
2247
2475
  async function executeHydrated(ctx, qb) {
2248
- const compiled = ctx.dialect.compileSelect(qb.getAST());
2476
+ const ast = qb.getAST();
2477
+ const compiled = ctx.dialect.compileSelect(ast);
2249
2478
  const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
2250
2479
  const rows = flattenResults(executed);
2480
+ if (ast.setOps && ast.setOps.length > 0) {
2481
+ return rows.map(
2482
+ (row) => createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
2483
+ );
2484
+ }
2251
2485
  const hydrated = hydrateRows(rows, qb.getHydrationPlan());
2252
2486
  return hydrated.map(
2253
2487
  (row) => createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
@@ -2294,6 +2528,10 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2294
2528
  const joinNode = createJoinNode(kind, table.name, condition);
2295
2529
  return this.applyAst(context, (service) => service.withJoin(joinNode));
2296
2530
  }
2531
+ applySetOperation(operator, query) {
2532
+ const subAst = this.resolveQueryNode(query);
2533
+ return this.applyAst(this.context, (service) => service.withSetOperation(operator, subAst));
2534
+ }
2297
2535
  /**
2298
2536
  * Selects specific columns for the query
2299
2537
  * @param columns - Record of column definitions, function nodes, case expressions, or window functions
@@ -2482,6 +2720,38 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2482
2720
  const nextContext = this.applyAst(this.context, (service) => service.withOffset(n));
2483
2721
  return this.clone(nextContext);
2484
2722
  }
2723
+ /**
2724
+ * Combines this query with another using UNION
2725
+ * @param query - Query to union with
2726
+ * @returns New query builder instance with the set operation
2727
+ */
2728
+ union(query) {
2729
+ return this.clone(this.applySetOperation("UNION", query));
2730
+ }
2731
+ /**
2732
+ * Combines this query with another using UNION ALL
2733
+ * @param query - Query to union with
2734
+ * @returns New query builder instance with the set operation
2735
+ */
2736
+ unionAll(query) {
2737
+ return this.clone(this.applySetOperation("UNION ALL", query));
2738
+ }
2739
+ /**
2740
+ * Combines this query with another using INTERSECT
2741
+ * @param query - Query to intersect with
2742
+ * @returns New query builder instance with the set operation
2743
+ */
2744
+ intersect(query) {
2745
+ return this.clone(this.applySetOperation("INTERSECT", query));
2746
+ }
2747
+ /**
2748
+ * Combines this query with another using EXCEPT
2749
+ * @param query - Query to subtract
2750
+ * @returns New query builder instance with the set operation
2751
+ */
2752
+ except(query) {
2753
+ return this.clone(this.applySetOperation("EXCEPT", query));
2754
+ }
2485
2755
  /**
2486
2756
  * Adds a WHERE EXISTS condition to the query
2487
2757
  * @param subquery - Subquery to check for existence
@@ -2771,7 +3041,8 @@ var Dialect = class {
2771
3041
  */
2772
3042
  compileSelect(ast) {
2773
3043
  const ctx = this.createCompilerContext();
2774
- const rawSql = this.compileSelectAst(ast, ctx).trim();
3044
+ const normalized = this.normalizeSelectAst(ast);
3045
+ const rawSql = this.compileSelectAst(normalized, ctx).trim();
2775
3046
  const sql = rawSql.endsWith(";") ? rawSql : `${rawSql};`;
2776
3047
  return {
2777
3048
  sql,
@@ -2805,6 +3076,9 @@ var Dialect = class {
2805
3076
  params: [...ctx.params]
2806
3077
  };
2807
3078
  }
3079
+ supportsReturning() {
3080
+ return false;
3081
+ }
2808
3082
  /**
2809
3083
  * Compiles a WHERE clause
2810
3084
  * @param where - WHERE expression
@@ -2829,7 +3103,11 @@ var Dialect = class {
2829
3103
  * @returns SQL for EXISTS subquery
2830
3104
  */
2831
3105
  compileSelectForExists(ast, ctx) {
2832
- const full = this.compileSelectAst(ast, ctx).trim().replace(/;$/, "");
3106
+ const normalized = this.normalizeSelectAst(ast);
3107
+ const full = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, "");
3108
+ if (normalized.setOps && normalized.setOps.length > 0) {
3109
+ return `SELECT 1 FROM (${full}) AS _exists`;
3110
+ }
2833
3111
  const upper = full.toUpperCase();
2834
3112
  const fromIndex = upper.indexOf(" FROM ");
2835
3113
  if (fromIndex === -1) {
@@ -2862,6 +3140,65 @@ var Dialect = class {
2862
3140
  formatPlaceholder(index) {
2863
3141
  return "?";
2864
3142
  }
3143
+ /**
3144
+ * Whether the current dialect supports a given set operation.
3145
+ * Override in concrete dialects to restrict support.
3146
+ */
3147
+ supportsSetOperation(kind) {
3148
+ return true;
3149
+ }
3150
+ /**
3151
+ * Validates set-operation semantics:
3152
+ * - Ensures the dialect supports requested operators.
3153
+ * - Enforces that only the outermost compound query may have ORDER/LIMIT/OFFSET.
3154
+ * @param ast - Query to validate
3155
+ * @param isOutermost - Whether this node is the outermost compound query
3156
+ */
3157
+ validateSetOperations(ast, isOutermost = true) {
3158
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
3159
+ if (!isOutermost && (ast.orderBy || ast.limit !== void 0 || ast.offset !== void 0)) {
3160
+ throw new Error("ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.");
3161
+ }
3162
+ if (hasSetOps) {
3163
+ for (const op of ast.setOps) {
3164
+ if (!this.supportsSetOperation(op.operator)) {
3165
+ throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
3166
+ }
3167
+ this.validateSetOperations(op.query, false);
3168
+ }
3169
+ }
3170
+ }
3171
+ /**
3172
+ * Hoists CTEs from set-operation operands to the outermost query so WITH appears once.
3173
+ * @param ast - Query AST
3174
+ * @returns Normalized AST without inner CTEs and a list of hoisted CTEs
3175
+ */
3176
+ hoistCtes(ast) {
3177
+ let hoisted = [];
3178
+ const normalizedSetOps = ast.setOps?.map((op) => {
3179
+ const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
3180
+ const childCtes = child.ctes ?? [];
3181
+ if (childCtes.length) {
3182
+ hoisted = hoisted.concat(childCtes);
3183
+ }
3184
+ hoisted = hoisted.concat(childHoisted);
3185
+ const queryWithoutCtes = childCtes.length ? { ...child, ctes: void 0 } : child;
3186
+ return { ...op, query: queryWithoutCtes };
3187
+ });
3188
+ const normalized = normalizedSetOps ? { ...ast, setOps: normalizedSetOps } : ast;
3189
+ return { normalized, hoistedCtes: hoisted };
3190
+ }
3191
+ /**
3192
+ * Normalizes a SELECT AST before compilation (validation + CTE hoisting).
3193
+ * @param ast - Query AST
3194
+ * @returns Normalized query AST
3195
+ */
3196
+ normalizeSelectAst(ast) {
3197
+ this.validateSetOperations(ast, true);
3198
+ const { normalized, hoistedCtes } = this.hoistCtes(ast);
3199
+ const combinedCtes = [...normalized.ctes ?? [], ...hoistedCtes];
3200
+ return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
3201
+ }
2865
3202
  constructor() {
2866
3203
  this.expressionCompilers = /* @__PURE__ */ new Map();
2867
3204
  this.operandCompilers = /* @__PURE__ */ new Map();
@@ -3010,16 +3347,18 @@ var SqlDialectBase = class extends Dialect {
3010
3347
  * Compiles SELECT query AST to SQL using common rules.
3011
3348
  */
3012
3349
  compileSelectAst(ast, ctx) {
3350
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
3013
3351
  const ctes = this.compileCtes(ast, ctx);
3014
- const columns = this.compileSelectColumns(ast, ctx);
3015
- const from = this.compileFrom(ast.from);
3016
- const joins = this.compileJoins(ast, ctx);
3017
- const whereClause = this.compileWhere(ast.where, ctx);
3018
- const groupBy = this.compileGroupBy(ast);
3019
- const having = this.compileHaving(ast, ctx);
3352
+ const baseAst = hasSetOps ? { ...ast, setOps: void 0, orderBy: void 0, limit: void 0, offset: void 0 } : ast;
3353
+ const baseSelect = this.compileSelectCore(baseAst, ctx);
3354
+ if (!hasSetOps) {
3355
+ return `${ctes}${baseSelect}`;
3356
+ }
3357
+ const compound = ast.setOps.map((op) => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`).join(" ");
3020
3358
  const orderBy = this.compileOrderBy(ast);
3021
3359
  const pagination = this.compilePagination(ast, orderBy);
3022
- return `${ctes}SELECT ${this.compileDistinct(ast)}${columns} FROM ${from}${joins}${whereClause}${groupBy}${having}${orderBy}${pagination}`;
3360
+ const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
3361
+ return `${ctes}${combined}${orderBy}${pagination}`;
3023
3362
  }
3024
3363
  compileInsertAst(ast, ctx) {
3025
3364
  const table = this.compileTableName(ast.into);
@@ -3028,6 +3367,20 @@ var SqlDialectBase = class extends Dialect {
3028
3367
  const returning = this.compileReturning(ast.returning, ctx);
3029
3368
  return `INSERT INTO ${table} (${columnList}) VALUES ${values}${returning}`;
3030
3369
  }
3370
+ /**
3371
+ * Compiles a single SELECT (no set operations, no CTE prefix).
3372
+ */
3373
+ compileSelectCore(ast, ctx) {
3374
+ const columns = this.compileSelectColumns(ast, ctx);
3375
+ const from = this.compileFrom(ast.from);
3376
+ const joins = this.compileJoins(ast, ctx);
3377
+ const whereClause = this.compileWhere(ast.where, ctx);
3378
+ const groupBy = this.compileGroupBy(ast);
3379
+ const having = this.compileHaving(ast, ctx);
3380
+ const orderBy = this.compileOrderBy(ast);
3381
+ const pagination = this.compilePagination(ast, orderBy);
3382
+ return `SELECT ${this.compileDistinct(ast)}${columns} FROM ${from}${joins}${whereClause}${groupBy}${having}${orderBy}${pagination}`;
3383
+ }
3031
3384
  compileUpdateAst(ast, ctx) {
3032
3385
  const table = this.compileTableName(ast.table);
3033
3386
  const assignments = ast.set.map((assignment) => {
@@ -3053,6 +3406,13 @@ var SqlDialectBase = class extends Dialect {
3053
3406
  if (!returning || returning.length === 0) return "";
3054
3407
  throw new Error("RETURNING is not supported by this dialect.");
3055
3408
  }
3409
+ formatReturningColumns(returning) {
3410
+ return returning.map((column) => {
3411
+ const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : "";
3412
+ const aliasPart = column.alias ? ` AS ${this.quoteIdentifier(column.alias)}` : "";
3413
+ return `${tablePart}${this.quoteIdentifier(column.name)}${aliasPart}`;
3414
+ }).join(", ");
3415
+ }
3056
3416
  /**
3057
3417
  * DISTINCT clause. Override for DISTINCT ON support.
3058
3418
  */
@@ -3118,7 +3478,7 @@ var SqlDialectBase = class extends Dialect {
3118
3478
  const cteDefs = ast.ctes.map((cte) => {
3119
3479
  const name = this.quoteIdentifier(cte.name);
3120
3480
  const cols = cte.columns && cte.columns.length ? `(${cte.columns.map((c) => this.quoteIdentifier(c)).join(", ")})` : "";
3121
- const query = this.stripTrailingSemicolon(this.compileSelectAst(cte.query, ctx));
3481
+ const query = this.stripTrailingSemicolon(this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx));
3122
3482
  return `${name}${cols} AS (${query})`;
3123
3483
  }).join(", ");
3124
3484
  return `${prefix}${cteDefs} `;
@@ -3126,6 +3486,10 @@ var SqlDialectBase = class extends Dialect {
3126
3486
  stripTrailingSemicolon(sql) {
3127
3487
  return sql.trim().replace(/;$/, "");
3128
3488
  }
3489
+ wrapSetOperand(sql) {
3490
+ const trimmed = this.stripTrailingSemicolon(sql);
3491
+ return `(${trimmed})`;
3492
+ }
3129
3493
  };
3130
3494
 
3131
3495
  // src/core/dialect/mysql/index.ts
@@ -3195,6 +3559,43 @@ var SqlServerDialect = class extends Dialect {
3195
3559
  * @returns SQL Server SQL string
3196
3560
  */
3197
3561
  compileSelectAst(ast, ctx) {
3562
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
3563
+ const ctes = this.compileCtes(ast, ctx);
3564
+ const baseAst = hasSetOps ? { ...ast, setOps: void 0, orderBy: void 0, limit: void 0, offset: void 0 } : ast;
3565
+ const baseSelect = this.compileSelectCore(baseAst, ctx);
3566
+ if (!hasSetOps) {
3567
+ return `${ctes}${baseSelect}`;
3568
+ }
3569
+ const compound = ast.setOps.map((op) => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`).join(" ");
3570
+ const orderBy = this.compileOrderBy(ast);
3571
+ const pagination = this.compilePagination(ast, orderBy);
3572
+ const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
3573
+ const tail = pagination || orderBy;
3574
+ return `${ctes}${combined}${tail}`;
3575
+ }
3576
+ compileInsertAst(ast, ctx) {
3577
+ const table = this.quoteIdentifier(ast.into.name);
3578
+ const columnList = ast.columns.map((column) => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(", ");
3579
+ const values = ast.values.map((row) => `(${row.map((value) => this.compileOperand(value, ctx)).join(", ")})`).join(", ");
3580
+ return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
3581
+ }
3582
+ compileUpdateAst(ast, ctx) {
3583
+ const table = this.quoteIdentifier(ast.table.name);
3584
+ const assignments = ast.set.map((assignment) => {
3585
+ const col2 = assignment.column;
3586
+ const target = `${this.quoteIdentifier(col2.table)}.${this.quoteIdentifier(col2.name)}`;
3587
+ const value = this.compileOperand(assignment.value, ctx);
3588
+ return `${target} = ${value}`;
3589
+ }).join(", ");
3590
+ const whereClause = this.compileWhere(ast.where, ctx);
3591
+ return `UPDATE ${table} SET ${assignments}${whereClause};`;
3592
+ }
3593
+ compileDeleteAst(ast, ctx) {
3594
+ const table = this.quoteIdentifier(ast.from.name);
3595
+ const whereClause = this.compileWhere(ast.where, ctx);
3596
+ return `DELETE FROM ${table}${whereClause};`;
3597
+ }
3598
+ compileSelectCore(ast, ctx) {
3198
3599
  const columns = ast.columns.map((c) => {
3199
3600
  let expr = "";
3200
3601
  if (c.type === "Function") {
@@ -3222,46 +3623,42 @@ var SqlServerDialect = class extends Dialect {
3222
3623
  const whereClause = this.compileWhere(ast.where, ctx);
3223
3624
  const groupBy = ast.groupBy && ast.groupBy.length > 0 ? " GROUP BY " + ast.groupBy.map((c) => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(", ") : "";
3224
3625
  const having = ast.having ? ` HAVING ${this.compileExpression(ast.having, ctx)}` : "";
3225
- const orderBy = ast.orderBy && ast.orderBy.length > 0 ? " ORDER BY " + ast.orderBy.map((o) => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(", ") : "";
3226
- let pagination = "";
3227
- if (ast.limit || ast.offset) {
3228
- const off = ast.offset || 0;
3229
- const orderClause = orderBy || " ORDER BY (SELECT NULL)";
3230
- pagination = `${orderClause} OFFSET ${off} ROWS`;
3231
- if (ast.limit) {
3232
- pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
3233
- }
3234
- return `SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${pagination};`;
3626
+ const orderBy = this.compileOrderBy(ast);
3627
+ const pagination = this.compilePagination(ast, orderBy);
3628
+ if (pagination) {
3629
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${pagination}`;
3630
+ }
3631
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${orderBy}`;
3632
+ }
3633
+ compileOrderBy(ast) {
3634
+ if (!ast.orderBy || ast.orderBy.length === 0) return "";
3635
+ return " ORDER BY " + ast.orderBy.map((o) => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(", ");
3636
+ }
3637
+ compilePagination(ast, orderBy) {
3638
+ const hasLimit = ast.limit !== void 0;
3639
+ const hasOffset = ast.offset !== void 0;
3640
+ if (!hasLimit && !hasOffset) return "";
3641
+ const off = ast.offset ?? 0;
3642
+ const orderClause = orderBy || " ORDER BY (SELECT NULL)";
3643
+ let pagination = `${orderClause} OFFSET ${off} ROWS`;
3644
+ if (hasLimit) {
3645
+ pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
3235
3646
  }
3236
- const ctes = ast.ctes && ast.ctes.length > 0 ? "WITH " + ast.ctes.map((cte) => {
3647
+ return pagination;
3648
+ }
3649
+ compileCtes(ast, ctx) {
3650
+ if (!ast.ctes || ast.ctes.length === 0) return "";
3651
+ const defs = ast.ctes.map((cte) => {
3237
3652
  const name = this.quoteIdentifier(cte.name);
3238
3653
  const cols = cte.columns ? `(${cte.columns.map((c) => this.quoteIdentifier(c)).join(", ")})` : "";
3239
- const query = this.compileSelectAst(cte.query, ctx).trim().replace(/;$/, "");
3654
+ const query = this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx).trim().replace(/;$/, "");
3240
3655
  return `${name}${cols} AS (${query})`;
3241
- }).join(", ") + " " : "";
3242
- return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${orderBy};`;
3243
- }
3244
- compileInsertAst(ast, ctx) {
3245
- const table = this.quoteIdentifier(ast.into.name);
3246
- const columnList = ast.columns.map((column) => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(", ");
3247
- const values = ast.values.map((row) => `(${row.map((value) => this.compileOperand(value, ctx)).join(", ")})`).join(", ");
3248
- return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
3249
- }
3250
- compileUpdateAst(ast, ctx) {
3251
- const table = this.quoteIdentifier(ast.table.name);
3252
- const assignments = ast.set.map((assignment) => {
3253
- const col2 = assignment.column;
3254
- const target = `${this.quoteIdentifier(col2.table)}.${this.quoteIdentifier(col2.name)}`;
3255
- const value = this.compileOperand(assignment.value, ctx);
3256
- return `${target} = ${value}`;
3257
3656
  }).join(", ");
3258
- const whereClause = this.compileWhere(ast.where, ctx);
3259
- return `UPDATE ${table} SET ${assignments}${whereClause};`;
3657
+ return `WITH ${defs} `;
3260
3658
  }
3261
- compileDeleteAst(ast, ctx) {
3262
- const table = this.quoteIdentifier(ast.from.name);
3263
- const whereClause = this.compileWhere(ast.where, ctx);
3264
- return `DELETE FROM ${table}${whereClause};`;
3659
+ wrapSetOperand(sql) {
3660
+ const trimmed = sql.trim().replace(/;$/, "");
3661
+ return `(${trimmed})`;
3265
3662
  }
3266
3663
  };
3267
3664
 
@@ -3292,12 +3689,12 @@ var SqliteDialect = class extends SqlDialectBase {
3292
3689
  }
3293
3690
  compileReturning(returning, ctx) {
3294
3691
  if (!returning || returning.length === 0) return "";
3295
- const columns = returning.map((column) => {
3296
- const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : "";
3297
- return `${tablePart}${this.quoteIdentifier(column.name)}`;
3298
- }).join(", ");
3692
+ const columns = this.formatReturningColumns(returning);
3299
3693
  return ` RETURNING ${columns}`;
3300
3694
  }
3695
+ supportsReturning() {
3696
+ return true;
3697
+ }
3301
3698
  };
3302
3699
 
3303
3700
  // src/core/dialect/postgres/index.ts
@@ -3327,12 +3724,12 @@ var PostgresDialect = class extends SqlDialectBase {
3327
3724
  }
3328
3725
  compileReturning(returning, ctx) {
3329
3726
  if (!returning || returning.length === 0) return "";
3330
- const columns = returning.map((column) => {
3331
- const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : "";
3332
- return `${tablePart}${this.quoteIdentifier(column.name)}`;
3333
- }).join(", ");
3727
+ const columns = this.formatReturningColumns(returning);
3334
3728
  return ` RETURNING ${columns}`;
3335
3729
  }
3730
+ supportsReturning() {
3731
+ return true;
3732
+ }
3336
3733
  };
3337
3734
 
3338
3735
  // src/core/ddl/dialects/base-schema-dialect.ts
@@ -5096,9 +5493,13 @@ var UnitOfWork = class {
5096
5493
  async flushInsert(tracked) {
5097
5494
  await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
5098
5495
  const payload = this.extractColumns(tracked.table, tracked.entity);
5099
- const builder = new InsertQueryBuilder(tracked.table).values(payload);
5496
+ let builder = new InsertQueryBuilder(tracked.table).values(payload);
5497
+ if (this.dialect.supportsReturning()) {
5498
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
5499
+ }
5100
5500
  const compiled = builder.compile(this.dialect);
5101
- await this.executeCompiled(compiled);
5501
+ const results = await this.executeCompiled(compiled);
5502
+ this.applyReturningResults(tracked, results);
5102
5503
  tracked.status = "managed" /* Managed */;
5103
5504
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
5104
5505
  tracked.pk = this.getPrimaryKeyValue(tracked);
@@ -5115,9 +5516,13 @@ var UnitOfWork = class {
5115
5516
  await this.runHook(tracked.table.hooks?.beforeUpdate, tracked);
5116
5517
  const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
5117
5518
  if (!pkColumn) return;
5118
- const builder = new UpdateQueryBuilder(tracked.table).set(changes).where(eq(pkColumn, tracked.pk));
5519
+ let builder = new UpdateQueryBuilder(tracked.table).set(changes).where(eq(pkColumn, tracked.pk));
5520
+ if (this.dialect.supportsReturning()) {
5521
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
5522
+ }
5119
5523
  const compiled = builder.compile(this.dialect);
5120
- await this.executeCompiled(compiled);
5524
+ const results = await this.executeCompiled(compiled);
5525
+ this.applyReturningResults(tracked, results);
5121
5526
  tracked.status = "managed" /* Managed */;
5122
5527
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
5123
5528
  this.registerIdentity(tracked);
@@ -5159,7 +5564,31 @@ var UnitOfWork = class {
5159
5564
  return payload;
5160
5565
  }
5161
5566
  async executeCompiled(compiled) {
5162
- await this.executor.executeSql(compiled.sql, compiled.params);
5567
+ return this.executor.executeSql(compiled.sql, compiled.params);
5568
+ }
5569
+ getReturningColumns(table) {
5570
+ return Object.values(table.columns).map((column) => ({
5571
+ type: "Column",
5572
+ table: table.name,
5573
+ name: column.name,
5574
+ alias: column.name
5575
+ }));
5576
+ }
5577
+ applyReturningResults(tracked, results) {
5578
+ if (!this.dialect.supportsReturning()) return;
5579
+ const first = results[0];
5580
+ if (!first || first.values.length === 0) return;
5581
+ const row = first.values[0];
5582
+ for (let i = 0; i < first.columns.length; i++) {
5583
+ const columnName = this.normalizeColumnName(first.columns[i]);
5584
+ if (!(columnName in tracked.table.columns)) continue;
5585
+ tracked.entity[columnName] = row[i];
5586
+ }
5587
+ }
5588
+ normalizeColumnName(column) {
5589
+ const parts = column.split(".");
5590
+ const candidate = parts[parts.length - 1];
5591
+ return candidate.replace(/^["`[\]]+|["`[\]]+$/g, "");
5163
5592
  }
5164
5593
  registerIdentity(tracked) {
5165
5594
  if (tracked.pk == null) return;
@@ -5180,22 +5609,46 @@ var UnitOfWork = class {
5180
5609
  }
5181
5610
  };
5182
5611
 
5612
+ // src/orm/query-logger.ts
5613
+ var createQueryLoggingExecutor = (executor, logger) => {
5614
+ if (!logger) {
5615
+ return executor;
5616
+ }
5617
+ const wrapped = {
5618
+ async executeSql(sql, params) {
5619
+ logger({ sql, params });
5620
+ return executor.executeSql(sql, params);
5621
+ }
5622
+ };
5623
+ if (executor.beginTransaction) {
5624
+ wrapped.beginTransaction = executor.beginTransaction.bind(executor);
5625
+ }
5626
+ if (executor.commitTransaction) {
5627
+ wrapped.commitTransaction = executor.commitTransaction.bind(executor);
5628
+ }
5629
+ if (executor.rollbackTransaction) {
5630
+ wrapped.rollbackTransaction = executor.rollbackTransaction.bind(executor);
5631
+ }
5632
+ return wrapped;
5633
+ };
5634
+
5183
5635
  // src/orm/orm-context.ts
5184
5636
  var OrmContext = class {
5185
5637
  constructor(options) {
5186
5638
  this.options = options;
5187
5639
  this.identityMap = new IdentityMap();
5188
5640
  this.interceptors = [...options.interceptors ?? []];
5641
+ this.executorWithLogging = createQueryLoggingExecutor(options.executor, options.queryLogger);
5189
5642
  this.unitOfWork = new UnitOfWork(
5190
5643
  options.dialect,
5191
- options.executor,
5644
+ this.executorWithLogging,
5192
5645
  this.identityMap,
5193
5646
  () => this
5194
5647
  );
5195
5648
  this.relationChanges = new RelationChangeProcessor(
5196
5649
  this.unitOfWork,
5197
5650
  options.dialect,
5198
- options.executor
5651
+ this.executorWithLogging
5199
5652
  );
5200
5653
  this.domainEvents = new DomainEventBus(options.domainEventHandlers);
5201
5654
  }
@@ -5203,7 +5656,7 @@ var OrmContext = class {
5203
5656
  return this.options.dialect;
5204
5657
  }
5205
5658
  get executor() {
5206
- return this.options.executor;
5659
+ return this.executorWithLogging;
5207
5660
  }
5208
5661
  get identityBuckets() {
5209
5662
  return this.unitOfWork.identityBuckets;