metal-orm 1.1.3 → 1.1.4

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 (37) hide show
  1. package/README.md +715 -703
  2. package/dist/index.cjs +655 -75
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +170 -8
  5. package/dist/index.d.ts +170 -8
  6. package/dist/index.js +649 -75
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/scripts/naming-strategy.mjs +16 -1
  10. package/src/core/ast/procedure.ts +21 -0
  11. package/src/core/ast/query.ts +47 -19
  12. package/src/core/ddl/introspect/utils.ts +56 -56
  13. package/src/core/dialect/abstract.ts +560 -547
  14. package/src/core/dialect/base/sql-dialect.ts +43 -29
  15. package/src/core/dialect/mssql/index.ts +369 -232
  16. package/src/core/dialect/mysql/index.ts +99 -7
  17. package/src/core/dialect/postgres/index.ts +121 -60
  18. package/src/core/dialect/sqlite/index.ts +97 -64
  19. package/src/core/execution/db-executor.ts +108 -90
  20. package/src/core/execution/executors/mssql-executor.ts +28 -24
  21. package/src/core/execution/executors/mysql-executor.ts +62 -27
  22. package/src/core/execution/executors/sqlite-executor.ts +10 -9
  23. package/src/index.ts +9 -6
  24. package/src/orm/execute-procedure.ts +77 -0
  25. package/src/orm/execute.ts +74 -73
  26. package/src/orm/interceptor-pipeline.ts +21 -17
  27. package/src/orm/pooled-executor-factory.ts +41 -20
  28. package/src/orm/unit-of-work.ts +6 -4
  29. package/src/query/index.ts +8 -5
  30. package/src/query-builder/delete.ts +3 -2
  31. package/src/query-builder/insert-query-state.ts +47 -19
  32. package/src/query-builder/insert.ts +142 -28
  33. package/src/query-builder/procedure-call.ts +122 -0
  34. package/src/query-builder/select/select-operations.ts +5 -2
  35. package/src/query-builder/select.ts +1146 -1105
  36. package/src/query-builder/update.ts +3 -2
  37. package/src/tree/tree-manager.ts +754 -754
@@ -1,6 +1,12 @@
1
- import { JsonPathNode } from '../../ast/expression.js';
2
- import { SqlDialectBase } from '../base/sql-dialect.js';
3
- import { MysqlFunctionStrategy } from './functions.js';
1
+ import { CompilerContext, CompiledProcedureCall } from '../abstract.js';
2
+ import { JsonPathNode } from '../../ast/expression.js';
3
+ import { InsertQueryNode } from '../../ast/query.js';
4
+ import { SqlDialectBase } from '../base/sql-dialect.js';
5
+ import { MysqlFunctionStrategy } from './functions.js';
6
+ import { ProcedureCallNode } from '../../ast/procedure.js';
7
+
8
+ const sanitizeVariableSuffix = (value: string): string =>
9
+ value.replace(/[^a-zA-Z0-9_]/g, '_');
4
10
 
5
11
  /**
6
12
  * MySQL dialect implementation
@@ -28,9 +34,95 @@ export class MySqlDialect extends SqlDialectBase {
28
34
  * @param node - JSON path node
29
35
  * @returns MySQL JSON path expression
30
36
  */
