metal-orm 1.0.11 → 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.
Files changed (54) hide show
  1. package/README.md +21 -18
  2. package/dist/decorators/index.cjs +317 -34
  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 +317 -34
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +1965 -267
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +273 -23
  11. package/dist/index.d.ts +273 -23
  12. package/dist/index.js +1947 -267
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-654m4qy8.d.cts → select-CCp1oz9p.d.cts} +254 -4
  15. package/dist/{select-654m4qy8.d.ts → select-CCp1oz9p.d.ts} +254 -4
  16. package/package.json +3 -2
  17. package/src/core/ast/query.ts +40 -22
  18. package/src/core/ddl/dialects/base-schema-dialect.ts +48 -0
  19. package/src/core/ddl/dialects/index.ts +5 -0
  20. package/src/core/ddl/dialects/mssql-schema-dialect.ts +97 -0
  21. package/src/core/ddl/dialects/mysql-schema-dialect.ts +109 -0
  22. package/src/core/ddl/dialects/postgres-schema-dialect.ts +99 -0
  23. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +103 -0
  24. package/src/core/ddl/introspect/mssql.ts +149 -0
  25. package/src/core/ddl/introspect/mysql.ts +99 -0
  26. package/src/core/ddl/introspect/postgres.ts +154 -0
  27. package/src/core/ddl/introspect/sqlite.ts +66 -0
  28. package/src/core/ddl/introspect/types.ts +19 -0
  29. package/src/core/ddl/introspect/utils.ts +27 -0
  30. package/src/core/ddl/schema-diff.ts +179 -0
  31. package/src/core/ddl/schema-generator.ts +229 -0
  32. package/src/core/ddl/schema-introspect.ts +32 -0
  33. package/src/core/ddl/schema-types.ts +39 -0
  34. package/src/core/dialect/abstract.ts +122 -37
  35. package/src/core/dialect/base/sql-dialect.ts +204 -0
  36. package/src/core/dialect/mssql/index.ts +125 -80
  37. package/src/core/dialect/mysql/index.ts +18 -112
  38. package/src/core/dialect/postgres/index.ts +29 -126
  39. package/src/core/dialect/sqlite/index.ts +28 -129
  40. package/src/index.ts +4 -0
  41. package/src/orm/execute.ts +25 -16
  42. package/src/orm/orm-context.ts +60 -55
  43. package/src/orm/query-logger.ts +38 -0
  44. package/src/orm/relations/belongs-to.ts +42 -26
  45. package/src/orm/relations/has-many.ts +41 -25
  46. package/src/orm/relations/many-to-many.ts +43 -27
  47. package/src/orm/unit-of-work.ts +60 -23
  48. package/src/query-builder/hydration-manager.ts +229 -25
  49. package/src/query-builder/query-ast-service.ts +27 -12
  50. package/src/query-builder/select-query-state.ts +24 -12
  51. package/src/query-builder/select.ts +58 -14
  52. package/src/schema/column.ts +206 -27
  53. package/src/schema/table.ts +89 -32
  54. package/src/schema/types.ts +8 -5
package/README.md CHANGED
@@ -49,9 +49,10 @@ Full docs live in the `docs/` folder:
49
49
  - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
50
50
  - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
51
51
  - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
52
- - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
53
- - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
54
- - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
52
+ - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
53
+ - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
54
+ - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
55
+ - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
55
56
 
56
57
  ---
57
58
 
@@ -61,15 +62,16 @@ Full docs live in the `docs/` folder:
61
62
  ### Level 1 – Query builder & hydration
62
63
 
63
64
  - **Declarative schema definition** with `defineTable`, `col.*`, and typed relations.
