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
package/src/ast/query.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { ColumnNode, FunctionNode, ExpressionNode, ScalarSubqueryNode, CaseExpressionNode, WindowFunctionNode } from './expression';
1
+ import {
2
+ ColumnNode,
3
+ FunctionNode,
4
+ ExpressionNode,
5
+ ScalarSubqueryNode,
6
+ CaseExpressionNode,
7
+ WindowFunctionNode,
8
+ OperandNode
9
+ } from './expression';
2
10
  import { JoinNode } from './join';
3
11
  import { RelationType } from '../schema/relation';
4
12
  import { OrderDirection } from '../constants/sql';
@@ -27,27 +35,39 @@ export interface OrderByNode {
27
35
  direction: OrderDirection;
28
36
  }
29
37
 
30
- /**
31
- * Plan for hydrating relationship data
32
- */
33
- export interface HydrationRelationPlan {
34
- /** Name of the relationship */
35
- name: string;
36
- /** Alias prefix for the relationship */
37
- aliasPrefix: string;
38
- /** Type of relationship */
39
- type: RelationType;
40
- /** Target table name */
41
- targetTable: string;
42
- /** Target table primary key */
43
- targetPrimaryKey: string;
44
- /** Foreign key column */
45
- foreignKey: string;
46
- /** Local key column */
47
- localKey: string;
48
- /** Columns to include */
49
- columns: string[];
50
- }
38
+ /**
39
+ * Plan describing pivot columns needed for hydration
40
+ */
41
+ export interface HydrationPivotPlan {
42
+ table: string;
43
+ primaryKey: string;
44
+ aliasPrefix: string;
45
+ columns: string[];
46
+ }
47
+
48
+ /**
49
+ * Plan for hydrating relationship data
50
+ */
51
+ export interface HydrationRelationPlan {
52
+ /** Name of the relationship */
53
+ name: string;
54
+ /** Alias prefix for the relationship */
55
+ aliasPrefix: string;
56
+ /** Type of relationship */
57
+ type: RelationType;
58
+ /** Target table name */
59
+ targetTable: string;
60
+ /** Target table primary key */
61
+ targetPrimaryKey: string;
62
+ /** Foreign key column */
63
+ foreignKey: string;
64
+ /** Local key column */
65
+ localKey: string;
66
+ /** Columns to include */
67
+ columns: string[];
68
+ /** Optional pivot plan for many-to-many relationships */
69
+ pivot?: HydrationPivotPlan;
70
+ }
51
71
 
52
72
  /**
53
73
  * Complete hydration plan for a query
@@ -89,30 +109,71 @@ export interface CommonTableExpressionNode {
89
109
  /**
90
110
  * AST node representing a complete SELECT query
91
111
  */