31
- protected compileJsonPath(node: JsonPathNode): string {
32
- const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
33
- // MySQL 5.7+ uses col->'$.path'
34
- return `${col}->'${node.path}'`;
37
+ protected compileJsonPath(node: JsonPathNode): string {
38
+ const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
39
+ // MySQL 5.7+ uses col->'$.path'
40
+ return `${col}->'${node.path}'`;
41
+ }
42
+
43
+ protected compileUpsertClause(ast: InsertQueryNode, ctx: CompilerContext): string {
44
+ if (!ast.onConflict) return '';
45
+
46
+ const clause = ast.onConflict;
47
+ if (clause.action.type === 'DoNothing') {
48
+ const noOpColumn = clause.target.columns[0] ?? ast.columns[0];
49
+ if (!noOpColumn) {
50
+ throw new Error('MySQL ON DUPLICATE KEY UPDATE requires at least one target column.');
51
+ }
52
+ const col = this.quoteIdentifier(noOpColumn.name);
53
+ return ` ON DUPLICATE KEY UPDATE ${col} = ${col}`;
54
+ }
55
+
56
+ if (clause.action.where) {
57
+ throw new Error('MySQL ON DUPLICATE KEY UPDATE does not support a WHERE clause.');
58
+ }
59
+ if (!clause.action.set.length) {
60
+ throw new Error('MySQL ON DUPLICATE KEY UPDATE requires at least one assignment.');
61
+ }
62
+
63
+ const assignments = clause.action.set
64
+ .map(assignment => {
65
+ const target = this.quoteIdentifier(assignment.column.name);
66
+ const value = this.compileOperand(assignment.value, ctx);
67
+ return `${target} = ${value}`;
68
+ })
69
+ .join(', ');
70
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
71
+ }
72
+
73
+ compileProcedureCall(ast: ProcedureCallNode): CompiledProcedureCall {
74
+ const ctx = this.createCompilerContext();
75
+ const qualifiedName = ast.ref.schema
76
+ ? `${this.quoteIdentifier(ast.ref.schema)}.${this.quoteIdentifier(ast.ref.name)}`
77
+ : this.quoteIdentifier(ast.ref.name);
78
+
79
+ const prelude: string[] = [];
80
+ const callArgs: string[] = [];
81
+ const outVars: Array<{ variable: string; name: string }> = [];
82
+
83
+ ast.params.forEach((param, index) => {
84
+ const suffix = sanitizeVariableSuffix(param.name || `p${index + 1}`);
85
+ const variable = `@__metal_${suffix}_${index + 1}`;
86
+
87
+ if (param.direction === 'in') {
88
+ if (!param.value) {
89
+ throw new Error(`Procedure parameter "${param.name}" requires a value for direction "in".`);
90
+ }
91
+ callArgs.push(this.compileOperand(param.value, ctx));
92
+ return;
93
+ }
94
+
95
+ if (param.direction === 'inout') {
96
+ if (!param.value) {
97
+ throw new Error(`Procedure parameter "${param.name}" requires a value for direction "inout".`);
98
+ }
99
+ prelude.push(`SET ${variable} = ${this.compileOperand(param.value, ctx)};`);
100
+ }
101
+
102
+ callArgs.push(variable);
103
+ outVars.push({ variable, name: param.name });
104
+ });
105
+
106
+ const statements: string[] = [];
107
+ if (prelude.length) {
108
+ statements.push(...prelude);
109
+ }
110
+ statements.push(`CALL ${qualifiedName}(${callArgs.join(', ')});`);
111
+
112
+ if (outVars.length) {
113
+ const selectOut = outVars
114
+ .map(({ variable, name }) => `${variable} AS ${this.quoteIdentifier(name)}`)
115
+ .join(', ');
116
+ statements.push(`SELECT ${selectOut};`);
117
+ }
118
+
119
+ return {
120
+ sql: statements.join(' '),
121
+ params: [...ctx.params],
122
+ outParams: {
123
+ source: outVars.length ? 'lastResultSet' : 'none',
124
+ names: outVars.map(item => item.name)
125
+ }
126
+ };
35
127
  }
36
128
  }
@@ -1,58 +1,59 @@
1
- import { CompilerContext } from '../abstract.js';
1
+ import { CompilerContext, CompiledProcedureCall } from '../abstract.js';
2
2
  import { JsonPathNode, ColumnNode, BitwiseExpressionNode } from '../../ast/expression.js';
3
- import { TableNode } from '../../ast/query.js';
3
+ import { InsertQueryNode, TableNode } from '../../ast/query.js';
4
4
  import { SqlDialectBase } from '../base/sql-dialect.js';
5
5
  import { PostgresFunctionStrategy } from './functions.js';
6
6
  import { PostgresTableFunctionStrategy } from './table-functions.js';