64
- - **Fluent query builder** over a real SQL AST
65
- (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
66
- - **Advanced SQL**: CTEs, aggregates, window functions, subqueries, JSON, CASE, EXISTS.
67
- - **Expression builders**: `eq`, `and`, `or`, `between`, `inList`, `exists`, `jsonPath`, `caseWhen`, window functions like `rowNumber`, `rank`, `lag`, `lead`, etc., all backed by typed AST nodes.
68
- - **Relation-aware hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.) using a hydration plan derived from the AST metadata.
69
- - **Multi-dialect**: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
70
- - **DML**: type-safe INSERT / UPDATE / DELETE with `RETURNING` where supported.
71
-
72
- Level 1 is ideal when you:
65
+ - **Fluent query builder** over a real SQL AST
66
+ (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
67
+ - **Advanced SQL**: CTEs, aggregates, window functions, subqueries, JSON, CASE, EXISTS.
68
+ - **Set operations**: `union`, `unionAll`, `intersect`, `except` across all dialects (ORDER/LIMIT apply to the combined result; hydration is disabled for compound queries so rows are returned as-is without collapsing duplicates).
69
+ - **Expression builders**: `eq`, `and`, `or`, `between`, `inList`, `exists`, `jsonPath`, `caseWhen`, window functions like `rowNumber`, `rank`, `lag`, `lead`, etc., all backed by typed AST nodes.
70
+ - **Relation-aware hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.) using a hydration plan derived from the AST metadata.
71
+ - **Multi-dialect**: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
72
+ - **DML**: type-safe INSERT / UPDATE / DELETE with `RETURNING` where supported.
73
+
74
+ Level 1 is ideal when you:
73
75
 
74
76
  - Already have a domain model and just want a serious SQL builder.
75
77
  - Want deterministic SQL (no magical query generation).
@@ -81,12 +83,13 @@ On top of the query builder, MetalORM ships a focused runtime:
81
83
 
82
84
  - **Entities inferred from your `TableDef`s** (no separate mapping file).
83
85
  - **Lazy, batched relations**: `user.posts.load()`, `user.roles.syncByIds([...])`, etc.