92
- export interface SelectQueryNode {
93
- type: 'SelectQuery';
94
- /** Optional CTEs (WITH clauses) */
95
- ctes?: CommonTableExpressionNode[];
96
- /** FROM clause table */
97
- from: TableNode;
98
- /** SELECT clause columns */
99
- columns: (ColumnNode | FunctionNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode)[];
100
- /** JOIN clauses */
101
- joins: JoinNode[];
102
- /** Optional WHERE clause */
103
- where?: ExpressionNode;
104
- /** Optional GROUP BY clause */
105
- groupBy?: ColumnNode[];
106
- /** Optional HAVING clause */
107
- having?: ExpressionNode;
108
- /** Optional ORDER BY clause */
109
- orderBy?: OrderByNode[];
110
- /** Optional LIMIT clause */
111
- limit?: number;
112
- /** Optional OFFSET clause */
113
- offset?: number;
114
- /** Optional query metadata */
115
- meta?: QueryMetadata;
116
- /** Optional DISTINCT clause */
117
- distinct?: ColumnNode[];
118
- }
112
+ export interface SelectQueryNode {
113
+ type: 'SelectQuery';
114
+ /** Optional CTEs (WITH clauses) */
115
+ ctes?: CommonTableExpressionNode[];
116
+ /** FROM clause table */
117
+ from: TableNode;
118
+ /** SELECT clause columns */
119
+ columns: (ColumnNode | FunctionNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode)[];
120
+ /** JOIN clauses */
121
+ joins: JoinNode[];
122
+ /** Optional WHERE clause */
123
+ where?: ExpressionNode;
124
+ /** Optional GROUP BY clause */
125
+ groupBy?: ColumnNode[];
126
+ /** Optional HAVING clause */
127
+ having?: ExpressionNode;
128
+ /** Optional ORDER BY clause */
129
+ orderBy?: OrderByNode[];
130
+ /** Optional LIMIT clause */
131
+ limit?: number;
132
+ /** Optional OFFSET clause */
133
+ offset?: number;
134
+ /** Optional query metadata */
135
+ meta?: QueryMetadata;
136
+ /** Optional DISTINCT clause */
137
+ distinct?: ColumnNode[];
138
+ }
139
+
140
+ export interface InsertQueryNode {
141
+ type: 'InsertQuery';
142
+ /** Target table */
143
+ into: TableNode;
144
+ /** Column order for inserted values */
145
+ columns: ColumnNode[];
146
+ /** Rows of values to insert */
147
+ values: OperandNode[][];
148
+ /** Optional RETURNING clause */
149
+ returning?: ColumnNode[];
150
+ }
151
+
152
+ export interface UpdateAssignmentNode {
153
+ /** Column to update */
154
+ column: ColumnNode;
155
+ /** Value to set */
156
+ value: OperandNode;
157
+ }
158
+
159
+ export interface UpdateQueryNode {
160
+ type: 'UpdateQuery';
161
+ /** Table being updated */
162
+ table: TableNode;
163
+ /** Assignments for SET clause */
164
+ set: UpdateAssignmentNode[];
165
+ /** Optional WHERE clause */
166
+ where?: ExpressionNode;
167
+ /** Optional RETURNING clause */
168
+ returning?: ColumnNode[];
169
+ }
170
+
171
+ export interface DeleteQueryNode {
172
+ type: 'DeleteQuery';
173
+ /** Table to delete from */
174
+ from: TableNode;
175
+ /** Optional WHERE clause */
176
+ where?: ExpressionNode;
177
+ /** Optional RETURNING clause */
178
+ returning?: ColumnNode[];
179
+ }
@@ -0,0 +1,42 @@
1
+ import { TableDef } from '../schema/table';
2
+ import { ColumnNode, ExpressionNode } from '../ast/expression';
3
+ import { TableNode, DeleteQueryNode } from '../ast/query';
4
+
5
+ const createTableNode = (table: TableDef): TableNode => ({
6
+ type: 'Table',
7
+ name: table.name
8
+ });
9
+
10
+ /**
11
+ * Maintains immutable state for DELETE queries
12
+ */
13
+ export class DeleteQueryState {
14
+ public readonly table: TableDef;
15
+ public readonly ast: DeleteQueryNode;
16
+
17
+ constructor(table: TableDef, ast?: DeleteQueryNode) {
18
+ this.table = table;
19
+ this.ast = ast ?? {
20
+ type: 'DeleteQuery',
21
+ from: createTableNode(table)
22
+ };
23
+ }
24
+
25
+ private clone(nextAst: DeleteQueryNode): DeleteQueryState {
26
+ return new DeleteQueryState(this.table, nextAst);
27
+ }
28
+
29
+ withWhere(expr: ExpressionNode): DeleteQueryState {
30
+ return this.clone({
31
+ ...this.ast,
32
+ where: expr
33
+ });
34
+ }
35
+
36
+ withReturning(columns: ColumnNode[]): DeleteQueryState {
37
+ return this.clone({
38
+ ...this.ast,
39
+ returning: [...columns]
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,57 @@
1
+ import { TableDef } from '../schema/table';
2
+ import { ColumnDef } from '../schema/column';
3
+ import { ColumnNode, ExpressionNode } from '../ast/expression';
4
+ import { CompiledQuery, DeleteCompiler } from '../dialect/abstract';
5
+ import { DeleteQueryNode } from '../ast/query';
6
+ import { DeleteQueryState } from './delete-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 DELETE queries
22
+ */
23
+ export class DeleteQueryBuilder<T> {
24
+ private readonly table: TableDef;
25
+ private readonly state: DeleteQueryState;
26
+
27
+ constructor(table: TableDef, state?: DeleteQueryState) {
28
+ this.table = table;
29
+ this.state = state ?? new DeleteQueryState(table);
30
+ }
31
+
32
+ private clone(state: DeleteQueryState): DeleteQueryBuilder<T> {
33
+ return new DeleteQueryBuilder(this.table, state);
34
+ }
35
+
36
+ where(expr: ExpressionNode): DeleteQueryBuilder<T> {
37
+ return this.clone(this.state.withWhere(expr));
38
+ }
39
+
40
+ returning(...columns: (ColumnDef | ColumnNode)[]): DeleteQueryBuilder<T> {
41
+ if (!columns.length) return this;
42
+ const nodes = columns.map(column => buildColumnNode(this.table, column));
43
+ return this.clone(this.state.withReturning(nodes));
44
+ }
45
+
46
+ compile(compiler: DeleteCompiler): CompiledQuery {
47
+ return compiler.compileDelete(this.state.ast);
48
+ }
49
+
50
+ toSql(compiler: DeleteCompiler): string {
51
+ return this.compile(compiler).sql;
52
+ }
53
+
54
+ getAST(): DeleteQueryNode {
55
+ return this.state.ast;
56
+ }
57
+ }
@@ -52,10 +52,11 @@ export class HydrationManager {
52
52
  relation: RelationDef,
53
53
  relationName: string,
54
54
  aliasPrefix: string,
55
- targetColumns: string[]
55
+ targetColumns: string[],
56
+ pivot?: { aliasPrefix: string; columns: string[] }
56
57
  ): HydrationManager {
57
58
  const withRoots = this.planner.captureRootColumns(state.ast.columns);
58
- const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns);
59
+ const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns, pivot);
59
60
  return this.clone(next);
60
61
  }
61
62
 
@@ -1,8 +1,9 @@
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 { ProjectionNode } from './select-query-state';
4
4
  import { HydrationPlan, HydrationRelationPlan } from '../ast/query';
5
5
  import { isRelationAlias } from '../utils/relation-alias';
6
+ import { buildDefaultPivotColumns } from './relation-utils';
6
7
 
7
8
  /**
8
9
  * Finds the primary key column name for a table
@@ -13,114 +14,169 @@ export const findPrimaryKey = (table: TableDef): string => {
13
14
  const pk = Object.values(table.columns).find(c => c.primary);
14
15
  return pk?.name || 'id';
15
16
  };
16
-
17
- /**
18
- * Manages hydration planning for query results
19
- */
20
- export class HydrationPlanner {
21
- /**
22
- * Creates a new HydrationPlanner instance
23
- * @param table - Table definition
24
- * @param plan - Optional existing hydration plan
25
- */
26
- constructor(private readonly table: TableDef, private readonly plan?: HydrationPlan) { }
27
-
28
- /**
29
- * Captures root table columns for hydration planning
30
- * @param columns - Columns to capture
31
- * @returns Updated HydrationPlanner with captured columns
32
- */
17
+
18
+ /**
19
+ * Manages hydration planning for query results
20
+ */
21
+ export class HydrationPlanner {
22
+ /**
23
+ * Creates a new HydrationPlanner instance
24
+ * @param table - Table definition
25
+ * @param plan - Optional existing hydration plan
26
+ */
27
+ constructor(private readonly table: TableDef, private readonly plan?: HydrationPlan) { }
28
+
29
+ /**
30
+ * Captures root table columns for hydration planning
31
+ * @param columns - Columns to capture
32
+ * @returns Updated HydrationPlanner with captured columns
33
+ */
33
34
  captureRootColumns(columns: ProjectionNode[]): HydrationPlanner {
34
35
  const currentPlan = this.getPlanOrDefault();
35
- const rootCols = new Set(currentPlan.rootColumns);
36
- let changed = false;
37
-
38
- columns.forEach(node => {
39
- if (node.type !== 'Column') return;
40
- if (node.table !== this.table.name) return;
41
-
42
- const alias = node.alias || node.name;
43
- if (isRelationAlias(alias)) return;
44
- if (!rootCols.has(alias)) {
45
- rootCols.add(alias);
46
- changed = true;
47
- }
48
- });
49
-
50
- if (!changed) return this;
51
- return new HydrationPlanner(this.table, {
52
- ...currentPlan,
53
- rootColumns: Array.from(rootCols)
54
- });
55
- }
56
-
57
- /**
58
- * Includes a relation in the hydration plan
59
- * @param rel - Relation definition
60
- * @param relationName - Name of the relation
61
- * @param aliasPrefix - Alias prefix for relation columns
62
- * @param columns - Columns to include from the relation
63
- * @returns Updated HydrationPlanner with included relation
64
- */
65
- includeRelation(rel: RelationDef, relationName: string, aliasPrefix: string, columns: string[]): HydrationPlanner {
66
- const currentPlan = this.getPlanOrDefault();
67
- const relations = currentPlan.relations.filter(r => r.name !== relationName);
68
- relations.push(this.buildRelationPlan(rel, relationName, aliasPrefix, columns));
69
- return new HydrationPlanner(this.table, {
70
- ...currentPlan,
71
- relations
72
- });
73
- }
74
-
75
- /**
76
- * Gets the current hydration plan
77
- * @returns Current hydration plan or undefined
78
- */
79
- getPlan(): HydrationPlan | undefined {
80
- return this.plan;
81
- }
82
-
83
- /**
84
- * Gets the current hydration plan or creates a default one
85
- * @returns Current hydration plan or default plan
86
- */
87
- private getPlanOrDefault(): HydrationPlan {
88
- return this.plan ?? buildDefaultHydrationPlan(this.table);
89
- }
90
-
91
- /**
92
- * Builds a relation plan for hydration
93
- * @param rel - Relation definition
94
- * @param relationName - Name of the relation
95
- * @param aliasPrefix - Alias prefix for relation columns
96
- * @param columns - Columns to include from the relation
97
- * @returns Hydration relation plan
98
- */
99
- private buildRelationPlan(rel: RelationDef, relationName: string, aliasPrefix: string, columns: string[]): HydrationRelationPlan {
100
- const localKeyFallback =
101
- rel.type === RelationKinds.HasMany ? findPrimaryKey(this.table) : findPrimaryKey(rel.target);
36
+ const rootCols = new Set(currentPlan.rootColumns);
37
+ let changed = false;
38
+
39
+ columns.forEach(node => {
40
+ if (node.type !== 'Column') return;
41
+ if (node.table !== this.table.name) return;
42
+
43
+ const alias = node.alias || node.name;
44
+ if (isRelationAlias(alias)) return;
45
+ if (!rootCols.has(alias)) {
46
+ rootCols.add(alias);
47
+ changed = true;
48
+ }
49
+ });
50
+
51
+ if (!changed) return this;
52
+ return new HydrationPlanner(this.table, {
53
+ ...currentPlan,
54
+ rootColumns: Array.from(rootCols)
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Includes a relation in the hydration plan
60
+ * @param rel - Relation definition
61
+ * @param relationName - Name of the relation
62
+ * @param aliasPrefix - Alias prefix for relation columns
63
+ * @param columns - Columns to include from the relation
64
+ * @returns Updated HydrationPlanner with included relation
65
+ */
66
+ includeRelation(
67
+ rel: RelationDef,
68
+ relationName: string,
69
+ aliasPrefix: string,
70
+ columns: string[],
71
+ pivot?: { aliasPrefix: string; columns: string[] }
72
+ ): HydrationPlanner {
73
+ const currentPlan = this.getPlanOrDefault();
74
+ const relations = currentPlan.relations.filter(r => r.name !== relationName);
75
+ relations.push(this.buildRelationPlan(rel, relationName, aliasPrefix, columns, pivot));
76
+ return new HydrationPlanner(this.table, {
77
+ ...currentPlan,
78
+ relations
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Gets the current hydration plan
84
+ * @returns Current hydration plan or undefined
85
+ */
86
+ getPlan(): HydrationPlan | undefined {
87
+ return this.plan;
88
+ }
89
+
90
+ /**
91
+ * Gets the current hydration plan or creates a default one
92
+ * @returns Current hydration plan or default plan
93
+ */
94
+ private getPlanOrDefault(): HydrationPlan {
95
+ return this.plan ?? buildDefaultHydrationPlan(this.table);
96
+ }
102
97
 
103
- return {
104
- name: relationName,
105
- aliasPrefix,
106
- type: rel.type,
107
- targetTable: rel.target.name,
108
- targetPrimaryKey: findPrimaryKey(rel.target),
109
- foreignKey: rel.foreignKey,
110
- localKey: rel.localKey || localKeyFallback,
111
- columns
112
- };
98
+ /**
99
+ * Builds a relation plan for hydration
100
+ * @param rel - Relation definition
101
+ * @param relationName - Name of the relation
102
+ * @param aliasPrefix - Alias prefix for relation columns
103
+ * @param columns - Columns to include from the relation
104
+ * @returns Hydration relation plan
105
+ */
106
+ private buildRelationPlan(
107
+ rel: RelationDef,
108
+ relationName: string,
109
+ aliasPrefix: string,
110
+ columns: string[],
111
+ pivot?: { aliasPrefix: string; columns: string[] }
112
+ ): HydrationRelationPlan {
113
+ switch (rel.type) {
114
+ case RelationKinds.HasMany: {
115
+ const localKey = rel.localKey || findPrimaryKey(this.table);
116
+ return {
117
+ name: relationName,
118
+ aliasPrefix,
119
+ type: rel.type,
120
+ targetTable: rel.target.name,
121
+ targetPrimaryKey: findPrimaryKey(rel.target),
122
+ foreignKey: rel.foreignKey,
123
+ localKey,
124
+ columns
125
+ };
126
+ }
127
+ case RelationKinds.BelongsTo: {
128
+ const localKey = rel.localKey || findPrimaryKey(rel.target);
129
+ return {
130
+ name: relationName,
131
+ aliasPrefix,
132
+ type: rel.type,
133
+ targetTable: rel.target.name,
134
+ targetPrimaryKey: findPrimaryKey(rel.target),
135
+ foreignKey: rel.foreignKey,
136
+ localKey,
137
+ columns
138
+ };
139
+ }
140
+ case RelationKinds.BelongsToMany: {
141
+ const many = rel as BelongsToManyRelation;
142
+ const localKey = many.localKey || findPrimaryKey(this.table);
143
+ const targetPk = many.targetKey || findPrimaryKey(many.target);
144
+ const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
145
+ const pivotAliasPrefix = pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
146
+ const pivotColumns =
147
+ pivot?.columns ??
148
+ many.defaultPivotColumns ??
149
+ buildDefaultPivotColumns(many, pivotPk);
150
+
151
+ return {
152
+ name: relationName,
153
+ aliasPrefix,
154
+ type: rel.type,
155
+ targetTable: many.target.name,
156
+ targetPrimaryKey: targetPk,
157
+ foreignKey: many.pivotForeignKeyToRoot,
158
+ localKey,
159
+ columns,
160
+ pivot: {
161
+ table: many.pivotTable.name,
162
+ primaryKey: pivotPk,
163
+ aliasPrefix: pivotAliasPrefix,
164
+ columns: pivotColumns
165
+ }
166
+ };
167
+ }
168
+ }
113
169
  }
114
- }
115
-
116
- /**
117
- * Builds a default hydration plan for a table
118
- * @param table - Table definition
119
- * @returns Default hydration plan
120
- */
170
+ }
171
+
172
+ /**
173
+ * Builds a default hydration plan for a table
174
+ * @param table - Table definition
175
+ * @returns Default hydration plan
176
+ */
121
177
  const buildDefaultHydrationPlan = (table: TableDef): HydrationPlan => ({
122
- rootTable: table.name,
123
- rootPrimaryKey: findPrimaryKey(table),
124
- rootColumns: [],
125
- relations: []
126
- });
178
+ rootTable: table.name,
179
+ rootPrimaryKey: findPrimaryKey(table),
180
+ rootColumns: [],
181
+ relations: []
182
+ });
@@ -0,0 +1,62 @@
1
+ import { TableDef } from '../schema/table';
2
+ import { InsertQueryNode, TableNode } from '../ast/query';
3
+ import { ColumnNode, OperandNode, valueToOperand } from '../ast/expression';
4
+
5
+ const createTableNode = (table: TableDef): TableNode => ({
6
+ type: 'Table',
7
+ name: table.name
8
+ });
9
+
10
+ const buildColumnNodes = (table: TableDef, names: string[]): ColumnNode[] =>
11
+ names.map(name => ({
12
+ type: 'Column',
13
+ table: table.name,
14
+ name
15
+ }));
16
+
17
+ /**
18
+ * Maintains immutable state for building INSERT queries
19
+ */
20
+ export class InsertQueryState {
21
+ public readonly table: TableDef;
22
+ public readonly ast: InsertQueryNode;
23
+
24
+ constructor(table: TableDef, ast?: InsertQueryNode) {
25
+ this.table = table;
26
+ this.ast = ast ?? {
27
+ type: 'InsertQuery',
28
+ into: createTableNode(table),
29
+ columns: [],
30
+ values: []
31
+ };
32
+ }
33
+
34
+ private clone(nextAst: InsertQueryNode): InsertQueryState {
35
+ return new InsertQueryState(this.table, nextAst);
36
+ }
37
+
38
+ withValues(rows: Record<string, unknown>[]): InsertQueryState {
39
+ if (!rows.length) return this;
40
+
41
+ const definedColumns = this.ast.columns.length
42
+ ? this.ast.columns
43
+ : buildColumnNodes(this.table, Object.keys(rows[0]));
44
+
45
+ const newRows: OperandNode[][] = rows.map(row =>
46
+ definedColumns.map(column => valueToOperand(row[column.name]))
47
+ );
48
+
49
+ return this.clone({
50
+ ...this.ast,
51
+ columns: definedColumns,
52
+ values: [...this.ast.values, ...newRows]
53
+ });
54
+ }
55
+
56
+ withReturning(columns: ColumnNode[]): InsertQueryState {
57
+ return this.clone({
58
+ ...this.ast,
59
+ returning: [...columns]
60
+ });
61
+ }
62
+ }