7
-
8
- /**
9
- * PostgreSQL dialect implementation
10
- */
11
- export class PostgresDialect extends SqlDialectBase {
12
- protected readonly dialect = 'postgres';
13
- /**
14
- * Creates a new PostgresDialect instance
15
- */
16
- public constructor() {
17
- super(new PostgresFunctionStrategy(), new PostgresTableFunctionStrategy());
18
- this.registerExpressionCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
19
- const left = this.compileOperand(node.left, ctx);
20
- const right = this.compileOperand(node.right, ctx);
21
- const op = node.operator === '^' ? '#' : node.operator;
22
- return `${left} ${op} ${right}`;
23
- });
24
- this.registerOperandCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
25
- const left = this.compileOperand(node.left, ctx);
26
- const right = this.compileOperand(node.right, ctx);
27
- const op = node.operator === '^' ? '#' : node.operator;
28
- return `(${left} ${op} ${right})`;
29
- });
30
- }
31
-
32
- /**
33
- * Quotes an identifier using PostgreSQL double-quote syntax
34
- * @param id - Identifier to quote
35
- * @returns Quoted identifier
36
- */
37
- quoteIdentifier(id: string): string {
38
- return `"${id}"`;
39
- }
40
-
41
- protected formatPlaceholder(index: number): string {
42
- return `$${index}`;
43
- }
44
-
45
- /**
46
- * Compiles JSON path expression using PostgreSQL syntax
47
- * @param node - JSON path node
48
- * @returns PostgreSQL JSON path expression
49
- */
50
- protected compileJsonPath(node: JsonPathNode): string {
51
- const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
52
- // Postgres uses col->>'path' for text extraction
53
- return `${col}->>'${node.path}'`;
54
- }
55
-
7
+ import { ProcedureCallNode } from '../../ast/procedure.js';
8
+
9
+ /**
10
+ * PostgreSQL dialect implementation
11
+ */
12
+ export class PostgresDialect extends SqlDialectBase {
13
+ protected readonly dialect = 'postgres';
14
+ /**
15
+ * Creates a new PostgresDialect instance
16
+ */
17
+ public constructor() {
18
+ super(new PostgresFunctionStrategy(), new PostgresTableFunctionStrategy());
19
+ this.registerExpressionCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
20
+ const left = this.compileOperand(node.left, ctx);
21
+ const right = this.compileOperand(node.right, ctx);
22
+ const op = node.operator === '^' ? '#' : node.operator;
23
+ return `${left} ${op} ${right}`;
24
+ });
25
+ this.registerOperandCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
26
+ const left = this.compileOperand(node.left, ctx);
27
+ const right = this.compileOperand(node.right, ctx);
28
+ const op = node.operator === '^' ? '#' : node.operator;
29
+ return `(${left} ${op} ${right})`;
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Quotes an identifier using PostgreSQL double-quote syntax
35
+ * @param id - Identifier to quote
36
+ * @returns Quoted identifier
37
+ */
38
+ quoteIdentifier(id: string): string {
39
+ return `"${id}"`;
40
+ }
41
+
42
+ protected formatPlaceholder(index: number): string {
43
+ return `$${index}`;
44
+ }
45
+
46
+ /**
47
+ * Compiles JSON path expression using PostgreSQL syntax
48
+ * @param node - JSON path node
49
+ * @returns PostgreSQL JSON path expression
50
+ */
51
+ protected compileJsonPath(node: JsonPathNode): string {
52
+ const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
53
+ // Postgres uses col->>'path' for text extraction
54
+ return `${col}->>'${node.path}'`;
55
+ }
56
+
56
57
  protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
57
58
  void ctx;
58
59
  if (!returning || returning.length === 0) return '';
@@ -60,14 +61,74 @@ export class PostgresDialect extends SqlDialectBase {
60
61
  return ` RETURNING ${columns}`;
61
62
  }
62
63
 
