metal-orm 1.0.82 → 1.0.83
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 +274 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +274 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/ast-validation.ts +188 -0
- package/src/core/ast/expression-builders.ts +43 -24
- package/src/core/ast/param-proxy.ts +1 -4
- package/src/core/dialect/abstract.ts +59 -39
- package/src/query-builder/select.ts +128 -109
package/package.json
CHANGED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { ExpressionNode, OperandNode } from './expression-nodes.js';
|
|
2
|
+
import { visitExpression, visitOperand } from './expression-visitor.js';
|
|
3
|
+
import type { SelectQueryNode } from './query.js';
|
|
4
|
+
|
|
5
|
+
const hasParamOperandsInExpression = (expr: ExpressionNode): boolean => {
|
|
6
|
+
let hasParams = false;
|
|
7
|
+
|
|
8
|
+
visitExpression(expr, {
|
|
9
|
+
visitBinaryExpression: (node) => {
|
|
10
|
+
visitOperand(node.left, {
|
|
11
|
+
visitParam: () => { hasParams = true; },
|
|
12
|
+
otherwise: () => {}
|
|
13
|
+
});
|
|
14
|
+
visitOperand(node.right, {
|
|
15
|
+
visitParam: () => { hasParams = true; },
|
|
16
|
+
otherwise: () => {}
|
|
17
|
+
});
|
|
18
|
+
if (node.escape) {
|
|
19
|
+
visitOperand(node.escape, {
|
|
20
|
+
visitParam: () => { hasParams = true; },
|
|
21
|
+
otherwise: () => {}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
visitLogicalExpression: (node) => {
|
|
26
|
+
node.operands.forEach(operand => {
|
|
27
|
+
if (hasParamOperandsInExpression(operand)) {
|
|
28
|
+
hasParams = true;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
visitNullExpression: () => {},
|
|
33
|
+
visitInExpression: (node) => {
|
|
34
|
+
visitOperand(node.left, {
|
|
35
|
+
visitParam: () => { hasParams = true; },
|
|
36
|
+
otherwise: () => {}
|
|
37
|
+
});
|
|
38
|
+
if (Array.isArray(node.right)) {
|
|
39
|
+
node.right.forEach(operand => visitOperand(operand, {
|
|
40
|
+
visitParam: () => { hasParams = true; },
|
|
41
|
+
otherwise: () => {}
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
visitExistsExpression: () => {},
|
|
46
|
+
visitBetweenExpression: (node) => {
|
|
47
|
+
visitOperand(node.left, {
|
|
48
|
+
visitParam: () => { hasParams = true; },
|
|
49
|
+
otherwise: () => {}
|
|
50
|
+
});
|
|
51
|
+
visitOperand(node.lower, {
|
|
52
|
+
visitParam: () => { hasParams = true; },
|
|
53
|
+
otherwise: () => {}
|
|
54
|
+
});
|
|
55
|
+
visitOperand(node.upper, {
|
|
56
|
+
visitParam: () => { hasParams = true; },
|
|
57
|
+
otherwise: () => {}
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
visitArithmeticExpression: (node) => {
|
|
61
|
+
visitOperand(node.left, {
|
|
62
|
+
visitParam: () => { hasParams = true; },
|
|
63
|
+
otherwise: () => {}
|
|
64
|
+
});
|
|
65
|
+
visitOperand(node.right, {
|
|
66
|
+
visitParam: () => { hasParams = true; },
|
|
67
|
+
otherwise: () => {}
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
visitBitwiseExpression: (node) => {
|
|
71
|
+
visitOperand(node.left, {
|
|
72
|
+
visitParam: () => { hasParams = true; },
|
|
73
|
+
otherwise: () => {}
|
|
74
|
+
});
|
|
75
|
+
visitOperand(node.right, {
|
|
76
|
+
visitParam: () => { hasParams = true; },
|
|
77
|
+
otherwise: () => {}
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
otherwise: () => {}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return hasParams;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const hasParamOperandsInOperand = (operand: OperandNode): boolean => {
|
|
87
|
+
let hasParams = false;
|
|
88
|
+
|
|
89
|
+
visitOperand(operand, {
|
|
90
|
+
visitColumn: () => {},
|
|
91
|
+
visitLiteral: () => {},
|
|
92
|
+
visitParam: () => { hasParams = true; },
|
|
93
|
+
visitFunction: (node) => {
|
|
94
|
+
node.args?.forEach(arg => {
|
|
95
|
+
if (hasParamOperandsInOperand(arg)) {
|
|
96
|
+
hasParams = true;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
visitJsonPath: () => {},
|
|
101
|
+
visitScalarSubquery: () => {},
|
|
102
|
+
visitCaseExpression: (node) => {
|
|
103
|
+
node.conditions.forEach(cond => {
|
|
104
|
+
if (hasParamOperandsInExpression(cond.when)) {
|
|
105
|
+
hasParams = true;
|
|
106
|
+
}
|
|
107
|
+
if (hasParamOperandsInOperand(cond.then)) {
|
|
108
|
+
hasParams = true;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (node.else && hasParamOperandsInOperand(node.else)) {
|
|
112
|
+
hasParams = true;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
visitCast: (node) => {
|
|
116
|
+
if (hasParamOperandsInOperand(node.expression)) {
|
|
117
|
+
hasParams = true;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
visitWindowFunction: (node) => {
|
|
121
|
+
node.args?.forEach(arg => {
|
|
122
|
+
if (hasParamOperandsInOperand(arg)) {
|
|
123
|
+
hasParams = true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
node.orderBy?.forEach(ord => {
|
|
127
|
+
if (ord.term) {
|
|
128
|
+
if (hasParamOperandsInOperand(ord.term as OperandNode)) {
|
|
129
|
+
hasParams = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
visitCollate: (node) => {
|
|
135
|
+
if (hasParamOperandsInOperand(node.expression)) {
|
|
136
|
+
hasParams = true;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
visitAliasRef: () => {},
|
|
140
|
+
otherwise: () => {}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return hasParams;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const hasParamOperandsInQuery = (ast: SelectQueryNode): boolean => {
|
|
147
|
+
if (ast.where && hasParamOperandsInExpression(ast.where)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (ast.having && hasParamOperandsInExpression(ast.having)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ast.columns?.forEach(col => {
|
|
156
|
+
if (typeof col === 'object' && col !== null && 'type' in col) {
|
|
157
|
+
if (hasParamOperandsInOperand(col as OperandNode)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
ast.orderBy?.forEach(ord => {
|
|
164
|
+
if (ord.term) {
|
|
165
|
+
if (hasParamOperandsInOperand(ord.term as OperandNode)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (ast.ctes) {
|
|
172
|
+
for (const cte of ast.ctes) {
|
|
173
|
+
if (cte.query.where && hasParamOperandsInExpression(cte.query.where)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (ast.setOps) {
|
|
180
|
+
for (const op of ast.setOps) {
|
|
181
|
+
if (hasParamOperandsInQuery(op.query)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
@@ -2,13 +2,14 @@ import { SelectQueryNode } from './query.js';
|
|
|
2
2
|
import { SqlOperator, BitwiseOperator } from '../sql/sql.js';
|
|
3
3
|
import { ColumnRef } from './types.js';
|
|
4
4
|
import {
|
|
5
|
-
ColumnNode,
|
|
6
|
-
LiteralNode,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
ColumnNode,
|
|
6
|
+
LiteralNode,
|
|
7
|
+
ParamNode,
|
|
8
|
+
JsonPathNode,
|
|
9
|
+
OperandNode,
|
|
10
|
+
CaseExpressionNode,
|
|
11
|
+
CastExpressionNode,
|
|
12
|
+
BinaryExpressionNode,
|
|
12
13
|
ExpressionNode,
|
|
13
14
|
LogicalExpressionNode,
|
|
14
15
|
NullExpressionNode,
|
|
@@ -47,12 +48,21 @@ const toLiteralNode = (value: LiteralValue): LiteralNode => ({
|
|
|
47
48
|
type: 'Literal',
|
|
48
49
|
value: value instanceof Date ? value.toISOString() : value
|
|
49
50
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
51
|
+
|
|
52
|
+
const toParamNode = (value: unknown): ParamNode | undefined => {
|
|
53
|
+
if (typeof value !== 'object' || value === null) return undefined;
|
|
54
|
+
const type = Object.getOwnPropertyDescriptor(value, 'type')?.value;
|
|
55
|
+
if (type !== 'Param') return undefined;
|
|
56
|
+
const name = Object.getOwnPropertyDescriptor(value, 'name')?.value;
|
|
57
|
+
if (typeof name !== 'string') return undefined;
|
|
58
|
+
return { type: 'Param', name };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Converts a ColumnRef to a ColumnNode
|
|
63
|
+
* @throws Error if the ColumnRef doesn't have a table specified
|
|
64
|
+
*/
|
|
65
|
+
const columnRefToNode = (col: ColumnRef): ColumnNode => {
|
|
56
66
|
if (!col.table) {
|
|
57
67
|
throw new Error(
|
|
58
68
|
`Column "${col.name}" requires a table reference. ` +
|
|
@@ -67,11 +77,16 @@ const columnRefToNode = (col: ColumnRef): ColumnNode => {
|
|
|
67
77
|
* @param value - Value to convert (OperandNode, ColumnRef, or literal value)
|
|
68
78
|
* @returns OperandNode representing the value
|
|
69
79
|
*/
|
|
70
|
-
const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNode => {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
return
|
|
74
|
-
}
|
|
80
|
+
const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNode => {
|
|
81
|
+
const paramNode = toParamNode(value);
|
|
82
|
+
if (paramNode) {
|
|
83
|
+
return paramNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Already an operand node
|
|
87
|
+
if (isOperandNode(value)) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
75
90
|
|
|
76
91
|
// Literal value
|
|
77
92
|
if (isLiteralValue(value)) {
|
|
@@ -87,12 +102,16 @@ const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNo
|
|
|
87
102
|
* @param value - Value or operand to normalize
|
|
88
103
|
* @returns OperandNode representing the value
|
|
89
104
|
*/
|
|
90
|
-
export const valueToOperand = (value: ValueOperandInput): OperandNode => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
export const valueToOperand = (value: ValueOperandInput): OperandNode => {
|
|
106
|
+
const paramNode = toParamNode(value);
|
|
107
|
+
if (paramNode) {
|
|
108
|
+
return paramNode;
|
|
109
|
+
}
|
|
110
|
+
if (isOperandNode(value)) {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
return toLiteralNode(value);
|
|
114
|
+
};
|
|
96
115
|
|
|
97
116
|
/**
|
|
98
117
|
* Converts various input types to an OperandNode
|
|
@@ -21,7 +21,7 @@ const buildParamProxy = (name: string): ParamProxy => {
|
|
|
21
21
|
const nextName = name ? `${name}.${trimmed}` : trimmed;
|
|
22
22
|
return buildParamProxy(nextName);
|
|
23
23
|
}
|
|
24
|
-
if (prop in t) {
|
|
24
|
+
if (prop in t && name === '') {
|
|
25
25
|
return (t as unknown as Record<string, unknown>)[prop];
|
|
26
26
|
}
|
|
27
27
|
const nextName = name ? `${name}.${prop}` : prop;
|
|
@@ -41,9 +41,6 @@ export const createParamProxy = (): ParamProxyRoot => {
|
|
|
41
41
|
if (typeof prop === 'string' && prop.startsWith('$')) {
|
|
42
42
|
return buildParamProxy(prop.slice(1));
|
|
43
43
|
}
|
|
44
|
-
if (prop in t) {
|
|
45
|
-
return (t as unknown as Record<string, unknown>)[prop];
|
|
46
|
-
}
|
|
47
44
|
return buildParamProxy(String(prop));
|
|
48
45
|
}
|
|
49
46
|
}) as ParamProxyRoot;
|
|
@@ -37,15 +37,17 @@ import { StandardFunctionStrategy } from '../functions/standard-strategy.js';
|
|
|
37
37
|
import type { TableFunctionStrategy } from '../functions/table-types.js';
|
|
38
38
|
import { StandardTableFunctionStrategy } from '../functions/standard-table-strategy.js';
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Context for SQL compilation with parameter management
|
|
42
|
-
*/
|
|
43
|
-
export interface CompilerContext {
|
|
44
|
-
/** Array of parameters */
|
|
45
|
-
params: unknown[];
|
|
46
|
-
/** Function to add a parameter and get its placeholder */
|
|
47
|
-
addParameter(value: unknown): string;
|
|
48
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Context for SQL compilation with parameter management
|
|
42
|
+
*/
|
|
43
|
+
export interface CompilerContext {
|
|
44
|
+
/** Array of parameters */
|
|
45
|
+
params: unknown[];
|
|
46
|
+
/** Function to add a parameter and get its placeholder */
|
|
47
|
+
addParameter(value: unknown): string;
|
|
48
|
+
/** Whether Param operands are allowed (for schema generation) */
|
|
49
|
+
allowParams?: boolean;
|
|
50
|
+
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* Result of SQL compilation
|
|
@@ -86,18 +88,29 @@ export abstract class Dialect
|
|
|
86
88
|
* @param ast - Query AST to compile
|
|
87
89
|
* @returns Compiled query with SQL and parameters
|
|
88
90
|
*/
|
|
89
|
-
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
90
|
-
const ctx = this.createCompilerContext();
|
|
91
|
-
const normalized = this.normalizeSelectAst(ast);
|
|
92
|
-
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
93
|
-
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
94
|
-
return {
|
|
95
|
-
sql,
|
|
96
|
-
params: [...ctx.params]
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
92
|
+
const ctx = this.createCompilerContext();
|
|
93
|
+
const normalized = this.normalizeSelectAst(ast);
|
|
94
|
+
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
95
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
96
|
+
return {
|
|
97
|
+
sql,
|
|
98
|
+
params: [...ctx.params]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
compileSelectWithOptions(ast: SelectQueryNode, options: { allowParams?: boolean } = {}): CompiledQuery {
|
|
103
|
+
const ctx = this.createCompilerContext(options);
|
|
104
|
+
const normalized = this.normalizeSelectAst(ast);
|
|
105
|
+
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
106
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
107
|
+
return {
|
|
108
|
+
sql,
|
|
109
|
+
params: [...ctx.params]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
compileInsert(ast: InsertQueryNode): CompiledQuery {
|
|
101
114
|
const ctx = this.createCompilerContext();
|
|
102
115
|
const rawSql = this.compileInsertAst(ast, ctx).trim();
|
|
103
116
|
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
@@ -198,22 +211,24 @@ export abstract class Dialect
|
|
|
198
211
|
return `SELECT 1${tail}`;
|
|
199
212
|
}
|
|
200
213
|
|
|
201
|
-
/**
|
|
202
|
-
* Creates a new compiler context
|
|
203
|
-
* @
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Creates a new compiler context
|
|
216
|
+
* @param options - Optional compiler context options
|
|
217
|
+
* @returns Compiler context with parameter management
|
|
218
|
+
*/
|
|
219
|
+
protected createCompilerContext(options: { allowParams?: boolean } = {}): CompilerContext {
|
|
220
|
+
const params: unknown[] = [];
|
|
221
|
+
let counter = 0;
|
|
222
|
+
return {
|
|
223
|
+
params,
|
|
224
|
+
allowParams: options.allowParams ?? false,
|
|
225
|
+
addParameter: (value: unknown) => {
|
|
226
|
+
counter += 1;
|
|
227
|
+
params.push(value);
|
|
228
|
+
return this.formatPlaceholder(counter);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
217
232
|
|
|
218
233
|
/**
|
|
219
234
|
* Formats a parameter placeholder
|
|
@@ -453,9 +468,14 @@ export abstract class Dialect
|
|
|
453
468
|
});
|
|
454
469
|
}
|
|
455
470
|
|
|
456
|
-
private registerDefaultOperandCompilers(): void {
|
|
471
|
+
private registerDefaultOperandCompilers(): void {
|
|
457
472
|
this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
|
|
458
|
-
this.registerOperandCompiler('Param', (_param: ParamNode, ctx) =>
|
|
473
|
+
this.registerOperandCompiler('Param', (_param: ParamNode, ctx) => {
|
|
474
|
+
if (!ctx.allowParams) {
|
|
475
|
+
throw new Error('Cannot compile query with Param operands. Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.');
|
|
476
|
+
}
|
|
477
|
+
return ctx.addParameter(null);
|
|
478
|
+
});
|
|
459
479
|
|
|
460
480
|
this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
|
|
461
481
|
void _ctx;
|