metal-orm 1.0.38 → 1.0.40

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.
@@ -1,100 +1,101 @@
1
- import { CompilerContext, Dialect } from '../abstract.js';
2
- import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode, TableSourceNode, DerivedTableNode } from '../../ast/query.js';
1
+ import { CompilerContext, Dialect } from '../abstract.js';
2
+ import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode, TableSourceNode, DerivedTableNode, OrderByNode } from '../../ast/query.js';
3
3
  import { JsonPathNode } from '../../ast/expression.js';
4
4
  import { MssqlFunctionStrategy } from './functions.js';
5
5
  import { FunctionTableFormatter } from '../base/function-table-formatter.js';
6
-
7
- /**
8
- * Microsoft SQL Server dialect implementation
9
- */
10
- export class SqlServerDialect extends Dialect {
11
- protected readonly dialect = 'mssql';
12
- /**
13
- * Creates a new SqlServerDialect instance
14
- */
15
- public constructor() {
16
- super(new MssqlFunctionStrategy());
17
- }
18
-
19
- /**
20
- * Quotes an identifier using SQL Server bracket syntax
21
- * @param id - Identifier to quote
22
- * @returns Quoted identifier
23
- */
24
- quoteIdentifier(id: string): string {
25
- return `[${id}]`;
26
- }
27
-
28
- /**
29
- * Compiles JSON path expression using SQL Server syntax
30
- * @param node - JSON path node
31
- * @returns SQL Server JSON path expression
32
- */
33
- protected compileJsonPath(node: JsonPathNode): string {
34
- const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
35
- // SQL Server uses JSON_VALUE(col, '$.path')
36
- return `JSON_VALUE(${col}, '${node.path}')`;
37
- }
38
-
39
- /**
40
- * Formats parameter placeholders using SQL Server named parameter syntax
41
- * @param index - Parameter index
42
- * @returns Named parameter placeholder
43
- */
44
- protected formatPlaceholder(index: number): string {
45
- return `@p${index}`;
46
- }
47
-
48
- /**
49
- * Compiles SELECT query AST to SQL Server SQL
50
- * @param ast - Query AST
51
- * @param ctx - Compiler context
52
- * @returns SQL Server SQL string
53
- */
54
- protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
55
- const hasSetOps = !!(ast.setOps && ast.setOps.length);
56
- const ctes = this.compileCtes(ast, ctx);
57
-
58
- const baseAst: SelectQueryNode = hasSetOps
59
- ? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
60
- : ast;
61
-
62
- const baseSelect = this.compileSelectCore(baseAst, ctx);
63
-
64
- if (!hasSetOps) {
65
- return `${ctes}${baseSelect}`;
66
- }
67
-
68
- const compound = ast.setOps!
69
- .map(op => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`)
70
- .join(' ');
71
-
72
- const orderBy = this.compileOrderBy(ast);
73
- const pagination = this.compilePagination(ast, orderBy);
74
- const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
75
- const tail = pagination || orderBy;
76
- return `${ctes}${combined}${tail}`;
77
- }
78
-
79
- protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
80
- const table = this.quoteIdentifier(ast.into.name);
81
- const columnList = ast.columns.map(column => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(', ');
82
- const values = ast.values.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`).join(', ');
83
- return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
84
- }
85
-
86
- protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
87
- const table = this.quoteIdentifier(ast.table.name);
88
- const assignments = ast.set.map(assignment => {
89
- const col = assignment.column;
90
- const target = `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`;
91
- const value = this.compileOperand(assignment.value, ctx);
92
- return `${target} = ${value}`;
93
- }).join(', ');
94
- const whereClause = this.compileWhere(ast.where, ctx);
95
- return `UPDATE ${table} SET ${assignments}${whereClause};`;
96
- }
97
-
6
+ import { OrderByCompiler } from '../base/orderby-compiler.js';
7
+
8
+ /**
9
+ * Microsoft SQL Server dialect implementation
10
+ */
11
+ export class SqlServerDialect extends Dialect {
12
+ protected readonly dialect = 'mssql';
13
+ /**
14
+ * Creates a new SqlServerDialect instance
15
+ */
16
+ public constructor() {
17
+ super(new MssqlFunctionStrategy());
18
+ }
19
+
20
+ /**
21
+ * Quotes an identifier using SQL Server bracket syntax
22
+ * @param id - Identifier to quote
23
+ * @returns Quoted identifier
24
+ */
25
+ quoteIdentifier(id: string): string {
26
+ return `[${id}]`;
27
+ }
28
+
29
+ /**
30
+ * Compiles JSON path expression using SQL Server syntax
31
+ * @param node - JSON path node
32
+ * @returns SQL Server JSON path expression
33
+ */
34
+ protected compileJsonPath(node: JsonPathNode): string {
35
+ const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
36
+ // SQL Server uses JSON_VALUE(col, '$.path')
37
+ return `JSON_VALUE(${col}, '${node.path}')`;
38
+ }
39
+
40
+ /**
41
+ * Formats parameter placeholders using SQL Server named parameter syntax
42
+ * @param index - Parameter index
43
+ * @returns Named parameter placeholder
44
+ */
45
+ protected formatPlaceholder(index: number): string {
46
+ return `@p${index}`;
47
+ }
48
+
49
+ /**
50
+ * Compiles SELECT query AST to SQL Server SQL
51
+ * @param ast - Query AST
52
+ * @param ctx - Compiler context
53
+ * @returns SQL Server SQL string
54
+ */
55
+ protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
56
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
57
+ const ctes = this.compileCtes(ast, ctx);
58
+
59
+ const baseAst: SelectQueryNode = hasSetOps
60
+ ? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
61
+ : ast;
62
+
63
+ const baseSelect = this.compileSelectCore(baseAst, ctx);
64
+
65
+ if (!hasSetOps) {
66
+ return `${ctes}${baseSelect}`;
67
+ }
68
+
69
+ const compound = ast.setOps!
70
+ .map(op => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`)
71
+ .join(' ');
72
+
73
+ const orderBy = this.compileOrderBy(ast, ctx);
74
+ const pagination = this.compilePagination(ast, orderBy);
75
+ const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
76
+ const tail = pagination || orderBy;
77
+ return `${ctes}${combined}${tail}`;
78
+ }
79
+
80
+ protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
81
+ const table = this.quoteIdentifier(ast.into.name);
82
+ const columnList = ast.columns.map(column => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(', ');
83
+ const values = ast.values.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`).join(', ');
84
+ return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
85
+ }
86
+
87
+ protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
88
+ const table = this.quoteIdentifier(ast.table.name);
89
+ const assignments = ast.set.map(assignment => {
90
+ const col = assignment.column;
91
+ const target = `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`;
92
+ const value = this.compileOperand(assignment.value, ctx);
93
+ return `${target} = ${value}`;
94
+ }).join(', ');
95
+ const whereClause = this.compileWhere(ast.where, ctx);
96
+ return `UPDATE ${table} SET ${assignments}${whereClause};`;
97
+ }
98
+
98
99
  protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
