metal-orm 1.0.4 → 1.0.6

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 (57) hide show
  1. package/README.md +299 -113
  2. package/docs/CHANGES.md +104 -0
  3. package/docs/advanced-features.md +92 -1
  4. package/docs/api-reference.md +13 -4
  5. package/docs/dml-operations.md +156 -0
  6. package/docs/getting-started.md +122 -55
  7. package/docs/hydration.md +77 -3
  8. package/docs/index.md +19 -14
  9. package/docs/multi-dialect-support.md +25 -0
  10. package/docs/query-builder.md +60 -0
  11. package/docs/runtime.md +105 -0
  12. package/docs/schema-definition.md +52 -1
  13. package/package.json +1 -1
  14. package/src/ast/expression.ts +630 -592
  15. package/src/ast/query.ts +110 -49
  16. package/src/builder/delete-query-state.ts +42 -0
  17. package/src/builder/delete.ts +57 -0
  18. package/src/builder/hydration-manager.ts +3 -2
  19. package/src/builder/hydration-planner.ts +163 -107
  20. package/src/builder/insert-query-state.ts +62 -0
  21. package/src/builder/insert.ts +59 -0
  22. package/src/builder/operations/relation-manager.ts +1 -23
  23. package/src/builder/relation-conditions.ts +45 -1
  24. package/src/builder/relation-service.ts +81 -18
  25. package/src/builder/relation-types.ts +15 -0
  26. package/src/builder/relation-utils.ts +12 -0
  27. package/src/builder/select.ts +427 -394
  28. package/src/builder/update-query-state.ts +59 -0
  29. package/src/builder/update.ts +61 -0
  30. package/src/constants/sql-operator-config.ts +3 -0
  31. package/src/constants/sql.ts +38 -32
  32. package/src/dialect/abstract.ts +107 -47
  33. package/src/dialect/mssql/index.ts +31 -6
  34. package/src/dialect/mysql/index.ts +31 -6
  35. package/src/dialect/postgres/index.ts +45 -6
  36. package/src/dialect/sqlite/index.ts +45 -6
  37. package/src/index.ts +22 -11
  38. package/src/playground/features/playground/data/scenarios/hydration.ts +23 -11
  39. package/src/playground/features/playground/data/scenarios/types.ts +18 -15
  40. package/src/playground/features/playground/data/schema.ts +6 -2
  41. package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
  42. package/src/runtime/entity-meta.ts +52 -0
  43. package/src/runtime/entity.ts +252 -0
  44. package/src/runtime/execute.ts +36 -0
  45. package/src/runtime/hydration.ts +100 -38
  46. package/src/runtime/lazy-batch.ts +205 -0
  47. package/src/runtime/orm-context.ts +539 -0
  48. package/src/runtime/relations/belongs-to.ts +92 -0
  49. package/src/runtime/relations/has-many.ts +111 -0
  50. package/src/runtime/relations/many-to-many.ts +149 -0
  51. package/src/schema/column.ts +15 -1
  52. package/src/schema/relation.ts +105 -40
  53. package/src/schema/table.ts +34 -22
  54. package/src/schema/types.ts +76 -0
  55. package/tests/belongs-to-many.test.ts +57 -0
  56. package/tests/dml.test.ts +206 -0
  57. package/tests/orm-runtime.test.ts +254 -0