63
- supportsDmlReturningClause(): boolean {
64
- return true;
65
- }
64
+ protected compileUpsertClause(ast: InsertQueryNode, ctx: CompilerContext): string {
65
+ if (!ast.onConflict) return '';
66
+
67
+ const clause = ast.onConflict;
68
+ const target = clause.target.constraint
69
+ ? ` ON CONFLICT ON CONSTRAINT ${this.quoteIdentifier(clause.target.constraint)}`
70
+ : (() => {
71
+ this.ensureConflictColumns(
72
+ clause,
73
+ 'PostgreSQL ON CONFLICT requires conflict columns or a constraint name.'
74
+ );
75
+ const cols = clause.target.columns.map(col => this.quoteIdentifier(col.name)).join(', ');
76
+ return ` ON CONFLICT (${cols})`;
77
+ })();
78
+
79
+ if (clause.action.type === 'DoNothing') {
80
+ return `${target} DO NOTHING`;
81
+ }
82
+
83
+ if (!clause.action.set.length) {
84
+ throw new Error('PostgreSQL ON CONFLICT DO UPDATE requires at least one assignment.');
85
+ }
66
86
 
67
- /**
68
- * PostgreSQL requires unqualified column names in SET clause
69
- */
70
- protected compileSetTarget(column: ColumnNode, _table: TableNode): string {
71
- return this.quoteIdentifier(column.name);
87
+ const assignments = this.compileUpdateAssignments(clause.action.set, ast.into, ctx);
88
+ const where = clause.action.where
89
+ ? ` WHERE ${this.compileExpression(clause.action.where, ctx)}`
90
+ : '';
91
+ return `${target} DO UPDATE SET ${assignments}${where}`;
72
92
  }
73
- }
93
+
94
+ supportsDmlReturningClause(): boolean {
95
+ return true;
96
+ }
97
+
98
+ compileProcedureCall(ast: ProcedureCallNode): CompiledProcedureCall {
99
+ const ctx = this.createCompilerContext();
100
+ const qualifiedName = ast.ref.schema
101
+ ? `${this.quoteIdentifier(ast.ref.schema)}.${this.quoteIdentifier(ast.ref.name)}`
102
+ : this.quoteIdentifier(ast.ref.name);
103
+
104
+ const args: string[] = [];
105
+ for (const param of ast.params) {
106
+ if (param.direction === 'out') continue;
107
+ if (!param.value) {
108
+ throw new Error(`Procedure parameter "${param.name}" requires a value for direction "${param.direction}".`);
109
+ }
110
+ args.push(this.compileOperand(param.value, ctx));
111
+ }
112
+
113
+ const outNames = ast.params
114
+ .filter(param => param.direction === 'out' || param.direction === 'inout')
115
+ .map(param => param.name);
116
+
117
+ const rawSql = `CALL ${qualifiedName}(${args.join(', ')})`;
118
+ return {
119
+ sql: `${rawSql};`,
120
+ params: [...ctx.params],
121
+ outParams: {
122
+ source: outNames.length ? 'firstResultSet' : 'none',
123
+ names: outNames
124
+ }
125
+ };
126
+ }
127
+
128
+ /**
129
+ * PostgreSQL requires unqualified column names in SET clause
130
+ */
131
+ protected compileSetTarget(column: ColumnNode, _table: TableNode): string {
132
+ return this.quoteIdentifier(column.name);
133
+ }
134
+ }
@@ -1,69 +1,70 @@
1
- import { CompilerContext } from '../abstract.js';
1
+ import { CompilerContext, CompiledProcedureCall } from '../abstract.js';
2
2
  import { JsonPathNode, ColumnNode, BitwiseExpressionNode } from '../../ast/expression.js';
3
- import { TableNode } from '../../ast/query.js';
3
+ import { InsertQueryNode, TableNode } from '../../ast/query.js';
4
4
  import { SqlDialectBase } from '../base/sql-dialect.js';
5
5
  import { SqliteFunctionStrategy } from './functions.js';
