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.cjs CHANGED
@@ -663,6 +663,82 @@ var SelectQueryState = class _SelectQueryState {
663
663
  ctes: [...this.ast.ctes ?? [], cte]
664
664
  });
665
665
  }
666
+ /**
667
+ * Adds a set operation (UNION/INTERSECT/EXCEPT) to the query
668
+ * @param op - Set operation node to add
669
+ * @returns New SelectQueryState with set operation
670
+ */
671
+ withSetOperation(op) {
672
+ return this.clone({
673
+ ...this.ast,
674
+ setOps: [...this.ast.setOps ?? [], op]
675
+ });
676
+ }
677
+ };
678
+
679
+ // src/core/ast/join-node.ts
680
+ var createJoinNode = (kind, tableName, condition, relationName) => ({
681
+ type: "Join",
682
+ kind,
683
+ table: { type: "Table", name: tableName },
684
+ condition,
685
+ relationName
686
+ });
687
+
688
+ // src/core/sql/sql.ts
689
+ var SQL_OPERATORS = {
690
+ /** Equality operator */
691
+ EQUALS: "=",
692
+ /** Not equals operator */
693
+ NOT_EQUALS: "!=",
694
+ /** Greater than operator */
695
+ GREATER_THAN: ">",
696
+ /** Greater than or equal operator */
697
+ GREATER_OR_EQUAL: ">=",
698
+ /** Less than operator */
699
+ LESS_THAN: "<",
700
+ /** Less than or equal operator */
701
+ LESS_OR_EQUAL: "<=",
702
+ /** LIKE pattern matching operator */
703
+ LIKE: "LIKE",
704
+ /** NOT LIKE pattern matching operator */
705
+ NOT_LIKE: "NOT LIKE",
706
+ /** IN membership operator */
707
+ IN: "IN",
708
+ /** NOT IN membership operator */
709
+ NOT_IN: "NOT IN",
710
+ /** BETWEEN range operator */
711
+ BETWEEN: "BETWEEN",
712
+ /** NOT BETWEEN range operator */
713
+ NOT_BETWEEN: "NOT BETWEEN",
714
+ /** IS NULL null check operator */
715
+ IS_NULL: "IS NULL",
716
+ /** IS NOT NULL null check operator */
717
+ IS_NOT_NULL: "IS NOT NULL",
718
+ /** Logical AND operator */
719
+ AND: "AND",
720
+ /** Logical OR operator */
721
+ OR: "OR",
722
+ /** EXISTS operator */
723
+ EXISTS: "EXISTS",
724
+ /** NOT EXISTS operator */
725
+ NOT_EXISTS: "NOT EXISTS"
726
+ };
727
+ var JOIN_KINDS = {
728
+ /** INNER JOIN type */
729
+ INNER: "INNER",
730
+ /** LEFT JOIN type */
731
+ LEFT: "LEFT",
732
+ /** RIGHT JOIN type */
733
+ RIGHT: "RIGHT",
734
+ /** CROSS JOIN type */
735
+ CROSS: "CROSS"
736
+ };
737
+ var ORDER_DIRECTIONS = {
738
+ /** Ascending order */
739
+ ASC: "ASC",
740
+ /** Descending order */
741
+ DESC: "DESC"
666
742
  };
667
743
 
668
744
  // src/query-builder/hydration-manager.ts
