metal-orm 1.0.15 → 1.0.17

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 (63) hide show
  1. package/README.md +64 -61
  2. package/dist/decorators/index.cjs +490 -175
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -5
  5. package/dist/decorators/index.d.ts +1 -5
  6. package/dist/decorators/index.js +490 -175
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +1044 -483
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +67 -15
  11. package/dist/index.d.ts +67 -15
  12. package/dist/index.js +1033 -482
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-Bkv8g8u_.d.cts → select-BPCn6MOH.d.cts} +486 -32
  15. package/dist/{select-Bkv8g8u_.d.ts → select-BPCn6MOH.d.ts} +486 -32
  16. package/package.json +2 -1
  17. package/src/codegen/naming-strategy.ts +64 -0
  18. package/src/codegen/typescript.ts +48 -53
  19. package/src/core/ast/aggregate-functions.ts +50 -4
  20. package/src/core/ast/expression-builders.ts +22 -15
  21. package/src/core/ast/expression-nodes.ts +6 -0
  22. package/src/core/ddl/introspect/functions/postgres.ts +2 -6
  23. package/src/core/ddl/schema-generator.ts +3 -2
  24. package/src/core/ddl/schema-introspect.ts +1 -1
  25. package/src/core/dialect/abstract.ts +40 -8
  26. package/src/core/dialect/mssql/functions.ts +24 -15
  27. package/src/core/dialect/postgres/functions.ts +33 -24
  28. package/src/core/dialect/sqlite/functions.ts +19 -12
  29. package/src/core/functions/datetime.ts +2 -1
  30. package/src/core/functions/numeric.ts +2 -1
  31. package/src/core/functions/standard-strategy.ts +52 -12
  32. package/src/core/functions/text.ts +2 -1
  33. package/src/core/functions/types.ts +8 -8
  34. package/src/decorators/column.ts +13 -4
  35. package/src/index.ts +13 -5
  36. package/src/orm/domain-event-bus.ts +43 -25
  37. package/src/orm/entity-context.ts +30 -0
  38. package/src/orm/entity-meta.ts +42 -2
  39. package/src/orm/entity-metadata.ts +1 -6
  40. package/src/orm/entity.ts +88 -88
  41. package/src/orm/execute.ts +42 -25
  42. package/src/orm/execution-context.ts +18 -0
  43. package/src/orm/hydration-context.ts +16 -0
  44. package/src/orm/identity-map.ts +4 -0
  45. package/src/orm/interceptor-pipeline.ts +29 -0
  46. package/src/orm/lazy-batch.ts +6 -6
  47. package/src/orm/orm-session.ts +245 -0
  48. package/src/orm/orm.ts +58 -0
  49. package/src/orm/query-logger.ts +15 -0
  50. package/src/orm/relation-change-processor.ts +5 -1
  51. package/src/orm/relations/belongs-to.ts +45 -44
  52. package/src/orm/relations/has-many.ts +44 -43
  53. package/src/orm/relations/has-one.ts +140 -139
  54. package/src/orm/relations/many-to-many.ts +46 -45
  55. package/src/orm/runtime-types.ts +60 -2
  56. package/src/orm/transaction-runner.ts +7 -0
  57. package/src/orm/unit-of-work.ts +7 -1
  58. package/src/query-builder/insert-query-state.ts +13 -3
  59. package/src/query-builder/select-helpers.ts +50 -0
  60. package/src/query-builder/select.ts +616 -18
  61. package/src/query-builder/update-query-state.ts +31 -9
  62. package/src/schema/types.ts +16 -6
  63. package/src/orm/orm-context.ts +0 -159
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@vitest/ui": "^4.0.14",
49
+ "sqlite3": "^5.1.7",
49
50
  "tsup": "^8.0.0",
50
51
  "typescript": "^5.5.0",
51
52
  "vitest": "^4.0.14"