6
-
7
- /**
8
- * SQLite dialect implementation
9
- */
10
- export class SqliteDialect extends SqlDialectBase {
11
- protected readonly dialect = 'sqlite';
12
- /**
13
- * Creates a new SqliteDialect instance
14
- */
15
- public constructor() {
16
- super(new SqliteFunctionStrategy());
17
- this.registerExpressionCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
18
- const left = this.compileOperand(node.left, ctx);
19
- const right = this.compileOperand(node.right, ctx);
20
- if (node.operator === '^') {
21
- return `(${left} | ${right}) & ~(${left} & ${right})`;
22
- }
23
- return `${left} ${node.operator} ${right}`;
24
- });
25
- this.registerOperandCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
26
- const left = this.compileOperand(node.left, ctx);
27
- const right = this.compileOperand(node.right, ctx);
28
- if (node.operator === '^') {
29
- return `((${left} | ${right}) & ~(${left} & ${right}))`;
30
- }
31
- return `(${left} ${node.operator} ${right})`;
32
- });
33
- }
34
-
35
- /**
36
- * Quotes an identifier using SQLite double-quote syntax
37
- * @param id - Identifier to quote
38
- * @returns Quoted identifier
39
- */
40
- quoteIdentifier(id: string): string {
41
- return `"${id}"`;
42
- }
43
-
44
- /**
45
- * Compiles JSON path expression using SQLite syntax
46
- * @param node - JSON path node
47
- * @returns SQLite JSON path expression
48
- */
49
- protected compileJsonPath(node: JsonPathNode): string {
50
- const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
51
- // SQLite uses json_extract(col, '$.path')
52
- return `json_extract(${col}, '${node.path}')`;
53
- }
54
-
55
- protected compileQualifiedColumn(column: ColumnNode, _table: TableNode): string {
56
- void _table;
57
- return this.quoteIdentifier(column.name);
58
- }
59
-
60
- protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
61
- void ctx;
62
- if (!returning || returning.length === 0) return '';
63
- const columns = this.formatReturningColumns(returning);
64
- return ` RETURNING ${columns}`;
65
- }
66
-
6
+ import { ProcedureCallNode } from '../../ast/procedure.js';
7
+
8
+ /**
9
+ * SQLite dialect implementation
10
+ */
11
+ export class SqliteDialect extends SqlDialectBase {
12
+ protected readonly dialect = 'sqlite';
13
+ /**
14
+ * Creates a new SqliteDialect instance
15
+ */
16
+ public constructor() {
17
+ super(new SqliteFunctionStrategy());
18
+ this.registerExpressionCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
19
+ const left = this.compileOperand(node.left, ctx);
20
+ const right = this.compileOperand(node.right, ctx);
21
+ if (node.operator === '^') {
22
+ return `(${left} | ${right}) & ~(${left} & ${right})`;
23
+ }
24
+ return `${left} ${node.operator} ${right}`;
25
+ });
26
+ this.registerOperandCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
27
+ const left = this.compileOperand(node.left, ctx);
28
+ const right = this.compileOperand(node.right, ctx);
29
+ if (node.operator === '^') {
30
+ return `((${left} | ${right}) & ~(${left} & ${right}))`;
31
+ }
32
+ return `(${left} ${node.operator} ${right})`;
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Quotes an identifier using SQLite double-quote syntax
38
+ * @param id - Identifier to quote
39
+ * @returns Quoted identifier
40
+ */
41
+ quoteIdentifier(id: string): string {
42
+ return `"${id}"`;
43
+ }
44
+
45
+ /**
46
+ * Compiles JSON path expression using SQLite syntax
47
+ * @param node - JSON path node
48
+ * @returns SQLite JSON path expression
49
+ */
50
+ protected compileJsonPath(node: JsonPathNode): string {
51
+ const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
52
+ // SQLite uses json_extract(col, '$.path')
53
+ return `json_extract(${col}, '${node.path}')`;
54
+ }
55
+
56
+ protected compileQualifiedColumn(column: ColumnNode, _table: TableNode): string {
57
+ void _table;
58
+ return this.quoteIdentifier(column.name);
59
+ }
60
+
61
+ protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
62
+ void ctx;
63
+ if (!returning || returning.length === 0) return '';
64
+ const columns = this.formatReturningColumns(returning);
65
+ return ` RETURNING ${columns}`;
66
+ }
67
+
67
68
  protected formatReturningColumns(returning: ColumnNode[]): string {
68
69
  return returning
69
70
  .map(column => {
@@ -73,7 +74,39 @@ export class SqliteDialect extends SqlDialectBase {
73
74
  .join(', ');
74
75
  }
75
76
 
77
+ protected compileUpsertClause(ast: InsertQueryNode, ctx: CompilerContext): string {
78
+ if (!ast.onConflict) return '';
79
+
80
+ const clause = ast.onConflict;
81
+ if (clause.target.constraint) {
82
+ throw new Error('SQLite ON CONFLICT does not support named constraints.');
83
+ }
84
+ this.ensureConflictColumns(clause, 'SQLite ON CONFLICT requires conflict columns.');
85
+
86
+ const cols = clause.target.columns.map(col => this.quoteIdentifier(col.name)).join(', ');
87
+ const target = ` ON CONFLICT (${cols})`;
88
+
89
+ if (clause.action.type === 'DoNothing') {
90
+ return `${target} DO NOTHING`;
91
+ }
92
+
93
+ if (!clause.action.set.length) {
94
+ throw new Error('SQLite ON CONFLICT DO UPDATE requires at least one assignment.');
95
+ }
96
+
97
+ const assignments = this.compileUpdateAssignments(clause.action.set, ast.into, ctx);
98
+ const where = clause.action.where
99
+ ? ` WHERE ${this.compileExpression(clause.action.where, ctx)}`
100
+ : '';
101
+ return `${target} DO UPDATE SET ${assignments}${where}`;
102
+ }
103
+
76
104
  supportsDmlReturningClause(): boolean {
77
105
  return true;
78
106
  }
79
- }
107
+
108
+ compileProcedureCall(_ast: ProcedureCallNode): CompiledProcedureCall {
109
+ void _ast;
110
+ throw new Error('Stored procedures are not supported by the SQLite dialect.');
111
+ }
112
+ }