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