@@ -0,0 +1,64 @@
1
+ import type { TableNode, FunctionTableNode } from '../core/ast/query.js';
2
+ import type { ColumnNode } from '../core/ast/expression.js';
3
+
4
+ /**
5
+ * Strategy interface for converting database names to TypeScript identifiers
6
+ */
7
+ export interface NamingStrategy {
8
+ /**
9
+ * Converts a table name to a TypeScript symbol name
10
+ * @param table - Table node, function table node, or name
11
+ * @returns Valid TypeScript identifier
12
+ */
13
+ tableToSymbol(table: TableNode | FunctionTableNode | string): string;
14
+
15
+ /**
16
+ * Converts a column reference to a property name
17
+ * @param column - Column node
18
+ * @returns Valid TypeScript property name
19
+ */
20
+ columnToProperty(column: ColumnNode): string;
21
+ }
22
+
23
+ /**
24
+ * Default naming strategy that maintains backward compatibility
25
+ * with the original capitalize() behavior
26
+ */
27
+ export class DefaultNamingStrategy implements NamingStrategy {
28
+ /**
29
+ * Converts table names to TypeScript symbols
30
+ * @param table - Table node, function table node, or string name
31
+ * @returns Capitalized table name (handles schema-qualified names)
32
+ */
33
+ tableToSymbol(table: TableNode | FunctionTableNode | string): string {
34
+ const tableName = typeof table === 'string' ? table : table.name;
35
+
36
+ // Handle schema-qualified names (e.g., "auth.user" → "AuthUser")
37
+ if (tableName.includes('.')) {
38
+ return tableName.split('.')
39
+ .map(part => this.capitalize(part))
40
+ .join('');
41
+ }
42
+
43
+ return this.capitalize(tableName);
44
+ }
45
+
46
+ /**
47
+ * Converts column references to property names
48
+ * @param column - Column node
49
+ * @returns Column name as-is (for backward compatibility)
50
+ */
51
+ columnToProperty(column: ColumnNode): string {
52
+ return column.name;
53
+ }
54
+
55
+ /**
56
+ * Capitalizes the first letter of a string
57
+ * @param s - String to capitalize
58
+ * @returns Capitalized string
59
+ */
60
+ private capitalize(s: string): string {
61
+ if (!s) return s;
62
+ return s.charAt(0).toUpperCase() + s.slice(1);
63
+ }
64
+ }
@@ -1,7 +1,7 @@
1
- import { SelectQueryNode } from '../core/ast/query.js';
2
- import {
3
- ExpressionNode,
4
- OperandNode,
1
+ import { SelectQueryNode } from '../core/ast/query.js';
2
+ import {
3
+ ExpressionNode,
4
+ OperandNode,
5
5
  BinaryExpressionNode,
6
6
  LogicalExpressionNode,
7
7
  InExpressionNode,
@@ -20,18 +20,12 @@ import {
20
20
  visitExpression,
21
21
  visitOperand
22
22
  } from '../core/ast/expression.js';
23
- import { SQL_OPERATOR_REGISTRY } from '../core/sql/sql-operator-config.js';
24
- import { SqlOperator } from '../core/sql/sql.js';
25
- import { isRelationAlias } from '../query-builder/relation-alias.js';
26
- import { HydrationMetadata } from '../core/hydration/types.js';
27
- import { getJoinRelationName } from '../core/ast/join-metadata.js';
28
-
29
- /**
30
- * Capitalizes the first letter of a string
31
- * @param s - String to capitalize
32
- * @returns Capitalized string
33
- */
34
- const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
23
+ import { SQL_OPERATOR_REGISTRY } from '../core/sql/sql-operator-config.js';
24
+ import { SqlOperator } from '../core/sql/sql.js';
25
+ import { isRelationAlias } from '../query-builder/relation-alias.js';
26
+ import { HydrationMetadata } from '../core/hydration/types.js';
27
+ import { getJoinRelationName } from '../core/ast/join-metadata.js';
28
+ import { NamingStrategy, DefaultNamingStrategy } from './naming-strategy.js';
35
29
 
36
30
  const assertNever = (value: never): never => {
37
31
  throw new Error(`Unhandled SQL operator: ${value}`);
@@ -41,6 +35,7 @@ const assertNever = (value: never): never => {
41
35
  * Generates TypeScript code from query AST nodes
42
36
  */
43
37
  export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVisitor<string> {
38
+ constructor(private namingStrategy: NamingStrategy = new DefaultNamingStrategy()) {}
44
39
 
45
40
  /**
46
41
  * Generates TypeScript code from a query AST
@@ -54,14 +49,14 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
54
49
  return lines.join('\n');
55
50
  }
56
51
 
57
- /**
58
- * Builds TypeScript method chain lines from query AST
59
- * @param ast - Query AST
60
- * @returns Array of TypeScript method chain lines
61
- */
62
- private buildSelectLines(ast: SelectQueryNode): string[] {
63
- const lines: string[] = [];
64
- const hydration = (ast.meta as HydrationMetadata | undefined)?.hydration;
52
+ /**
53
+ * Builds TypeScript method chain lines from query AST
54
+ * @param ast - Query AST
55
+ * @returns Array of TypeScript method chain lines
56
+ */
57
+ private buildSelectLines(ast: SelectQueryNode): string[] {
58
+ const lines: string[] = [];
59
+ const hydration = (ast.meta as HydrationMetadata | undefined)?.hydration;
65
60
  const hydratedRelations = new Set(hydration?.relations?.map(r => r.name) ?? []);
66
61
 
67
62
  const selections = ast.columns
@@ -77,29 +72,29 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
77
72
  lines.push(` ${sel}${index < selections.length - 1 ? ',' : ''}`);
78
73
  });
79
74
  lines.push(`})`);
80
- lines.push(`.from(${capitalize(ast.from.name)})`);
81
-
82
- if (ast.distinct && ast.distinct.length) {
83
- const cols = ast.distinct.map(c => `${capitalize(c.table)}.${c.name}`).join(', ');
84
- lines.push(`.distinct(${cols})`);
85
- }
86
-
87
- ast.joins.forEach(join => {
88
- const relationName = getJoinRelationName(join);
89
- if (relationName && hydratedRelations.has(relationName)) {
90
- return;
91
- }
92
-
93
- if (relationName) {
94
- if (join.kind === 'INNER') {
95
- lines.push(`.joinRelation('${relationName}')`);
96
- } else {
97
- lines.push(`.joinRelation('${relationName}', '${join.kind}')`);
98
- }
99
- } else {
100
- const table = capitalize(join.table.name);
101
- const cond = this.printExpression(join.condition);
102
- let method = 'innerJoin';
75
+ lines.push(`.from(${this.namingStrategy.tableToSymbol(ast.from)})`);
76
+
77
+ if (ast.distinct && ast.distinct.length) {
78
+ const cols = ast.distinct.map(c => `${this.namingStrategy.tableToSymbol(c.table)}.${c.name}`).join(', ');
79
+ lines.push(`.distinct(${cols})`);
80
+ }
81
+
82
+ ast.joins.forEach(join => {
83
+ const relationName = getJoinRelationName(join);
84
+ if (relationName && hydratedRelations.has(relationName)) {
85
+ return;
86
+ }
87
+
88
+ if (relationName) {
89
+ if (join.kind === 'INNER') {
90
+ lines.push(`.joinRelation('${relationName}')`);
91
+ } else {
92
+ lines.push(`.joinRelation('${relationName}', '${join.kind}')`);
93
+ }
94
+ } else {
95
+ const table = this.namingStrategy.tableToSymbol(join.table);
96
+ const cond = this.printExpression(join.condition);
97
+ let method = 'innerJoin';
103
98
  if (join.kind === 'LEFT') method = 'leftJoin';
104
99
  if (join.kind === 'RIGHT') method = 'rightJoin';
105
100
  lines.push(`.${method}(${table}, ${cond})`);
@@ -121,7 +116,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
121
116
  }
122
117
 
123
118
  if (ast.groupBy && ast.groupBy.length) {
124
- const cols = ast.groupBy.map(c => `${capitalize(c.table)}.${c.name}`).join(', ');
119
+ const cols = ast.groupBy.map(c => `${this.namingStrategy.tableToSymbol(c.table)}.${c.name}`).join(', ');
125
120
  lines.push(`.groupBy(${cols})`);
126
121
  }
127
122
 
@@ -131,7 +126,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
131
126
 
132
127
  if (ast.orderBy && ast.orderBy.length) {
133
128
  ast.orderBy.forEach(o => {
134
- lines.push(`.orderBy(${capitalize(o.column.table)}.${o.column.name}, '${o.direction}')`);
129
+ lines.push(`.orderBy(${this.namingStrategy.tableToSymbol(o.column.table)}.${o.column.name}, '${o.direction}')`);
135
130
  });
136
131
  }
137
132
 
@@ -292,7 +287,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
292
287
  * @returns TypeScript code representation
293
288
  */
294
289
  private printColumnOperand(column: ColumnNode): string {
295
- return `${capitalize(column.table)}.${column.name}`;
290
+ return `${this.namingStrategy.tableToSymbol(column.table)}.${column.name}`;
296
291
  }
297
292
 
298
293
  /**
@@ -321,7 +316,7 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
321
316
  * @returns TypeScript code representation
322
317
  */
323
318
  private printJsonPathOperand(json: JsonPathNode): string {
324
- return `jsonPath(${capitalize(json.column.table)}.${json.column.name}, '${json.path}')`;
319
+ return `jsonPath(${this.namingStrategy.tableToSymbol(json.column.table)}.${json.column.name}, '${json.path}')`;
325
320
  }
326
321
 
327
322
  /**
@@ -364,14 +359,14 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
364
359
 
365
360
  if (node.partitionBy && node.partitionBy.length > 0) {
366
361
  const partitionClause =
367
- 'PARTITION BY ' + node.partitionBy.map(col => `${capitalize(col.table)}.${col.name}`).join(', ');
362
+ 'PARTITION BY ' + node.partitionBy.map(col => `${this.namingStrategy.tableToSymbol(col.table)}.${col.name}`).join(', ');
368
363
  parts.push(partitionClause);
369
364
  }
370
365
 
371
366
  if (node.orderBy && node.orderBy.length > 0) {
372
367
  const orderClause =
373
368
  'ORDER BY ' +
374
- node.orderBy.map(o => `${capitalize(o.column.table)}.${o.column.name} ${o.direction}`).join(', ');
369
+ node.orderBy.map(o => `${this.namingStrategy.tableToSymbol(o.column.table)}.${o.column.name} ${o.direction}`).join(', ');
375
370
  parts.push(orderClause);
376
371
  }
377
372
 
@@ -1,6 +1,8 @@
1
1
  import { ColumnNode, FunctionNode } from './expression-nodes.js';
2
- import { columnOperand } from './expression-builders.js';
2
+ import { columnOperand, valueToOperand, ValueOperandInput } from './expression-builders.js';
3
3
  import { ColumnRef } from './types.js';
4
+ import { OrderByNode } from './query.js';
5
+ import { ORDER_DIRECTIONS, OrderDirection } from '../sql/sql.js';
4
6
 
5
7
  const buildAggregate = (name: string) => (col: ColumnRef | ColumnNode): FunctionNode => ({
6
8
  type: 'Function',
@@ -25,6 +27,50 @@ export const sum = buildAggregate('SUM');
25
27
  /**
26
28
  * Creates an AVG function expression
27
29
  * @param col - Column to average
28
- * @returns Function node with AVG
29
- */
30
- export const avg = buildAggregate('AVG');
30
+ * @returns Function node with AVG
31
+ */
32
+ export const avg = buildAggregate('AVG');
33
+
34
+ /**
35
+ * Creates a MIN function expression
36
+ * @param col - Column to take the minimum of
37
+ * @returns Function node with MIN
38
+ */
39
+ export const min = buildAggregate('MIN');
40
+
41
+ /**
42
+ * Creates a MAX function expression
43
+ * @param col - Column to take the maximum of
44
+ * @returns Function node with MAX
45
+ */
46
+ export const max = buildAggregate('MAX');
47
+
48
+ type GroupConcatOrderByInput = {
49
+ column: ColumnRef | ColumnNode;
50
+ direction?: OrderDirection;
51
+ };
52
+
53
+ export type GroupConcatOptions = {
54
+ separator?: ValueOperandInput;
55
+ orderBy?: GroupConcatOrderByInput[];
56
+ };
57
+
58
+ const toOrderByNode = (order: GroupConcatOrderByInput): OrderByNode => ({
59
+ type: 'OrderBy',
60
+ column: columnOperand(order.column),
61
+ direction: order.direction ?? ORDER_DIRECTIONS.ASC
62
+ });
63
+
64
+ /**
65
+ * Aggregates grouped strings into a single value.
66
+ */
67
+ export const groupConcat = (
68
+ col: ColumnRef | ColumnNode,
69
+ options?: GroupConcatOptions
70
+ ): FunctionNode => ({
71
+ type: 'Function',
72
+ name: 'GROUP_CONCAT',
73
+ args: [columnOperand(col)],
74
+ orderBy: options?.orderBy?.map(toOrderByNode),
75
+ separator: options?.separator !== undefined ? valueToOperand(options.separator) : undefined
76
+ });
@@ -19,22 +19,23 @@ import {
19
19
  isOperandNode
20
20
  } from './expression-nodes.js';
21
21
 
22
+ export type LiteralValue = LiteralNode['value'];
23
+ export type ValueOperandInput = OperandNode | LiteralValue;
24
+
22
25
  /**
23
26
  * Converts a primitive or existing operand into an operand node
24
27
  * @param value - Value or operand to normalize
25
28
  * @returns OperandNode representing the value
26
29
  */
27
- export const valueToOperand = (value: unknown): OperandNode => {
28
- if (
29
- value === null ||
30
- value === undefined ||
31
- typeof value === 'string' ||
32
- typeof value === 'number' ||
33
- typeof value === 'boolean'
34
- ) {
35
- return { type: 'Literal', value: value === undefined ? null : value } as LiteralNode;
30
+ export const valueToOperand = (value: ValueOperandInput): OperandNode => {
31
+ if (isOperandNode(value)) {
32
+ return value;
36
33
  }
37
- return value as OperandNode;
34
+
35
+ return {
36
+ type: 'Literal',
37
+ value
38
+ } as LiteralNode;
38
39
  };
39
40
 
40
41
  const toNode = (col: ColumnRef | OperandNode): OperandNode => {
@@ -48,12 +49,18 @@ const toLiteralNode = (value: string | number | boolean | null): LiteralNode =>
48
49
  value
49
50
  });
50
51
 
51
- const toOperand = (val: OperandNode | ColumnRef | string | number | boolean | null): OperandNode => {
52
- if (val === null) return { type: 'Literal', value: null };
53
- if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
54
- return { type: 'Literal', value: val };
52
+ const isLiteralValue = (value: unknown): value is LiteralValue =>
53
+ value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
54
+
55
+ export const isValueOperandInput = (value: unknown): value is ValueOperandInput =>
56
+ isOperandNode(value) || isLiteralValue(value);
57
+
58
+ const toOperand = (val: OperandNode | ColumnRef | LiteralValue): OperandNode => {
59
+ if (isLiteralValue(val)) {
60
+ return valueToOperand(val);
55
61
  }
56
- return toNode(val as OperandNode | ColumnRef);
62
+
63
+ return toNode(val);
57
64
  };
58
65
 
59
66
  export const columnOperand = (col: ColumnRef | ColumnNode): ColumnNode => toNode(col) as ColumnNode;
@@ -37,6 +37,12 @@ export interface FunctionNode {
37
37
  args: OperandNode[];
38
38
  /** Optional alias for the function result */
39
39
  alias?: string;
40
+ /** Optional ORDER BY clause used by aggregations like GROUP_CONCAT */
41
+ orderBy?: OrderByNode[];
42
+ /** Optional separator argument used by GROUP_CONCAT-like functions */
43
+ separator?: OperandNode;
44
+ /** Optional DISTINCT modifier */
45
+ distinct?: boolean;
40
46
  }
41
47
 
42
48
  /**
@@ -1,14 +1,10 @@
1
1
  // Small helpers to build Postgres-specific function calls as AST FunctionNodes
2
- import { columnOperand, valueToOperand } from '../../../ast/expression-builders.js';
2
+ import { valueToOperand } from '../../../ast/expression-builders.js';
3
3
  import type { OperandNode, FunctionNode } from '../../../ast/expression.js';
4
4
 
5
5
  type OperandInput = OperandNode | string | number | boolean | null;
6
6
 
7
- const toOperand = (v: OperandInput) => {
8
- if (v === null) return valueToOperand(null);
9
- if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return valueToOperand(v);
10
- return v as OperandNode;
11
- };
7
+ const toOperand = (v: OperandInput): OperandNode => valueToOperand(v);
12
8
 
13
9
  const fn = (name: string, args: OperandInput[]): FunctionNode => ({
14
10
  type: 'Function',
@@ -1,6 +1,6 @@
1
1
  import type { TableDef, IndexDef, IndexColumn } from '../../schema/table.js';
2
2
  import type { ColumnDef, ForeignKeyReference } from '../../schema/column.js';
3
- import type { SchemaDialect, DialectName } from './schema-dialect.js';
3
+ import type { SchemaDialect } from './schema-dialect.js';
4
4
  import { deriveIndexName } from './naming-strategy.js';
5
5
  import {
6
6
  formatLiteral,
@@ -10,6 +10,7 @@ import {
10
10
  Quoter
11
11
  } from './sql-writing.js';
12
12
  import { DatabaseTable, DatabaseColumn, ColumnDiff } from './schema-types.js';
13
+ import { DialectName } from './schema-dialect.js';
13
14
 
14
15
  export interface SchemaGenerateResult {
15
16
  tableSql: string;
@@ -152,4 +153,4 @@ const orderTablesByDependencies = (tables: TableDef[]): TableDef[] => {
152
153
  };
153
154
 
154
155
  // Re-export DialectName for backward compatibility
155
- export { DialectName };
156
+ export type { DialectName };
@@ -1,4 +1,4 @@
1
- import { DialectName } from './schema-generator.js';
1
+ import type { DialectName } from './schema-generator.js';
2
2
  import { DatabaseSchema } from './schema-types.js';
3
3
  import { DbExecutor } from '../execution/db-executor.js';
4
4
  import type { IntrospectOptions, SchemaIntrospector, IntrospectContext } from './introspect/types.js';
@@ -291,6 +291,34 @@ export abstract class Dialect
291
291
  this.registerDefaultExpressionCompilers();
292
292
  }
293
293
 
294
+ /**
295
+ * Creates a new Dialect instance (for testing purposes)
296
+ * @param functionStrategy - Optional function strategy
297
+ * @returns New Dialect instance
298
+ */
299
+ static create(functionStrategy?: FunctionStrategy): Dialect {
300
+ // Create a minimal concrete implementation for testing
301
+ class TestDialect extends Dialect {
302
+ protected readonly dialect: DialectName = 'sqlite';
303
+ quoteIdentifier(id: string): string {
304
+ return `"${id}"`;
305
+ }
306
+ protected compileSelectAst(): never {
307
+ throw new Error('Not implemented');
308
+ }
309
+ protected compileInsertAst(): never {
310
+ throw new Error('Not implemented');
311
+ }
312
+ protected compileUpdateAst(): never {
313
+ throw new Error('Not implemented');
314
+ }
315
+ protected compileDeleteAst(): never {
316
+ throw new Error('Not implemented');
317
+ }
318
+ }
319
+ return new TestDialect(functionStrategy);
320
+ }
321
+
294
322
  /**
295
323
  * Registers an expression compiler for a specific node type
296
324
  * @param type - Expression node type
@@ -448,12 +476,16 @@ export abstract class Dialect
448
476
  /**
449
477
  * Compiles a function operand, using the dialect's function strategy.
450
478
  */
451
- protected compileFunctionOperand(fnNode: FunctionNode, ctx: CompilerContext): string {
452
- const compiledArgs = fnNode.args.map(arg => this.compileOperand(arg, ctx));
453
- const renderer = this.functionStrategy.getRenderer(fnNode.name);
454
- if (renderer) {
455
- return renderer({ node: fnNode, compiledArgs });
456
- }
457
- return `${fnNode.name}(${compiledArgs.join(', ')})`;
458
- }
479
+ protected compileFunctionOperand(fnNode: FunctionNode, ctx: CompilerContext): string {
480
+ const compiledArgs = fnNode.args.map(arg => this.compileOperand(arg, ctx));
481
+ const renderer = this.functionStrategy.getRenderer(fnNode.name);
482
+ if (renderer) {
483
+ return renderer({
484
+ node: fnNode,
485
+ compiledArgs,
486
+ compileOperand: operand => this.compileOperand(operand, ctx)
487
+ });
488
+ }
489
+ return `${fnNode.name}(${compiledArgs.join(', ')})`;
490
+ }
459
491
  }
@@ -84,18 +84,27 @@ export class MssqlFunctionStrategy extends StandardFunctionStrategy {
84
84
  return `DATEPART(dw, ${compiledArgs[0]})`;
85
85
  });
86
86
 
87
- this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
88
- if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
89
- return `DATEPART(wk, ${compiledArgs[0]})`;
90
- });
91
-
92
- this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
93
- if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
94
- const [, date] = compiledArgs;
95
- const partArg = node.args[0] as LiteralNode;
96
- const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
97
- // SQL Server 2022+ has DATETRUNC
98
- return `DATETRUNC(${partClean}, ${date})`;
99
- });
100
- }
101
- }
87
+ this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
88
+ if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
89
+ return `DATEPART(wk, ${compiledArgs[0]})`;
90
+ });
91
+
92
+ this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
93
+ if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
94
+ const [, date] = compiledArgs;
95
+ const partArg = node.args[0] as LiteralNode;
96
+ const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
97
+ // SQL Server 2022+ has DATETRUNC
98
+ return `DATETRUNC(${partClean}, ${date})`;
99
+ });
100
+
101
+ this.add('GROUP_CONCAT', ctx => {
102
+ const arg = ctx.compiledArgs[0];
103
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
104
+ const separator = ctx.compileOperand(separatorOperand);
105
+ const orderClause = this.buildOrderByExpression(ctx);
106
+ const withinGroup = orderClause ? ` WITHIN GROUP (${orderClause})` : '';
107
+ return `STRING_AGG(${arg}, ${separator})${withinGroup}`;
108
+ });
109
+ }
110
+ }
@@ -69,27 +69,36 @@ export class PostgresFunctionStrategy extends StandardFunctionStrategy {
69
69
  return `TO_CHAR(${date}, ${format})`;
70
70
  });
