linkgress-orm 0.0.1
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/LICENSE +21 -0
- package/README.md +196 -0
- package/dist/database/database-client.interface.d.ts +45 -0
- package/dist/database/database-client.interface.d.ts.map +1 -0
- package/dist/database/database-client.interface.js +20 -0
- package/dist/database/database-client.interface.js.map +1 -0
- package/dist/database/index.d.ts +5 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +10 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/pg-client.d.ts +30 -0
- package/dist/database/pg-client.d.ts.map +1 -0
- package/dist/database/pg-client.js +76 -0
- package/dist/database/pg-client.js.map +1 -0
- package/dist/database/postgres-client.d.ts +44 -0
- package/dist/database/postgres-client.d.ts.map +1 -0
- package/dist/database/postgres-client.js +111 -0
- package/dist/database/postgres-client.js.map +1 -0
- package/dist/database/types.d.ts +200 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +8 -0
- package/dist/database/types.js.map +1 -0
- package/dist/entity/base-entity.d.ts +21 -0
- package/dist/entity/base-entity.d.ts.map +1 -0
- package/dist/entity/base-entity.js +27 -0
- package/dist/entity/base-entity.js.map +1 -0
- package/dist/entity/db-column.d.ts +61 -0
- package/dist/entity/db-column.d.ts.map +1 -0
- package/dist/entity/db-column.js +35 -0
- package/dist/entity/db-column.js.map +1 -0
- package/dist/entity/db-context.d.ts +665 -0
- package/dist/entity/db-context.d.ts.map +1 -0
- package/dist/entity/db-context.js +1463 -0
- package/dist/entity/db-context.js.map +1 -0
- package/dist/entity/entity-base.d.ts +76 -0
- package/dist/entity/entity-base.d.ts.map +1 -0
- package/dist/entity/entity-base.js +42 -0
- package/dist/entity/entity-base.js.map +1 -0
- package/dist/entity/entity-builder.d.ts +171 -0
- package/dist/entity/entity-builder.d.ts.map +1 -0
- package/dist/entity/entity-builder.js +376 -0
- package/dist/entity/entity-builder.js.map +1 -0
- package/dist/entity/model-config.d.ts +18 -0
- package/dist/entity/model-config.d.ts.map +1 -0
- package/dist/entity/model-config.js +157 -0
- package/dist/entity/model-config.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/migration/db-schema-manager.d.ts +228 -0
- package/dist/migration/db-schema-manager.d.ts.map +1 -0
- package/dist/migration/db-schema-manager.js +1055 -0
- package/dist/migration/db-schema-manager.js.map +1 -0
- package/dist/migration/enum-migrator.d.ts +29 -0
- package/dist/migration/enum-migrator.d.ts.map +1 -0
- package/dist/migration/enum-migrator.js +137 -0
- package/dist/migration/enum-migrator.js.map +1 -0
- package/dist/query/collection-strategy.factory.d.ts +16 -0
- package/dist/query/collection-strategy.factory.d.ts.map +1 -0
- package/dist/query/collection-strategy.factory.js +37 -0
- package/dist/query/collection-strategy.factory.js.map +1 -0
- package/dist/query/collection-strategy.interface.d.ts +146 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -0
- package/dist/query/collection-strategy.interface.js +3 -0
- package/dist/query/collection-strategy.interface.js.map +1 -0
- package/dist/query/conditions.d.ts +222 -0
- package/dist/query/conditions.d.ts.map +1 -0
- package/dist/query/conditions.js +446 -0
- package/dist/query/conditions.js.map +1 -0
- package/dist/query/cte-builder.d.ts +95 -0
- package/dist/query/cte-builder.d.ts.map +1 -0
- package/dist/query/cte-builder.js +172 -0
- package/dist/query/cte-builder.js.map +1 -0
- package/dist/query/grouped-query.d.ts +186 -0
- package/dist/query/grouped-query.d.ts.map +1 -0
- package/dist/query/grouped-query.js +588 -0
- package/dist/query/grouped-query.js.map +1 -0
- package/dist/query/join-builder.d.ts +106 -0
- package/dist/query/join-builder.d.ts.map +1 -0
- package/dist/query/join-builder.js +275 -0
- package/dist/query/join-builder.js.map +1 -0
- package/dist/query/query-builder.d.ts +543 -0
- package/dist/query/query-builder.d.ts.map +1 -0
- package/dist/query/query-builder.js +2649 -0
- package/dist/query/query-builder.js.map +1 -0
- package/dist/query/strategies/jsonb-collection-strategy.d.ts +51 -0
- package/dist/query/strategies/jsonb-collection-strategy.d.ts.map +1 -0
- package/dist/query/strategies/jsonb-collection-strategy.js +210 -0
- package/dist/query/strategies/jsonb-collection-strategy.js.map +1 -0
- package/dist/query/strategies/temptable-collection-strategy.d.ts +95 -0
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -0
- package/dist/query/strategies/temptable-collection-strategy.js +456 -0
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -0
- package/dist/query/subquery.d.ts +152 -0
- package/dist/query/subquery.d.ts.map +1 -0
- package/dist/query/subquery.js +206 -0
- package/dist/query/subquery.js.map +1 -0
- package/dist/schema/column-builder.d.ts +127 -0
- package/dist/schema/column-builder.d.ts.map +1 -0
- package/dist/schema/column-builder.js +184 -0
- package/dist/schema/column-builder.js.map +1 -0
- package/dist/schema/inference.d.ts +26 -0
- package/dist/schema/inference.d.ts.map +1 -0
- package/dist/schema/inference.js +3 -0
- package/dist/schema/inference.js.map +1 -0
- package/dist/schema/navigation.d.ts +215 -0
- package/dist/schema/navigation.d.ts.map +1 -0
- package/dist/schema/navigation.js +233 -0
- package/dist/schema/navigation.js.map +1 -0
- package/dist/schema/row-type.d.ts +26 -0
- package/dist/schema/row-type.d.ts.map +1 -0
- package/dist/schema/row-type.js +3 -0
- package/dist/schema/row-type.js.map +1 -0
- package/dist/schema/sequence-builder.d.ts +87 -0
- package/dist/schema/sequence-builder.d.ts.map +1 -0
- package/dist/schema/sequence-builder.js +123 -0
- package/dist/schema/sequence-builder.js.map +1 -0
- package/dist/schema/table-builder.d.ts +122 -0
- package/dist/schema/table-builder.d.ts.map +1 -0
- package/dist/schema/table-builder.js +132 -0
- package/dist/schema/table-builder.js.map +1 -0
- package/dist/schema/typed-schema.d.ts +22 -0
- package/dist/schema/typed-schema.d.ts.map +1 -0
- package/dist/schema/typed-schema.js +28 -0
- package/dist/schema/typed-schema.js.map +1 -0
- package/dist/types/column-types.d.ts +20 -0
- package/dist/types/column-types.d.ts.map +1 -0
- package/dist/types/column-types.js +14 -0
- package/dist/types/column-types.js.map +1 -0
- package/dist/types/custom-types.d.ts +85 -0
- package/dist/types/custom-types.d.ts.map +1 -0
- package/dist/types/custom-types.js +132 -0
- package/dist/types/custom-types.js.map +1 -0
- package/dist/types/enum-builder.d.ts +31 -0
- package/dist/types/enum-builder.d.ts.map +1 -0
- package/dist/types/enum-builder.js +46 -0
- package/dist/types/enum-builder.js.map +1 -0
- package/dist/types/metadata.d.ts +67 -0
- package/dist/types/metadata.d.ts.map +1 -0
- package/dist/types/metadata.js +57 -0
- package/dist/types/metadata.js.map +1 -0
- package/dist/types/type-mapper.d.ts +49 -0
- package/dist/types/type-mapper.d.ts.map +1 -0
- package/dist/types/type-mapper.js +49 -0
- package/dist/types/type-mapper.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DbSchemaManager = void 0;
|
|
37
|
+
const readline = __importStar(require("readline"));
|
|
38
|
+
const enum_builder_1 = require("../types/enum-builder");
|
|
39
|
+
/**
|
|
40
|
+
* Database schema manager - handles schema creation, deletion, and automatic migrations
|
|
41
|
+
*/
|
|
42
|
+
class DbSchemaManager {
|
|
43
|
+
constructor(client, schemaRegistry, options) {
|
|
44
|
+
this.client = client;
|
|
45
|
+
this.schemaRegistry = schemaRegistry;
|
|
46
|
+
this.rl = null;
|
|
47
|
+
this.logQueries = options?.logQueries ?? false;
|
|
48
|
+
this.postMigrationHook = options?.postMigrationHook;
|
|
49
|
+
this.sequenceRegistry = options?.sequenceRegistry ?? new Map();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get or create readline interface for interactive prompts
|
|
53
|
+
*/
|
|
54
|
+
getReadlineInterface() {
|
|
55
|
+
if (!this.rl) {
|
|
56
|
+
this.rl = readline.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return this.rl;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Close readline interface if it exists
|
|
65
|
+
*/
|
|
66
|
+
closeReadlineInterface() {
|
|
67
|
+
if (this.rl) {
|
|
68
|
+
this.rl.close();
|
|
69
|
+
this.rl = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get qualified table name with schema prefix if specified
|
|
74
|
+
*/
|
|
75
|
+
getQualifiedTableName(tableName, schema) {
|
|
76
|
+
return schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create all schemas used by tables
|
|
80
|
+
*/
|
|
81
|
+
async createSchemas() {
|
|
82
|
+
const schemas = new Set();
|
|
83
|
+
// Collect all unique schemas from tables
|
|
84
|
+
for (const tableSchema of this.schemaRegistry.values()) {
|
|
85
|
+
if (tableSchema.schema) {
|
|
86
|
+
schemas.add(tableSchema.schema);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (schemas.size === 0)
|
|
90
|
+
return;
|
|
91
|
+
if (this.logQueries) {
|
|
92
|
+
console.log('Creating schemas...\n');
|
|
93
|
+
}
|
|
94
|
+
for (const schemaName of schemas) {
|
|
95
|
+
const createSchemaSQL = `CREATE SCHEMA IF NOT EXISTS "${schemaName}"`;
|
|
96
|
+
if (this.logQueries) {
|
|
97
|
+
console.log(` Creating schema "${schemaName}"...`);
|
|
98
|
+
}
|
|
99
|
+
await this.client.query(createSchemaSQL);
|
|
100
|
+
if (this.logQueries) {
|
|
101
|
+
console.log(` ✓ Schema "${schemaName}" created\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Create all ENUM types used in the schema
|
|
107
|
+
*/
|
|
108
|
+
async createEnumTypes() {
|
|
109
|
+
const enums = enum_builder_1.EnumTypeRegistry.getAll();
|
|
110
|
+
if (enums.size === 0)
|
|
111
|
+
return;
|
|
112
|
+
if (this.logQueries) {
|
|
113
|
+
console.log('Creating ENUM types...\n');
|
|
114
|
+
}
|
|
115
|
+
for (const [enumName, enumDef] of enums.entries()) {
|
|
116
|
+
// Check if enum already exists
|
|
117
|
+
const checkSQL = `
|
|
118
|
+
SELECT EXISTS (
|
|
119
|
+
SELECT 1 FROM pg_type WHERE typname = $1
|
|
120
|
+
) as exists
|
|
121
|
+
`;
|
|
122
|
+
const result = await this.client.query(checkSQL, [enumName]);
|
|
123
|
+
const exists = result.rows[0]?.exists;
|
|
124
|
+
if (!exists) {
|
|
125
|
+
const values = enumDef.values.map(v => `'${v}'`).join(', ');
|
|
126
|
+
const createEnumSQL = `CREATE TYPE "${enumName}" AS ENUM (${values})`;
|
|
127
|
+
if (this.logQueries) {
|
|
128
|
+
console.log(` Creating ENUM type "${enumName}"...`);
|
|
129
|
+
}
|
|
130
|
+
await this.client.query(createEnumSQL);
|
|
131
|
+
if (this.logQueries) {
|
|
132
|
+
console.log(` ✓ ENUM type "${enumName}" created\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (this.logQueries) {
|
|
136
|
+
console.log(` ENUM type "${enumName}" already exists, skipping\n`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create all sequences registered in the schema
|
|
142
|
+
*/
|
|
143
|
+
async createSequences() {
|
|
144
|
+
if (this.sequenceRegistry.size === 0)
|
|
145
|
+
return;
|
|
146
|
+
if (this.logQueries) {
|
|
147
|
+
console.log('Creating sequences...\n');
|
|
148
|
+
}
|
|
149
|
+
for (const [_, config] of this.sequenceRegistry.entries()) {
|
|
150
|
+
const qualifiedName = config.schema
|
|
151
|
+
? `"${config.schema}"."${config.name}"`
|
|
152
|
+
: `"${config.name}"`;
|
|
153
|
+
// Check if sequence already exists
|
|
154
|
+
const checkSQL = config.schema
|
|
155
|
+
? `SELECT EXISTS (
|
|
156
|
+
SELECT 1 FROM information_schema.sequences
|
|
157
|
+
WHERE sequence_schema = $1 AND sequence_name = $2
|
|
158
|
+
) as exists`
|
|
159
|
+
: `SELECT EXISTS (
|
|
160
|
+
SELECT 1 FROM information_schema.sequences
|
|
161
|
+
WHERE sequence_name = $1 AND sequence_schema = 'public'
|
|
162
|
+
) as exists`;
|
|
163
|
+
const checkParams = config.schema ? [config.schema, config.name] : [config.name];
|
|
164
|
+
const result = await this.client.query(checkSQL, checkParams);
|
|
165
|
+
const exists = result.rows[0]?.exists;
|
|
166
|
+
if (!exists) {
|
|
167
|
+
// Build CREATE SEQUENCE statement
|
|
168
|
+
let createSQL = `CREATE SEQUENCE ${qualifiedName}`;
|
|
169
|
+
const options = [];
|
|
170
|
+
if (config.startWith !== undefined) {
|
|
171
|
+
options.push(`START WITH ${config.startWith}`);
|
|
172
|
+
}
|
|
173
|
+
if (config.incrementBy !== undefined) {
|
|
174
|
+
options.push(`INCREMENT BY ${config.incrementBy}`);
|
|
175
|
+
}
|
|
176
|
+
if (config.minValue !== undefined) {
|
|
177
|
+
options.push(`MINVALUE ${config.minValue}`);
|
|
178
|
+
}
|
|
179
|
+
if (config.maxValue !== undefined) {
|
|
180
|
+
options.push(`MAXVALUE ${config.maxValue}`);
|
|
181
|
+
}
|
|
182
|
+
if (config.cache !== undefined) {
|
|
183
|
+
options.push(`CACHE ${config.cache}`);
|
|
184
|
+
}
|
|
185
|
+
if (config.cycle) {
|
|
186
|
+
options.push(`CYCLE`);
|
|
187
|
+
}
|
|
188
|
+
if (options.length > 0) {
|
|
189
|
+
createSQL += ` ${options.join(' ')}`;
|
|
190
|
+
}
|
|
191
|
+
if (this.logQueries) {
|
|
192
|
+
console.log(` Creating sequence ${qualifiedName}...`);
|
|
193
|
+
}
|
|
194
|
+
await this.client.query(createSQL);
|
|
195
|
+
if (this.logQueries) {
|
|
196
|
+
console.log(` ✓ Sequence ${qualifiedName} created\n`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (this.logQueries) {
|
|
200
|
+
console.log(` Sequence ${qualifiedName} already exists, skipping\n`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Create a single table
|
|
206
|
+
*/
|
|
207
|
+
async createTable(tableName, tableSchema) {
|
|
208
|
+
const columnDefs = [];
|
|
209
|
+
const primaryKeys = [];
|
|
210
|
+
for (const [colKey, colBuilder] of Object.entries(tableSchema.columns)) {
|
|
211
|
+
const config = colBuilder.build();
|
|
212
|
+
let def = `"${config.name}" ${config.type}`;
|
|
213
|
+
if (config.length) {
|
|
214
|
+
def += `(${config.length})`;
|
|
215
|
+
}
|
|
216
|
+
else if (config.precision && config.scale) {
|
|
217
|
+
def += `(${config.precision}, ${config.scale})`;
|
|
218
|
+
}
|
|
219
|
+
else if (config.precision) {
|
|
220
|
+
def += `(${config.precision})`;
|
|
221
|
+
}
|
|
222
|
+
// Handle GENERATED ALWAYS AS IDENTITY
|
|
223
|
+
if (config.identity) {
|
|
224
|
+
def += ' GENERATED ALWAYS AS IDENTITY';
|
|
225
|
+
// Add sequence options if specified
|
|
226
|
+
const seqOptions = [];
|
|
227
|
+
if (config.identity.startWith !== undefined) {
|
|
228
|
+
seqOptions.push(`START WITH ${config.identity.startWith}`);
|
|
229
|
+
}
|
|
230
|
+
if (config.identity.incrementBy !== undefined) {
|
|
231
|
+
seqOptions.push(`INCREMENT BY ${config.identity.incrementBy}`);
|
|
232
|
+
}
|
|
233
|
+
if (seqOptions.length > 0) {
|
|
234
|
+
def += ` (${seqOptions.join(' ')})`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!config.nullable) {
|
|
238
|
+
def += ' NOT NULL';
|
|
239
|
+
}
|
|
240
|
+
if (config.unique && !config.primaryKey) {
|
|
241
|
+
def += ' UNIQUE';
|
|
242
|
+
}
|
|
243
|
+
if (config.default !== undefined && !config.identity) {
|
|
244
|
+
def += ` DEFAULT ${this.formatDefaultValue(config.default)}`;
|
|
245
|
+
}
|
|
246
|
+
columnDefs.push(def);
|
|
247
|
+
if (config.primaryKey) {
|
|
248
|
+
primaryKeys.push(`"${config.name}"`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Add primary key constraint
|
|
252
|
+
if (primaryKeys.length > 0) {
|
|
253
|
+
columnDefs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`);
|
|
254
|
+
}
|
|
255
|
+
// Add foreign key constraints from schema.foreignKeys (includes ON DELETE/ON UPDATE actions)
|
|
256
|
+
const foreignKeys = tableSchema.foreignKeys || [];
|
|
257
|
+
for (const fk of foreignKeys) {
|
|
258
|
+
const columnList = fk.columns.map(c => `"${c}"`).join(', ');
|
|
259
|
+
const refColumnList = fk.referencedColumns.map(c => `"${c}"`).join(', ');
|
|
260
|
+
// Find the referenced table's schema
|
|
261
|
+
const referencedTableSchema = this.schemaRegistry.get(fk.referencedTable);
|
|
262
|
+
const qualifiedReferencedTable = this.getQualifiedTableName(fk.referencedTable, referencedTableSchema?.schema);
|
|
263
|
+
let fkDef = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnList}) REFERENCES ${qualifiedReferencedTable}(${refColumnList})`;
|
|
264
|
+
if (fk.onDelete) {
|
|
265
|
+
fkDef += ` ON DELETE ${fk.onDelete.toUpperCase()}`;
|
|
266
|
+
}
|
|
267
|
+
if (fk.onUpdate) {
|
|
268
|
+
fkDef += ` ON UPDATE ${fk.onUpdate.toUpperCase()}`;
|
|
269
|
+
}
|
|
270
|
+
columnDefs.push(fkDef);
|
|
271
|
+
}
|
|
272
|
+
// Fallback: Add foreign key constraints from column references (for backward compatibility)
|
|
273
|
+
// Only add if not already added via foreignKeys array
|
|
274
|
+
const addedFkColumns = new Set(foreignKeys.flatMap(fk => fk.columns));
|
|
275
|
+
for (const [colKey, colBuilder] of Object.entries(tableSchema.columns)) {
|
|
276
|
+
const config = colBuilder.build();
|
|
277
|
+
if (config.references && !addedFkColumns.has(config.name)) {
|
|
278
|
+
columnDefs.push(`FOREIGN KEY ("${config.name}") REFERENCES "${config.references.table}"("${config.references.column}")`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const qualifiedTableName = this.getQualifiedTableName(tableName, tableSchema.schema);
|
|
282
|
+
const createTableSQL = `
|
|
283
|
+
CREATE TABLE IF NOT EXISTS ${qualifiedTableName} (
|
|
284
|
+
${columnDefs.join(',\n ')}
|
|
285
|
+
)
|
|
286
|
+
`;
|
|
287
|
+
if (this.logQueries) {
|
|
288
|
+
console.log(` Creating table ${qualifiedTableName}...`);
|
|
289
|
+
}
|
|
290
|
+
await this.client.query(createTableSQL);
|
|
291
|
+
if (this.logQueries) {
|
|
292
|
+
console.log(` ✓ Table ${qualifiedTableName} created\n`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Create all tables in the database
|
|
297
|
+
*/
|
|
298
|
+
async ensureCreated() {
|
|
299
|
+
if (this.logQueries) {
|
|
300
|
+
console.log('Creating database schema...\n');
|
|
301
|
+
}
|
|
302
|
+
// Create schemas first
|
|
303
|
+
await this.createSchemas();
|
|
304
|
+
// Create enum types
|
|
305
|
+
await this.createEnumTypes();
|
|
306
|
+
// Create sequences
|
|
307
|
+
await this.createSequences();
|
|
308
|
+
// Create tables
|
|
309
|
+
for (const [tableName, tableSchema] of this.schemaRegistry.entries()) {
|
|
310
|
+
await this.createTable(tableName, tableSchema);
|
|
311
|
+
}
|
|
312
|
+
if (this.logQueries) {
|
|
313
|
+
console.log('✓ Database schema created successfully\n');
|
|
314
|
+
}
|
|
315
|
+
// Execute post-migration hook if provided
|
|
316
|
+
if (this.postMigrationHook) {
|
|
317
|
+
if (this.logQueries) {
|
|
318
|
+
console.log('Executing post-migration scripts...\n');
|
|
319
|
+
}
|
|
320
|
+
await this.postMigrationHook(this.client);
|
|
321
|
+
if (this.logQueries) {
|
|
322
|
+
console.log('✓ Post-migration scripts completed\n');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Drop all tables
|
|
328
|
+
*/
|
|
329
|
+
async ensureDeleted() {
|
|
330
|
+
if (this.logQueries) {
|
|
331
|
+
console.log('Dropping database schema...\n');
|
|
332
|
+
}
|
|
333
|
+
for (const [tableName, tableSchema] of this.schemaRegistry.entries()) {
|
|
334
|
+
const qualifiedTableName = this.getQualifiedTableName(tableName, tableSchema.schema);
|
|
335
|
+
if (this.logQueries) {
|
|
336
|
+
console.log(` Dropping table ${qualifiedTableName}...`);
|
|
337
|
+
}
|
|
338
|
+
await this.client.query(`DROP TABLE IF EXISTS ${qualifiedTableName} CASCADE`);
|
|
339
|
+
if (this.logQueries) {
|
|
340
|
+
console.log(` ✓ Table ${qualifiedTableName} dropped\n`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Drop sequences
|
|
344
|
+
if (this.sequenceRegistry.size > 0 && this.logQueries) {
|
|
345
|
+
console.log('Dropping sequences...\n');
|
|
346
|
+
}
|
|
347
|
+
for (const [_, config] of this.sequenceRegistry.entries()) {
|
|
348
|
+
const qualifiedName = config.schema
|
|
349
|
+
? `"${config.schema}"."${config.name}"`
|
|
350
|
+
: `"${config.name}"`;
|
|
351
|
+
if (this.logQueries) {
|
|
352
|
+
console.log(` Dropping sequence ${qualifiedName}...`);
|
|
353
|
+
}
|
|
354
|
+
await this.client.query(`DROP SEQUENCE IF EXISTS ${qualifiedName} CASCADE`);
|
|
355
|
+
if (this.logQueries) {
|
|
356
|
+
console.log(` ✓ Sequence ${qualifiedName} dropped\n`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Drop enum types
|
|
360
|
+
const enums = enum_builder_1.EnumTypeRegistry.getAll();
|
|
361
|
+
if (enums.size > 0 && this.logQueries) {
|
|
362
|
+
console.log('Dropping ENUM types...\n');
|
|
363
|
+
}
|
|
364
|
+
for (const [enumName, _] of enums.entries()) {
|
|
365
|
+
if (this.logQueries) {
|
|
366
|
+
console.log(` Dropping ENUM type "${enumName}"...`);
|
|
367
|
+
}
|
|
368
|
+
await this.client.query(`DROP TYPE IF EXISTS "${enumName}" CASCADE`);
|
|
369
|
+
if (this.logQueries) {
|
|
370
|
+
console.log(` ✓ ENUM type "${enumName}" dropped\n`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Drop schemas (note: CASCADE will drop all objects in the schema)
|
|
374
|
+
const schemas = new Set();
|
|
375
|
+
for (const tableSchema of this.schemaRegistry.values()) {
|
|
376
|
+
if (tableSchema.schema) {
|
|
377
|
+
schemas.add(tableSchema.schema);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (schemas.size > 0 && this.logQueries) {
|
|
381
|
+
console.log('Dropping schemas...\n');
|
|
382
|
+
}
|
|
383
|
+
for (const schemaName of schemas) {
|
|
384
|
+
if (this.logQueries) {
|
|
385
|
+
console.log(` Dropping schema "${schemaName}"...`);
|
|
386
|
+
}
|
|
387
|
+
await this.client.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
|
|
388
|
+
if (this.logQueries) {
|
|
389
|
+
console.log(` ✓ Schema "${schemaName}" dropped\n`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (this.logQueries) {
|
|
393
|
+
console.log('✓ Database schema dropped successfully\n');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Analyze differences between current DB and model schema
|
|
398
|
+
*/
|
|
399
|
+
async analyze() {
|
|
400
|
+
console.log('🔍 Analyzing database schema...\n');
|
|
401
|
+
const operations = [];
|
|
402
|
+
// Check schemas
|
|
403
|
+
const modelSchemas = new Set();
|
|
404
|
+
for (const tableSchema of this.schemaRegistry.values()) {
|
|
405
|
+
if (tableSchema.schema) {
|
|
406
|
+
modelSchemas.add(tableSchema.schema);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Add schema creation operations if needed
|
|
410
|
+
for (const schemaName of modelSchemas) {
|
|
411
|
+
const result = await this.client.query(`
|
|
412
|
+
SELECT EXISTS (
|
|
413
|
+
SELECT 1 FROM information_schema.schemata WHERE schema_name = $1
|
|
414
|
+
) as exists
|
|
415
|
+
`, [schemaName]);
|
|
416
|
+
if (!result.rows[0]?.exists) {
|
|
417
|
+
operations.push({ type: 'create_schema', schemaName });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Check enums
|
|
421
|
+
const modelEnums = enum_builder_1.EnumTypeRegistry.getAll();
|
|
422
|
+
for (const [enumName, enumDef] of modelEnums.entries()) {
|
|
423
|
+
const result = await this.client.query(`
|
|
424
|
+
SELECT EXISTS (
|
|
425
|
+
SELECT 1 FROM pg_type WHERE typname = $1
|
|
426
|
+
) as exists
|
|
427
|
+
`, [enumName]);
|
|
428
|
+
if (!result.rows[0]?.exists) {
|
|
429
|
+
operations.push({ type: 'create_enum', enumName, values: enumDef.values });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Get all existing tables
|
|
433
|
+
const existingTables = await this.getExistingTables();
|
|
434
|
+
const modelTables = new Set(this.schemaRegistry.keys());
|
|
435
|
+
// Find tables to create
|
|
436
|
+
for (const [tableName, schema] of this.schemaRegistry.entries()) {
|
|
437
|
+
if (!existingTables.has(tableName)) {
|
|
438
|
+
operations.push({ type: 'create_table', tableName, schema });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Compare columns for existing tables
|
|
442
|
+
for (const [tableName, schema] of this.schemaRegistry.entries()) {
|
|
443
|
+
if (existingTables.has(tableName)) {
|
|
444
|
+
const existingColumns = await this.getExistingColumns(tableName, schema.schema);
|
|
445
|
+
const modelColumns = new Map();
|
|
446
|
+
// Build model columns map
|
|
447
|
+
for (const [colKey, colBuilder] of Object.entries(schema.columns)) {
|
|
448
|
+
const config = colBuilder.build();
|
|
449
|
+
modelColumns.set(config.name, config);
|
|
450
|
+
}
|
|
451
|
+
// Find columns to add
|
|
452
|
+
for (const [colName, config] of modelColumns.entries()) {
|
|
453
|
+
if (!existingColumns.has(colName)) {
|
|
454
|
+
operations.push({ type: 'add_column', tableName, columnName: colName, config });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Find columns to alter
|
|
458
|
+
for (const [colName, dbInfo] of existingColumns.entries()) {
|
|
459
|
+
const modelConfig = modelColumns.get(colName);
|
|
460
|
+
if (modelConfig && this.needsAlter(dbInfo, modelConfig)) {
|
|
461
|
+
operations.push({ type: 'alter_column', tableName, columnName: colName, from: dbInfo, to: modelConfig });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Compare indexes
|
|
465
|
+
const existingIndexes = await this.getExistingIndexes(tableName, schema.schema);
|
|
466
|
+
const modelIndexes = schema.indexes || [];
|
|
467
|
+
// Find indexes to create
|
|
468
|
+
for (const modelIndex of modelIndexes) {
|
|
469
|
+
const exists = existingIndexes.some(dbIndex => dbIndex.index_name === modelIndex.name);
|
|
470
|
+
if (!exists) {
|
|
471
|
+
operations.push({
|
|
472
|
+
type: 'create_index',
|
|
473
|
+
tableName,
|
|
474
|
+
indexName: modelIndex.name,
|
|
475
|
+
columns: modelIndex.columns
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Compare foreign key constraints
|
|
480
|
+
const existingForeignKeys = await this.getExistingForeignKeys(tableName, schema.schema);
|
|
481
|
+
const modelForeignKeys = schema.foreignKeys || [];
|
|
482
|
+
// Find foreign keys to create
|
|
483
|
+
for (const modelFk of modelForeignKeys) {
|
|
484
|
+
const exists = existingForeignKeys.some(dbFk => dbFk.constraint_name === modelFk.name);
|
|
485
|
+
if (!exists) {
|
|
486
|
+
operations.push({
|
|
487
|
+
type: 'create_foreign_key',
|
|
488
|
+
tableName,
|
|
489
|
+
constraint: modelFk
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return operations;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Perform automatic migration - analyze and apply changes
|
|
499
|
+
*/
|
|
500
|
+
async migrate() {
|
|
501
|
+
try {
|
|
502
|
+
const operations = await this.analyze();
|
|
503
|
+
if (operations.length === 0) {
|
|
504
|
+
console.log('✓ Database schema is already in sync with model\n');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.log(`📋 Found ${operations.length} operations to perform:\n`);
|
|
508
|
+
// Show all operations
|
|
509
|
+
for (let i = 0; i < operations.length; i++) {
|
|
510
|
+
console.log(`${i + 1}. ${this.describeOperation(operations[i])}`);
|
|
511
|
+
}
|
|
512
|
+
console.log('');
|
|
513
|
+
// Execute operations with confirmations for destructive ones
|
|
514
|
+
for (const operation of operations) {
|
|
515
|
+
await this.executeOperation(operation);
|
|
516
|
+
}
|
|
517
|
+
console.log('\n✓ Migration completed successfully\n');
|
|
518
|
+
// Execute post-migration hook if provided
|
|
519
|
+
if (this.postMigrationHook) {
|
|
520
|
+
if (this.logQueries) {
|
|
521
|
+
console.log('Executing post-migration scripts...\n');
|
|
522
|
+
}
|
|
523
|
+
await this.postMigrationHook(this.client);
|
|
524
|
+
if (this.logQueries) {
|
|
525
|
+
console.log('✓ Post-migration scripts completed\n');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
this.closeReadlineInterface();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Execute a single migration operation
|
|
535
|
+
*/
|
|
536
|
+
async executeOperation(operation) {
|
|
537
|
+
switch (operation.type) {
|
|
538
|
+
case 'create_schema':
|
|
539
|
+
await this.executeCreateSchema(operation.schemaName);
|
|
540
|
+
break;
|
|
541
|
+
case 'create_enum':
|
|
542
|
+
await this.executeCreateEnum(operation.enumName, operation.values);
|
|
543
|
+
break;
|
|
544
|
+
case 'create_table':
|
|
545
|
+
await this.createTable(operation.tableName, operation.schema);
|
|
546
|
+
break;
|
|
547
|
+
case 'drop_table':
|
|
548
|
+
if (await this.confirm(`Drop table "${operation.tableName}"? This will DELETE ALL DATA in the table.`)) {
|
|
549
|
+
await this.executeDropTable(operation.tableName);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
console.log(` ⊘ Skipped dropping table "${operation.tableName}"\n`);
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
case 'add_column':
|
|
556
|
+
await this.executeAddColumn(operation.tableName, operation.columnName, operation.config);
|
|
557
|
+
break;
|
|
558
|
+
case 'drop_column':
|
|
559
|
+
if (await this.confirm(`Drop column "${operation.tableName}"."${operation.columnName}"? This will DELETE ALL DATA in the column.`)) {
|
|
560
|
+
await this.executeDropColumn(operation.tableName, operation.columnName);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
console.log(` ⊘ Skipped dropping column "${operation.tableName}"."${operation.columnName}"\n`);
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
case 'alter_column':
|
|
567
|
+
await this.executeAlterColumn(operation.tableName, operation.columnName, operation.from, operation.to);
|
|
568
|
+
break;
|
|
569
|
+
case 'create_index':
|
|
570
|
+
await this.executeCreateIndex(operation.tableName, operation.indexName, operation.columns);
|
|
571
|
+
break;
|
|
572
|
+
case 'drop_index':
|
|
573
|
+
if (await this.confirm(`Drop index "${operation.indexName}"?`)) {
|
|
574
|
+
await this.executeDropIndex(operation.indexName);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
console.log(` ⊘ Skipped dropping index "${operation.indexName}"\n`);
|
|
578
|
+
}
|
|
579
|
+
break;
|
|
580
|
+
case 'create_foreign_key':
|
|
581
|
+
await this.executeCreateForeignKey(operation.tableName, operation.constraint);
|
|
582
|
+
break;
|
|
583
|
+
case 'drop_foreign_key':
|
|
584
|
+
if (await this.confirm(`Drop foreign key "${operation.constraintName}"?`)) {
|
|
585
|
+
await this.executeDropForeignKey(operation.tableName, operation.constraintName);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
console.log(` ⊘ Skipped dropping foreign key "${operation.constraintName}"\n`);
|
|
589
|
+
}
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Execute create schema
|
|
595
|
+
*/
|
|
596
|
+
async executeCreateSchema(schemaName) {
|
|
597
|
+
console.log(` Creating schema "${schemaName}"...`);
|
|
598
|
+
await this.client.query(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`);
|
|
599
|
+
console.log(` ✓ Schema "${schemaName}" created\n`);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Execute create enum
|
|
603
|
+
*/
|
|
604
|
+
async executeCreateEnum(enumName, values) {
|
|
605
|
+
console.log(` Creating ENUM type "${enumName}"...`);
|
|
606
|
+
const valueList = values.map(v => `'${v}'`).join(', ');
|
|
607
|
+
await this.client.query(`CREATE TYPE "${enumName}" AS ENUM (${valueList})`);
|
|
608
|
+
console.log(` ✓ ENUM type "${enumName}" created\n`);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Execute drop table
|
|
612
|
+
*/
|
|
613
|
+
async executeDropTable(tableName) {
|
|
614
|
+
console.log(` Dropping table "${tableName}"...`);
|
|
615
|
+
await this.client.query(`DROP TABLE "${tableName}" CASCADE`);
|
|
616
|
+
console.log(` ✓ Table "${tableName}" dropped\n`);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Execute add column
|
|
620
|
+
*/
|
|
621
|
+
async executeAddColumn(tableName, columnName, config) {
|
|
622
|
+
console.log(` Adding column "${tableName}"."${columnName}"...`);
|
|
623
|
+
let def = `${config.type}`;
|
|
624
|
+
if (config.length) {
|
|
625
|
+
def += `(${config.length})`;
|
|
626
|
+
}
|
|
627
|
+
else if (config.precision && config.scale) {
|
|
628
|
+
def += `(${config.precision}, ${config.scale})`;
|
|
629
|
+
}
|
|
630
|
+
else if (config.precision) {
|
|
631
|
+
def += `(${config.precision})`;
|
|
632
|
+
}
|
|
633
|
+
if (!config.nullable) {
|
|
634
|
+
def += ' NOT NULL';
|
|
635
|
+
}
|
|
636
|
+
if (config.unique) {
|
|
637
|
+
def += ' UNIQUE';
|
|
638
|
+
}
|
|
639
|
+
if (config.default !== undefined) {
|
|
640
|
+
def += ` DEFAULT ${this.formatDefaultValue(config.default)}`;
|
|
641
|
+
}
|
|
642
|
+
const sql = `ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${def}`;
|
|
643
|
+
await this.client.query(sql);
|
|
644
|
+
console.log(` ✓ Column "${tableName}"."${columnName}" added\n`);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Execute drop column
|
|
648
|
+
*/
|
|
649
|
+
async executeDropColumn(tableName, columnName) {
|
|
650
|
+
console.log(` Dropping column "${tableName}"."${columnName}"...`);
|
|
651
|
+
await this.client.query(`ALTER TABLE "${tableName}" DROP COLUMN "${columnName}"`);
|
|
652
|
+
console.log(` ✓ Column "${tableName}"."${columnName}" dropped\n`);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Execute alter column
|
|
656
|
+
*/
|
|
657
|
+
async executeAlterColumn(tableName, columnName, from, to) {
|
|
658
|
+
console.log(` Altering column "${tableName}"."${columnName}"...`);
|
|
659
|
+
console.log(` From: ${this.describeDbColumn(from)}`);
|
|
660
|
+
console.log(` To: ${this.describeModelColumn(to)}`);
|
|
661
|
+
// PostgreSQL requires separate ALTER COLUMN commands for different changes
|
|
662
|
+
// Change type if needed
|
|
663
|
+
const fromType = this.normalizeType(from.data_type);
|
|
664
|
+
const toType = this.normalizeType(to.type);
|
|
665
|
+
if (fromType !== toType) {
|
|
666
|
+
const typeDef = this.buildTypeDefinition(to);
|
|
667
|
+
await this.client.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${typeDef} USING "${columnName}"::${typeDef}`);
|
|
668
|
+
console.log(` ✓ Type changed from ${fromType} to ${toType}`);
|
|
669
|
+
}
|
|
670
|
+
// Change nullability if needed
|
|
671
|
+
const fromNullable = from.is_nullable === 'YES';
|
|
672
|
+
const toNullable = to.nullable;
|
|
673
|
+
if (fromNullable !== toNullable) {
|
|
674
|
+
if (toNullable) {
|
|
675
|
+
await this.client.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP NOT NULL`);
|
|
676
|
+
console.log(` ✓ Nullability changed to NULLABLE`);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
await this.client.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET NOT NULL`);
|
|
680
|
+
console.log(` ✓ Nullability changed to NOT NULL`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// Change default if needed
|
|
684
|
+
const fromDefault = from.column_default;
|
|
685
|
+
const toDefault = to.default !== undefined ? this.formatDefaultValue(to.default) : null;
|
|
686
|
+
if (fromDefault !== toDefault) {
|
|
687
|
+
if (toDefault !== null) {
|
|
688
|
+
await this.client.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET DEFAULT ${toDefault}`);
|
|
689
|
+
console.log(` ✓ Default changed to ${toDefault}`);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
await this.client.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT`);
|
|
693
|
+
console.log(` ✓ Default removed`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
console.log(` ✓ Column "${tableName}"."${columnName}" altered\n`);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Execute create index
|
|
700
|
+
*/
|
|
701
|
+
async executeCreateIndex(tableName, indexName, columns) {
|
|
702
|
+
console.log(` Creating index "${indexName}" on "${tableName}"...`);
|
|
703
|
+
const columnList = columns.map(col => `"${col}"`).join(', ');
|
|
704
|
+
const sql = `CREATE INDEX "${indexName}" ON "${tableName}" (${columnList})`;
|
|
705
|
+
await this.client.query(sql);
|
|
706
|
+
console.log(` ✓ Index "${indexName}" created\n`);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Execute drop index
|
|
710
|
+
*/
|
|
711
|
+
async executeDropIndex(indexName) {
|
|
712
|
+
console.log(` Dropping index "${indexName}"...`);
|
|
713
|
+
await this.client.query(`DROP INDEX "${indexName}"`);
|
|
714
|
+
console.log(` ✓ Index "${indexName}" dropped\n`);
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Execute create foreign key
|
|
718
|
+
*/
|
|
719
|
+
async executeCreateForeignKey(tableName, constraint) {
|
|
720
|
+
console.log(` Creating foreign key constraint "${constraint.name}" on "${tableName}"...`);
|
|
721
|
+
const columnList = constraint.columns.map((col) => `"${col}"`).join(', ');
|
|
722
|
+
const refColumnList = constraint.referencedColumns.map((col) => `"${col}"`).join(', ');
|
|
723
|
+
let sql = `ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraint.name}" `;
|
|
724
|
+
sql += `FOREIGN KEY (${columnList}) `;
|
|
725
|
+
sql += `REFERENCES "${constraint.referencedTable}" (${refColumnList})`;
|
|
726
|
+
if (constraint.onDelete) {
|
|
727
|
+
sql += ` ON DELETE ${constraint.onDelete.toUpperCase()}`;
|
|
728
|
+
}
|
|
729
|
+
if (constraint.onUpdate) {
|
|
730
|
+
sql += ` ON UPDATE ${constraint.onUpdate.toUpperCase()}`;
|
|
731
|
+
}
|
|
732
|
+
await this.client.query(sql);
|
|
733
|
+
console.log(` ✓ Foreign key constraint "${constraint.name}" created\n`);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Execute drop foreign key
|
|
737
|
+
*/
|
|
738
|
+
async executeDropForeignKey(tableName, constraintName) {
|
|
739
|
+
console.log(` Dropping foreign key constraint "${constraintName}"...`);
|
|
740
|
+
await this.client.query(`ALTER TABLE "${tableName}" DROP CONSTRAINT "${constraintName}"`);
|
|
741
|
+
console.log(` ✓ Foreign key constraint "${constraintName}" dropped\n`);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Get all existing tables in the database
|
|
745
|
+
*/
|
|
746
|
+
async getExistingTables() {
|
|
747
|
+
const result = await this.client.query(`
|
|
748
|
+
SELECT table_name
|
|
749
|
+
FROM information_schema.tables
|
|
750
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
751
|
+
`);
|
|
752
|
+
const tables = new Map();
|
|
753
|
+
for (const row of result.rows) {
|
|
754
|
+
tables.set(row.table_name, true);
|
|
755
|
+
}
|
|
756
|
+
return tables;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Get all columns for a table
|
|
760
|
+
*/
|
|
761
|
+
async getExistingColumns(tableName, schemaName) {
|
|
762
|
+
const result = await this.client.query(`
|
|
763
|
+
SELECT
|
|
764
|
+
column_name,
|
|
765
|
+
data_type,
|
|
766
|
+
character_maximum_length,
|
|
767
|
+
numeric_precision,
|
|
768
|
+
numeric_scale,
|
|
769
|
+
is_nullable,
|
|
770
|
+
column_default
|
|
771
|
+
FROM information_schema.columns
|
|
772
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
773
|
+
ORDER BY ordinal_position
|
|
774
|
+
`, [schemaName || 'public', tableName]);
|
|
775
|
+
const columns = new Map();
|
|
776
|
+
for (const row of result.rows) {
|
|
777
|
+
columns.set(row.column_name, row);
|
|
778
|
+
}
|
|
779
|
+
return columns;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Get all indexes for a table
|
|
783
|
+
*/
|
|
784
|
+
async getExistingIndexes(tableName, schemaName) {
|
|
785
|
+
const result = await this.client.query(`
|
|
786
|
+
SELECT
|
|
787
|
+
i.relname as index_name,
|
|
788
|
+
array_agg(a.attname ORDER BY k.ordinality) as column_names
|
|
789
|
+
FROM pg_index ix
|
|
790
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
791
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
792
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
793
|
+
CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY k(attnum, ordinality)
|
|
794
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
795
|
+
WHERE
|
|
796
|
+
n.nspname = $1
|
|
797
|
+
AND t.relname = $2
|
|
798
|
+
AND NOT ix.indisprimary
|
|
799
|
+
AND NOT ix.indisunique
|
|
800
|
+
GROUP BY i.relname
|
|
801
|
+
`, [schemaName || 'public', tableName]);
|
|
802
|
+
return result.rows.map(row => ({
|
|
803
|
+
index_name: row.index_name,
|
|
804
|
+
column_names: row.column_names
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Get all foreign key constraints for a table
|
|
809
|
+
*/
|
|
810
|
+
async getExistingForeignKeys(tableName, schemaName) {
|
|
811
|
+
const result = await this.client.query(`
|
|
812
|
+
SELECT
|
|
813
|
+
tc.constraint_name,
|
|
814
|
+
array_agg(kcu.column_name ORDER BY kcu.ordinal_position) as column_names,
|
|
815
|
+
ccu.table_name AS referenced_table,
|
|
816
|
+
array_agg(ccu.column_name ORDER BY kcu.ordinal_position) as referenced_column_names,
|
|
817
|
+
rc.delete_rule as on_delete,
|
|
818
|
+
rc.update_rule as on_update
|
|
819
|
+
FROM information_schema.table_constraints AS tc
|
|
820
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
821
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
822
|
+
AND tc.table_schema = kcu.table_schema
|
|
823
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
824
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
825
|
+
AND ccu.table_schema = tc.table_schema
|
|
826
|
+
JOIN information_schema.referential_constraints AS rc
|
|
827
|
+
ON rc.constraint_name = tc.constraint_name
|
|
828
|
+
AND rc.constraint_schema = tc.table_schema
|
|
829
|
+
WHERE
|
|
830
|
+
tc.table_schema = $1
|
|
831
|
+
AND tc.table_name = $2
|
|
832
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
|
833
|
+
GROUP BY tc.constraint_name, ccu.table_name, rc.delete_rule, rc.update_rule
|
|
834
|
+
`, [schemaName || 'public', tableName]);
|
|
835
|
+
return result.rows.map(row => ({
|
|
836
|
+
constraint_name: row.constraint_name,
|
|
837
|
+
column_names: row.column_names,
|
|
838
|
+
referenced_table: row.referenced_table,
|
|
839
|
+
referenced_column_names: row.referenced_column_names,
|
|
840
|
+
on_delete: row.on_delete,
|
|
841
|
+
on_update: row.on_update
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Check if a column needs to be altered
|
|
846
|
+
*/
|
|
847
|
+
needsAlter(dbInfo, modelConfig) {
|
|
848
|
+
// Compare type
|
|
849
|
+
const dbType = this.normalizeType(dbInfo.data_type);
|
|
850
|
+
const modelType = this.normalizeType(modelConfig.type);
|
|
851
|
+
if (dbType !== modelType) {
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
// Compare nullability
|
|
855
|
+
const dbNullable = dbInfo.is_nullable === 'YES';
|
|
856
|
+
const modelNullable = modelConfig.nullable;
|
|
857
|
+
if (dbNullable !== modelNullable) {
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
// Compare default (normalize for comparison)
|
|
861
|
+
const dbDefault = this.normalizeDefault(dbInfo.column_default);
|
|
862
|
+
const modelDefault = modelConfig.default !== undefined
|
|
863
|
+
? this.normalizeDefault(this.formatDefaultValue(modelConfig.default))
|
|
864
|
+
: null;
|
|
865
|
+
if (dbDefault !== modelDefault) {
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Normalize default values for comparison
|
|
872
|
+
*/
|
|
873
|
+
normalizeDefault(value) {
|
|
874
|
+
if (value === null)
|
|
875
|
+
return null;
|
|
876
|
+
let normalized = value.toLowerCase().trim();
|
|
877
|
+
// Remove type casts like ::character varying, ::regclass
|
|
878
|
+
normalized = normalized.replace(/::[a-z_]+(\s+varying)?/g, '');
|
|
879
|
+
// Remove function call parentheses for comparison
|
|
880
|
+
normalized = normalized.replace(/\(\)/g, '');
|
|
881
|
+
// Remove single quotes around strings for comparison
|
|
882
|
+
normalized = normalized.replace(/^'(.*)'$/, '$1');
|
|
883
|
+
// Normalize nextval sequences
|
|
884
|
+
if (normalized.includes('nextval')) {
|
|
885
|
+
return 'auto'; // Treat all nextval as equivalent
|
|
886
|
+
}
|
|
887
|
+
return normalized;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Normalize PostgreSQL type names for comparison
|
|
891
|
+
*/
|
|
892
|
+
normalizeType(type) {
|
|
893
|
+
const normalized = type.toLowerCase().trim();
|
|
894
|
+
// Map common variations
|
|
895
|
+
const typeMap = {
|
|
896
|
+
'character varying': 'varchar',
|
|
897
|
+
'character': 'char',
|
|
898
|
+
'integer': 'int',
|
|
899
|
+
'bigint': 'int8',
|
|
900
|
+
'smallint': 'int2',
|
|
901
|
+
'double precision': 'float8',
|
|
902
|
+
'real': 'float4',
|
|
903
|
+
'timestamp without time zone': 'timestamp',
|
|
904
|
+
'timestamp with time zone': 'timestamptz',
|
|
905
|
+
'time without time zone': 'time',
|
|
906
|
+
'time with time zone': 'timetz',
|
|
907
|
+
'serial': 'int',
|
|
908
|
+
'bigserial': 'int8',
|
|
909
|
+
'smallserial': 'int2',
|
|
910
|
+
'numeric': 'decimal',
|
|
911
|
+
};
|
|
912
|
+
return typeMap[normalized] || normalized;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Build type definition for ALTER COLUMN TYPE
|
|
916
|
+
*/
|
|
917
|
+
buildTypeDefinition(config) {
|
|
918
|
+
let type = config.type;
|
|
919
|
+
if (type === 'serial')
|
|
920
|
+
type = 'integer';
|
|
921
|
+
if (type === 'bigserial')
|
|
922
|
+
type = 'bigint';
|
|
923
|
+
if (type === 'smallserial')
|
|
924
|
+
type = 'smallint';
|
|
925
|
+
let def = type;
|
|
926
|
+
if (config.length) {
|
|
927
|
+
def += `(${config.length})`;
|
|
928
|
+
}
|
|
929
|
+
else if (config.precision && config.scale) {
|
|
930
|
+
def += `(${config.precision}, ${config.scale})`;
|
|
931
|
+
}
|
|
932
|
+
else if (config.precision) {
|
|
933
|
+
def += `(${config.precision})`;
|
|
934
|
+
}
|
|
935
|
+
return def;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Describe a database column
|
|
939
|
+
*/
|
|
940
|
+
describeDbColumn(col) {
|
|
941
|
+
let desc = col.data_type;
|
|
942
|
+
if (col.character_maximum_length) {
|
|
943
|
+
desc += `(${col.character_maximum_length})`;
|
|
944
|
+
}
|
|
945
|
+
else if (col.numeric_precision) {
|
|
946
|
+
desc += `(${col.numeric_precision}${col.numeric_scale ? ',' + col.numeric_scale : ''})`;
|
|
947
|
+
}
|
|
948
|
+
desc += col.is_nullable === 'YES' ? ' NULL' : ' NOT NULL';
|
|
949
|
+
if (col.column_default) {
|
|
950
|
+
desc += ` DEFAULT ${col.column_default}`;
|
|
951
|
+
}
|
|
952
|
+
return desc;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Describe a model column
|
|
956
|
+
*/
|
|
957
|
+
describeModelColumn(config) {
|
|
958
|
+
let desc = config.type;
|
|
959
|
+
if (config.length) {
|
|
960
|
+
desc += `(${config.length})`;
|
|
961
|
+
}
|
|
962
|
+
else if (config.precision) {
|
|
963
|
+
desc += `(${config.precision}${config.scale ? ',' + config.scale : ''})`;
|
|
964
|
+
}
|
|
965
|
+
desc += config.nullable ? ' NULL' : ' NOT NULL';
|
|
966
|
+
if (config.default !== undefined) {
|
|
967
|
+
desc += ` DEFAULT ${this.formatDefaultValue(config.default)}`;
|
|
968
|
+
}
|
|
969
|
+
return desc;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Describe a migration operation
|
|
973
|
+
*/
|
|
974
|
+
describeOperation(operation) {
|
|
975
|
+
switch (operation.type) {
|
|
976
|
+
case 'create_schema':
|
|
977
|
+
return `Create schema "${operation.schemaName}"`;
|
|
978
|
+
case 'create_enum':
|
|
979
|
+
return `Create ENUM type "${operation.enumName}" (${operation.values.join(', ')})`;
|
|
980
|
+
case 'create_table':
|
|
981
|
+
return `Create table "${operation.tableName}"`;
|
|
982
|
+
case 'drop_table':
|
|
983
|
+
return `Drop table "${operation.tableName}" (DESTRUCTIVE)`;
|
|
984
|
+
case 'add_column':
|
|
985
|
+
return `Add column "${operation.tableName}"."${operation.columnName}" (${this.describeModelColumn(operation.config)})`;
|
|
986
|
+
case 'drop_column':
|
|
987
|
+
return `Drop column "${operation.tableName}"."${operation.columnName}" (DESTRUCTIVE)`;
|
|
988
|
+
case 'alter_column':
|
|
989
|
+
return `Alter column "${operation.tableName}"."${operation.columnName}"`;
|
|
990
|
+
case 'create_index':
|
|
991
|
+
return `Create index "${operation.indexName}" on "${operation.tableName}" (${operation.columns.join(', ')})`;
|
|
992
|
+
case 'drop_index':
|
|
993
|
+
return `Drop index "${operation.indexName}" (DESTRUCTIVE)`;
|
|
994
|
+
case 'create_foreign_key':
|
|
995
|
+
const fk = operation.constraint;
|
|
996
|
+
let desc = `Create foreign key "${fk.name}" on "${operation.tableName}" (${fk.columns.join(', ')}) references "${fk.referencedTable}" (${fk.referencedColumns.join(', ')})`;
|
|
997
|
+
if (fk.onDelete || fk.onUpdate) {
|
|
998
|
+
const actions = [];
|
|
999
|
+
if (fk.onDelete)
|
|
1000
|
+
actions.push(`ON DELETE ${fk.onDelete.toUpperCase()}`);
|
|
1001
|
+
if (fk.onUpdate)
|
|
1002
|
+
actions.push(`ON UPDATE ${fk.onUpdate.toUpperCase()}`);
|
|
1003
|
+
desc += ` [${actions.join(', ')}]`;
|
|
1004
|
+
}
|
|
1005
|
+
return desc;
|
|
1006
|
+
case 'drop_foreign_key':
|
|
1007
|
+
return `Drop foreign key "${operation.constraintName}" (DESTRUCTIVE)`;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Ask user for confirmation (CLI prompt)
|
|
1012
|
+
*/
|
|
1013
|
+
async confirm(question) {
|
|
1014
|
+
// If not running in a TTY (like in tests), default to NO for destructive operations
|
|
1015
|
+
if (!process.stdin.isTTY) {
|
|
1016
|
+
console.log(`\n⚠️ ${question} [y/N]: N (non-interactive mode, defaulting to NO)`);
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
const rl = this.getReadlineInterface();
|
|
1020
|
+
return new Promise((resolve) => {
|
|
1021
|
+
rl.question(`\n⚠️ ${question} [y/N]: `, (answer) => {
|
|
1022
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Format default value for SQL
|
|
1028
|
+
*/
|
|
1029
|
+
formatDefaultValue(value) {
|
|
1030
|
+
if (value === null)
|
|
1031
|
+
return 'NULL';
|
|
1032
|
+
if (typeof value === 'string') {
|
|
1033
|
+
// Check if it's a SQL function
|
|
1034
|
+
if (value.toUpperCase().includes('NOW()') || value.toUpperCase().includes('CURRENT_')) {
|
|
1035
|
+
return value;
|
|
1036
|
+
}
|
|
1037
|
+
return `'${value}'`;
|
|
1038
|
+
}
|
|
1039
|
+
if (typeof value === 'boolean')
|
|
1040
|
+
return value ? 'TRUE' : 'FALSE';
|
|
1041
|
+
if (value instanceof Date)
|
|
1042
|
+
return `'${value.toISOString()}'`;
|
|
1043
|
+
return String(value);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Close the schema manager and any open resources
|
|
1047
|
+
*/
|
|
1048
|
+
close() {
|
|
1049
|
+
if (this.rl) {
|
|
1050
|
+
this.rl.close();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
exports.DbSchemaManager = DbSchemaManager;
|
|
1055
|
+
//# sourceMappingURL=db-schema-manager.js.map
|