@@ -0,0 +1,59 @@
1
+ import { TableDef } from '../schema/table';
2
+ import { ColumnDef } from '../schema/column';
3
+ import { ColumnNode } from '../ast/expression';
4
+ import { CompiledQuery, InsertCompiler } from '../dialect/abstract';
5
+ import { InsertQueryNode } from '../ast/query';
6
+ import { InsertQueryState } from './insert-query-state';
7
+
8
+ const buildColumnNode = (table: TableDef, column: ColumnDef | ColumnNode): ColumnNode => {
9
+ if ((column as ColumnNode).type === 'Column') {
10
+ return column as ColumnNode;
11
+ }
12
+ const def = column as ColumnDef;
13
+ return {
14
+ type: 'Column',
15
+ table: def.table || table.name,
16
+ name: def.name
17
+ };
18
+ };
19
+
20
+ /**
21
+ * Builder for INSERT queries
22
+ */
23
+ export class InsertQueryBuilder<T> {
24
+ private readonly table: TableDef;
25
+ private readonly state: InsertQueryState;
26
+
27
+ constructor(table: TableDef, state?: InsertQueryState) {
28
+ this.table = table;
29
+ this.state = state ?? new InsertQueryState(table);
30
+ }
31
+
32
+ private clone(state: InsertQueryState): InsertQueryBuilder<T> {
33
+ return new InsertQueryBuilder(this.table, state);
34
+ }
35
+
36
+ values(rowOrRows: Record<string, unknown> | Record<string, unknown>[]): InsertQueryBuilder<T> {
37
+ const rows = Array.isArray(rowOrRows) ? rowOrRows : [rowOrRows];
38
+ if (!rows.length) return this;
39
+ return this.clone(this.state.withValues(rows));
40
+ }
41
+
42
+ returning(...columns: (ColumnDef | ColumnNode)[]): InsertQueryBuilder<T> {
43
+ if (!columns.length) return this;
44
+ const nodes = columns.map(column => buildColumnNode(this.table, column));
45
+ return this.clone(this.state.withReturning(nodes));
46
+ }
47
+
48
+ compile(compiler: InsertCompiler): CompiledQuery {
49
+ return compiler.compileInsert(this.state.ast);
50
+ }
51
+
52
+ toSql(compiler: InsertCompiler): string {
53
+ return this.compile(compiler).sql;
54
+ }
55
+
56
+ getAST(): InsertQueryNode {
57
+ return this.state.ast;
58
+ }
59
+ }
@@ -2,29 +2,7 @@ import { ExpressionNode } from '../../ast/expression';
2
2
  import { SelectQueryNode } from '../../ast/query';
3
3
  import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
4
4
  import { JoinKind } from '../../constants/sql';
5
- import { RelationIncludeJoinKind } from '../relation-types';
6
-
7
- /**
8
- * Options for including relations in queries
9
- */
10
- export interface RelationIncludeOptions {
11
- /**
12
- * Columns to include from the related table
13
- */
14
- columns?: string[];
15
- /**
16
- * Alias prefix for the relation columns
17
- */
18
- aliasPrefix?: string;
19
- /**
20
- * Filter expression to apply to the relation
21
- */
22
- filter?: ExpressionNode;
23
- /**
24
- * Type of join to use for the relation
25
- */
26
- joinKind?: RelationIncludeJoinKind;
27
- }
5
+ import { RelationIncludeOptions } from '../relation-types';
28
6
 