71
71
 
72
- this.add('END_OF_MONTH', ({ compiledArgs }) => {
73
- if (compiledArgs.length !== 1) throw new Error('END_OF_MONTH expects 1 argument');
74
- return `(date_trunc('month', ${compiledArgs[0]}) + interval '1 month' - interval '1 day')::DATE`;
75
- });
76
-
77
- this.add('DAY_OF_WEEK', ({ compiledArgs }) => {
78
- if (compiledArgs.length !== 1) throw new Error('DAY_OF_WEEK expects 1 argument');
79
- return `EXTRACT(DOW FROM ${compiledArgs[0]})`;
80
- });
81
-
82
- this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
83
- if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
84
- return `EXTRACT(WEEK FROM ${compiledArgs[0]})`;
85
- });
86
-
87
- this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
88
- if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
89
- const [, date] = compiledArgs;
90
- const partArg = node.args[0] as LiteralNode;
91
- const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
92
- return `DATE_TRUNC('${partClean}', ${date})`;
93
- });
94
- }
95
- }
72
+ this.add('END_OF_MONTH', ({ compiledArgs }) => {
73
+ if (compiledArgs.length !== 1) throw new Error('END_OF_MONTH expects 1 argument');
74
+ return `(date_trunc('month', ${compiledArgs[0]}) + interval '1 month' - interval '1 day')::DATE`;
75
+ });
76
+
77
+ this.add('DAY_OF_WEEK', ({ compiledArgs }) => {
78
+ if (compiledArgs.length !== 1) throw new Error('DAY_OF_WEEK expects 1 argument');
79
+ return `EXTRACT(DOW FROM ${compiledArgs[0]})`;
80
+ });
81
+
82
+ this.add('WEEK_OF_YEAR', ({ compiledArgs }) => {
83
+ if (compiledArgs.length !== 1) throw new Error('WEEK_OF_YEAR expects 1 argument');
84
+ return `EXTRACT(WEEK FROM ${compiledArgs[0]})`;
85
+ });
86
+
87
+ this.add('DATE_TRUNC', ({ node, compiledArgs }) => {
88
+ if (compiledArgs.length !== 2) throw new Error('DATE_TRUNC expects 2 arguments (part, date)');
89
+ const [, date] = compiledArgs;
90
+ const partArg = node.args[0] as LiteralNode;
91
+ const partClean = String(partArg.value).replace(/['"]/g, '').toLowerCase();
92
+ return `DATE_TRUNC('${partClean}', ${date})`;
93
+ });
94
+
95
+ this.add('GROUP_CONCAT', ctx => {
96
+ const arg = ctx.compiledArgs[0];
97
+ const orderClause = this.buildOrderByExpression(ctx);
98
+ const orderSegment = orderClause ? ` ${orderClause}` : '';
99
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
100
+ const separator = ctx.compileOperand(separatorOperand);
101
+ return `STRING_AGG(${arg}, ${separator}${orderSegment})`;
102
+ });
103
+ }
104
+ }
@@ -1,12 +1,12 @@
1
- import { StandardFunctionStrategy } from '../../functions/standard-strategy.js';
2
- import { FunctionRenderContext } from '../../functions/types.js';
3
- import { LiteralNode } from '../../ast/expression.js';
4
-
5
- export class SqliteFunctionStrategy extends StandardFunctionStrategy {
6
- constructor() {
7
- super();
8
- this.registerOverrides();
9
- }
1
+ import { StandardFunctionStrategy } from '../../functions/standard-strategy.js';
2
+ import { FunctionRenderContext } from '../../functions/types.js';
3
+ import { LiteralNode } from '../../ast/expression.js';
4
+
5
+ export class SqliteFunctionStrategy extends StandardFunctionStrategy {
6
+ constructor() {
7
+ super();
8
+ this.registerOverrides();
9
+ }
10
10
 
11
11
  private registerOverrides() {
12
12
  // Override Standard/Abstract definitions with SQLite specifics
@@ -110,6 +110,13 @@ export class SqliteFunctionStrategy extends StandardFunctionStrategy {
110
110
  return `date(${date})`;
111
111
  }
112
112
  return `date(${date}, 'start of ${partClean}')`;
113
- });
114
- }
115
- }
113
+ });
114
+
115
+ this.add('GROUP_CONCAT', ctx => {
116
+ const arg = ctx.compiledArgs[0];
117
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
118
+ const separator = ctx.compileOperand(separatorOperand);
119
+ return `GROUP_CONCAT(${arg}, ${separator})`;
120
+ });
121
+ }
122
+ }
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
11
11
  const toOperand = (input: OperandInput): OperandNode => {
12
12
  if (isOperandNode(input)) return input;
13
13
  if (isColumnDef(input)) return columnOperand(input);
14
- return valueToOperand(input as any);
14
+
15
+ return valueToOperand(input);
15
16
  };
16
17
 
17
18
  const fn = (key: string, args: OperandInput[]): FunctionNode => ({
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
11
11
  const toOperand = (input: OperandInput): OperandNode => {
12
12
  if (isOperandNode(input)) return input;
13
13
  if (isColumnDef(input)) return columnOperand(input);
14
- return valueToOperand(input as any);
14
+
15
+ return valueToOperand(input);
15
16
  };
16
17
 
17
18
  const fn = (key: string, args: OperandInput[]): FunctionNode => ({