99
100
  if (ast.from.type !== 'Table') {
100
101
  throw new Error('DELETE only supports base tables in the MSSQL dialect.');
@@ -105,24 +106,24 @@ export class SqlServerDialect extends Dialect {
105
106
  }
106
107
 
107
108
  private compileSelectCore(ast: SelectQueryNode, ctx: CompilerContext): string {
108
- const columns = ast.columns.map(c => {
109
- let expr = '';
110
- if (c.type === 'Function') {
111
- expr = this.compileOperand(c, ctx);
112
- } else if (c.type === 'Column') {
113
- expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
114
- } else if (c.type === 'ScalarSubquery') {
115
- expr = this.compileOperand(c, ctx);
116
- } else if (c.type === 'WindowFunction') {
117
- expr = this.compileOperand(c, ctx);
118
- }
119
-
120
- if (c.alias) {
121
- if (c.alias.includes('(')) return c.alias;
122
- return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
123
- }
124
- return expr;
125
- }).join(', ');
109
+ const columns = ast.columns.map(c => {
110
+ let expr = '';
111
+ if (c.type === 'Function') {
112
+ expr = this.compileOperand(c, ctx);
113
+ } else if (c.type === 'Column') {
114
+ expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
115
+ } else if (c.type === 'ScalarSubquery') {
116
+ expr = this.compileOperand(c, ctx);
117
+ } else if (c.type === 'WindowFunction') {
118
+ expr = this.compileOperand(c, ctx);
119
+ }
120
+
121
+ if (c.alias) {
122
+ if (c.alias.includes('(')) return c.alias;
123
+ return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
124
+ }
125
+ return expr;
126
+ }).join(', ');
126
127
 
127
128
  const distinct = ast.distinct ? 'DISTINCT ' : '';
128
129
  const from = this.compileTableSource(ast.from, ctx);
@@ -132,47 +133,57 @@ export class SqlServerDialect extends Dialect {
132
133
  const cond = this.compileExpression(j.condition, ctx);
133
134
  return `${j.kind} JOIN ${table} ON ${cond}`;
134
135
  }).join(' ');
135
- const whereClause = this.compileWhere(ast.where, ctx);
136
-
137
- const groupBy = ast.groupBy && ast.groupBy.length > 0
138
- ? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
139
- : '';
140
-
141
- const having = ast.having
142
- ? ` HAVING ${this.compileExpression(ast.having, ctx)}`
143
- : '';
144
-
145
- const orderBy = this.compileOrderBy(ast);
146
- const pagination = this.compilePagination(ast, orderBy);
147
-
148
- if (pagination) {
149
- return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${pagination}`;
150
- }
151
-
152
- return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}`;
153
- }
154
-
155
- private compileOrderBy(ast: SelectQueryNode): string {
156
- if (!ast.orderBy || ast.orderBy.length === 0) return '';
157
- return ' ORDER BY ' + ast.orderBy
158
- .map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`)
159
- .join(', ');
160
- }
161
-
136
+ const whereClause = this.compileWhere(ast.where, ctx);
137
+
138
+ const groupBy = ast.groupBy && ast.groupBy.length > 0
139
+ ? ' GROUP BY ' + ast.groupBy.map(term => this.compileOrderingTerm(term, ctx)).join(', ')
140
+ : '';
141
+
142
+ const having = ast.having
143
+ ? ` HAVING ${this.compileExpression(ast.having, ctx)}`
144
+ : '';
145
+
146
+ const orderBy = this.compileOrderBy(ast, ctx);
147
+ const pagination = this.compilePagination(ast, orderBy);
148
+
149
+ if (pagination) {
150
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${pagination}`;
151
+ }
152
+
153
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}`;
154
+ }
155
+
156
+ private compileOrderBy(ast: SelectQueryNode, ctx: CompilerContext): string {
157
+ return OrderByCompiler.compileOrderBy(
158
+ ast,
159
+ term => this.compileOrderingTerm(term, ctx),
160
+ this.renderOrderByNulls.bind(this),
161
+ this.renderOrderByCollation.bind(this)
162
+ );
163
+ }
164
+
162
165
  private compilePagination(ast: SelectQueryNode, orderBy: string): string {
163
166
  const hasLimit = ast.limit !== undefined;
164
167
  const hasOffset = ast.offset !== undefined;
165
168
  if (!hasLimit && !hasOffset) return '';
166
-
167
- const off = ast.offset ?? 0;
168
- const orderClause = orderBy || ' ORDER BY (SELECT NULL)';
169
- let pagination = `${orderClause} OFFSET ${off} ROWS`;
169
+
170
+ const off = ast.offset ?? 0;
171
+ const orderClause = orderBy || ' ORDER BY (SELECT NULL)';
172
+ let pagination = `${orderClause} OFFSET ${off} ROWS`;
170
173
  if (hasLimit) {
171
174
  pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
172
175
  }
173
176
  return pagination;
174
177
  }
175
178
 
179
+ private renderOrderByNulls(order: OrderByNode): string | undefined {
180
+ return order.nulls ? ` NULLS ${order.nulls}` : '';
181
+ }
182
+
183
+ private renderOrderByCollation(order: OrderByNode): string | undefined {
184
+ return order.collation ? ` COLLATE ${order.collation}` : '';
185
+ }
186
+
176
187
  private compileTableSource(table: TableSourceNode, ctx: CompilerContext): string {
177
188
  if (table.type === 'FunctionTable') {
178
189
  return FunctionTableFormatter.format(table, ctx, this as any);
@@ -193,21 +204,21 @@ export class SqlServerDialect extends Dialect {
193
204
  : '';
194
205
  return `(${sub}) AS ${this.quoteIdentifier(table.alias)}${cols}`;
195
206
  }
196
-
197
- private compileCtes(ast: SelectQueryNode, ctx: CompilerContext): string {
198
- if (!ast.ctes || ast.ctes.length === 0) return '';
199
- // MSSQL does not use RECURSIVE keyword, but supports recursion when CTE references itself.
200
- const defs = ast.ctes.map(cte => {
201
- const name = this.quoteIdentifier(cte.name);
202
- const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
203
- const query = this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx).trim().replace(/;$/, '');
204
- return `${name}${cols} AS (${query})`;
205
- }).join(', ');
206
- return `WITH ${defs} `;
207
- }
208
-
209
- private wrapSetOperand(sql: string): string {
210
- const trimmed = sql.trim().replace(/;$/, '');
211
- return `(${trimmed})`;
212
- }
213
- }
207
+
208
+ private compileCtes(ast: SelectQueryNode, ctx: CompilerContext): string {
209
+ if (!ast.ctes || ast.ctes.length === 0) return '';
210
+ // MSSQL does not use RECURSIVE keyword, but supports recursion when CTE references itself.
211
+ const defs = ast.ctes.map(cte => {
212
+ const name = this.quoteIdentifier(cte.name);
213
+ const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
214
+ const query = this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx).trim().replace(/;$/, '');
215
+ return `${name}${cols} AS (${query})`;
216
+ }).join(', ');
217
+ return `WITH ${defs} `;
218
+ }
219
+
220
+ private wrapSetOperand(sql: string): string {
221
+ const trimmed = sql.trim().replace(/;$/, '');
222
+ return `(${trimmed})`;
223
+ }
224
+ }
@@ -1,13 +1,13 @@
1
1
  import { FunctionStrategy, FunctionRenderer, FunctionRenderContext } from './types.js';
2
- import { LiteralNode, OperandNode } from '../ast/expression.js';
3
-
4
- export class StandardFunctionStrategy implements FunctionStrategy {
5
- protected renderers: Map<string, FunctionRenderer> = new Map();
6
-
7
- constructor() {
8
- this.registerStandard();
9
- }
10
-
2
+ import { LiteralNode, OperandNode, isOperandNode } from '../ast/expression.js';
3
+
4
+ export class StandardFunctionStrategy implements FunctionStrategy {
5
+ protected renderers: Map<string, FunctionRenderer> = new Map();
6
+
7
+ constructor() {
8
+ this.registerStandard();
9
+ }
10
+
11
11
  protected registerStandard() {
12
12
  // Register ANSI standard implementations
13
13
  this.add('COUNT', ({ compiledArgs }) => `COUNT(${compiledArgs.join(', ')})`);
@@ -18,36 +18,36 @@ export class StandardFunctionStrategy implements FunctionStrategy {
18
18
  this.add('ABS', ({ compiledArgs }) => `ABS(${compiledArgs[0]})`);
19
19
  this.add('UPPER', ({ compiledArgs }) => `UPPER(${compiledArgs[0]})`);
20
20
  this.add('LOWER', ({ compiledArgs }) => `LOWER(${compiledArgs[0]})`);
21
- this.add('LENGTH', ({ compiledArgs }) => `LENGTH(${compiledArgs[0]})`);
22
- this.add('TRIM', ({ compiledArgs }) => `TRIM(${compiledArgs[0]})`);
23
- this.add('LTRIM', ({ compiledArgs }) => `LTRIM(${compiledArgs[0]})`);
24
- this.add('RTRIM', ({ compiledArgs }) => `RTRIM(${compiledArgs[0]})`);
25
- this.add('SUBSTRING', ({ compiledArgs }) => `SUBSTRING(${compiledArgs.join(', ')})`);
26
- this.add('CONCAT', ({ compiledArgs }) => `CONCAT(${compiledArgs.join(', ')})`);
27
- this.add('NOW', () => `NOW()`);
28
- this.add('CURRENT_DATE', () => `CURRENT_DATE`);
29
- this.add('CURRENT_TIME', () => `CURRENT_TIME`);
30
- this.add('EXTRACT', ({ compiledArgs }) => `EXTRACT(${compiledArgs[0]} FROM ${compiledArgs[1]})`);
31
- this.add('YEAR', ({ compiledArgs }) => `EXTRACT(YEAR FROM ${compiledArgs[0]})`);
32
- this.add('MONTH', ({ compiledArgs }) => `EXTRACT(MONTH FROM ${compiledArgs[0]})`);
33
- this.add('DAY', ({ compiledArgs }) => `EXTRACT(DAY FROM ${compiledArgs[0]})`);
34
- this.add('DATE_ADD', ({ compiledArgs }) => `(${compiledArgs[0]} + INTERVAL ${compiledArgs[1]} ${compiledArgs[2]})`);
35
- this.add('DATE_SUB', ({ compiledArgs }) => `(${compiledArgs[0]} - INTERVAL ${compiledArgs[1]} ${compiledArgs[2]})`);
36
- this.add('DATE_DIFF', ({ compiledArgs }) => `DATEDIFF(${compiledArgs[0]}, ${compiledArgs[1]})`);
37
- this.add('DATE_FORMAT', ({ compiledArgs }) => `DATE_FORMAT(${compiledArgs[0]}, ${compiledArgs[1]})`);
38
- this.add('UNIX_TIMESTAMP', () => `UNIX_TIMESTAMP()`);
39
- this.add('FROM_UNIXTIME', ({ compiledArgs }) => `FROM_UNIXTIME(${compiledArgs[0]})`);
40
- this.add('END_OF_MONTH', ({ compiledArgs }) => `LAST_DAY(${compiledArgs[0]})`);
41
- this.add('DAY_OF_WEEK', ({ compiledArgs }) => `DAYOFWEEK(${compiledArgs[0]})`);
21
+ this.add('LENGTH', ({ compiledArgs }) => `LENGTH(${compiledArgs[0]})`);
22
+ this.add('TRIM', ({ compiledArgs }) => `TRIM(${compiledArgs[0]})`);
23
+ this.add('LTRIM', ({ compiledArgs }) => `LTRIM(${compiledArgs[0]})`);
24
+ this.add('RTRIM', ({ compiledArgs }) => `RTRIM(${compiledArgs[0]})`);
25
+ this.add('SUBSTRING', ({ compiledArgs }) => `SUBSTRING(${compiledArgs.join(', ')})`);
26
+ this.add('CONCAT', ({ compiledArgs }) => `CONCAT(${compiledArgs.join(', ')})`);
27
+ this.add('NOW', () => `NOW()`);
28
+ this.add('CURRENT_DATE', () => `CURRENT_DATE`);
29
+ this.add('CURRENT_TIME', () => `CURRENT_TIME`);
30
+ this.add('EXTRACT', ({ compiledArgs }) => `EXTRACT(${compiledArgs[0]} FROM ${compiledArgs[1]})`);
31
+ this.add('YEAR', ({ compiledArgs }) => `EXTRACT(YEAR FROM ${compiledArgs[0]})`);
32
+ this.add('MONTH', ({ compiledArgs }) => `EXTRACT(MONTH FROM ${compiledArgs[0]})`);
33
+ this.add('DAY', ({ compiledArgs }) => `EXTRACT(DAY FROM ${compiledArgs[0]})`);
34
+ this.add('DATE_ADD', ({ compiledArgs }) => `(${compiledArgs[0]} + INTERVAL ${compiledArgs[1]} ${compiledArgs[2]})`);
35
+ this.add('DATE_SUB', ({ compiledArgs }) => `(${compiledArgs[0]} - INTERVAL ${compiledArgs[1]} ${compiledArgs[2]})`);
36
+ this.add('DATE_DIFF', ({ compiledArgs }) => `DATEDIFF(${compiledArgs[0]}, ${compiledArgs[1]})`);
37
+ this.add('DATE_FORMAT', ({ compiledArgs }) => `DATE_FORMAT(${compiledArgs[0]}, ${compiledArgs[1]})`);
38
+ this.add('UNIX_TIMESTAMP', () => `UNIX_TIMESTAMP()`);
39
+ this.add('FROM_UNIXTIME', ({ compiledArgs }) => `FROM_UNIXTIME(${compiledArgs[0]})`);
40
+ this.add('END_OF_MONTH', ({ compiledArgs }) => `LAST_DAY(${compiledArgs[0]})`);
41
+ this.add('DAY_OF_WEEK', ({ compiledArgs }) => `DAYOFWEEK(${compiledArgs[0]})`);
42
42
  this.add('WEEK_OF_YEAR', ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
43
43
  this.add('DATE_TRUNC', ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
44
44
  this.add('GROUP_CONCAT', ctx => this.renderGroupConcat(ctx));
45
- }
46
-
47
- protected add(name: string, renderer: FunctionRenderer) {
48
- this.renderers.set(name, renderer);
49
- }
50
-
45
+ }
46
+
47
+ protected add(name: string, renderer: FunctionRenderer) {
48
+ this.renderers.set(name, renderer);
49
+ }
50
+
51
51
  getRenderer(name: string): FunctionRenderer | undefined {
52
52
  return this.renderers.get(name);
53
53
  }
@@ -65,7 +65,16 @@ export class StandardFunctionStrategy implements FunctionStrategy {
65
65
  if (!orderBy || orderBy.length === 0) {
66
66
  return '';
67
67
  }
68
- const parts = orderBy.map(order => `${ctx.compileOperand(order.column)} ${order.direction}`);
68
+ const parts = orderBy.map(order => {
69
+ const term = isOperandNode(order.term)
70
+ ? ctx.compileOperand(order.term)
71
+ : (() => {
72
+ throw new Error('ORDER BY expressions inside functions must be operands');
73
+ })();
74
+ const collation = order.collation ? ` COLLATE ${order.collation}` : '';
75
+ const nulls = order.nulls ? ` NULLS ${order.nulls}` : '';
76
+ return `${term} ${order.direction}${collation}${nulls}`;
77
+ });
69
78
  return `ORDER BY ${parts.join(', ')}`;
70
79
  }
71
80