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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.82",
3
+ "version": "1.0.83",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -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
- JsonPathNode,
8
- OperandNode,
9
- CaseExpressionNode,
10
- CastExpressionNode,
11
- BinaryExpressionNode,
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
- * Converts a ColumnRef to a ColumnNode
53
- * @throws Error if the ColumnRef doesn't have a table specified
54
- */
55
- const columnRefToNode = (col: ColumnRef): ColumnNode => {
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
- // Already an operand node
72
- if (isOperandNode(value)) {
73
- return value;
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
- if (isOperandNode(value)) {
92
- return value;
93
- }
94
- return toLiteralNode(value);
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
- compileInsert(ast: InsertQueryNode): CompiledQuery {
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
- * @returns Compiler context with parameter management
204
- */
205
- protected createCompilerContext(): CompilerContext {
206
- const params: unknown[] = [];
207
- let counter = 0;
208
- return {
209
- params,
210
- addParameter: (value: unknown) => {
211
- counter += 1;
212
- params.push(value);
213
- return this.formatPlaceholder(counter);
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) => ctx.addParameter(null));
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;