relq 1.0.5 → 1.0.6
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/dist/cjs/cli/commands/add.cjs +252 -12
- package/dist/cjs/cli/commands/commit.cjs +12 -1
- package/dist/cjs/cli/commands/export.cjs +25 -19
- package/dist/cjs/cli/commands/import.cjs +219 -100
- package/dist/cjs/cli/commands/init.cjs +86 -14
- package/dist/cjs/cli/commands/pull.cjs +104 -23
- package/dist/cjs/cli/commands/push.cjs +38 -3
- package/dist/cjs/cli/index.cjs +9 -1
- package/dist/cjs/cli/utils/ast/codegen/builder.cjs +297 -0
- package/dist/cjs/cli/utils/ast/codegen/constraints.cjs +185 -0
- package/dist/cjs/cli/utils/ast/codegen/defaults.cjs +311 -0
- package/dist/cjs/cli/utils/ast/codegen/index.cjs +24 -0
- package/dist/cjs/cli/utils/ast/codegen/type-map.cjs +116 -0
- package/dist/cjs/cli/utils/ast/codegen/utils.cjs +69 -0
- package/dist/cjs/cli/utils/ast/index.cjs +19 -0
- package/dist/cjs/cli/utils/ast/transformer/helpers.cjs +154 -0
- package/dist/cjs/cli/utils/ast/transformer/index.cjs +25 -0
- package/dist/cjs/cli/utils/ast/types.cjs +2 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +949 -0
- package/dist/cjs/cli/utils/ast-transformer.cjs +916 -0
- package/dist/cjs/cli/utils/change-tracker.cjs +50 -1
- package/dist/cjs/cli/utils/cli-utils.cjs +151 -0
- package/dist/cjs/cli/utils/fast-introspect.cjs +149 -23
- package/dist/cjs/cli/utils/pg-parser.cjs +1 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +121 -4
- package/dist/cjs/cli/utils/schema-comparator.cjs +98 -14
- package/dist/cjs/cli/utils/schema-introspect.cjs +56 -19
- package/dist/cjs/cli/utils/snapshot-manager.cjs +0 -1
- package/dist/cjs/cli/utils/sql-generator.cjs +353 -64
- package/dist/cjs/cli/utils/type-generator.cjs +114 -15
- package/dist/cjs/core/relq-client.cjs +22 -6
- package/dist/cjs/schema-definition/column-types.cjs +149 -13
- package/dist/cjs/schema-definition/defaults.cjs +72 -0
- package/dist/cjs/schema-definition/index.cjs +15 -1
- package/dist/cjs/schema-definition/introspection.cjs +7 -3
- package/dist/cjs/schema-definition/pg-relations.cjs +169 -0
- package/dist/cjs/schema-definition/pg-view.cjs +30 -0
- package/dist/cjs/schema-definition/table-definition.cjs +110 -4
- package/dist/cjs/types/config-types.cjs +13 -4
- package/dist/cjs/utils/aws-dsql.cjs +177 -0
- package/dist/config.d.ts +146 -1
- package/dist/esm/cli/commands/add.js +250 -13
- package/dist/esm/cli/commands/commit.js +12 -1
- package/dist/esm/cli/commands/export.js +25 -19
- package/dist/esm/cli/commands/import.js +221 -102
- package/dist/esm/cli/commands/init.js +86 -14
- package/dist/esm/cli/commands/pull.js +106 -25
- package/dist/esm/cli/commands/push.js +39 -4
- package/dist/esm/cli/index.js +9 -1
- package/dist/esm/cli/utils/ast/codegen/builder.js +291 -0
- package/dist/esm/cli/utils/ast/codegen/constraints.js +176 -0
- package/dist/esm/cli/utils/ast/codegen/defaults.js +305 -0
- package/dist/esm/cli/utils/ast/codegen/index.js +6 -0
- package/dist/esm/cli/utils/ast/codegen/type-map.js +111 -0
- package/dist/esm/cli/utils/ast/codegen/utils.js +60 -0
- package/dist/esm/cli/utils/ast/index.js +3 -0
- package/dist/esm/cli/utils/ast/transformer/helpers.js +141 -0
- package/dist/esm/cli/utils/ast/transformer/index.js +2 -0
- package/dist/esm/cli/utils/ast/types.js +1 -0
- package/dist/esm/cli/utils/ast-codegen.js +945 -0
- package/dist/esm/cli/utils/ast-transformer.js +907 -0
- package/dist/esm/cli/utils/change-tracker.js +50 -1
- package/dist/esm/cli/utils/cli-utils.js +147 -0
- package/dist/esm/cli/utils/fast-introspect.js +149 -23
- package/dist/esm/cli/utils/pg-parser.js +1 -0
- package/dist/esm/cli/utils/repo-manager.js +114 -4
- package/dist/esm/cli/utils/schema-comparator.js +98 -14
- package/dist/esm/cli/utils/schema-introspect.js +56 -19
- package/dist/esm/cli/utils/snapshot-manager.js +0 -1
- package/dist/esm/cli/utils/sql-generator.js +353 -64
- package/dist/esm/cli/utils/type-generator.js +114 -15
- package/dist/esm/core/relq-client.js +23 -7
- package/dist/esm/schema-definition/column-types.js +146 -12
- package/dist/esm/schema-definition/defaults.js +69 -0
- package/dist/esm/schema-definition/index.js +3 -0
- package/dist/esm/schema-definition/introspection.js +7 -3
- package/dist/esm/schema-definition/pg-relations.js +161 -0
- package/dist/esm/schema-definition/pg-view.js +24 -0
- package/dist/esm/schema-definition/table-definition.js +110 -4
- package/dist/esm/types/config-types.js +12 -4
- package/dist/esm/utils/aws-dsql.js +139 -0
- package/dist/index.d.ts +159 -1
- package/dist/schema-builder.d.ts +1314 -32
- package/package.json +1 -1
|
@@ -37,6 +37,8 @@ export function generateChangeSQL(change) {
|
|
|
37
37
|
return generateTableCommentSQL(change);
|
|
38
38
|
case 'COLUMN_COMMENT':
|
|
39
39
|
return generateColumnCommentSQL(change);
|
|
40
|
+
case 'INDEX_COMMENT':
|
|
41
|
+
return generateIndexCommentSQL(change);
|
|
40
42
|
case 'VIEW':
|
|
41
43
|
return generateViewSQL(change);
|
|
42
44
|
case 'MATERIALIZED_VIEW':
|
|
@@ -181,6 +183,13 @@ function generateColumnSQL(change) {
|
|
|
181
183
|
else if (change.type === 'DROP') {
|
|
182
184
|
return `ALTER TABLE "${tableName}" DROP COLUMN IF EXISTS "${change.objectName}" CASCADE;`;
|
|
183
185
|
}
|
|
186
|
+
else if (change.type === 'RENAME' && data) {
|
|
187
|
+
const before = change.before;
|
|
188
|
+
if (before?.name) {
|
|
189
|
+
return `ALTER TABLE "${tableName}" RENAME COLUMN "${before.name}" TO "${data.name}";`;
|
|
190
|
+
}
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
184
193
|
else if (change.type === 'ALTER' && data) {
|
|
185
194
|
const before = change.before;
|
|
186
195
|
const lines = [];
|
|
@@ -221,6 +230,13 @@ function generateIndexSQL(change) {
|
|
|
221
230
|
else if (change.type === 'DROP') {
|
|
222
231
|
return `DROP INDEX IF EXISTS "${change.objectName}";`;
|
|
223
232
|
}
|
|
233
|
+
else if (change.type === 'RENAME' && data) {
|
|
234
|
+
const before = change.before;
|
|
235
|
+
if (before?.name) {
|
|
236
|
+
return `ALTER INDEX "${before.name}" RENAME TO "${data.name}";`;
|
|
237
|
+
}
|
|
238
|
+
return '';
|
|
239
|
+
}
|
|
224
240
|
return '';
|
|
225
241
|
}
|
|
226
242
|
function generateConstraintSQL(change) {
|
|
@@ -276,6 +292,19 @@ function generateColumnCommentSQL(change) {
|
|
|
276
292
|
}
|
|
277
293
|
return '';
|
|
278
294
|
}
|
|
295
|
+
function generateIndexCommentSQL(change) {
|
|
296
|
+
const data = change.after;
|
|
297
|
+
if ((change.type === 'CREATE' || change.type === 'ALTER') && data) {
|
|
298
|
+
const escaped = data.comment.replace(/'/g, "''");
|
|
299
|
+
return `COMMENT ON INDEX "${data.indexName}" IS '${escaped}';`;
|
|
300
|
+
}
|
|
301
|
+
else if (change.type === 'DROP') {
|
|
302
|
+
const beforeData = change.before;
|
|
303
|
+
const indexName = beforeData?.indexName || change.objectName;
|
|
304
|
+
return `COMMENT ON INDEX "${indexName}" IS NULL;`;
|
|
305
|
+
}
|
|
306
|
+
return '';
|
|
307
|
+
}
|
|
279
308
|
function generateViewSQL(change) {
|
|
280
309
|
const data = change.after;
|
|
281
310
|
if (change.type === 'CREATE' && data) {
|
|
@@ -403,7 +432,8 @@ export function getChangeDisplayName(change) {
|
|
|
403
432
|
const prefix = change.type === 'CREATE' ? '+' :
|
|
404
433
|
change.type === 'DROP' ? '-' :
|
|
405
434
|
change.type === 'ALTER' ? '~' :
|
|
406
|
-
'
|
|
435
|
+
change.type === 'RENAME' ? '→' :
|
|
436
|
+
'>';
|
|
407
437
|
if (change.objectType === 'INDEX') {
|
|
408
438
|
const data = change.after;
|
|
409
439
|
const tableName = data?.tableName || change.parentName;
|
|
@@ -411,6 +441,14 @@ export function getChangeDisplayName(change) {
|
|
|
411
441
|
return `${prefix} ${change.objectType} ${change.objectName} on ${tableName}`;
|
|
412
442
|
}
|
|
413
443
|
}
|
|
444
|
+
if (change.objectType === 'INDEX_COMMENT') {
|
|
445
|
+
const data = (change.after || change.before);
|
|
446
|
+
const tableName = data?.tableName || change.parentName;
|
|
447
|
+
if (tableName) {
|
|
448
|
+
return `${prefix} INDEX COMMENT ${change.objectName} on ${tableName}`;
|
|
449
|
+
}
|
|
450
|
+
return `${prefix} INDEX COMMENT ${change.objectName}`;
|
|
451
|
+
}
|
|
414
452
|
if (change.objectType === 'PARTITION') {
|
|
415
453
|
const data = change.after;
|
|
416
454
|
if (data?.tableName && data?.type && data?.key) {
|
|
@@ -440,6 +478,16 @@ export function getChangeDisplayName(change) {
|
|
|
440
478
|
}
|
|
441
479
|
return `${prefix} ${actionWord} ${change.objectType.replace('_', ' ')} ${change.objectName}`;
|
|
442
480
|
}
|
|
481
|
+
if (change.type === 'RENAME') {
|
|
482
|
+
const before = change.before;
|
|
483
|
+
const after = change.after;
|
|
484
|
+
if (before?.name && after?.name) {
|
|
485
|
+
if (change.parentName) {
|
|
486
|
+
return `${prefix} RENAME ${change.objectType} ${change.parentName}.${before.name} → ${after.name}`;
|
|
487
|
+
}
|
|
488
|
+
return `${prefix} RENAME ${change.objectType} ${before.name} → ${after.name}`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
443
491
|
if (change.parentName) {
|
|
444
492
|
return `${prefix} ${change.objectType} ${change.parentName}.${change.objectName}`;
|
|
445
493
|
}
|
|
@@ -462,6 +510,7 @@ export function sortChangesByDependency(changes) {
|
|
|
462
510
|
'COLUMN': 13,
|
|
463
511
|
'COLUMN_COMMENT': 12,
|
|
464
512
|
'INDEX': 13,
|
|
513
|
+
'INDEX_COMMENT': 13,
|
|
465
514
|
'CONSTRAINT': 14,
|
|
466
515
|
'PRIMARY_KEY': 14,
|
|
467
516
|
'FOREIGN_KEY': 15,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as readline from 'readline';
|
|
2
|
+
import cliSpinners from 'cli-spinners';
|
|
2
3
|
const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4
|
+
const isTTY = process.stdout.isTTY;
|
|
3
5
|
export const colors = {
|
|
4
6
|
reset: '\x1b[0m',
|
|
5
7
|
red: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
|
|
@@ -167,3 +169,148 @@ export function requireInit(isInitialized, projectRoot) {
|
|
|
167
169
|
fatal('not a relq repository (or any of the parent directories): .relq', "run 'relq init' to initialize a repository");
|
|
168
170
|
}
|
|
169
171
|
}
|
|
172
|
+
export function createMultiProgress() {
|
|
173
|
+
const spinner = cliSpinners.dots;
|
|
174
|
+
let items = [];
|
|
175
|
+
let frameIndex = 0;
|
|
176
|
+
let interval = null;
|
|
177
|
+
let isRunning = false;
|
|
178
|
+
let lineCount = 0;
|
|
179
|
+
let lastPrintedStatus = new Map();
|
|
180
|
+
const moveCursorUp = (lines) => {
|
|
181
|
+
if (isTTY && lines > 0) {
|
|
182
|
+
process.stdout.write(`\x1b[${lines}A`);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const clearLine = () => {
|
|
186
|
+
if (isTTY) {
|
|
187
|
+
process.stdout.write('\x1b[2K\r');
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const padNumber = (n, width = 4) => {
|
|
191
|
+
return n.toString().padStart(width);
|
|
192
|
+
};
|
|
193
|
+
const render = () => {
|
|
194
|
+
if (!isRunning || !isTTY)
|
|
195
|
+
return;
|
|
196
|
+
if (lineCount > 0) {
|
|
197
|
+
moveCursorUp(lineCount);
|
|
198
|
+
}
|
|
199
|
+
const frame = spinner.frames[frameIndex];
|
|
200
|
+
frameIndex = (frameIndex + 1) % spinner.frames.length;
|
|
201
|
+
for (const item of items) {
|
|
202
|
+
clearLine();
|
|
203
|
+
let prefix;
|
|
204
|
+
let statusText;
|
|
205
|
+
let countStr = padNumber(item.count);
|
|
206
|
+
if (item.status === 'pending') {
|
|
207
|
+
prefix = colors.gray('[ ]');
|
|
208
|
+
statusText = colors.gray(`${countStr} ${item.label} waiting`);
|
|
209
|
+
}
|
|
210
|
+
else if (item.status === 'fetching') {
|
|
211
|
+
prefix = colors.cyan(`[${frame}]`);
|
|
212
|
+
statusText = colors.white(`${countStr} ${item.label} fetching`);
|
|
213
|
+
}
|
|
214
|
+
else if (item.status === 'done') {
|
|
215
|
+
prefix = colors.green('[+]');
|
|
216
|
+
statusText = colors.green(`${countStr} ${item.label} fetched`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
prefix = colors.red('[x]');
|
|
220
|
+
statusText = colors.red(`${countStr} ${item.label} error`);
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write(`${prefix} ${statusText}\n`);
|
|
223
|
+
}
|
|
224
|
+
lineCount = items.length;
|
|
225
|
+
};
|
|
226
|
+
const printStaticLine = (item) => {
|
|
227
|
+
let prefix;
|
|
228
|
+
let statusText;
|
|
229
|
+
let countStr = padNumber(item.count);
|
|
230
|
+
if (item.status === 'error') {
|
|
231
|
+
prefix = '[x]';
|
|
232
|
+
statusText = `${countStr} ${item.label} error`;
|
|
233
|
+
}
|
|
234
|
+
else if (item.status === 'done' && item.count > 0) {
|
|
235
|
+
prefix = '[+]';
|
|
236
|
+
statusText = `${countStr} ${item.label} fetched`;
|
|
237
|
+
}
|
|
238
|
+
else if (item.status === 'done') {
|
|
239
|
+
prefix = '[+]';
|
|
240
|
+
statusText = `${countStr} ${item.label} fetched`;
|
|
241
|
+
}
|
|
242
|
+
else if (item.status === 'fetching') {
|
|
243
|
+
prefix = '[~]';
|
|
244
|
+
statusText = `${countStr} ${item.label} fetching`;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
prefix = '[ ]';
|
|
248
|
+
statusText = `${countStr} ${item.label} waiting`;
|
|
249
|
+
}
|
|
250
|
+
console.log(`${prefix} ${statusText}`);
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
setItems(newItems) {
|
|
254
|
+
items = [...newItems];
|
|
255
|
+
lastPrintedStatus.clear();
|
|
256
|
+
},
|
|
257
|
+
updateItem(id, updates) {
|
|
258
|
+
const item = items.find(i => i.id === id);
|
|
259
|
+
if (item) {
|
|
260
|
+
const prevStatus = item.status;
|
|
261
|
+
Object.assign(item, updates);
|
|
262
|
+
if (!isTTY && isRunning) {
|
|
263
|
+
const statusKey = `${item.id}:${item.status}:${item.count}`;
|
|
264
|
+
if (lastPrintedStatus.get(item.id) !== statusKey) {
|
|
265
|
+
printStaticLine(item);
|
|
266
|
+
lastPrintedStatus.set(item.id, statusKey);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
start() {
|
|
272
|
+
if (isRunning)
|
|
273
|
+
return;
|
|
274
|
+
isRunning = true;
|
|
275
|
+
frameIndex = 0;
|
|
276
|
+
lineCount = 0;
|
|
277
|
+
lastPrintedStatus.clear();
|
|
278
|
+
if (isTTY) {
|
|
279
|
+
interval = setInterval(render, spinner.interval);
|
|
280
|
+
render();
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
stop() {
|
|
284
|
+
if (interval) {
|
|
285
|
+
clearInterval(interval);
|
|
286
|
+
interval = null;
|
|
287
|
+
}
|
|
288
|
+
isRunning = false;
|
|
289
|
+
},
|
|
290
|
+
complete() {
|
|
291
|
+
this.stop();
|
|
292
|
+
if (isTTY && lineCount > 0) {
|
|
293
|
+
moveCursorUp(lineCount);
|
|
294
|
+
for (const item of items) {
|
|
295
|
+
clearLine();
|
|
296
|
+
let prefix;
|
|
297
|
+
let statusText;
|
|
298
|
+
let countStr = padNumber(item.count);
|
|
299
|
+
if (item.status === 'error') {
|
|
300
|
+
prefix = colors.red('[x]');
|
|
301
|
+
statusText = colors.red(`${countStr} ${item.label} error`);
|
|
302
|
+
}
|
|
303
|
+
else if (item.status === 'done' && item.count > 0) {
|
|
304
|
+
prefix = colors.green('[+]');
|
|
305
|
+
statusText = colors.green(`${countStr} ${item.label} fetched`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
prefix = colors.dim('[+]');
|
|
309
|
+
statusText = colors.dim(`${countStr} ${item.label} fetched`);
|
|
310
|
+
}
|
|
311
|
+
process.stdout.write(`${prefix} ${statusText}\n`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
@@ -11,7 +11,7 @@ function parseOptionsArray(options) {
|
|
|
11
11
|
return result;
|
|
12
12
|
}
|
|
13
13
|
export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
14
|
-
const { includeFunctions = false, includeTriggers = false } = options || {};
|
|
14
|
+
const { includeFunctions = false, includeTriggers = false, onDetailedProgress } = options || {};
|
|
15
15
|
const { Pool } = await import("../../addon/pg/index.js");
|
|
16
16
|
onProgress?.('connecting', connection.database);
|
|
17
17
|
const pool = new Pool({
|
|
@@ -25,6 +25,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
25
25
|
});
|
|
26
26
|
try {
|
|
27
27
|
onProgress?.('fetching_tables');
|
|
28
|
+
onDetailedProgress?.({ step: 'tables', count: 0, status: 'fetching' });
|
|
28
29
|
const tablesResult = await pool.query(`
|
|
29
30
|
SELECT
|
|
30
31
|
c.relname as table_name,
|
|
@@ -32,6 +33,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
32
33
|
c.reltuples::bigint as row_count,
|
|
33
34
|
c.relispartition as is_partition,
|
|
34
35
|
c.relkind = 'p' as is_partitioned,
|
|
36
|
+
obj_description(c.oid, 'pg_class') as table_comment,
|
|
35
37
|
CASE
|
|
36
38
|
WHEN pt.partstrat = 'l' THEN 'LIST'
|
|
37
39
|
WHEN pt.partstrat = 'r' THEN 'RANGE'
|
|
@@ -52,7 +54,9 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
52
54
|
AND NOT c.relispartition
|
|
53
55
|
ORDER BY c.relname;
|
|
54
56
|
`);
|
|
57
|
+
onDetailedProgress?.({ step: 'tables', count: tablesResult.rows.length, status: 'done' });
|
|
55
58
|
onProgress?.('fetching_columns');
|
|
59
|
+
onDetailedProgress?.({ step: 'columns', count: 0, status: 'fetching' });
|
|
56
60
|
const columnsResult = await pool.query(`
|
|
57
61
|
SELECT
|
|
58
62
|
c.table_name,
|
|
@@ -74,26 +78,43 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
74
78
|
WHERE c.table_schema = 'public'
|
|
75
79
|
ORDER BY c.table_name, c.ordinal_position;
|
|
76
80
|
`);
|
|
81
|
+
onDetailedProgress?.({ step: 'columns', count: columnsResult.rows.length, status: 'done' });
|
|
82
|
+
onDetailedProgress?.({ step: 'constraints', count: 0, status: 'fetching' });
|
|
77
83
|
const constraintsResult = await pool.query(`
|
|
78
84
|
SELECT
|
|
79
85
|
tc.table_name,
|
|
80
86
|
tc.constraint_name,
|
|
81
87
|
tc.constraint_type,
|
|
82
|
-
|
|
88
|
+
(
|
|
89
|
+
SELECT array_agg(kcu.column_name ORDER BY kcu.ordinal_position)
|
|
90
|
+
FROM information_schema.key_column_usage kcu
|
|
91
|
+
WHERE kcu.constraint_name = tc.constraint_name
|
|
92
|
+
AND kcu.table_schema = tc.table_schema
|
|
93
|
+
AND kcu.table_name = tc.table_name
|
|
94
|
+
) as columns,
|
|
83
95
|
ccu.table_name as referenced_table,
|
|
84
|
-
|
|
96
|
+
(
|
|
97
|
+
SELECT array_agg(a.attname ORDER BY array_position(con.confkey, a.attnum))
|
|
98
|
+
FROM pg_constraint con
|
|
99
|
+
JOIN pg_class rel ON con.conrelid = rel.oid
|
|
100
|
+
JOIN pg_namespace nsp ON rel.relnamespace = nsp.oid
|
|
101
|
+
JOIN pg_attribute a ON a.attrelid = con.confrelid AND a.attnum = ANY(con.confkey)
|
|
102
|
+
WHERE con.conname = tc.constraint_name
|
|
103
|
+
AND nsp.nspname = tc.table_schema
|
|
104
|
+
AND rel.relname = tc.table_name
|
|
105
|
+
) as referenced_columns,
|
|
85
106
|
rc.update_rule as on_update,
|
|
86
107
|
rc.delete_rule as on_delete
|
|
87
108
|
FROM information_schema.table_constraints tc
|
|
88
|
-
JOIN information_schema.key_column_usage kcu
|
|
89
|
-
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
90
109
|
LEFT JOIN information_schema.constraint_column_usage ccu
|
|
91
110
|
ON tc.constraint_name = ccu.constraint_name AND tc.constraint_type = 'FOREIGN KEY'
|
|
92
111
|
LEFT JOIN information_schema.referential_constraints rc
|
|
93
112
|
ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema
|
|
94
113
|
WHERE tc.table_schema = 'public'
|
|
95
|
-
GROUP BY tc.table_name, tc.constraint_name, tc.constraint_type, ccu.table_name, rc.update_rule, rc.delete_rule;
|
|
114
|
+
GROUP BY tc.table_schema, tc.table_name, tc.constraint_name, tc.constraint_type, ccu.table_name, rc.update_rule, rc.delete_rule;
|
|
96
115
|
`);
|
|
116
|
+
onDetailedProgress?.({ step: 'constraints', count: constraintsResult.rows.length, status: 'done' });
|
|
117
|
+
onDetailedProgress?.({ step: 'indexes', count: 0, status: 'fetching' });
|
|
97
118
|
const indexesResult = await pool.query(`
|
|
98
119
|
SELECT
|
|
99
120
|
t.relname as table_name,
|
|
@@ -104,6 +125,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
104
125
|
pg_get_indexdef(ix.indexrelid) as index_definition,
|
|
105
126
|
pg_get_expr(ix.indpred, ix.indrelid) as where_clause,
|
|
106
127
|
pg_get_expr(ix.indexprs, ix.indrelid) as expression,
|
|
128
|
+
obj_description(i.oid, 'pg_class') as index_comment,
|
|
107
129
|
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) FILTER (WHERE a.attnum > 0) as columns
|
|
108
130
|
FROM pg_index ix
|
|
109
131
|
JOIN pg_class t ON t.oid = ix.indrelid
|
|
@@ -113,8 +135,10 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
113
135
|
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
114
136
|
WHERE n.nspname = 'public'
|
|
115
137
|
AND NOT ix.indisprimary
|
|
116
|
-
GROUP BY t.relname, i.relname, am.amname, ix.indisunique, ix.indisprimary, ix.indexrelid, ix.indpred, ix.indexprs, ix.indrelid;
|
|
138
|
+
GROUP BY t.relname, i.relname, i.oid, am.amname, ix.indisunique, ix.indisprimary, ix.indexrelid, ix.indpred, ix.indexprs, ix.indrelid;
|
|
117
139
|
`);
|
|
140
|
+
onDetailedProgress?.({ step: 'indexes', count: indexesResult.rows.length, status: 'done' });
|
|
141
|
+
onDetailedProgress?.({ step: 'checks', count: 0, status: 'fetching' });
|
|
118
142
|
const checksResult = await pool.query(`
|
|
119
143
|
SELECT
|
|
120
144
|
c.relname as table_name,
|
|
@@ -125,7 +149,9 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
125
149
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
126
150
|
WHERE n.nspname = 'public' AND con.contype = 'c';
|
|
127
151
|
`);
|
|
152
|
+
onDetailedProgress?.({ step: 'checks', count: checksResult.rows.length, status: 'done' });
|
|
128
153
|
onProgress?.('fetching_enums');
|
|
154
|
+
onDetailedProgress?.({ step: 'enums', count: 0, status: 'fetching' });
|
|
129
155
|
const enumsResult = await pool.query(`
|
|
130
156
|
SELECT
|
|
131
157
|
t.typname as enum_name,
|
|
@@ -138,6 +164,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
138
164
|
GROUP BY t.typname, n.nspname
|
|
139
165
|
ORDER BY t.typname;
|
|
140
166
|
`);
|
|
167
|
+
onDetailedProgress?.({ step: 'enums', count: enumsResult.rows.length, status: 'done' });
|
|
141
168
|
const enums = enumsResult.rows.map(row => ({
|
|
142
169
|
name: row.enum_name,
|
|
143
170
|
schema: row.enum_schema,
|
|
@@ -173,32 +200,46 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
173
200
|
if (!constraintsByTable.has(con.table_name)) {
|
|
174
201
|
constraintsByTable.set(con.table_name, new Map());
|
|
175
202
|
}
|
|
203
|
+
let constraintColumns;
|
|
204
|
+
if (Array.isArray(con.columns)) {
|
|
205
|
+
constraintColumns = con.columns;
|
|
206
|
+
}
|
|
207
|
+
else if (typeof con.columns === 'string') {
|
|
208
|
+
constraintColumns = con.columns.replace(/^\{|\}$/g, '').split(',').filter(Boolean);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
constraintColumns = [];
|
|
212
|
+
}
|
|
176
213
|
constraintsByTable.get(con.table_name).set(con.constraint_name, {
|
|
177
214
|
type: con.constraint_type,
|
|
178
|
-
columns:
|
|
215
|
+
columns: constraintColumns,
|
|
216
|
+
referenced_table: con.referenced_table,
|
|
217
|
+
referenced_columns: con.referenced_columns,
|
|
218
|
+
on_delete: con.on_delete,
|
|
219
|
+
on_update: con.on_update,
|
|
179
220
|
});
|
|
180
221
|
const columns = columnsByTable.get(con.table_name);
|
|
181
222
|
if (columns) {
|
|
182
|
-
for (const
|
|
183
|
-
|
|
223
|
+
for (const rawColName of constraintColumns) {
|
|
224
|
+
if (!rawColName)
|
|
225
|
+
continue;
|
|
226
|
+
const normalizedColName = rawColName.replace(/^"|"$/g, '').toLowerCase();
|
|
227
|
+
const col = columns.find(c => c.name.toLowerCase() === normalizedColName);
|
|
184
228
|
if (col) {
|
|
185
229
|
if (con.constraint_type === 'PRIMARY KEY')
|
|
186
230
|
col.isPrimaryKey = true;
|
|
187
|
-
if (con.constraint_type === 'UNIQUE')
|
|
188
|
-
col.isUnique = true;
|
|
189
|
-
if (con.constraint_type === 'FOREIGN KEY' && con.referenced_table) {
|
|
190
|
-
col.references = {
|
|
191
|
-
table: con.referenced_table,
|
|
192
|
-
column: con.referenced_columns?.[0] || 'id',
|
|
193
|
-
onDelete: con.on_delete || 'NO ACTION',
|
|
194
|
-
onUpdate: con.on_update || 'NO ACTION',
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
231
|
}
|
|
198
232
|
}
|
|
199
233
|
}
|
|
200
234
|
}
|
|
201
235
|
for (const idx of indexesResult.rows) {
|
|
236
|
+
const tableConstraints = constraintsByTable.get(idx.table_name);
|
|
237
|
+
if (tableConstraints?.has(idx.index_name)) {
|
|
238
|
+
const con = tableConstraints.get(idx.index_name);
|
|
239
|
+
if (con?.type === 'UNIQUE' || con?.type === 'PRIMARY KEY') {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
202
243
|
if (!indexesByTable.has(idx.table_name)) {
|
|
203
244
|
indexesByTable.set(idx.table_name, []);
|
|
204
245
|
}
|
|
@@ -214,6 +255,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
214
255
|
whereClause: idx.where_clause || null,
|
|
215
256
|
expression: idx.expression || null,
|
|
216
257
|
operatorClasses: operatorClasses.length > 0 ? operatorClasses : undefined,
|
|
258
|
+
comment: idx.index_comment || null,
|
|
217
259
|
});
|
|
218
260
|
}
|
|
219
261
|
for (const chk of checksResult.rows) {
|
|
@@ -232,19 +274,87 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
232
274
|
const tableName = row.table_name;
|
|
233
275
|
if (tableName.startsWith('_relq') || tableName.startsWith('_kuery'))
|
|
234
276
|
continue;
|
|
277
|
+
const allConstraints = [
|
|
278
|
+
...(checksByTable.get(tableName) || []),
|
|
279
|
+
];
|
|
280
|
+
const tableConstraints = constraintsByTable.get(tableName);
|
|
281
|
+
if (tableConstraints) {
|
|
282
|
+
for (const [conName, con] of tableConstraints) {
|
|
283
|
+
let cols = [];
|
|
284
|
+
if (Array.isArray(con.columns)) {
|
|
285
|
+
cols = con.columns;
|
|
286
|
+
}
|
|
287
|
+
else if (typeof con.columns === 'string') {
|
|
288
|
+
cols = con.columns.replace(/^\{|\}$/g, '').split(',').filter(Boolean);
|
|
289
|
+
}
|
|
290
|
+
if (con.type === 'PRIMARY KEY' && cols.length > 0) {
|
|
291
|
+
const colList = cols.map(c => `"${c}"`).join(', ');
|
|
292
|
+
allConstraints.push({
|
|
293
|
+
name: conName,
|
|
294
|
+
type: 'PRIMARY KEY',
|
|
295
|
+
columns: cols,
|
|
296
|
+
definition: `PRIMARY KEY (${colList})`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else if (con.type === 'UNIQUE' && cols.length > 0) {
|
|
300
|
+
const colList = cols.map(c => `"${c}"`).join(', ');
|
|
301
|
+
allConstraints.push({
|
|
302
|
+
name: conName,
|
|
303
|
+
type: 'UNIQUE',
|
|
304
|
+
columns: cols,
|
|
305
|
+
definition: `UNIQUE (${colList})`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
else if (con.type === 'FOREIGN KEY' && cols.length > 0) {
|
|
309
|
+
const refTable = con.referenced_table;
|
|
310
|
+
const rawRefCols = con.referenced_columns;
|
|
311
|
+
const onDelete = con.on_delete;
|
|
312
|
+
const onUpdate = con.on_update;
|
|
313
|
+
let refCols = [];
|
|
314
|
+
if (Array.isArray(rawRefCols)) {
|
|
315
|
+
refCols = rawRefCols;
|
|
316
|
+
}
|
|
317
|
+
else if (typeof rawRefCols === 'string') {
|
|
318
|
+
refCols = rawRefCols.replace(/^\{|\}$/g, '').split(',').filter(Boolean);
|
|
319
|
+
}
|
|
320
|
+
const colList = cols.map(c => `"${c}"`).join(', ');
|
|
321
|
+
const refColList = refCols.map((c) => `"${c}"`).join(', ');
|
|
322
|
+
let definition = `FOREIGN KEY (${colList}) REFERENCES "${refTable}" (${refColList})`;
|
|
323
|
+
if (onDelete && onDelete !== 'NO ACTION')
|
|
324
|
+
definition += ` ON DELETE ${onDelete}`;
|
|
325
|
+
if (onUpdate && onUpdate !== 'NO ACTION')
|
|
326
|
+
definition += ` ON UPDATE ${onUpdate}`;
|
|
327
|
+
allConstraints.push({
|
|
328
|
+
name: conName,
|
|
329
|
+
type: 'FOREIGN KEY',
|
|
330
|
+
columns: cols,
|
|
331
|
+
definition,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
let partitionKey = [];
|
|
337
|
+
if (Array.isArray(row.partition_key)) {
|
|
338
|
+
partitionKey = row.partition_key;
|
|
339
|
+
}
|
|
340
|
+
else if (typeof row.partition_key === 'string') {
|
|
341
|
+
partitionKey = row.partition_key.replace(/^\{|\}$/g, '').split(',').filter(Boolean);
|
|
342
|
+
}
|
|
235
343
|
tables.push({
|
|
236
344
|
name: tableName,
|
|
237
345
|
schema: row.table_schema,
|
|
238
346
|
columns: columnsByTable.get(tableName) || [],
|
|
239
347
|
indexes: indexesByTable.get(tableName) || [],
|
|
240
|
-
constraints:
|
|
348
|
+
constraints: allConstraints,
|
|
241
349
|
rowCount: parseInt(row.row_count) || 0,
|
|
242
350
|
isPartitioned: row.is_partitioned || false,
|
|
243
351
|
partitionType: row.partition_type,
|
|
244
|
-
partitionKey:
|
|
352
|
+
partitionKey: partitionKey,
|
|
353
|
+
comment: row.table_comment || null,
|
|
245
354
|
});
|
|
246
355
|
}
|
|
247
356
|
onProgress?.('fetching_partitions');
|
|
357
|
+
onDetailedProgress?.({ step: 'partitions', count: 0, status: 'fetching' });
|
|
248
358
|
const partitionsResult = await pool.query(`
|
|
249
359
|
SELECT
|
|
250
360
|
child.relname as partition_name,
|
|
@@ -272,19 +382,26 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
272
382
|
}
|
|
273
383
|
partitionsByParent.get(partition.parentTable).push(partition);
|
|
274
384
|
}
|
|
385
|
+
let attachedPartitionCount = 0;
|
|
275
386
|
for (const table of tables) {
|
|
276
387
|
if (table.isPartitioned) {
|
|
277
|
-
|
|
388
|
+
const childPartitions = partitionsByParent.get(table.name) || [];
|
|
389
|
+
table.childPartitions = childPartitions;
|
|
390
|
+
attachedPartitionCount += childPartitions.length;
|
|
278
391
|
}
|
|
279
392
|
}
|
|
393
|
+
onDetailedProgress?.({ step: 'partitions', count: attachedPartitionCount, status: 'done' });
|
|
280
394
|
onProgress?.('fetching_extensions');
|
|
395
|
+
onDetailedProgress?.({ step: 'extensions', count: 0, status: 'fetching' });
|
|
281
396
|
const extensionsResult = await pool.query(`
|
|
282
397
|
SELECT extname FROM pg_extension WHERE extname != 'plpgsql';
|
|
283
398
|
`);
|
|
284
399
|
const extensions = extensionsResult.rows.map(r => r.extname);
|
|
400
|
+
onDetailedProgress?.({ step: 'extensions', count: extensions.length, status: 'done' });
|
|
285
401
|
let functions = [];
|
|
286
402
|
if (includeFunctions) {
|
|
287
403
|
onProgress?.('fetching_functions');
|
|
404
|
+
onDetailedProgress?.({ step: 'functions', count: 0, status: 'fetching' });
|
|
288
405
|
const functionsResult = await pool.query(`
|
|
289
406
|
SELECT
|
|
290
407
|
p.proname as name,
|
|
@@ -312,10 +429,12 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
312
429
|
isAggregate: f.is_aggregate || false,
|
|
313
430
|
volatility: f.volatility === 'i' ? 'IMMUTABLE' : f.volatility === 's' ? 'STABLE' : 'VOLATILE',
|
|
314
431
|
}));
|
|
432
|
+
onDetailedProgress?.({ step: 'functions', count: functions.length, status: 'done' });
|
|
315
433
|
}
|
|
316
434
|
let triggers = [];
|
|
317
435
|
if (includeTriggers) {
|
|
318
436
|
onProgress?.('fetching_triggers');
|
|
437
|
+
onDetailedProgress?.({ step: 'triggers', count: 0, status: 'fetching' });
|
|
319
438
|
const triggersResult = await pool.query(`
|
|
320
439
|
SELECT
|
|
321
440
|
t.tgname as name,
|
|
@@ -351,8 +470,10 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
351
470
|
definition: t.definition || '',
|
|
352
471
|
isEnabled: t.is_enabled,
|
|
353
472
|
}));
|
|
473
|
+
onDetailedProgress?.({ step: 'triggers', count: triggers.length, status: 'done' });
|
|
354
474
|
}
|
|
355
475
|
onProgress?.('fetching_collations');
|
|
476
|
+
onDetailedProgress?.({ step: 'collations', count: 0, status: 'fetching' });
|
|
356
477
|
const collationsResult = await pool.query(`
|
|
357
478
|
SELECT
|
|
358
479
|
c.collname as name,
|
|
@@ -374,7 +495,9 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
374
495
|
lcCtype: c.lc_ctype,
|
|
375
496
|
deterministic: c.deterministic,
|
|
376
497
|
}));
|
|
498
|
+
onDetailedProgress?.({ step: 'collations', count: collations.length, status: 'done' });
|
|
377
499
|
onProgress?.('fetching_foreign_servers');
|
|
500
|
+
onDetailedProgress?.({ step: 'foreign_servers', count: 0, status: 'fetching' });
|
|
378
501
|
const foreignServersResult = await pool.query(`
|
|
379
502
|
SELECT
|
|
380
503
|
s.srvname as name,
|
|
@@ -389,7 +512,9 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
389
512
|
foreignDataWrapper: s.fdw,
|
|
390
513
|
options: parseOptionsArray(s.options),
|
|
391
514
|
}));
|
|
515
|
+
onDetailedProgress?.({ step: 'foreign_servers', count: foreignServers.length, status: 'done' });
|
|
392
516
|
onProgress?.('fetching_foreign_tables');
|
|
517
|
+
onDetailedProgress?.({ step: 'foreign_tables', count: 0, status: 'fetching' });
|
|
393
518
|
const foreignTablesResult = await pool.query(`
|
|
394
519
|
SELECT
|
|
395
520
|
c.relname as name,
|
|
@@ -445,6 +570,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
445
570
|
})),
|
|
446
571
|
options: parseOptionsArray(t.options),
|
|
447
572
|
}));
|
|
573
|
+
onDetailedProgress?.({ step: 'foreign_tables', count: foreignTables.length, status: 'done' });
|
|
448
574
|
onProgress?.('complete');
|
|
449
575
|
return {
|
|
450
576
|
tables,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|