@@ -714,8 +790,26 @@ var HydrationManager = class _HydrationManager {
714
790
  * @returns AST with hydration metadata
715
791
  */
716
792
  applyToAst(ast) {
793
+ if (ast.setOps && ast.setOps.length > 0) {
794
+ return ast;
795
+ }
717
796
  const plan = this.planner.getPlan();
718
797
  if (!plan) return ast;
798
+ const needsPaginationGuard = this.requiresParentPagination(ast, plan);
799
+ const rewritten = needsPaginationGuard ? this.wrapForParentPagination(ast, plan) : ast;
800
+ return this.attachHydrationMeta(rewritten, plan);
801
+ }
802
+ /**
803
+ * Gets the current hydration plan
804
+ * @returns Hydration plan or undefined if none exists
805
+ */
806
+ getPlan() {
807
+ return this.planner.getPlan();
808
+ }
809
+ /**
810
+ * Attaches hydration metadata to a query AST node.
811
+ */
812
+ attachHydrationMeta(ast, plan) {
719
813
  return {
720
814
  ...ast,
721
815
  meta: {
@@ -725,11 +819,154 @@ var HydrationManager = class _HydrationManager {
725
819
  };
726
820
  }
727
821
  /**
728
- * Gets the current hydration plan
729
- * @returns Hydration plan or undefined if none exists
822
+ * Determines whether the query needs pagination rewriting to keep LIMIT/OFFSET
823
+ * applied to parent rows when eager-loading multiplicative relations.
730
824
  */
731
- getPlan() {
732
- return this.planner.getPlan();
825
+ requiresParentPagination(ast, plan) {
826
+ const hasPagination = ast.limit !== void 0 || ast.offset !== void 0;
827
+ return hasPagination && this.hasMultiplyingRelations(plan);
828
+ }
829
+ hasMultiplyingRelations(plan) {
830
+ return plan.relations.some(
831
+ (rel) => rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany
832
+ );
833
+ }
834
+ /**
835
+ * Rewrites the query using CTEs so LIMIT/OFFSET target distinct parent rows
836
+ * instead of the joined result set.
837
+ *
838
+ * The strategy:
839
+ * - Hoist the original query (minus limit/offset) into a base CTE.
840
+ * - Select distinct parent ids from that base CTE with the original ordering and pagination.
841
+ * - Join the base CTE against the paged ids to retrieve the joined rows for just that page.
842
+ */
843
+ wrapForParentPagination(ast, plan) {
844
+ const projectionNames = this.getProjectionNames(ast.columns);
845
+ if (!projectionNames) {
846
+ return ast;
847
+ }
848
+ const projectionAliases = this.buildProjectionAliasMap(ast.columns);
849
+ const projectionSet = new Set(projectionNames);
850
+ const rootPkAlias = projectionAliases.get(`${plan.rootTable}.${plan.rootPrimaryKey}`) ?? plan.rootPrimaryKey;
851
+ const baseCteName = this.nextCteName(ast.ctes, "__metal_pagination_base");
852
+ const baseQuery = {
853
+ ...ast,
854
+ ctes: void 0,
855
+ limit: void 0,
856
+ offset: void 0,
857
+ orderBy: void 0,
858
+ meta: void 0
859
+ };
860
+ const baseCte = {
861
+ type: "CommonTableExpression",
862
+ name: baseCteName,
863
+ query: baseQuery,
864
+ recursive: false
865
+ };
866
+ const orderBy = this.mapOrderBy(ast.orderBy, plan, projectionAliases, baseCteName, projectionSet);
867
+ if (orderBy === null) {
868
+ return ast;
869
+ }
870
+ const pageCteName = this.nextCteName([...ast.ctes ?? [], baseCte], "__metal_pagination_page");
871
+ const pagingColumns = this.buildPagingColumns(rootPkAlias, orderBy, baseCteName);
872
+ const pageCte = {
873
+ type: "CommonTableExpression",
874
+ name: pageCteName,
875
+ query: {
876
+ type: "SelectQuery",
877
+ from: { type: "Table", name: baseCteName },
878
+ columns: pagingColumns,
879
+ joins: [],
880
+ distinct: [{ type: "Column", table: baseCteName, name: rootPkAlias }],
881
+ orderBy,
882
+ limit: ast.limit,
883
+ offset: ast.offset
884
+ },
885
+ recursive: false
886
+ };
887
+ const joinCondition = eq(
888
+ { type: "Column", table: baseCteName, name: rootPkAlias },
889
+ { type: "Column", table: pageCteName, name: rootPkAlias }
890
+ );
891
+ const outerColumns = projectionNames.map((name) => ({
892
+ type: "Column",
893
+ table: baseCteName,
894
+ name,
895
+ alias: name
896
+ }));
897
+ return {
898
+ type: "SelectQuery",
899
+ from: { type: "Table", name: baseCteName },
900
+ columns: outerColumns,
901
+ joins: [createJoinNode(JOIN_KINDS.INNER, pageCteName, joinCondition)],
902
+ orderBy,
903
+ ctes: [...ast.ctes ?? [], baseCte, pageCte]
904
+ };
905
+ }
906
+ nextCteName(existing, baseName) {
907
+ const names = new Set((existing ?? []).map((cte) => cte.name));
908
+ let candidate = baseName;
909
+ let suffix = 1;
910
+ while (names.has(candidate)) {
911
+ suffix += 1;
912
+ candidate = `${baseName}_${suffix}`;
913
+ }
914
+ return candidate;
915
+ }
916
+ getProjectionNames(columns) {
917
+ const names = [];
918
+ for (const col2 of columns) {
919
+ const alias = col2.alias ?? col2.name;
920
+ if (!alias) return void 0;
921
+ names.push(alias);
922
+ }
923
+ return names;
924
+ }
925
+ buildProjectionAliasMap(columns) {
926
+ const map = /* @__PURE__ */ new Map();
927
+ for (const col2 of columns) {
928
+ if (col2.type !== "Column") continue;
929
+ const node = col2;
930
+ const key = `${node.table}.${node.name}`;
931
+ map.set(key, node.alias ?? node.name);
932
+ }
933
+ return map;
934
+ }
935
+ mapOrderBy(orderBy, plan, projectionAliases, baseAlias, availableColumns) {
936
+ if (!orderBy || orderBy.length === 0) {
937
+ return void 0;
938
+ }
939
+ const mapped = [];
940
+ for (const ob of orderBy) {
941
+ if (ob.column.table !== plan.rootTable) {
942
+ return null;
943
+ }
944
+ const alias = projectionAliases.get(`${ob.column.table}.${ob.column.name}`) ?? ob.column.name;
945
+ if (!availableColumns.has(alias)) {
946
+ return null;
947
+ }
948
+ mapped.push({
949
+ type: "OrderBy",
950
+ column: { type: "Column", table: baseAlias, name: alias },
951
+ direction: ob.direction
952
+ });
953
+ }
954
+ return mapped;
955
+ }
956
+ buildPagingColumns(primaryKey, orderBy, tableAlias) {
957
+ const columns = [{ type: "Column", table: tableAlias, name: primaryKey, alias: primaryKey }];
958
+ if (!orderBy) return columns;
959
+ for (const ob of orderBy) {
960
+ if (!columns.some((col2) => col2.name === ob.column.name)) {
961
+ columns.push({
962
+ type: "Column",
963
+ table: tableAlias,
964
+ name: ob.column.name,
965
+ alias: ob.column.name
966
+ });
967
+ }
968
+ }
969
+ return columns;
733
970
  }
734
971
  };
735
972
 
@@ -992,6 +1229,20 @@ var QueryAstService = class {
992
1229
  };
993
1230
  return this.state.withCte(cte);
994
1231
  }
1232
+ /**
1233
+ * Adds a set operation (UNION/UNION ALL/INTERSECT/EXCEPT) to the query
1234
+ * @param operator - Set operator
1235
+ * @param query - Right-hand side query
1236
+ * @returns Updated query state with set operation
1237
+ */
1238
+ withSetOperation(operator, query) {
1239
+ const op = {
1240
+ type: "SetOperation",
1241
+ operator,
1242
+ query
1243
+ };
1244
+ return this.state.withSetOperation(op);
1245
+ }
995
1246
  /**
996
1247
  * Selects a subquery as a column
997
1248
  * @param alias - Alias for the subquery
@@ -1144,15 +1395,6 @@ var RelationProjectionHelper = class {
1144
1395
  }
1145
1396
  };
1146
1397
 
1147
- // src/core/ast/join-node.ts
1148
- var createJoinNode = (kind, tableName, condition, relationName) => ({
1149
- type: "Join",
1150
- kind,
1151
- table: { type: "Table", name: tableName },
1152
- condition,
1153
- relationName
1154
- });
1155
-
1156
1398
  // src/query-builder/relation-conditions.ts
1157
1399
  var assertNever = (value) => {
1158
1400
  throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
@@ -1208,62 +1450,6 @@ var buildRelationCorrelation = (root, relation) => {
1208
1450
  return baseRelationCondition(root, relation);
1209
1451
  };
1210
1452
 
1211
- // src/core/sql/sql.ts
1212
- var SQL_OPERATORS = {
1213
- /** Equality operator */
1214
- EQUALS: "=",
1215
- /** Not equals operator */
1216
- NOT_EQUALS: "!=",
1217
- /** Greater than operator */
1218
- GREATER_THAN: ">",
1219
- /** Greater than or equal operator */
1220
- GREATER_OR_EQUAL: ">=",
1221
- /** Less than operator */
1222
- LESS_THAN: "<",
1223
- /** Less than or equal operator */
1224
- LESS_OR_EQUAL: "<=",
1225
- /** LIKE pattern matching operator */
1226
- LIKE: "LIKE",
1227
- /** NOT LIKE pattern matching operator */
1228
- NOT_LIKE: "NOT LIKE",
1229
- /** IN membership operator */
1230
- IN: "IN",
1231
- /** NOT IN membership operator */
1232
- NOT_IN: "NOT IN",
1233
- /** BETWEEN range operator */
1234
- BETWEEN: "BETWEEN",
1235
- /** NOT BETWEEN range operator */
1236
- NOT_BETWEEN: "NOT BETWEEN",
1237
- /** IS NULL null check operator */
1238
- IS_NULL: "IS NULL",
1239
- /** IS NOT NULL null check operator */
1240
- IS_NOT_NULL: "IS NOT NULL",
1241
- /** Logical AND operator */
1242
- AND: "AND",
1243
- /** Logical OR operator */
1244
- OR: "OR",
1245
- /** EXISTS operator */
1246
- EXISTS: "EXISTS",
1247
- /** NOT EXISTS operator */
1248
- NOT_EXISTS: "NOT EXISTS"
1249
- };
1250
- var JOIN_KINDS = {
1251
- /** INNER JOIN type */
1252
- INNER: "INNER",
1253
- /** LEFT JOIN type */
1254
- LEFT: "LEFT",
1255
- /** RIGHT JOIN type */
1256
- RIGHT: "RIGHT",
1257
- /** CROSS JOIN type */
1258
- CROSS: "CROSS"
1259
- };
1260
- var ORDER_DIRECTIONS = {
1261
- /** Ascending order */
1262
- ASC: "ASC",
1263
- /** Descending order */
1264
- DESC: "DESC"
1265
- };
1266
-
1267
1453
  // src/query-builder/relation-service.ts
1268
1454
  var RelationService = class {
1269
1455
  /**
@@ -1709,6 +1895,16 @@ var hasEntityMeta = (entity) => {
1709
1895
 
1710
1896
  // src/orm/relations/has-many.ts
1711
1897
  var toKey2 = (value) => value === null || value === void 0 ? "" : String(value);
1898
+ var hideInternal = (obj, keys) => {
1899
+ for (const key of keys) {
1900
+ Object.defineProperty(obj, key, {
1901
+ value: obj[key],
1902
+ writable: false,
1903
+ configurable: false,
1904
+ enumerable: false
1905
+ });
1906
+ }
1907
+ };
1712
1908
  var DefaultHasManyCollection = class {
1713
1909
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1714
1910
  this.ctx = ctx;
@@ -1724,6 +1920,7 @@ var DefaultHasManyCollection = class {
1724
1920
  this.items = [];
1725
1921
  this.added = /* @__PURE__ */ new Set();
1726
1922
  this.removed = /* @__PURE__ */ new Set();
1923
+ hideInternal(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1727
1924
  this.hydrateFromCache();
1728
1925
  }
1729
1926
  async load() {
@@ -1799,10 +1996,23 @@ var DefaultHasManyCollection = class {
1799
1996
  this.items = rows.map((row) => this.createEntity(row));
1800
1997
  this.loaded = true;
1801
1998
  }
1999
+ toJSON() {
2000
+ return this.items;
2001
+ }
1802
2002
  };
1803
2003
 
1804
2004
  // src/orm/relations/belongs-to.ts
1805
2005
  var toKey3 = (value) => value === null || value === void 0 ? "" : String(value);
2006
+ var hideInternal2 = (obj, keys) => {
2007
+ for (const key of keys) {
2008
+ Object.defineProperty(obj, key, {
2009
+ value: obj[key],
2010
+ writable: false,
2011
+ configurable: false,
2012
+ enumerable: false
2013
+ });
2014
+ }
2015
+ };
1806
2016
  var DefaultBelongsToReference = class {
1807
2017
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, targetKey) {
1808
2018
  this.ctx = ctx;
@@ -1816,6 +2026,7 @@ var DefaultBelongsToReference = class {
1816
2026
  this.targetKey = targetKey;
1817
2027
  this.loaded = false;
1818
2028
  this.current = null;
2029
+ hideInternal2(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "targetKey"]);
1819
2030
  this.populateFromHydrationCache();
1820
2031
  }
1821
2032
  async load() {
@@ -1876,10 +2087,23 @@ var DefaultBelongsToReference = class {
1876
2087
  this.current = this.createEntity(row);
1877
2088
  this.loaded = true;
1878
2089
  }
2090
+ toJSON() {
2091
+ return this.current;
2092
+ }
1879
2093
  };
1880
2094
 
1881
2095
  // src/orm/relations/many-to-many.ts
1882
2096
  var toKey4 = (value) => value === null || value === void 0 ? "" : String(value);
2097
+ var hideInternal3 = (obj, keys) => {
2098
+ for (const key of keys) {
2099
+ Object.defineProperty(obj, key, {
2100
+ value: obj[key],
2101
+ writable: false,
2102
+ configurable: false,
2103
+ enumerable: false
2104
+ });
2105
+ }
2106
+ };
1883
2107
  var DefaultManyToManyCollection = class {
1884
2108
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1885
2109
  this.ctx = ctx;
@@ -1893,6 +2117,7 @@ var DefaultManyToManyCollection = class {
1893
2117
  this.localKey = localKey;
1894
2118
  this.loaded = false;
1895
2119
  this.items = [];
2120
+ hideInternal3(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1896
2121
  this.hydrateFromCache();
1897
2122
  }
1898
2123
  async load() {
@@ -1998,6 +2223,9 @@ var DefaultManyToManyCollection = class {
1998
2223
  });
1999
2224
  this.loaded = true;
2000
2225
  }
2226
+ toJSON() {
2227
+ return this.items;
2228
+ }
2001
2229
  };
2002
2230
 
2003
2231
  // src/orm/lazy-batch.ts
@@ -2358,9 +2586,15 @@ var flattenResults = (results) => {
2358
2586
  return rows;
2359
2587
  };
2360
2588
  async function executeHydrated(ctx, qb) {
2361
- const compiled = ctx.dialect.compileSelect(qb.getAST());
2589
+ const ast = qb.getAST();
2590
+ const compiled = ctx.dialect.compileSelect(ast);
2362
2591
  const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
2363
2592
  const rows = flattenResults(executed);
2593
+ if (ast.setOps && ast.setOps.length > 0) {
2594
+ return rows.map(
2595
+ (row) => createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
2596
+ );
2597
+ }
2364
2598
  const hydrated = hydrateRows(rows, qb.getHydrationPlan());
2365
2599
  return hydrated.map(
2366
2600
  (row) => createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
@@ -2407,6 +2641,10 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2407
2641
  const joinNode = createJoinNode(kind, table.name, condition);
2408
2642
  return this.applyAst(context, (service) => service.withJoin(joinNode));
2409
2643
  }
2644
+ applySetOperation(operator, query) {
2645
+ const subAst = this.resolveQueryNode(query);
2646
+ return this.applyAst(this.context, (service) => service.withSetOperation(operator, subAst));
2647
+ }
2410
2648
  /**
2411
2649
  * Selects specific columns for the query
2412
2650
  * @param columns - Record of column definitions, function nodes, case expressions, or window functions
@@ -2595,6 +2833,38 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2595
2833
  const nextContext = this.applyAst(this.context, (service) => service.withOffset(n));
2596
2834
  return this.clone(nextContext);
2597
2835
  }
2836
+ /**
2837
+ * Combines this query with another using UNION
2838
+ * @param query - Query to union with
2839
+ * @returns New query builder instance with the set operation
2840
+ */
2841
+ union(query) {
2842
+ return this.clone(this.applySetOperation("UNION", query));
2843
+ }
2844
+ /**
2845
+ * Combines this query with another using UNION ALL
2846
+ * @param query - Query to union with
2847
+ * @returns New query builder instance with the set operation
2848
+ */
2849
+ unionAll(query) {
2850
+ return this.clone(this.applySetOperation("UNION ALL", query));
2851
+ }
2852
+ /**
2853
+ * Combines this query with another using INTERSECT
2854
+ * @param query - Query to intersect with
2855
+ * @returns New query builder instance with the set operation
2856
+ */
2857
+ intersect(query) {
2858
+ return this.clone(this.applySetOperation("INTERSECT", query));
2859
+ }
2860
+ /**
2861
+ * Combines this query with another using EXCEPT
2862
+ * @param query - Query to subtract
2863
+ * @returns New query builder instance with the set operation
2864
+ */
2865
+ except(query) {
2866
+ return this.clone(this.applySetOperation("EXCEPT", query));
2867
+ }
2598
2868
  /**
2599
2869
  * Adds a WHERE EXISTS condition to the query
2600
2870
  * @param subquery - Subquery to check for existence
@@ -2884,7 +3154,8 @@ var Dialect = class {
2884
3154
  */
2885
3155
  compileSelect(ast) {
2886
3156
  const ctx = this.createCompilerContext();
2887
- const rawSql = this.compileSelectAst(ast, ctx).trim();
3157
+ const normalized = this.normalizeSelectAst(ast);
3158
+ const rawSql = this.compileSelectAst(normalized, ctx).trim();
2888
3159
  const sql = rawSql.endsWith(";") ? rawSql : `${rawSql};`;
2889
3160
  return {
2890
3161
  sql,
@@ -2918,6 +3189,9 @@ var Dialect = class {
2918
3189
  params: [...ctx.params]
2919
3190
  };
2920
3191
  }
3192
+ supportsReturning() {
3193
+ return false;
3194
+ }
2921
3195
  /**
2922
3196
  * Compiles a WHERE clause
2923
3197
  * @param where - WHERE expression
@@ -2942,7 +3216,11 @@ var Dialect = class {
2942
3216
  * @returns SQL for EXISTS subquery
2943
3217
  */
2944
3218
  compileSelectForExists(ast, ctx) {
2945
- const full = this.compileSelectAst(ast, ctx).trim().replace(/;$/, "");
3219
+ const normalized = this.normalizeSelectAst(ast);
3220
+ const full = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, "");
3221
+ if (normalized.setOps && normalized.setOps.length > 0) {
3222
+ return `SELECT 1 FROM (${full}) AS _exists`;
3223
+ }
2946
3224
  const upper = full.toUpperCase();
2947
3225
  const fromIndex = upper.indexOf(" FROM ");
2948
3226
  if (fromIndex === -1) {
@@ -2975,6 +3253,65 @@ var Dialect = class {
2975
3253
  formatPlaceholder(index) {
2976
3254
  return "?";
2977
3255
  }
3256
+ /**
3257
+ * Whether the current dialect supports a given set operation.
3258
+ * Override in concrete dialects to restrict support.
3259
+ */
3260
+ supportsSetOperation(kind) {
3261
+ return true;
3262
+ }
3263
+ /**
3264
+ * Validates set-operation semantics:
3265
+ * - Ensures the dialect supports requested operators.
3266
+ * - Enforces that only the outermost compound query may have ORDER/LIMIT/OFFSET.
3267
+ * @param ast - Query to validate
3268
+ * @param isOutermost - Whether this node is the outermost compound query
3269
+ */
3270
+ validateSetOperations(ast, isOutermost = true) {
3271
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
3272
+ if (!isOutermost && (ast.orderBy || ast.limit !== void 0 || ast.offset !== void 0)) {
3273
+ throw new Error("ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.");
3274
+ }
3275
+ if (hasSetOps) {
3276
+ for (const op of ast.setOps) {
3277
+ if (!this.supportsSetOperation(op.operator)) {
3278
+ throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
3279
+ }
3280
+ this.validateSetOperations(op.query, false);
3281
+ }
3282
+ }
3283
+ }
3284
+ /**
3285
+ * Hoists CTEs from set-operation operands to the outermost query so WITH appears once.
3286
+ * @param ast - Query AST
3287
+ * @returns Normalized AST without inner CTEs and a list of hoisted CTEs
3288
+ */
3289
+ hoistCtes(ast) {
3290
+ let hoisted = [];
3291
+ const normalizedSetOps = ast.setOps?.map((op) => {
3292
+ const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
3293
+ const childCtes = child.ctes ?? [];
3294
+ if (childCtes.length) {
3295
+ hoisted = hoisted.concat(childCtes);
3296
+ }
3297
+ hoisted = hoisted.concat(childHoisted);
3298
+ const queryWithoutCtes = childCtes.length ? { ...child, ctes: void 0 } : child;
3299
+ return { ...op, query: queryWithoutCtes };
3300
+ });
3301
+ const normalized = normalizedSetOps ? { ...ast, setOps: normalizedSetOps } : ast;
3302
+ return { normalized, hoistedCtes: hoisted };
3303
+ }
3304
+ /**
3305
+ * Normalizes a SELECT AST before compilation (validation + CTE hoisting).
3306
+ * @param ast - Query AST
3307
+ * @returns Normalized query AST
3308
+ */
3309
+ normalizeSelectAst(ast) {
3310
+ this.validateSetOperations(ast, true);
3311
+ const { normalized, hoistedCtes } = this.hoistCtes(ast);
3312
+ const combinedCtes = [...normalized.ctes ?? [], ...hoistedCtes];
3313
+ return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
3314
+ }
2978
3315
  constructor() {
2979
3316
  this.expressionCompilers = /* @__PURE__ */ new Map();
2980
3317
  this.operandCompilers = /* @__PURE__ */ new Map();
@@ -3123,16 +3460,18 @@ var SqlDialectBase = class extends Dialect {
3123
3460
  * Compiles SELECT query AST to SQL using common rules.
3124
3461
  */
3125
3462
  compileSelectAst(ast, ctx) {
3463
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
3126
3464
  const ctes = this.compileCtes(ast, ctx);
3127
- const columns = this.compileSelectColumns(ast, ctx);
3128
- const from = this.compileFrom(ast.from);
3129
- const joins = this.compileJoins(ast, ctx);
3130
- const whereClause = this.compileWhere(ast.where, ctx);
3131
- const groupBy = this.compileGroupBy(ast);
3132
- const having = this.compileHaving(ast, ctx);
3465
+ const baseAst = hasSetOps ? { ...ast, setOps: void 0, orderBy: void 0, limit: void 0, offset: void 0 } : ast;
3466
+ const baseSelect = this.compileSelectCore(baseAst, ctx);
3467
+ if (!hasSetOps) {
3468
+ return `${ctes}${baseSelect}`;
3469
+ }
3470
+ const compound = ast.setOps.map((op) => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`).join(" ");
3133
3471
  const orderBy = this.compileOrderBy(ast);
3134
3472
  const pagination = this.compilePagination(ast, orderBy);
3135
- return `${ctes}SELECT ${this.compileDistinct(ast)}${columns} FROM ${from}${joins}${whereClause}${groupBy}${having}${orderBy}${pagination}`;
3473
+ const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
3474
+ return `${ctes}${combined}${orderBy}${pagination}`;
3136
3475
  }
3137
3476
  compileInsertAst(ast, ctx) {
3138
3477
  const table = this.compileTableName(ast.into);
@@ -3141,6 +3480,20 @@ var SqlDialectBase = class extends Dialect {
3141
3480
  const returning = this.compileReturning(ast.returning, ctx);
3142
3481
  return `INSERT INTO ${table} (${columnList}) VALUES ${values}${returning}`;
3143
3482
  }
3483
+ /**
3484
+ * Compiles a single SELECT (no set operations, no CTE prefix).
3485
+ */
3486
+ compileSelectCore(ast, ctx) {
3487
+ const columns = this.compileSelectColumns(ast, ctx);
3488
+ const from = this.compileFrom(ast.from);
3489
+ const joins = this.compileJoins(ast, ctx);
3490
+ const whereClause = this.compileWhere(ast.where, ctx);
3491
+ const groupBy = this.compileGroupBy(ast);
3492
+ const having = this.compileHaving(ast, ctx);
3493
+ const orderBy = this.compileOrderBy(ast);
3494
+ const pagination = this.compilePagination(ast, orderBy);
3495
+ return `SELECT ${this.compileDistinct(ast)}${columns} FROM ${from}${joins}${whereClause}${groupBy}${having}${orderBy}${pagination}`;
3496
+ }
3144
3497
  compileUpdateAst(ast, ctx) {
3145
3498
  const table = this.compileTableName(ast.table);
3146
3499
  const assignments = ast.set.map((assignment) => {
@@ -3166,6 +3519,13 @@ var SqlDialectBase = class extends Dialect {
3166
3519
  if (!returning || returning.length === 0) return "";
3167
3520
  throw new Error("RETURNING is not supported by this dialect.");
3168
3521
  }
3522
+ formatReturningColumns(returning) {
3523
+ return returning.map((column) => {
3524
+ const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : "";
3525
+ const aliasPart = column.alias ? ` AS ${this.quoteIdentifier(column.alias)}` : "";
3526
+ return `${tablePart}${this.quoteIdentifier(column.name)}${aliasPart}`;
3527
+ }).join(", ");
3528
+ }
3169
3529
  /**
3170
3530
  * DISTINCT clause. Override for DISTINCT ON support.
3171
3531
  */
@@ -3231,7 +3591,7 @@ var SqlDialectBase = class extends Dialect {
3231
3591
  const cteDefs = ast.ctes.map((cte) => {
3232
3592
  const name = this.quoteIdentifier(cte.name);
3233
3593
  const cols = cte.columns && cte.columns.length ? `(${cte.columns.map((c) => this.quoteIdentifier(c)).join(", ")})` : "";
3234
- const query = this.stripTrailingSemicolon(this.compileSelectAst(cte.query, ctx));
3594
+ const query = this.stripTrailingSemicolon(this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx));
3235
3595
  return `${name}${cols} AS (${query})`;
3236
3596
  }).join(", ");
3237
3597
  return `${prefix}${cteDefs} `;
@@ -3239,6 +3599,10 @@ var SqlDialectBase = class extends Dialect {
3239
3599
  stripTrailingSemicolon(sql) {
3240
3600
  return sql.trim().replace(/;$/, "");
3241
3601
  }
3602
+ wrapSetOperand(sql) {
3603
+ const trimmed = this.stripTrailingSemicolon(sql);
3604
+ return `(${trimmed})`;
3605
+ }
3242
3606
  };
3243
3607
 
3244
3608
  // src/core/dialect/mysql/index.ts
@@ -3308,6 +3672,43 @@ var SqlServerDialect = class extends Dialect {
3308
3672
  * @returns SQL Server SQL string
3309
3673
  */
3310
3674
  compileSelectAst(ast, ctx) {
3675
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
3676
+ const ctes = this.compileCtes(ast, ctx);
3677
+ const baseAst = hasSetOps ? { ...ast, setOps: void 0, orderBy: void 0, limit: void 0, offset: void 0 } : ast;
3678
+ const baseSelect = this.compileSelectCore(baseAst, ctx);
3679
+ if (!hasSetOps) {
3680
+ return `${ctes}${baseSelect}`;
3681
+ }
3682
+ const compound = ast.setOps.map((op) => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`).join(" ");
3683
+ const orderBy = this.compileOrderBy(ast);
3684
+ const pagination = this.compilePagination(ast, orderBy);
3685
+ const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
3686
+ const tail = pagination || orderBy;
3687
+ return `${ctes}${combined}${tail}`;
3688
+ }
3689
+ compileInsertAst(ast, ctx) {
3690
+ const table = this.quoteIdentifier(ast.into.name);
3691
+ const columnList = ast.columns.map((column) => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(", ");
3692
+ const values = ast.values.map((row) => `(${row.map((value) => this.compileOperand(value, ctx)).join(", ")})`).join(", ");
3693
+ return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
3694
+ }
3695
+ compileUpdateAst(ast, ctx) {
3696
+ const table = this.quoteIdentifier(ast.table.name);
3697
+ const assignments = ast.set.map((assignment) => {
3698
+ const col2 = assignment.column;
3699
+ const target = `${this.quoteIdentifier(col2.table)}.${this.quoteIdentifier(col2.name)}`;
3700
+ const value = this.compileOperand(assignment.value, ctx);
3701
+ return `${target} = ${value}`;
3702
+ }).join(", ");
3703
+ const whereClause = this.compileWhere(ast.where, ctx);
3704
+ return `UPDATE ${table} SET ${assignments}${whereClause};`;
3705
+ }
3706
+ compileDeleteAst(ast, ctx) {
3707
+ const table = this.quoteIdentifier(ast.from.name);
3708
+ const whereClause = this.compileWhere(ast.where, ctx);
3709
+ return `DELETE FROM ${table}${whereClause};`;
3710
+ }
3711
+ compileSelectCore(ast, ctx) {
3311
3712
  const columns = ast.columns.map((c) => {
3312
3713
  let expr = "";
3313
3714
  if (c.type === "Function") {
@@ -3335,46 +3736,42 @@ var SqlServerDialect = class extends Dialect {
3335
3736
  const whereClause = this.compileWhere(ast.where, ctx);
3336
3737
  const groupBy = ast.groupBy && ast.groupBy.length > 0 ? " GROUP BY " + ast.groupBy.map((c) => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(", ") : "";
3337
3738
  const having = ast.having ? ` HAVING ${this.compileExpression(ast.having, ctx)}` : "";
3338
- 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(", ") : "";
3339
- let pagination = "";
3340
- if (ast.limit || ast.offset) {
3341
- const off = ast.offset || 0;
3342
- const orderClause = orderBy || " ORDER BY (SELECT NULL)";
3343
- pagination = `${orderClause} OFFSET ${off} ROWS`;
3344
- if (ast.limit) {
3345
- pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
3346
- }
3347
- return `SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${pagination};`;
3739
+ const orderBy = this.compileOrderBy(ast);
3740
+ const pagination = this.compilePagination(ast, orderBy);
3741
+ if (pagination) {
3742
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${pagination}`;
3743
+ }
3744
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${orderBy}`;
3745
+ }
3746
+ compileOrderBy(ast) {
3747
+ if (!ast.orderBy || ast.orderBy.length === 0) return "";
3748
+ return " ORDER BY " + ast.orderBy.map((o) => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(", ");
3749
+ }
3750
+ compilePagination(ast, orderBy) {
3751
+ const hasLimit = ast.limit !== void 0;
3752
+ const hasOffset = ast.offset !== void 0;
3753
+ if (!hasLimit && !hasOffset) return "";
3754
+ const off = ast.offset ?? 0;
3755
+ const orderClause = orderBy || " ORDER BY (SELECT NULL)";
3756
+ let pagination = `${orderClause} OFFSET ${off} ROWS`;
3757
+ if (hasLimit) {
3758
+ pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
3348
3759
  }
3349
- const ctes = ast.ctes && ast.ctes.length > 0 ? "WITH " + ast.ctes.map((cte) => {
3760
+ return pagination;
3761
+ }
3762
+ compileCtes(ast, ctx) {
3763
+ if (!ast.ctes || ast.ctes.length === 0) return "";
3764
+ const defs = ast.ctes.map((cte) => {
3350
3765
  const name = this.quoteIdentifier(cte.name);
3351
3766
  const cols = cte.columns ? `(${cte.columns.map((c) => this.quoteIdentifier(c)).join(", ")})` : "";
3352
- const query = this.compileSelectAst(cte.query, ctx).trim().replace(/;$/, "");
3767
+ const query = this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx).trim().replace(/;$/, "");
3353
3768
  return `${name}${cols} AS (${query})`;
3354
- }).join(", ") + " " : "";
3355
- return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? " " + joins : ""}${whereClause}${groupBy}${having}${orderBy};`;
3356
- }
3357
- compileInsertAst(ast, ctx) {
3358
- const table = this.quoteIdentifier(ast.into.name);
3359
- const columnList = ast.columns.map((column) => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(", ");
3360
- const values = ast.values.map((row) => `(${row.map((value) => this.compileOperand(value, ctx)).join(", ")})`).join(", ");
3361
- return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
3362
- }
3363
- compileUpdateAst(ast, ctx) {
3364
- const table = this.quoteIdentifier(ast.table.name);
3365
- const assignments = ast.set.map((assignment) => {
3366
- const col2 = assignment.column;
3367
- const target = `${this.quoteIdentifier(col2.table)}.${this.quoteIdentifier(col2.name)}`;
3368
- const value = this.compileOperand(assignment.value, ctx);
3369
- return `${target} = ${value}`;
3370
3769
  }).join(", ");
3371
- const whereClause = this.compileWhere(ast.where, ctx);
3372
- return `UPDATE ${table} SET ${assignments}${whereClause};`;
3770
+ return `WITH ${defs} `;
3373
3771
  }
3374
- compileDeleteAst(ast, ctx) {
3375
- const table = this.quoteIdentifier(ast.from.name);
3376
- const whereClause = this.compileWhere(ast.where, ctx);
3377
- return `DELETE FROM ${table}${whereClause};`;
3772
+ wrapSetOperand(sql) {
3773
+ const trimmed = sql.trim().replace(/;$/, "");
3774
+ return `(${trimmed})`;
3378
3775
  }
3379
3776
  };
3380
3777
 
@@ -3405,12 +3802,12 @@ var SqliteDialect = class extends SqlDialectBase {
3405
3802
  }
3406
3803
  compileReturning(returning, ctx) {
3407
3804
  if (!returning || returning.length === 0) return "";
3408
- const columns = returning.map((column) => {
3409
- const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : "";
3410
- return `${tablePart}${this.quoteIdentifier(column.name)}`;
3411
- }).join(", ");
3805
+ const columns = this.formatReturningColumns(returning);
3412
3806
  return ` RETURNING ${columns}`;
3413
3807
  }
3808
+ supportsReturning() {
3809
+ return true;
3810
+ }
3414
3811
  };
3415
3812
 
3416
3813
  // src/core/dialect/postgres/index.ts
@@ -3440,12 +3837,12 @@ var PostgresDialect = class extends SqlDialectBase {
3440
3837
  }
3441
3838
  compileReturning(returning, ctx) {
3442
3839
  if (!returning || returning.length === 0) return "";
3443
- const columns = returning.map((column) => {
3444
- const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : "";
3445
- return `${tablePart}${this.quoteIdentifier(column.name)}`;
3446
- }).join(", ");
3840
+ const columns = this.formatReturningColumns(returning);
3447
3841
  return ` RETURNING ${columns}`;
3448
3842
  }
3843
+ supportsReturning() {
3844
+ return true;
3845
+ }
3449
3846
  };
3450
3847
 
3451
3848
  // src/core/ddl/dialects/base-schema-dialect.ts
@@ -5209,9 +5606,13 @@ var UnitOfWork = class {
5209
5606
  async flushInsert(tracked) {
5210
5607
  await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
5211
5608
  const payload = this.extractColumns(tracked.table, tracked.entity);
5212
- const builder = new InsertQueryBuilder(tracked.table).values(payload);
5609
+ let builder = new InsertQueryBuilder(tracked.table).values(payload);
5610
+ if (this.dialect.supportsReturning()) {
5611
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
5612
+ }
5213
5613
  const compiled = builder.compile(this.dialect);
5214
- await this.executeCompiled(compiled);
5614
+ const results = await this.executeCompiled(compiled);
5615
+ this.applyReturningResults(tracked, results);
5215
5616
  tracked.status = "managed" /* Managed */;
5216
5617
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
5217
5618
  tracked.pk = this.getPrimaryKeyValue(tracked);
@@ -5228,9 +5629,13 @@ var UnitOfWork = class {
5228
5629
  await this.runHook(tracked.table.hooks?.beforeUpdate, tracked);
5229
5630
  const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
5230
5631
  if (!pkColumn) return;
5231
- const builder = new UpdateQueryBuilder(tracked.table).set(changes).where(eq(pkColumn, tracked.pk));
5632
+ let builder = new UpdateQueryBuilder(tracked.table).set(changes).where(eq(pkColumn, tracked.pk));
5633
+ if (this.dialect.supportsReturning()) {
5634
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
5635
+ }
5232
5636
  const compiled = builder.compile(this.dialect);
5233
- await this.executeCompiled(compiled);
5637
+ const results = await this.executeCompiled(compiled);
5638
+ this.applyReturningResults(tracked, results);
5234
5639
  tracked.status = "managed" /* Managed */;
5235
5640
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
5236
5641
  this.registerIdentity(tracked);
@@ -5272,7 +5677,31 @@ var UnitOfWork = class {
5272
5677
  return payload;
5273
5678
  }
5274
5679
  async executeCompiled(compiled) {
5275
- await this.executor.executeSql(compiled.sql, compiled.params);
5680
+ return this.executor.executeSql(compiled.sql, compiled.params);
5681
+ }
5682
+ getReturningColumns(table) {
5683
+ return Object.values(table.columns).map((column) => ({
5684
+ type: "Column",
5685
+ table: table.name,
5686
+ name: column.name,
5687
+ alias: column.name
5688
+ }));
5689
+ }
5690
+ applyReturningResults(tracked, results) {
5691
+ if (!this.dialect.supportsReturning()) return;
5692
+ const first = results[0];
5693
+ if (!first || first.values.length === 0) return;
5694
+ const row = first.values[0];
5695
+ for (let i = 0; i < first.columns.length; i++) {
5696
+ const columnName = this.normalizeColumnName(first.columns[i]);
5697
+ if (!(columnName in tracked.table.columns)) continue;
5698
+ tracked.entity[columnName] = row[i];
5699
+ }
5700
+ }
5701
+ normalizeColumnName(column) {
5702
+ const parts = column.split(".");
5703
+ const candidate = parts[parts.length - 1];
5704
+ return candidate.replace(/^["`[\]]+|["`[\]]+$/g, "");
5276
5705
  }
5277
5706
  registerIdentity(tracked) {
5278
5707
  if (tracked.pk == null) return;
@@ -5293,22 +5722,46 @@ var UnitOfWork = class {
5293
5722
  }
5294
5723
  };
5295
5724
 
5725
+ // src/orm/query-logger.ts
5726
+ var createQueryLoggingExecutor = (executor, logger) => {
5727
+ if (!logger) {
5728
+ return executor;
5729
+ }
5730
+ const wrapped = {
5731
+ async executeSql(sql, params) {
5732
+ logger({ sql, params });
5733
+ return executor.executeSql(sql, params);
5734
+ }
5735
+ };
5736
+ if (executor.beginTransaction) {
5737
+ wrapped.beginTransaction = executor.beginTransaction.bind(executor);
5738
+ }
5739
+ if (executor.commitTransaction) {
5740
+ wrapped.commitTransaction = executor.commitTransaction.bind(executor);
5741
+ }
5742
+ if (executor.rollbackTransaction) {
5743
+ wrapped.rollbackTransaction = executor.rollbackTransaction.bind(executor);
5744
+ }
5745
+ return wrapped;
5746
+ };
5747
+
5296
5748
  // src/orm/orm-context.ts
5297
5749
  var OrmContext = class {
5298
5750
  constructor(options) {
5299
5751
  this.options = options;
5300
5752
  this.identityMap = new IdentityMap();
5301
5753
  this.interceptors = [...options.interceptors ?? []];
5754
+ this.executorWithLogging = createQueryLoggingExecutor(options.executor, options.queryLogger);
5302
5755
  this.unitOfWork = new UnitOfWork(
5303
5756
  options.dialect,
5304
- options.executor,
5757
+ this.executorWithLogging,
5305
5758
  this.identityMap,
5306
5759
  () => this
5307
5760
  );
5308
5761
  this.relationChanges = new RelationChangeProcessor(
5309
5762
  this.unitOfWork,
5310
5763
  options.dialect,
5311
- options.executor
5764
+ this.executorWithLogging
5312
5765
  );
5313
5766
  this.domainEvents = new DomainEventBus(options.domainEventHandlers);
5314
5767
  }
@@ -5316,7 +5769,7 @@ var OrmContext = class {
5316
5769
  return this.options.dialect;
5317
5770
  }
5318
5771
  get executor() {
5319
- return this.options.executor;
5772
+ return this.executorWithLogging;
5320
5773
  }
5321
5774
  get identityBuckets() {
5322
5775
  return this.unitOfWork.identityBuckets;