metal-orm 1.0.42 → 1.0.43
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 +22 -7
- package/dist/index.cjs +130 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +121 -96
- package/dist/index.d.ts +121 -96
- package/dist/index.js +128 -74
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/scripts/run-eslint.mjs +34 -0
- package/src/codegen/typescript.ts +32 -15
- package/src/core/ast/builders.ts +7 -2
- package/src/core/ast/expression-builders.ts +0 -2
- package/src/core/ast/expression-nodes.ts +14 -5
- package/src/core/ast/expression-visitor.ts +11 -8
- package/src/core/ast/join-node.ts +1 -1
- package/src/core/ast/query.ts +6 -6
- package/src/core/ast/window-functions.ts +10 -2
- package/src/core/ddl/dialects/base-schema-dialect.ts +30 -3
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +4 -0
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +2 -0
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +13 -1
- package/src/core/ddl/dialects/render-reference.test.ts +69 -0
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +9 -0
- package/src/core/ddl/introspect/mssql.ts +42 -8
- package/src/core/ddl/introspect/mysql.ts +30 -6
- package/src/core/ddl/introspect/postgres.ts +88 -34
- package/src/core/ddl/introspect/run-select.ts +6 -4
- package/src/core/ddl/introspect/sqlite.ts +56 -11
- package/src/core/ddl/introspect/types.ts +0 -1
- package/src/core/ddl/introspect/utils.ts +3 -3
- package/src/core/ddl/schema-dialect.ts +1 -0
- package/src/core/ddl/schema-generator.ts +4 -12
- package/src/core/ddl/sql-writing.ts +4 -4
- package/src/core/dialect/abstract.ts +18 -6
- package/src/core/dialect/base/function-table-formatter.ts +3 -2
- package/src/core/dialect/base/join-compiler.ts +5 -3
- package/src/core/dialect/base/returning-strategy.ts +1 -0
- package/src/core/dialect/base/sql-dialect.ts +3 -3
- package/src/core/dialect/mssql/functions.ts +24 -25
- package/src/core/dialect/mssql/index.ts +1 -4
- package/src/core/dialect/mysql/functions.ts +0 -1
- package/src/core/dialect/postgres/functions.ts +33 -34
- package/src/core/dialect/postgres/index.ts +1 -0
- package/src/core/dialect/sqlite/functions.ts +18 -19
- package/src/core/dialect/sqlite/index.ts +2 -0
- package/src/core/execution/db-executor.ts +1 -1
- package/src/core/execution/executors/mysql-executor.ts +2 -2
- package/src/core/execution/executors/postgres-executor.ts +1 -1
- package/src/core/execution/pooling/pool.ts +2 -0
- package/src/core/functions/datetime.ts +1 -1
- package/src/core/functions/numeric.ts +1 -1
- package/src/core/functions/text.ts +1 -1
- package/src/decorators/bootstrap.ts +27 -8
- package/src/decorators/column.ts +3 -11
- package/src/decorators/decorator-metadata.ts +3 -9
- package/src/decorators/entity.ts +21 -5
- package/src/decorators/relations.ts +2 -11
- package/src/orm/entity-context.ts +8 -8
- package/src/orm/entity-meta.ts +8 -8
- package/src/orm/entity-metadata.ts +11 -9
- package/src/orm/entity.ts +28 -29
- package/src/orm/execute.ts +4 -4
- package/src/orm/hydration.ts +42 -39
- package/src/orm/identity-map.ts +1 -1
- package/src/orm/lazy-batch.ts +9 -9
- package/src/orm/orm-session.ts +24 -23
- package/src/orm/orm.ts +2 -5
- package/src/orm/relation-change-processor.ts +12 -11
- package/src/orm/relations/belongs-to.ts +11 -11
- package/src/orm/relations/has-many.ts +10 -10
- package/src/orm/relations/has-one.ts +8 -7
- package/src/orm/relations/many-to-many.ts +13 -13
- package/src/orm/runtime-types.ts +4 -4
- package/src/orm/save-graph.ts +31 -25
- package/src/orm/unit-of-work.ts +17 -17
- package/src/query-builder/delete.ts +4 -3
- package/src/query-builder/hydration-manager.ts +6 -5
- package/src/query-builder/insert.ts +12 -8
- package/src/query-builder/query-ast-service.ts +2 -2
- package/src/query-builder/raw-column-parser.ts +2 -1
- package/src/query-builder/select-helpers.ts +2 -2
- package/src/query-builder/select.ts +31 -31
- package/src/query-builder/update.ts +4 -3
- package/src/schema/column.ts +26 -26
- package/src/schema/table.ts +47 -18
- package/src/schema/types.ts +22 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metal-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"engines": {
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"gen:entities": "node scripts/generate-entities.mjs",
|
|
29
29
|
"test": "vitest",
|
|
30
30
|
"test:ui": "vitest --ui",
|
|
31
|
-
"show-sql": "node scripts/show-sql.mjs"
|
|
31
|
+
"show-sql": "node scripts/show-sql.mjs",
|
|
32
|
+
"lint": "node scripts/run-eslint.mjs",
|
|
33
|
+
"lint:fix": "node scripts/run-eslint.mjs --fix"
|
|
32
34
|
},
|
|
33
35
|
"peerDependencies": {
|
|
34
36
|
"mysql2": "^3.9.0",
|
|
@@ -52,6 +54,10 @@
|
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@vitest/ui": "^4.0.14",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
|
58
|
+
"@typescript-eslint/parser": "^8.20.0",
|
|
59
|
+
"eslint": "^8.57.0",
|
|
60
|
+
"eslint-plugin-deprecation": "^3.0.0",
|
|
55
61
|
"mysql2": "^3.15.3",
|
|
56
62
|
"pg": "^8.16.3",
|
|
57
63
|
"sqlite3": "^5.1.7",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
const [, , ...userArgs] = process.argv;
|
|
6
|
+
|
|
7
|
+
const TARGETS = {
|
|
8
|
+
src: ['./src'],
|
|
9
|
+
all: ['./src', './tests', './scripts', './playground'],
|
|
10
|
+
playground: ['./playground'],
|
|
11
|
+
'src/core': ['./src/core'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const [firstArg, ...restArgs] = userArgs;
|
|
15
|
+
const hasTarget = firstArg && !firstArg.startsWith('-');
|
|
16
|
+
|
|
17
|
+
const targetKey = hasTarget ? firstArg : 'all';
|
|
18
|
+
const extraArgs = hasTarget ? restArgs : userArgs;
|
|
19
|
+
|
|
20
|
+
const paths = TARGETS[targetKey] ?? [targetKey];
|
|
21
|
+
|
|
22
|
+
if (paths.length === 0) {
|
|
23
|
+
console.error(`Unknown lint target: ${targetKey}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const eslintArgs = [...paths, '--ext', '.ts,.tsx,.mjs', ...extraArgs];
|
|
28
|
+
|
|
29
|
+
const eslintBinBase = path.join(process.cwd(), 'node_modules', '.bin', 'eslint');
|
|
30
|
+
const executable = process.platform === 'win32' ? `${eslintBinBase}.cmd` : eslintBinBase;
|
|
31
|
+
|
|
32
|
+
const child = spawn(executable, eslintArgs, { stdio: 'inherit', shell: true });
|
|
33
|
+
|
|
34
|
+
child.on('exit', (code) => process.exit(code));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SelectQueryNode } from '../core/ast/query.js';
|
|
1
|
+
import { OrderingTerm, SelectQueryNode } from '../core/ast/query.js';
|
|
2
2
|
import {
|
|
3
3
|
ExpressionNode,
|
|
4
4
|
OperandNode,
|
|
@@ -36,8 +36,15 @@ const assertNever = (value: never): never => {
|
|
|
36
36
|
/**
|
|
37
37
|
* Generates TypeScript code from query AST nodes
|
|
38
38
|
*/
|
|
39
|
+
type SelectionColumn =
|
|
40
|
+
| ColumnNode
|
|
41
|
+
| FunctionNode
|
|
42
|
+
| ScalarSubqueryNode
|
|
43
|
+
| CaseExpressionNode
|
|
44
|
+
| WindowFunctionNode;
|
|
45
|
+
|
|
39
46
|
export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVisitor<string> {
|
|
40
|
-
constructor(private namingStrategy: NamingStrategy = new DefaultNamingStrategy()) {}
|
|
47
|
+
constructor(private namingStrategy: NamingStrategy = new DefaultNamingStrategy()) { }
|
|
41
48
|
|
|
42
49
|
/**
|
|
43
50
|
* Generates TypeScript code from a query AST
|
|
@@ -62,12 +69,8 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
62
69
|
const hydratedRelations = new Set(hydration?.relations?.map(r => r.name) ?? []);
|
|
63
70
|
|
|
64
71
|
const selections = ast.columns
|
|
65
|
-
.filter(col => !(hydration && isRelationAlias(
|
|
66
|
-
.map(col => {
|
|
67
|
-
const key = (col as any).alias || (col as any).name;
|
|
68
|
-
const operand = col as OperandNode;
|
|
69
|
-
return `${key}: ${this.printOperand(operand)}`;
|
|
70
|
-
});
|
|
72
|
+
.filter((col): col is SelectionColumn => !(hydration && isRelationAlias(col.alias)))
|
|
73
|
+
.map((col, index) => `${this.getSelectionKey(col, index)}: ${this.printOperand(col)}`);
|
|
71
74
|
|
|
72
75
|
lines.push(`db.select({`);
|
|
73
76
|
selections.forEach((sel, index) => {
|
|
@@ -168,26 +171,40 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
|
|
|
168
171
|
/**
|
|
169
172
|
* Prints an ordering term (operand/expression/alias) to TypeScript code.
|
|
170
173
|
*/
|
|
171
|
-
private printOrderingTerm(term:
|
|
172
|
-
if (!term || !(
|
|
174
|
+
private printOrderingTerm(term: OrderingTerm): string {
|
|
175
|
+
if (!term || !('type' in term)) {
|
|
173
176
|
throw new Error('Unsupported ordering term');
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
switch (
|
|
179
|
+
switch (term.type) {
|
|
177
180
|
case 'Column':
|
|
178
|
-
return `${this.namingStrategy.tableToSymbol(
|
|
181
|
+
return `${this.namingStrategy.tableToSymbol(term.table)}.${term.name}`;
|
|
179
182
|
case 'AliasRef':
|
|
180
|
-
return this.visitAliasRef(term
|
|
183
|
+
return this.visitAliasRef(term);
|
|
181
184
|
case 'Literal':
|
|
182
185
|
case 'Function':
|
|
183
186
|
case 'JsonPath':
|
|
184
187
|
case 'ScalarSubquery':
|
|
185
188
|
case 'CaseExpression':
|
|
186
189
|
case 'WindowFunction':
|
|
187
|
-
return this.printOperand(term
|
|
190
|
+
return this.printOperand(term);
|
|
188
191
|
default:
|
|
189
|
-
return this.printExpression(term
|
|
192
|
+
return this.printExpression(term);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private getSelectionKey(selection: SelectionColumn, index: number): string {
|
|
197
|
+
if (selection.alias) {
|
|
198
|
+
return selection.alias;
|
|
199
|
+
}
|
|
200
|
+
if (this.isNamedSelection(selection)) {
|
|
201
|
+
return selection.name;
|
|
190
202
|
}
|
|
203
|
+
return `selection${index + 1}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private isNamedSelection(selection: SelectionColumn): selection is ColumnNode | FunctionNode | WindowFunctionNode {
|
|
207
|
+
return 'name' in selection;
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
public visitBinaryExpression(binary: BinaryExpressionNode): string {
|
package/src/core/ast/builders.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColumnNode } from './expression-nodes.js';
|
|
1
|
+
import { ColumnNode, OperandNode } from './expression-nodes.js';
|
|
2
2
|
import { TableNode, FunctionTableNode, DerivedTableNode } from './query.js';
|
|
3
3
|
import { ColumnRef, TableRef } from './types.js';
|
|
4
4
|
|
|
@@ -50,7 +50,12 @@ export const createTableNode = (table: TableRef): TableNode => ({
|
|
|
50
50
|
/**
|
|
51
51
|
* Creates a FunctionTable node for expressions like `function_name(args...)` used in FROM
|
|
52
52
|
*/
|
|
53
|
-
export const fnTable = (
|
|
53
|
+
export const fnTable = (
|
|
54
|
+
name: string,
|
|
55
|
+
args: OperandNode[] = [],
|
|
56
|
+
alias?: string,
|
|
57
|
+
opts?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
|
|
58
|
+
): FunctionTableNode => ({
|
|
54
59
|
type: 'FunctionTable',
|
|
55
60
|
name,
|
|
56
61
|
args,
|
|
@@ -3,12 +3,10 @@ import { SqlOperator } from '../sql/sql.js';
|
|
|
3
3
|
import { ColumnRef } from './types.js';
|
|
4
4
|
import {
|
|
5
5
|
ColumnNode,
|
|
6
|
-
FunctionNode,
|
|
7
6
|
LiteralNode,
|
|
8
7
|
JsonPathNode,
|
|
9
8
|
OperandNode,
|
|
10
9
|
CaseExpressionNode,
|
|
11
|
-
WindowFunctionNode,
|
|
12
10
|
BinaryExpressionNode,
|
|
13
11
|
ExpressionNode,
|
|
14
12
|
LogicalExpressionNode,
|
|
@@ -146,11 +146,20 @@ const operandTypes = new Set<OperandNode['type']>([
|
|
|
146
146
|
'WindowFunction'
|
|
147
147
|
]);
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
export const
|
|
153
|
-
|
|
149
|
+
const hasTypeProperty = (value: unknown): value is { type?: string } =>
|
|
150
|
+
typeof value === 'object' && value !== null && 'type' in value;
|
|
151
|
+
|
|
152
|
+
export const isOperandNode = (node: unknown): node is OperandNode => {
|
|
153
|
+
if (!hasTypeProperty(node)) return false;
|
|
154
|
+
return operandTypes.has(node.type as OperandNode['type']);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const isFunctionNode = (node: unknown): node is FunctionNode =>
|
|
158
|
+
isOperandNode(node) && node.type === 'Function';
|
|
159
|
+
export const isCaseExpressionNode = (node: unknown): node is CaseExpressionNode =>
|
|
160
|
+
isOperandNode(node) && node.type === 'CaseExpression';
|
|
161
|
+
export const isWindowFunctionNode = (node: unknown): node is WindowFunctionNode =>
|
|
162
|
+
isOperandNode(node) && node.type === 'WindowFunction';
|
|
154
163
|
export const isExpressionSelectionNode = (
|
|
155
164
|
node: ColumnRef | FunctionNode | CaseExpressionNode | WindowFunctionNode
|
|
156
165
|
): node is FunctionNode | CaseExpressionNode | WindowFunctionNode =>
|
|
@@ -47,8 +47,8 @@ export interface OperandVisitor<R> {
|
|
|
47
47
|
otherwise?(node: OperandNode): R;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
type ExpressionDispatch = <R>(node:
|
|
51
|
-
type OperandDispatch = <R>(node:
|
|
50
|
+
type ExpressionDispatch = <R>(node: ExpressionNode, visitor: ExpressionVisitor<R>) => R;
|
|
51
|
+
type OperandDispatch = <R>(node: OperandNode, visitor: OperandVisitor<R>) => R;
|
|
52
52
|
|
|
53
53
|
const expressionDispatchers = new Map<string, ExpressionDispatch>();
|
|
54
54
|
const operandDispatchers = new Map<string, OperandDispatch>();
|
|
@@ -75,12 +75,15 @@ export const registerOperandDispatcher = (type: string, dispatcher: OperandDispa
|
|
|
75
75
|
export const clearExpressionDispatchers = (): void => expressionDispatchers.clear();
|
|
76
76
|
export const clearOperandDispatchers = (): void => operandDispatchers.clear();
|
|
77
77
|
|
|
78
|
+
const getNodeType = (node: { type?: string } | null | undefined): string | undefined =>
|
|
79
|
+
typeof node === 'object' && node !== null && typeof node.type === 'string' ? node.type : undefined;
|
|
80
|
+
|
|
78
81
|
const unsupportedExpression = (node: ExpressionNode): never => {
|
|
79
|
-
throw new Error(`Unsupported expression type "${(node
|
|
82
|
+
throw new Error(`Unsupported expression type "${getNodeType(node) ?? 'unknown'}"`);
|
|
80
83
|
};
|
|
81
84
|
|
|
82
85
|
const unsupportedOperand = (node: OperandNode): never => {
|
|
83
|
-
throw new Error(`Unsupported operand type "${(node
|
|
86
|
+
throw new Error(`Unsupported operand type "${getNodeType(node) ?? 'unknown'}"`);
|
|
84
87
|
};
|
|
85
88
|
/**
|
|
86
89
|
* Dispatches an expression node to the visitor
|
|
@@ -88,8 +91,8 @@ const unsupportedOperand = (node: OperandNode): never => {
|
|
|
88
91
|
* @param visitor - Visitor implementation
|
|
89
92
|
*/
|
|
90
93
|
export const visitExpression = <R>(node: ExpressionNode, visitor: ExpressionVisitor<R>): R => {
|
|
91
|
-
const dynamic = expressionDispatchers.get(
|
|
92
|
-
if (dynamic) return dynamic(node
|
|
94
|
+
const dynamic = expressionDispatchers.get(node.type);
|
|
95
|
+
if (dynamic) return dynamic(node, visitor);
|
|
93
96
|
|
|
94
97
|
switch (node.type) {
|
|
95
98
|
case 'BinaryExpression':
|
|
@@ -126,8 +129,8 @@ export const visitExpression = <R>(node: ExpressionNode, visitor: ExpressionVisi
|
|
|
126
129
|
* @param visitor - Visitor implementation
|
|
127
130
|
*/
|
|
128
131
|
export const visitOperand = <R>(node: OperandNode, visitor: OperandVisitor<R>): R => {
|
|
129
|
-
const dynamic = operandDispatchers.get(
|
|
130
|
-
if (dynamic) return dynamic(node
|
|
132
|
+
const dynamic = operandDispatchers.get(node.type);
|
|
133
|
+
if (dynamic) return dynamic(node, visitor);
|
|
131
134
|
|
|
132
135
|
switch (node.type) {
|
|
133
136
|
case 'Column':
|
|
@@ -2,7 +2,7 @@ import { JoinNode } from './join.js';
|
|
|
2
2
|
import { ExpressionNode } from './expression.js';
|
|
3
3
|
import { JoinKind } from '../sql/sql.js';
|
|
4
4
|
import { JoinMetadata } from './join-metadata.js';
|
|
5
|
-
import { TableSourceNode, TableNode
|
|
5
|
+
import { TableSourceNode, TableNode } from './query.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Creates a JoinNode ready for AST insertion.
|
package/src/core/ast/query.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AliasRefNode,
|
|
3
|
+
CaseExpressionNode,
|
|
2
4
|
ColumnNode,
|
|
3
|
-
FunctionNode,
|
|
4
5
|
ExpressionNode,
|
|
5
|
-
|
|
6
|
-
CaseExpressionNode,
|
|
7
|
-
WindowFunctionNode,
|
|
6
|
+
FunctionNode,
|
|
8
7
|
OperandNode,
|
|
9
|
-
|
|
8
|
+
ScalarSubqueryNode,
|
|
9
|
+
WindowFunctionNode
|
|
10
10
|
} from './expression.js';
|
|
11
11
|
import { JoinNode } from './join.js';
|
|
12
12
|
import { OrderDirection } from '../sql/sql.js';
|
|
@@ -34,7 +34,7 @@ export interface FunctionTableNode {
|
|
|
34
34
|
/** Optional schema for the function (some dialects) */
|
|
35
35
|
schema?: string;
|
|
36
36
|
/** Function arguments as operand nodes */
|
|
37
|
-
args?:
|
|
37
|
+
args?: OperandNode[];
|
|
38
38
|
/** Optional alias for the function table */
|
|
39
39
|
alias?: string;
|
|
40
40
|
/** LATERAL flag */
|
|
@@ -60,7 +60,11 @@ export const ntile = (n: number): WindowFunctionNode =>
|
|
|
60
60
|
* @param defaultValue - Default value if no row exists
|
|
61
61
|
* @returns Window function node for LAG
|
|
62
62
|
*/
|
|
63
|
-
export const lag = (
|
|
63
|
+
export const lag = (
|
|
64
|
+
col: ColumnRef | ColumnNode,
|
|
65
|
+
offset: number = 1,
|
|
66
|
+
defaultValue?: LiteralNode['value']
|
|
67
|
+
): WindowFunctionNode => {
|
|
64
68
|
const args: (ColumnNode | LiteralNode | JsonPathNode)[] = [
|
|
65
69
|
columnOperand(col),
|
|
66
70
|
{ type: 'Literal', value: offset }
|
|
@@ -78,7 +82,11 @@ export const lag = (col: ColumnRef | ColumnNode, offset: number = 1, defaultValu
|
|
|
78
82
|
* @param defaultValue - Default value if no row exists
|
|
79
83
|
* @returns Window function node for LEAD
|
|
80
84
|
*/
|
|
81
|
-
export const lead = (
|
|
85
|
+
export const lead = (
|
|
86
|
+
col: ColumnRef | ColumnNode,
|
|
87
|
+
offset: number = 1,
|
|
88
|
+
defaultValue?: LiteralNode['value']
|
|
89
|
+
): WindowFunctionNode => {
|
|
82
90
|
const args: (ColumnNode | LiteralNode | JsonPathNode)[] = [
|
|
83
91
|
columnOperand(col),
|
|
84
92
|
{ type: 'Literal', value: offset }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaDialect, DialectName } from '../schema-dialect.js';
|
|
2
|
-
import { formatLiteral, quoteQualified,
|
|
2
|
+
import { formatLiteral, quoteQualified, LiteralFormatter } from '../sql-writing.js';
|
|
3
3
|
import { ColumnDef, ForeignKeyReference } from '../../../schema/column.js';
|
|
4
4
|
import { IndexDef, TableDef } from '../../../schema/table.js';
|
|
5
5
|
import { DatabaseTable, DatabaseColumn, ColumnDiff } from '../schema-types.js';
|
|
@@ -29,16 +29,26 @@ export abstract class BaseSchemaDialect implements SchemaDialect {
|
|
|
29
29
|
abstract get literalFormatter(): LiteralFormatter;
|
|
30
30
|
|
|
31
31
|
renderDefault(value: unknown, _column: ColumnDef): string {
|
|
32
|
+
void _column;
|
|
32
33
|
return formatLiteral(this.literalFormatter, value);
|
|
33
34
|
}
|
|
34
35
|
renderReference(ref: ForeignKeyReference, _table: TableDef): string {
|
|
36
|
+
void _table;
|
|
35
37
|
const parts = ['REFERENCES', quoteQualified(this, ref.table), `(${this.quoteIdentifier(ref.column)})`];
|
|
36
38
|
if (ref.onDelete) parts.push('ON DELETE', ref.onDelete);
|
|
37
39
|
if (ref.onUpdate) parts.push('ON UPDATE', ref.onUpdate);
|
|
38
|
-
|
|
40
|
+
const suffix = this.renderReferenceSuffix(ref, _table);
|
|
41
|
+
if (suffix) parts.push(suffix);
|
|
39
42
|
return parts.join(' ');
|
|
40
43
|
}
|
|
44
|
+
|
|
45
|
+
protected renderReferenceSuffix(ref: ForeignKeyReference, _table: TableDef): string | undefined {
|
|
46
|
+
void ref;
|
|
47
|
+
void _table;
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
41
50
|
renderTableOptions(_table: TableDef): string | undefined {
|
|
51
|
+
void _table;
|
|
42
52
|
return undefined;
|
|
43
53
|
}
|
|
44
54
|
dropTableSql(table: DatabaseTable): string[] {
|
|
@@ -51,12 +61,29 @@ export abstract class BaseSchemaDialect implements SchemaDialect {
|
|
|
51
61
|
return [`DROP INDEX ${this.quoteIdentifier(index)};`];
|
|
52
62
|
}
|
|
53
63
|
warnDropColumn(_table: DatabaseTable, _column: string): string | undefined {
|
|
64
|
+
void _table;
|
|
65
|
+
void _column;
|
|
54
66
|
return undefined;
|
|
55
67
|
}
|
|
56
|
-
alterColumnSql?(
|
|
68
|
+
alterColumnSql?(_table: TableDef, _column: ColumnDef, _actualColumn: DatabaseColumn, _diff: ColumnDiff): string[] {
|
|
69
|
+
void _table;
|
|
70
|
+
void _column;
|
|
71
|
+
void _actualColumn;
|
|
72
|
+
void _diff;
|
|
57
73
|
return [];
|
|
58
74
|
}
|
|
59
75
|
warnAlterColumn?(_table: TableDef, _column: ColumnDef, _actual: DatabaseColumn, _diff: ColumnDiff): string | undefined {
|
|
76
|
+
void _table;
|
|
77
|
+
void _column;
|
|
78
|
+
void _actual;
|
|
79
|
+
void _diff;
|
|
60
80
|
return undefined;
|
|
61
81
|
}
|
|
82
|
+
|
|
83
|
+
preferInlinePkAutoincrement(_column: ColumnDef, _table: TableDef, _pk: string[]): boolean {
|
|
84
|
+
void _column;
|
|
85
|
+
void _table;
|
|
86
|
+
void _pk;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
62
89
|
}
|
|
@@ -120,6 +120,7 @@ export class MSSqlSchemaDialect extends BaseSchemaDialect {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
alterColumnSql(table: TableDef, column: ColumnDef, _actual: DatabaseColumn, diff: ColumnDiff): string[] {
|
|
123
|
+
void _actual;
|
|
123
124
|
const stmts: string[] = [];
|
|
124
125
|
if (diff.typeChanged || diff.nullabilityChanged) {
|
|
125
126
|
const nullability = column.notNull ? 'NOT NULL' : 'NULL';
|
|
@@ -131,6 +132,9 @@ export class MSSqlSchemaDialect extends BaseSchemaDialect {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
warnAlterColumn(_table: TableDef, _column: ColumnDef, _actual: DatabaseColumn, diff: ColumnDiff): string | undefined {
|
|
135
|
+
void _table;
|
|
136
|
+
void _column;
|
|
137
|
+
void _actual;
|
|
134
138
|
if (diff.defaultChanged || diff.autoIncrementChanged) {
|
|
135
139
|
return 'Altering defaults or identity on MSSQL is not automated (requires dropping/adding default or identity constraints manually).';
|
|
136
140
|
}
|
|
@@ -125,6 +125,8 @@ export class MySqlSchemaDialect extends BaseSchemaDialect {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
alterColumnSql(table: TableDef, column: ColumnDef, _actual: DatabaseColumn, _diff: ColumnDiff): string[] {
|
|
128
|
+
void _actual;
|
|
129
|
+
void _diff;
|
|
128
130
|
const rendered = renderColumnDefinition(table, column, this);
|
|
129
131
|
return [`ALTER TABLE ${this.formatTableName(table)} MODIFY COLUMN ${rendered.sql};`];
|
|
130
132
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseSchemaDialect } from './base-schema-dialect.js';
|
|
2
2
|
import { deriveIndexName } from '../naming-strategy.js';
|
|
3
3
|
import { renderIndexColumns, createLiteralFormatter } from '../sql-writing.js';
|
|
4
|
-
import { ColumnDef } from '../../../schema/column.js';
|
|
4
|
+
import { ColumnDef, ForeignKeyReference } from '../../../schema/column.js';
|
|
5
5
|
import { IndexDef, TableDef } from '../../../schema/table.js';
|
|
6
6
|
import { ColumnDiff, DatabaseColumn, DatabaseTable } from '../schema-types.js';
|
|
7
7
|
import { DialectName } from '../schema-dialect.js';
|
|
@@ -102,6 +102,14 @@ export class PostgresSchemaDialect extends BaseSchemaDialect {
|
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
protected renderReferenceSuffix(ref: ForeignKeyReference, _table: TableDef): string | undefined {
|
|
106
|
+
void _table;
|
|
107
|
+
if (ref.deferrable) {
|
|
108
|
+
return 'DEFERRABLE INITIALLY DEFERRED';
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
105
113
|
dropColumnSql(table: DatabaseTable, column: string): string[] {
|
|
106
114
|
return [`ALTER TABLE ${this.formatTableName(table)} DROP COLUMN ${this.quoteIdentifier(column)};`];
|
|
107
115
|
}
|
|
@@ -114,6 +122,7 @@ export class PostgresSchemaDialect extends BaseSchemaDialect {
|
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
alterColumnSql(table: TableDef, column: ColumnDef, actualColumn: DatabaseColumn, diff: ColumnDiff): string[] {
|
|
125
|
+
void actualColumn;
|
|
117
126
|
const stmts: string[] = [];
|
|
118
127
|
const tableName = this.formatTableName(table);
|
|
119
128
|
const colName = this.quoteIdentifier(column.name);
|
|
@@ -145,6 +154,9 @@ export class PostgresSchemaDialect extends BaseSchemaDialect {
|
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
warnAlterColumn(_table: TableDef, _column: ColumnDef, _actual: DatabaseColumn, diff: ColumnDiff): string | undefined {
|
|
157
|
+
void _table;
|
|
158
|
+
void _column;
|
|
159
|
+
void _actual;
|
|
148
160
|
if (diff.autoIncrementChanged) {
|
|
149
161
|
return 'Altering identity properties may fail if an existing sequence is attached; verify generated column state.';
|
|
150
162
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { BaseSchemaDialect } from './base-schema-dialect.js';
|
|
3
|
+
import { PostgresSchemaDialect } from './postgres-schema-dialect.js';
|
|
4
|
+
import { TableDef } from '../../../schema/table.js';
|
|
5
|
+
import { ForeignKeyReference } from '../../../schema/column.js';
|
|
6
|
+
import { createLiteralFormatter } from '../sql-writing.js';
|
|
7
|
+
|
|
8
|
+
class DummySchemaDialect extends BaseSchemaDialect {
|
|
9
|
+
readonly name = 'sqlite';
|
|
10
|
+
private readonly formatter = createLiteralFormatter({
|
|
11
|
+
booleanTrue: '1',
|
|
12
|
+
booleanFalse: '0',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
get literalFormatter() {
|
|
16
|
+
return this.formatter;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
quoteIdentifier(id: string): string {
|
|
20
|
+
return `"${id}"`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
renderColumnType(): string {
|
|
24
|
+
return 'INTEGER';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
renderAutoIncrement(): string | undefined {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
renderIndex(): string {
|
|
32
|
+
return 'CREATE INDEX dummy;';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const table: TableDef = {
|
|
37
|
+
name: 'child',
|
|
38
|
+
columns: {},
|
|
39
|
+
relations: {},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const deferrableReference: ForeignKeyReference = {
|
|
43
|
+
table: 'parent',
|
|
44
|
+
column: 'id',
|
|
45
|
+
deferrable: true,
|
|
46
|
+
onDelete: 'CASCADE',
|
|
47
|
+
onUpdate: 'NO ACTION',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe('renderReference deferrable handling', () => {
|
|
51
|
+
it('base dialect remains agnostic to deferrable flags', () => {
|
|
52
|
+
const dialect = new DummySchemaDialect();
|
|
53
|
+
const sql = dialect.renderReference(deferrableReference, table);
|
|
54
|
+
expect(sql).toContain('REFERENCES "parent"');
|
|
55
|
+
expect(sql).not.toContain('DEFERRABLE INITIALLY DEFERRED');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('Postgres dialect renders the deferrable clause', () => {
|
|
59
|
+
const dialect = new PostgresSchemaDialect();
|
|
60
|
+
const sql = dialect.renderReference(deferrableReference, table);
|
|
61
|
+
expect(sql).toContain('DEFERRABLE INITIALLY DEFERRED');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('Postgres dialect skips the clause when the flag is missing', () => {
|
|
65
|
+
const dialect = new PostgresSchemaDialect();
|
|
66
|
+
const sql = dialect.renderReference({ table: 'parent', column: 'id' }, table);
|
|
67
|
+
expect(sql).not.toContain('DEFERRABLE INITIALLY DEFERRED');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -103,10 +103,13 @@ export class SQLiteSchemaDialect extends BaseSchemaDialect {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
dropColumnSql(_table: DatabaseTable, _column: string): string[] {
|
|
106
|
+
void _table;
|
|
107
|
+
void _column;
|
|
106
108
|
return [];
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
dropIndexSql(_table: DatabaseTable, index: string): string[] {
|
|
112
|
+
void _table;
|
|
110
113
|
return [`DROP INDEX IF EXISTS ${this.quoteIdentifier(index)};`];
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -116,10 +119,16 @@ export class SQLiteSchemaDialect extends BaseSchemaDialect {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
alterColumnSql(_table: TableDef, _column: ColumnDef, _actual: DatabaseColumn, _diff: ColumnDiff): string[] {
|
|
122
|
+
void _table;
|
|
123
|
+
void _column;
|
|
124
|
+
void _actual;
|
|
125
|
+
void _diff;
|
|
119
126
|
return [];
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
warnAlterColumn(table: TableDef, column: ColumnDef, _actual: DatabaseColumn, _diff: ColumnDiff): string | undefined {
|
|
130
|
+
void _actual;
|
|
131
|
+
void _diff;
|
|
123
132
|
const key = table.schema ? `${table.schema}.${table.name}` : table.name;
|
|
124
133
|
return `SQLite ALTER COLUMN is not supported; rebuild table ${key} to change column ${column.name}.`;
|
|
125
134
|
}
|
|
@@ -3,13 +3,47 @@ import { queryRows, shouldIncludeTable } from './utils.js';
|
|
|
3
3
|
import { DatabaseSchema, DatabaseTable, DatabaseIndex, DatabaseColumn } from '../schema-types.js';
|
|
4
4
|
import { DbExecutor } from '../../execution/db-executor.js';
|
|
5
5
|
|
|
6
|
+
type MssqlColumnRow = {
|
|
7
|
+
table_schema: string;
|
|
8
|
+
table_name: string;
|
|
9
|
+
column_name: string;
|
|
10
|
+
data_type: string;
|
|
11
|
+
is_nullable: boolean | number;
|
|
12
|
+
is_identity: boolean | number;
|
|
13
|
+
column_default: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type MssqlPrimaryKeyRow = {
|
|
17
|
+
table_schema: string;
|
|
18
|
+
table_name: string;
|
|
19
|
+
column_name: string;
|
|
20
|
+
key_ordinal: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type MssqlIndexRow = {
|
|
24
|
+
table_schema: string;
|
|
25
|
+
table_name: string;
|
|
26
|
+
index_name: string;
|
|
27
|
+
is_unique: boolean | number;
|
|
28
|
+
has_filter: boolean | number;
|
|
29
|
+
filter_definition: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type MssqlIndexColumnRow = {
|
|
33
|
+
table_schema: string;
|
|
34
|
+
table_name: string;
|
|
35
|
+
index_name: string;
|
|
36
|
+
column_name: string;
|
|
37
|
+
key_ordinal: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
6
40
|
export const mssqlIntrospector: SchemaIntrospector = {
|
|
7
41
|
async introspect(ctx: { executor: DbExecutor }, options: IntrospectOptions): Promise<DatabaseSchema> {
|
|
8
42
|
const schema = options.schema;
|
|
9
43
|
const filterSchema = schema ? 'sch.name = @p1' : '1=1';
|
|
10
44
|
const params = schema ? [schema] : [];
|
|
11
45
|
|
|
12
|
-
const columnRows = await queryRows(
|
|
46
|
+
const columnRows = (await queryRows(
|
|
13
47
|
ctx.executor,
|
|
14
48
|
`
|
|
15
49
|
SELECT
|
|
@@ -27,9 +61,9 @@ export const mssqlIntrospector: SchemaIntrospector = {
|
|
|
27
61
|
WHERE t.is_ms_shipped = 0 AND ${filterSchema}
|
|
28
62
|
`,
|
|
29
63
|
params
|
|
30
|
-
);
|
|
64
|
+
)) as MssqlColumnRow[];
|
|
31
65
|
|
|
32
|
-
const pkRows = await queryRows(
|
|
66
|
+
const pkRows = (await queryRows(
|
|
33
67
|
ctx.executor,
|
|
34
68
|
`
|
|
35
69
|
SELECT
|
|
@@ -46,7 +80,7 @@ export const mssqlIntrospector: SchemaIntrospector = {
|
|
|
46
80
|
ORDER BY ic.key_ordinal
|
|
47
81
|
`,
|
|
48
82
|
params
|
|
49
|
-
);
|
|
83
|
+
)) as MssqlPrimaryKeyRow[];
|
|
50
84
|
|
|
51
85
|
const pkMap = new Map<string, string[]>();
|
|
52
86
|
pkRows.forEach(r => {
|
|
@@ -56,7 +90,7 @@ export const mssqlIntrospector: SchemaIntrospector = {
|
|
|
56
90
|
pkMap.set(key, list);
|
|
57
91
|
});
|
|
58
92
|
|
|
59
|
-
const indexRows = await queryRows(
|
|
93
|
+
const indexRows = (await queryRows(
|
|
60
94
|
ctx.executor,
|
|
61
95
|
`
|
|
62
96
|
SELECT
|
|
@@ -72,9 +106,9 @@ export const mssqlIntrospector: SchemaIntrospector = {
|
|
|
72
106
|
WHERE i.is_primary_key = 0 AND i.is_hypothetical = 0 AND ${filterSchema}
|
|
73
107
|
`,
|
|
74
108
|
params
|
|
75
|
-
);
|
|
109
|
+
)) as MssqlIndexRow[];
|
|
76
110
|
|
|
77
|
-
const indexColsRows = await queryRows(
|
|
111
|
+
const indexColsRows = (await queryRows(
|
|
78
112
|
ctx.executor,
|
|
79
113
|
`
|
|
80
114
|
SELECT
|
|
@@ -92,7 +126,7 @@ export const mssqlIntrospector: SchemaIntrospector = {
|
|
|
92
126
|
ORDER BY ic.key_ordinal
|
|
93
127
|
`,
|
|
94
128
|
params
|
|
95
|
-
);
|
|
129
|
+
)) as MssqlIndexColumnRow[];
|
|
96
130
|
|
|
97
131
|
const indexColumnsMap = new Map<string, { column: string; order: number }[]>();
|
|
98
132
|
indexColsRows.forEach(r => {
|