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.
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 +149 -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 +146 -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
@@ -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
- array_agg(DISTINCT kcu.column_name ORDER BY kcu.column_name) as columns,
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
- array_agg(DISTINCT ccu.column_name) as referenced_columns,
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: con.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 colName of con.columns) {
183
- const col = columns.find(c => c.name === colName);
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: checksByTable.get(tableName) || [],
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: row.partition_key || [],
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
- table.childPartitions = partitionsByParent.get(table.name) || [];
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";