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.
- package/README.md +64 -61
- package/dist/decorators/index.cjs +490 -175
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -5
- package/dist/decorators/index.d.ts +1 -5
- package/dist/decorators/index.js +490 -175
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +1044 -483
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -15
- package/dist/index.d.ts +67 -15
- package/dist/index.js +1033 -482
- package/dist/index.js.map +1 -1
- package/dist/{select-Bkv8g8u_.d.cts → select-BPCn6MOH.d.cts} +486 -32
- package/dist/{select-Bkv8g8u_.d.ts → select-BPCn6MOH.d.ts} +486 -32
- package/package.json +2 -1
- package/src/codegen/naming-strategy.ts +64 -0
- package/src/codegen/typescript.ts +48 -53
- package/src/core/ast/aggregate-functions.ts +50 -4
- package/src/core/ast/expression-builders.ts +22 -15
- package/src/core/ast/expression-nodes.ts +6 -0
- package/src/core/ddl/introspect/functions/postgres.ts +2 -6
- package/src/core/ddl/schema-generator.ts +3 -2
- package/src/core/ddl/schema-introspect.ts +1 -1
- package/src/core/dialect/abstract.ts +40 -8
- package/src/core/dialect/mssql/functions.ts +24 -15
- package/src/core/dialect/postgres/functions.ts +33 -24
- package/src/core/dialect/sqlite/functions.ts +19 -12
- package/src/core/functions/datetime.ts +2 -1
- package/src/core/functions/numeric.ts +2 -1
- package/src/core/functions/standard-strategy.ts +52 -12
- package/src/core/functions/text.ts +2 -1
- package/src/core/functions/types.ts +8 -8
- package/src/decorators/column.ts +13 -4
- package/src/index.ts +13 -5
- package/src/orm/domain-event-bus.ts +43 -25
- package/src/orm/entity-context.ts +30 -0
- package/src/orm/entity-meta.ts +42 -2
- package/src/orm/entity-metadata.ts +1 -6
- package/src/orm/entity.ts +88 -88
- package/src/orm/execute.ts +42 -25
- package/src/orm/execution-context.ts +18 -0
- package/src/orm/hydration-context.ts +16 -0
- package/src/orm/identity-map.ts +4 -0
- package/src/orm/interceptor-pipeline.ts +29 -0
- package/src/orm/lazy-batch.ts +6 -6
- package/src/orm/orm-session.ts +245 -0
- package/src/orm/orm.ts +58 -0
- package/src/orm/query-logger.ts +15 -0
- package/src/orm/relation-change-processor.ts +5 -1
- package/src/orm/relations/belongs-to.ts +45 -44
- package/src/orm/relations/has-many.ts +44 -43
- package/src/orm/relations/has-one.ts +140 -139
- package/src/orm/relations/many-to-many.ts +46 -45
- package/src/orm/runtime-types.ts +60 -2
- package/src/orm/transaction-runner.ts +7 -0
- package/src/orm/unit-of-work.ts +7 -1
- package/src/query-builder/insert-query-state.ts +13 -3
- package/src/query-builder/select-helpers.ts +50 -0
- package/src/query-builder/select.ts +616 -18
- package/src/query-builder/update-query-state.ts +31 -9
- package/src/schema/types.ts +16 -6
- 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.
|
|
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(${
|
|
81
|
-
|
|
82
|
-
if (ast.distinct && ast.distinct.length) {
|
|
83
|
-
const cols = ast.distinct.map(c => `${
|
|
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 =
|
|
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 => `${
|
|
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(${
|
|
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 `${
|
|
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(${
|
|
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 => `${
|
|
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 => `${
|
|
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:
|
|
28
|
-
if (
|
|
29
|
-
value
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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({
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
|
|
15
|
+
return valueToOperand(input);
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const fn = (key: string, args: OperandInput[]): FunctionNode => ({
|