29
7
  /**
30
8
  * Manages relation operations (joins, includes, etc.) for query building
@@ -1,7 +1,10 @@
1
1
  import { TableDef } from '../schema/table';
2
- import { RelationDef, RelationKinds } from '../schema/relation';
2
+ import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation';
3
3
  import { ExpressionNode, eq, and } from '../ast/expression';
4
4
  import { findPrimaryKey } from './hydration-planner';
5
+ import { JoinNode } from '../ast/join';
6
+ import { JoinKind } from '../constants/sql';
7
+ import { createJoinNode } from '../utils/join-node';
5
8
 
6
9
  /**
7
10
  * Utility function to handle unreachable code paths
@@ -36,11 +39,52 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef): Expressio
36
39
  { type: 'Column', table: relation.target.name, name: localKey },
37
40
  { type: 'Column', table: root.name, name: relation.foreignKey }
38
41
  );
42
+ case RelationKinds.BelongsToMany:
43
+ throw new Error('BelongsToMany relations do not support the standard join condition builder');
39
44
  default:
40
45
  return assertNever(relation);
41
46
  }
42
47
  };
43
48
 
49
+ /**
50
+ * Builds the join nodes required to include a BelongsToMany relation.
51
+ */
52
+ export const buildBelongsToManyJoins = (
53
+ root: TableDef,
54
+ relationName: string,
55
+ relation: BelongsToManyRelation,
56
+ joinKind: JoinKind,
57
+ extra?: ExpressionNode
58
+ ): JoinNode[] => {
59
+ const rootKey = relation.localKey || findPrimaryKey(root);
60
+ const targetKey = relation.targetKey || findPrimaryKey(relation.target);
61
+
62
+ const pivotCondition = eq(
63
+ { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
64
+ { type: 'Column', table: root.name, name: rootKey }
65
+ );
66
+
67
+ const pivotJoin = createJoinNode(joinKind, relation.pivotTable.name, pivotCondition);
68
+
69
+ let targetCondition: ExpressionNode = eq(
70
+ { type: 'Column', table: relation.target.name, name: targetKey },
71
+ { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToTarget }
72
+ );
73
+
74
+ if (extra) {
75
+ targetCondition = and(targetCondition, extra);
76
+ }
77
+
78
+ const targetJoin = createJoinNode(
79
+ joinKind,
80
+ relation.target.name,
81
+ targetCondition,
82
+ relationName
83
+ );
84
+
85
+ return [pivotJoin, targetJoin];
86
+ };
87
+
44
88
  /**
45
89
  * Builds a relation join condition with optional extra conditions
46
90
  * @param root - Root table definition
@@ -1,6 +1,6 @@
1
1
  import { TableDef } from '../schema/table';
2
2
  import { ColumnDef } from '../schema/column';
3
- import { RelationDef } from '../schema/relation';
3
+ import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation';
4
4
  import { SelectQueryNode } from '../ast/query';
5
5
  import {
6
6
  ColumnNode,
@@ -13,11 +13,16 @@ import { QueryAstService } from './query-ast-service';
13
13
  import { findPrimaryKey } from './hydration-planner';
14
14
  import { RelationProjectionHelper } from './relation-projection-helper';
15
15
  import type { RelationResult } from './relation-projection-helper';
16
- import { buildRelationJoinCondition, buildRelationCorrelation } from './relation-conditions';
16
+ import {
17
+ buildRelationJoinCondition,
18
+ buildRelationCorrelation,
19
+ buildBelongsToManyJoins
20
+ } from './relation-conditions';
17
21
  import { JoinKind, JOIN_KINDS } from '../constants/sql';
18
- import { RelationIncludeJoinKind } from './relation-types';
22
+ import { RelationIncludeOptions } from './relation-types';
19
23
  import { createJoinNode } from '../utils/join-node';
20
24
  import { makeRelationAlias } from '../utils/relation-alias';
25
+ import { buildDefaultPivotColumns } from './relation-utils';
21
26
 
22
27
  /**
23
28
  * Service for handling relation operations (joins, includes, etc.)
@@ -82,10 +87,7 @@ export class RelationService {
82
87
  * @param options - Options for relation inclusion
83
88
  * @returns Relation result with updated state and hydration
84
89
  */
85
- include(
86
- relationName: string,
87
- options?: { columns?: string[]; aliasPrefix?: string; filter?: ExpressionNode; joinKind?: RelationIncludeJoinKind }
88
- ): RelationResult {
90
+ include(relationName: string, options?: RelationIncludeOptions): RelationResult {
89
91
  let state = this.state;
90
92
  let hydration = this.hydration;
91
93
 
@@ -106,16 +108,66 @@ export class RelationService {
106
108
  ? options.columns
107
109
  : Object.keys(relation.target.columns);
108
110
 
109
- const relationSelection = targetColumns.reduce((acc, key) => {
110
- const def = (relation.target.columns as any)[key];
111
- if (!def) {
112
- throw new Error(`Column '${key}' not found on relation '${relationName}'`);
113
- }
114
- acc[makeRelationAlias(aliasPrefix, key)] = def;
115
- return acc;
116
- }, {} as Record<string, ColumnDef>);
111
+ const buildTypedSelection = (
112
+ columns: Record<string, ColumnDef>,
113
+ prefix: string,
114
+ keys: string[],
115
+ missingMsg: (col: string) => string
116
+ ) : Record<string, ColumnDef> => {
117
+ return keys.reduce((acc, key) => {
118
+ const def = columns[key];
119
+ if (!def) {
120
+ throw new Error(missingMsg(key));
121
+ }
122
+ acc[makeRelationAlias(prefix, key)] = def;
123
+ return acc;
124
+ }, {} as Record<string, ColumnDef>);
125
+ };
126
+
127
+ const targetSelection = buildTypedSelection(
128
+ relation.target.columns as Record<string, ColumnDef>,
129
+ aliasPrefix,
130
+ targetColumns,
131
+ key => `Column '${key}' not found on relation '${relationName}'`
132
+ );
133
+
134
+ if (relation.type !== RelationKinds.BelongsToMany) {
135
+ const relationSelectionResult = this.selectColumns(state, hydration, targetSelection);
136
+ state = relationSelectionResult.state;
137
+ hydration = relationSelectionResult.hydration;
117
138
 
118
- const relationSelectionResult = this.selectColumns(state, hydration, relationSelection);
139
+ hydration = hydration.onRelationIncluded(
140
+ state,
141
+ relation,
142
+ relationName,
143
+ aliasPrefix,
144
+ targetColumns
145
+ );
146
+
147
+ return { state, hydration };
148
+ }
149
+
150
+ const many = relation as BelongsToManyRelation;
151
+ const pivotAliasPrefix = options?.pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
152
+ const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
153
+ const pivotColumns =
154
+ options?.pivot?.columns ??
155
+ many.defaultPivotColumns ??
156
+ buildDefaultPivotColumns(many, pivotPk);
157
+
158
+ const pivotSelection = buildTypedSelection(
159
+ many.pivotTable.columns as Record<string, ColumnDef>,
160
+ pivotAliasPrefix,
161
+ pivotColumns,
162
+ key => `Column '${key}' not found on pivot table '${many.pivotTable.name}'`
163
+ );
164
+
165
+ const combinedSelection = {
166
+ ...targetSelection,
167
+ ...pivotSelection
168
+ };
169
+
170
+ const relationSelectionResult = this.selectColumns(state, hydration, combinedSelection);
119
171
  state = relationSelectionResult.state;
120
172
  hydration = relationSelectionResult.hydration;
121
173
 
@@ -124,7 +176,8 @@ export class RelationService {
124
176
  relation,
125
177
  relationName,
126
178
  aliasPrefix,
127
- targetColumns
179
+ targetColumns,
180
+ { aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
128
181
  );
129
182
 
130
183
  return { state, hydration };
@@ -167,8 +220,18 @@ export class RelationService {
167
220
  extraCondition?: ExpressionNode
168
221
  ): SelectQueryState {
169
222
  const relation = this.getRelation(relationName);
170
- const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
223
+ if (relation.type === RelationKinds.BelongsToMany) {
224
+ const joins = buildBelongsToManyJoins(
225
+ this.table,
226
+ relationName,
227
+ relation as BelongsToManyRelation,
228
+ joinKind,
229
+ extraCondition
230
+ );
231
+ return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
232
+ }
171
233
 
234
+ const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
172
235
  const joinNode = createJoinNode(joinKind, relation.target.name, condition, relationName);
173
236
 
174
237
  return this.astService(state).withJoin(joinNode);
@@ -1,6 +1,21 @@
1
+ import { ExpressionNode } from '../ast/expression';
1
2
  import { JOIN_KINDS } from '../constants/sql';
2
3
 
3
4
  /**
4
5
  * Join kinds allowed when including a relation using `.include(...)`.
5
6
  */
6
7
  export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;
8
+
9
+ /**
10
+ * Options for including a relation in a query
11
+ */
12
+ export interface RelationIncludeOptions {
13
+ columns?: string[];
14
+ aliasPrefix?: string;
15
+ filter?: ExpressionNode;
16
+ joinKind?: RelationIncludeJoinKind;
17
+ pivot?: {
18
+ columns?: string[];
19
+ aliasPrefix?: string;
20
+ };
21
+ }
@@ -0,0 +1,12 @@
1
+ import { BelongsToManyRelation } from '../schema/relation';
2
+
3
+ /**
4
+ * Builds a default set of pivot columns, excluding keys used for joins.
5
+ */
6
+ export const buildDefaultPivotColumns = (
7
+ rel: BelongsToManyRelation,
8
+ pivotPk: string
9
+ ): string[] => {
10
+ const excluded = new Set([pivotPk, rel.pivotForeignKeyToRoot, rel.pivotForeignKeyToTarget]);
11
+ return Object.keys(rel.pivotTable.columns).filter(col => !excluded.has(col));
12
+ };