84
- - **Identity map**: the same row becomes the same entity instance within a context (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
85
- - **Unit of Work (`OrmContext`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/Unit_of_work).
86
- - **Graph persistence**: mutate a whole object graph and flush once with `ctx.saveChanges()`.
87
- - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
88
- - **Interceptors**: `beforeFlush` / `afterFlush` hooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.).
89
- - **Domain events**: `addDomainEvent` and a DomainEventBus integrated into `saveChanges()`, aligned with domain events from [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
86
+ - **Identity map**: the same row becomes the same entity instance within a context (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
87
+ - **Unit of Work (`OrmContext`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/Unit_of_work).
88
+ - **Graph persistence**: mutate a whole object graph and flush once with `ctx.saveChanges()`.
89
+ - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
90
+ - **Interceptors**: `beforeFlush` / `afterFlush` hooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.).
91
+ - **Domain events**: `addDomainEvent` and a DomainEventBus integrated into `saveChanges()`, aligned with domain events from [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
92
+ - **JSON-safe entities**: relation wrappers hide internal references and implement `toJSON`, so `JSON.stringify` of hydrated entities works without circular reference errors.
90
93
 
91
94
  Use this layer where:
92
95
 
@@ -32,12 +32,25 @@ __export(decorators_exports, {
32
32
  module.exports = __toCommonJS(decorators_exports);
33
33
 
34
34
  // src/schema/table.ts
35
- var defineTable = (name, columns, relations = {}, hooks) => {
35
+ var defineTable = (name, columns, relations = {}, hooks, options = {}) => {
36
36
  const colsWithNames = Object.entries(columns).reduce((acc, [key, def]) => {
37
37
  acc[key] = { ...def, name: key, table: name };
38
38
  return acc;
39
39
  }, {});
40
- return { name, columns: colsWithNames, relations, hooks };
40
+ return {
41
+ name,
42
+ schema: options.schema,
43
+ columns: colsWithNames,
44
+ relations,
45
+ hooks,
46
+ primaryKey: options.primaryKey,
47
+ indexes: options.indexes,
48
+ checks: options.checks,
49
+ comment: options.comment,
50
+ engine: options.engine,
51
+ charset: options.charset,
52
+ collation: options.collation
53
+ };
41
54
  };
42
55
 
43
56
  // src/orm/entity-metadata.ts
@@ -487,6 +500,44 @@ var SelectQueryState = class _SelectQueryState {
487
500
  ctes: [...this.ast.ctes ?? [], cte]
488
501
  });
489
502
  }
503
+ /**
504
+ * Adds a set operation (UNION/INTERSECT/EXCEPT) to the query
505
+ * @param op - Set operation node to add
506
+ * @returns New SelectQueryState with set operation
507
+ */
508
+ withSetOperation(op) {
509
+ return this.clone({
510
+ ...this.ast,
511
+ setOps: [...this.ast.setOps ?? [], op]
512
+ });
513
+ }
514
+ };
515
+
516
+ // src/core/ast/join-node.ts
517
+ var createJoinNode = (kind, tableName, condition, relationName) => ({
518
+ type: "Join",
519
+ kind,
520
+ table: { type: "Table", name: tableName },
521
+ condition,
522
+ relationName
523
+ });
524
+
525
+ // src/core/sql/sql.ts
526
+ var JOIN_KINDS = {
527
+ /** INNER JOIN type */
528
+ INNER: "INNER",
529
+ /** LEFT JOIN type */
530
+ LEFT: "LEFT",
531
+ /** RIGHT JOIN type */
532
+ RIGHT: "RIGHT",
533
+ /** CROSS JOIN type */
534
+ CROSS: "CROSS"
535
+ };
536
+ var ORDER_DIRECTIONS = {
537
+ /** Ascending order */
538
+ ASC: "ASC",
539
+ /** Descending order */
540
+ DESC: "DESC"
490
541
  };
491
542
 
492
543
  // src/query-builder/hydration-manager.ts
@@ -538,8 +589,26 @@ var HydrationManager = class _HydrationManager {
538
589
  * @returns AST with hydration metadata
539
590
  */
540
591
  applyToAst(ast) {
592
+ if (ast.setOps && ast.setOps.length > 0) {
593
+ return ast;
594
+ }
541
595
  const plan = this.planner.getPlan();
542
596
  if (!plan) return ast;
597
+ const needsPaginationGuard = this.requiresParentPagination(ast, plan);
598
+ const rewritten = needsPaginationGuard ? this.wrapForParentPagination(ast, plan) : ast;
599
+ return this.attachHydrationMeta(rewritten, plan);
600
+ }
601
+ /**
602
+ * Gets the current hydration plan
603
+ * @returns Hydration plan or undefined if none exists
604
+ */
605
+ getPlan() {
606
+ return this.planner.getPlan();
607
+ }
608
+ /**
609
+ * Attaches hydration metadata to a query AST node.
610
+ */
611
+ attachHydrationMeta(ast, plan) {
543
612
  return {
544
613
  ...ast,
545
614
  meta: {
@@ -549,11 +618,154 @@ var HydrationManager = class _HydrationManager {
549
618
  };
550
619
  }
551
620
  /**
552
- * Gets the current hydration plan
553
- * @returns Hydration plan or undefined if none exists
621
+ * Determines whether the query needs pagination rewriting to keep LIMIT/OFFSET
622
+ * applied to parent rows when eager-loading multiplicative relations.
554
623
  */
555
- getPlan() {
556
- return this.planner.getPlan();
624
+ requiresParentPagination(ast, plan) {
625
+ const hasPagination = ast.limit !== void 0 || ast.offset !== void 0;
626
+ return hasPagination && this.hasMultiplyingRelations(plan);
627
+ }
628
+ hasMultiplyingRelations(plan) {
629
+ return plan.relations.some(
630
+ (rel) => rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany
631
+ );
632
+ }
633
+ /**
634
+ * Rewrites the query using CTEs so LIMIT/OFFSET target distinct parent rows
635
+ * instead of the joined result set.
636
+ *
637
+ * The strategy:
638
+ * - Hoist the original query (minus limit/offset) into a base CTE.
639
+ * - Select distinct parent ids from that base CTE with the original ordering and pagination.
640
+ * - Join the base CTE against the paged ids to retrieve the joined rows for just that page.
641
+ */
642
+ wrapForParentPagination(ast, plan) {
643
+ const projectionNames = this.getProjectionNames(ast.columns);
644
+ if (!projectionNames) {
645
+ return ast;
646
+ }
647
+ const projectionAliases = this.buildProjectionAliasMap(ast.columns);
648
+ const projectionSet = new Set(projectionNames);
649
+ const rootPkAlias = projectionAliases.get(`${plan.rootTable}.${plan.rootPrimaryKey}`) ?? plan.rootPrimaryKey;
650
+ const baseCteName = this.nextCteName(ast.ctes, "__metal_pagination_base");
651
+ const baseQuery = {
652
+ ...ast,
653
+ ctes: void 0,
654
+ limit: void 0,
655
+ offset: void 0,
656
+ orderBy: void 0,
657
+ meta: void 0
658
+ };
659
+ const baseCte = {
660
+ type: "CommonTableExpression",
661
+ name: baseCteName,
662
+ query: baseQuery,
663
+ recursive: false
664
+ };
665
+ const orderBy = this.mapOrderBy(ast.orderBy, plan, projectionAliases, baseCteName, projectionSet);
666
+ if (orderBy === null) {
667
+ return ast;
668
+ }
669
+ const pageCteName = this.nextCteName([...ast.ctes ?? [], baseCte], "__metal_pagination_page");
670
+ const pagingColumns = this.buildPagingColumns(rootPkAlias, orderBy, baseCteName);
671
+ const pageCte = {
672
+ type: "CommonTableExpression",
673
+ name: pageCteName,
674
+ query: {
675
+ type: "SelectQuery",
676
+ from: { type: "Table", name: baseCteName },
677
+ columns: pagingColumns,
678
+ joins: [],
679
+ distinct: [{ type: "Column", table: baseCteName, name: rootPkAlias }],
680
+ orderBy,
681
+ limit: ast.limit,
682
+ offset: ast.offset
683
+ },
684
+ recursive: false
685
+ };
686
+ const joinCondition = eq(
687
+ { type: "Column", table: baseCteName, name: rootPkAlias },
688
+ { type: "Column", table: pageCteName, name: rootPkAlias }
689
+ );
690
+ const outerColumns = projectionNames.map((name) => ({
691
+ type: "Column",
692
+ table: baseCteName,
693
+ name,
694
+ alias: name
695
+ }));
696
+ return {
697
+ type: "SelectQuery",
698
+ from: { type: "Table", name: baseCteName },
699
+ columns: outerColumns,
700
+ joins: [createJoinNode(JOIN_KINDS.INNER, pageCteName, joinCondition)],
701
+ orderBy,
702
+ ctes: [...ast.ctes ?? [], baseCte, pageCte]
703
+ };
704
+ }
705
+ nextCteName(existing, baseName) {
706
+ const names = new Set((existing ?? []).map((cte) => cte.name));
707
+ let candidate = baseName;
708
+ let suffix = 1;
709
+ while (names.has(candidate)) {
710
+ suffix += 1;
711
+ candidate = `${baseName}_${suffix}`;
712
+ }
713
+ return candidate;
714
+ }
715
+ getProjectionNames(columns) {
716
+ const names = [];
717
+ for (const col of columns) {
718
+ const alias = col.alias ?? col.name;
719
+ if (!alias) return void 0;
720
+ names.push(alias);
721
+ }
722
+ return names;
723
+ }
724
+ buildProjectionAliasMap(columns) {
725
+ const map = /* @__PURE__ */ new Map();
726
+ for (const col of columns) {
727
+ if (col.type !== "Column") continue;
728
+ const node = col;
729
+ const key = `${node.table}.${node.name}`;
730
+ map.set(key, node.alias ?? node.name);
731
+ }
732
+ return map;
733
+ }
734
+ mapOrderBy(orderBy, plan, projectionAliases, baseAlias, availableColumns) {
735
+ if (!orderBy || orderBy.length === 0) {
736
+ return void 0;
737
+ }
738
+ const mapped = [];
739
+ for (const ob of orderBy) {
740
+ if (ob.column.table !== plan.rootTable) {
741
+ return null;
742
+ }
743
+ const alias = projectionAliases.get(`${ob.column.table}.${ob.column.name}`) ?? ob.column.name;
744
+ if (!availableColumns.has(alias)) {
745
+ return null;
746
+ }
747
+ mapped.push({
748
+ type: "OrderBy",
749
+ column: { type: "Column", table: baseAlias, name: alias },
750
+ direction: ob.direction
751
+ });
752
+ }
753
+ return mapped;
754
+ }
755
+ buildPagingColumns(primaryKey, orderBy, tableAlias) {
756
+ const columns = [{ type: "Column", table: tableAlias, name: primaryKey, alias: primaryKey }];
757
+ if (!orderBy) return columns;
758
+ for (const ob of orderBy) {
759
+ if (!columns.some((col) => col.name === ob.column.name)) {
760
+ columns.push({
761
+ type: "Column",
762
+ table: tableAlias,
763
+ name: ob.column.name,
764
+ alias: ob.column.name
765
+ });
766
+ }
767
+ }
768
+ return columns;
557
769
  }
558
770
  };
559
771
 
@@ -807,6 +1019,20 @@ var QueryAstService = class {
807
1019
  };
808
1020
  return this.state.withCte(cte);
809
1021
  }
1022
+ /**
1023
+ * Adds a set operation (UNION/UNION ALL/INTERSECT/EXCEPT) to the query
1024
+ * @param operator - Set operator
1025
+ * @param query - Right-hand side query
1026
+ * @returns Updated query state with set operation
1027
+ */
1028
+ withSetOperation(operator, query) {
1029
+ const op = {
1030
+ type: "SetOperation",
1031
+ operator,
1032
+ query
1033
+ };
1034
+ return this.state.withSetOperation(op);
1035
+ }
810
1036
  /**
811
1037
  * Selects a subquery as a column
812
1038
  * @param alias - Alias for the subquery
@@ -959,15 +1185,6 @@ var RelationProjectionHelper = class {
959
1185
  }
960
1186
  };
961
1187
 
962
- // src/core/ast/join-node.ts
963
- var createJoinNode = (kind, tableName, condition, relationName) => ({
964
- type: "Join",
965
- kind,
966
- table: { type: "Table", name: tableName },
967
- condition,
968
- relationName
969
- });
970
-
971
1188
  // src/query-builder/relation-conditions.ts
972
1189
  var assertNever = (value) => {
973
1190
  throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
@@ -1023,24 +1240,6 @@ var buildRelationCorrelation = (root, relation) => {
1023
1240
  return baseRelationCondition(root, relation);
1024
1241
  };
1025
1242
 
1026
- // src/core/sql/sql.ts
1027
- var JOIN_KINDS = {
1028
- /** INNER JOIN type */
1029
- INNER: "INNER",
1030
- /** LEFT JOIN type */
1031
- LEFT: "LEFT",
1032
- /** RIGHT JOIN type */
1033
- RIGHT: "RIGHT",
1034
- /** CROSS JOIN type */
1035
- CROSS: "CROSS"
1036
- };
1037
- var ORDER_DIRECTIONS = {
1038
- /** Ascending order */
1039
- ASC: "ASC",
1040
- /** Descending order */
1041
- DESC: "DESC"
1042
- };
1043
-
1044
1243
  // src/query-builder/relation-service.ts
1045
1244
  var RelationService = class {
1046
1245
  /**
@@ -1486,6 +1685,16 @@ var hasEntityMeta = (entity) => {
1486
1685
 
1487
1686
  // src/orm/relations/has-many.ts
1488
1687
  var toKey2 = (value) => value === null || value === void 0 ? "" : String(value);
1688
+ var hideInternal = (obj, keys) => {
1689
+ for (const key of keys) {
1690
+ Object.defineProperty(obj, key, {
1691
+ value: obj[key],
1692
+ writable: false,
1693
+ configurable: false,
1694
+ enumerable: false
1695
+ });
1696
+ }
1697
+ };
1489
1698
  var DefaultHasManyCollection = class {
1490
1699
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1491
1700
  this.ctx = ctx;
@@ -1501,6 +1710,7 @@ var DefaultHasManyCollection = class {
1501
1710
  this.items = [];
1502
1711
  this.added = /* @__PURE__ */ new Set();
1503
1712
  this.removed = /* @__PURE__ */ new Set();
1713
+ hideInternal(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1504
1714
  this.hydrateFromCache();
1505
1715
  }
1506
1716
  async load() {
@@ -1576,10 +1786,23 @@ var DefaultHasManyCollection = class {
1576
1786
  this.items = rows.map((row) => this.createEntity(row));
1577
1787
  this.loaded = true;
1578
1788
  }
1789
+ toJSON() {
1790
+ return this.items;
1791
+ }
1579
1792
  };
1580
1793
 
1581
1794
  // src/orm/relations/belongs-to.ts
1582
1795
  var toKey3 = (value) => value === null || value === void 0 ? "" : String(value);
1796
+ var hideInternal2 = (obj, keys) => {
1797
+ for (const key of keys) {
1798
+ Object.defineProperty(obj, key, {
1799
+ value: obj[key],
1800
+ writable: false,
1801
+ configurable: false,
1802
+ enumerable: false
1803
+ });
1804
+ }
1805
+ };
1583
1806
  var DefaultBelongsToReference = class {
1584
1807
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, targetKey) {
1585
1808
  this.ctx = ctx;
@@ -1593,6 +1816,7 @@ var DefaultBelongsToReference = class {
1593
1816
  this.targetKey = targetKey;
1594
1817
  this.loaded = false;
1595
1818
  this.current = null;
1819
+ hideInternal2(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "targetKey"]);
1596
1820
  this.populateFromHydrationCache();
1597
1821
  }
1598
1822
  async load() {
@@ -1653,10 +1877,23 @@ var DefaultBelongsToReference = class {
1653
1877
  this.current = this.createEntity(row);
1654
1878
  this.loaded = true;
1655
1879
  }
1880
+ toJSON() {
1881
+ return this.current;
1882
+ }
1656
1883
  };
1657
1884
 
1658
1885
  // src/orm/relations/many-to-many.ts
1659
1886
  var toKey4 = (value) => value === null || value === void 0 ? "" : String(value);
1887
+ var hideInternal3 = (obj, keys) => {
1888
+ for (const key of keys) {
1889
+ Object.defineProperty(obj, key, {
1890
+ value: obj[key],
1891
+ writable: false,
1892
+ configurable: false,
1893
+ enumerable: false
1894
+ });
1895
+ }
1896
+ };
1660
1897
  var DefaultManyToManyCollection = class {
1661
1898
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1662
1899
  this.ctx = ctx;
@@ -1670,6 +1907,7 @@ var DefaultManyToManyCollection = class {
1670
1907
  this.localKey = localKey;
1671
1908
  this.loaded = false;
1672
1909
  this.items = [];
1910
+ hideInternal3(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1673
1911
  this.hydrateFromCache();
1674
1912
  }
1675
1913
  async load() {
@@ -1775,6 +2013,9 @@ var DefaultManyToManyCollection = class {
1775
2013
  });
1776
2014
  this.loaded = true;
1777
2015
  }
2016
+ toJSON() {
2017
+ return this.items;
2018
+ }
1778
2019
  };
1779
2020
 
1780
2021
  // src/orm/lazy-batch.ts
@@ -2135,9 +2376,15 @@ var flattenResults = (results) => {
2135
2376
  return rows;
2136
2377
  };
2137
2378
  async function executeHydrated(ctx, qb) {
2138
- const compiled = ctx.dialect.compileSelect(qb.getAST());
2379
+ const ast = qb.getAST();
2380
+ const compiled = ctx.dialect.compileSelect(ast);
2139
2381
  const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
2140
2382
  const rows = flattenResults(executed);
2383
+ if (ast.setOps && ast.setOps.length > 0) {
2384
+ return rows.map(
2385
+ (row) => createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
2386
+ );
2387
+ }
2141
2388
  const hydrated = hydrateRows(rows, qb.getHydrationPlan());
2142
2389
  return hydrated.map(
2143
2390
  (row) => createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
@@ -2184,6 +2431,10 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2184
2431
  const joinNode = createJoinNode(kind, table.name, condition);
2185
2432
  return this.applyAst(context, (service) => service.withJoin(joinNode));
2186
2433
  }
2434
+ applySetOperation(operator, query) {
2435
+ const subAst = this.resolveQueryNode(query);
2436
+ return this.applyAst(this.context, (service) => service.withSetOperation(operator, subAst));
2437
+ }
2187
2438
  /**
2188
2439
  * Selects specific columns for the query
2189
2440
  * @param columns - Record of column definitions, function nodes, case expressions, or window functions
@@ -2372,6 +2623,38 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2372
2623
  const nextContext = this.applyAst(this.context, (service) => service.withOffset(n));
2373
2624
  return this.clone(nextContext);
2374
2625
  }
2626
+ /**
2627
+ * Combines this query with another using UNION
2628
+ * @param query - Query to union with
2629
+ * @returns New query builder instance with the set operation
2630
+ */
2631
+ union(query) {
2632
+ return this.clone(this.applySetOperation("UNION", query));
2633
+ }
2634
+ /**
2635
+ * Combines this query with another using UNION ALL
2636
+ * @param query - Query to union with
2637
+ * @returns New query builder instance with the set operation
2638
+ */
2639
+ unionAll(query) {
2640
+ return this.clone(this.applySetOperation("UNION ALL", query));
2641
+ }
2642
+ /**
2643
+ * Combines this query with another using INTERSECT
2644
+ * @param query - Query to intersect with
2645
+ * @returns New query builder instance with the set operation
2646
+ */
2647
+ intersect(query) {
2648
+ return this.clone(this.applySetOperation("INTERSECT", query));
2649
+ }
2650
+ /**
2651
+ * Combines this query with another using EXCEPT
2652
+ * @param query - Query to subtract
2653
+ * @returns New query builder instance with the set operation
2654
+ */
2655
+ except(query) {
2656
+ return this.clone(this.applySetOperation("EXCEPT", query));
2657
+ }
2375
2658
  /**
2376
2659
  * Adds a WHERE EXISTS condition to the query
2377
2660
  * @param subquery - Subquery to check for existence