relq 1.0.5 → 1.0.7

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 (86) hide show
  1. package/dist/cjs/cli/commands/add.cjs +257 -17
  2. package/dist/cjs/cli/commands/commit.cjs +13 -2
  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/config/config.cjs +29 -10
  32. package/dist/cjs/core/relq-client.cjs +22 -6
  33. package/dist/cjs/schema-definition/column-types.cjs +149 -13
  34. package/dist/cjs/schema-definition/defaults.cjs +72 -0
  35. package/dist/cjs/schema-definition/index.cjs +15 -1
  36. package/dist/cjs/schema-definition/introspection.cjs +7 -3
  37. package/dist/cjs/schema-definition/pg-relations.cjs +169 -0
  38. package/dist/cjs/schema-definition/pg-view.cjs +30 -0
  39. package/dist/cjs/schema-definition/table-definition.cjs +110 -4
  40. package/dist/cjs/types/config-types.cjs +13 -4
  41. package/dist/cjs/utils/aws-dsql.cjs +177 -0
  42. package/dist/config.d.ts +147 -2
  43. package/dist/esm/cli/commands/add.js +255 -18
  44. package/dist/esm/cli/commands/commit.js +13 -2
  45. package/dist/esm/cli/commands/export.js +25 -19
  46. package/dist/esm/cli/commands/import.js +221 -102
  47. package/dist/esm/cli/commands/init.js +86 -14
  48. package/dist/esm/cli/commands/pull.js +106 -25
  49. package/dist/esm/cli/commands/push.js +39 -4
  50. package/dist/esm/cli/index.js +9 -1
  51. package/dist/esm/cli/utils/ast/codegen/builder.js +291 -0
  52. package/dist/esm/cli/utils/ast/codegen/constraints.js +176 -0
  53. package/dist/esm/cli/utils/ast/codegen/defaults.js +305 -0
  54. package/dist/esm/cli/utils/ast/codegen/index.js +6 -0
  55. package/dist/esm/cli/utils/ast/codegen/type-map.js +111 -0
  56. package/dist/esm/cli/utils/ast/codegen/utils.js +60 -0
  57. package/dist/esm/cli/utils/ast/index.js +3 -0
  58. package/dist/esm/cli/utils/ast/transformer/helpers.js +141 -0
  59. package/dist/esm/cli/utils/ast/transformer/index.js +2 -0
  60. package/dist/esm/cli/utils/ast/types.js +1 -0
  61. package/dist/esm/cli/utils/ast-codegen.js +945 -0
  62. package/dist/esm/cli/utils/ast-transformer.js +907 -0
  63. package/dist/esm/cli/utils/change-tracker.js +50 -1
  64. package/dist/esm/cli/utils/cli-utils.js +147 -0
  65. package/dist/esm/cli/utils/fast-introspect.js +149 -23
  66. package/dist/esm/cli/utils/pg-parser.js +1 -0
  67. package/dist/esm/cli/utils/repo-manager.js +114 -4
  68. package/dist/esm/cli/utils/schema-comparator.js +98 -14
  69. package/dist/esm/cli/utils/schema-introspect.js +56 -19
  70. package/dist/esm/cli/utils/snapshot-manager.js +0 -1
  71. package/dist/esm/cli/utils/sql-generator.js +353 -64
  72. package/dist/esm/cli/utils/type-generator.js +114 -15
  73. package/dist/esm/config/config.js +29 -10
  74. package/dist/esm/core/relq-client.js +23 -7
  75. package/dist/esm/schema-definition/column-types.js +146 -12
  76. package/dist/esm/schema-definition/defaults.js +69 -0
  77. package/dist/esm/schema-definition/index.js +3 -0
  78. package/dist/esm/schema-definition/introspection.js +7 -3
  79. package/dist/esm/schema-definition/pg-relations.js +161 -0
  80. package/dist/esm/schema-definition/pg-view.js +24 -0
  81. package/dist/esm/schema-definition/table-definition.js +110 -4
  82. package/dist/esm/types/config-types.js +12 -4
  83. package/dist/esm/utils/aws-dsql.js +139 -0
  84. package/dist/index.d.ts +159 -1
  85. package/dist/schema-builder.d.ts +1314 -32
  86. package/package.json +1 -1
@@ -67,6 +67,81 @@ export function generateHash(schema) {
67
67
  export function shortHash(hash) {
68
68
  return hash.substring(0, 7);
69
69
  }
70
+ export function getConnectionId(connection) {
71
+ const parts = [
72
+ connection.host || 'localhost',
73
+ String(connection.port || 5432),
74
+ connection.database || 'postgres',
75
+ connection.user || 'postgres',
76
+ ];
77
+ return crypto.createHash('sha256').update(parts.join('/')).digest('hex').substring(0, 12);
78
+ }
79
+ export function getConnectionLabel(connection) {
80
+ const user = connection.user || 'postgres';
81
+ const host = connection.host || 'localhost';
82
+ const db = connection.database || 'postgres';
83
+ return `${user}@${host}/${db}`;
84
+ }
85
+ export function getRemoteRef(connection) {
86
+ return {
87
+ id: getConnectionId(connection),
88
+ label: getConnectionLabel(connection),
89
+ };
90
+ }
91
+ export function isCommitSyncedWith(commit, connection) {
92
+ const connectionId = getConnectionId(connection);
93
+ if (commit.remotes?.pushed?.some((r) => r.id === connectionId)) {
94
+ return { synced: true, type: 'pushed' };
95
+ }
96
+ if (commit.remotes?.pulled?.some((r) => r.id === connectionId)) {
97
+ return { synced: true, type: 'pulled' };
98
+ }
99
+ if (commit.message?.startsWith('pull: sync from ')) {
100
+ const pullTarget = commit.message.replace('pull: sync from ', '');
101
+ const currentLabel = getConnectionLabel(connection);
102
+ const legacyLabel = `${connection.user}@${connection.host}:${connection.port || 5432}/${connection.database}`;
103
+ if (pullTarget === currentLabel || pullTarget === legacyLabel) {
104
+ return { synced: true, type: 'pulled' };
105
+ }
106
+ }
107
+ return { synced: false, type: null };
108
+ }
109
+ export function markCommitAsPushed(commitHash, connection, projectRoot = process.cwd()) {
110
+ const commitPath = path.join(projectRoot, RELQ_DIR, COMMITS_DIR, `${commitHash}.json`);
111
+ if (!fs.existsSync(commitPath)) {
112
+ return;
113
+ }
114
+ const commit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
115
+ const remoteRef = getRemoteRef(connection);
116
+ if (!commit.remotes) {
117
+ commit.remotes = {};
118
+ }
119
+ if (!commit.remotes.pushed) {
120
+ commit.remotes.pushed = [];
121
+ }
122
+ if (!commit.remotes.pushed.some((r) => r.id === remoteRef.id)) {
123
+ commit.remotes.pushed.push(remoteRef);
124
+ }
125
+ fs.writeFileSync(commitPath, JSON.stringify(commit, null, 2), 'utf-8');
126
+ }
127
+ export function markCommitAsPulled(commitHash, connection, projectRoot = process.cwd()) {
128
+ const commitPath = path.join(projectRoot, RELQ_DIR, COMMITS_DIR, `${commitHash}.json`);
129
+ if (!fs.existsSync(commitPath)) {
130
+ return;
131
+ }
132
+ const commit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
133
+ const remoteRef = getRemoteRef(connection);
134
+ if (!commit.remotes) {
135
+ commit.remotes = {};
136
+ }
137
+ if (!commit.remotes.pulled) {
138
+ commit.remotes.pulled = [];
139
+ }
140
+ if (!commit.remotes.pulled.some((r) => r.id === remoteRef.id)) {
141
+ commit.remotes.pulled.push(remoteRef);
142
+ }
143
+ fs.writeFileSync(commitPath, JSON.stringify(commit, null, 2), 'utf-8');
144
+ }
70
145
  export function saveCommit(commit, projectRoot = process.cwd()) {
71
146
  const commitsPath = path.join(projectRoot, RELQ_DIR, COMMITS_DIR);
72
147
  const commitPath = path.join(commitsPath, `${commit.hash}.json`);
@@ -269,7 +344,9 @@ export function stageChanges(patterns, projectRoot = process.cwd()) {
269
344
  remaining.push(change);
270
345
  }
271
346
  }
272
- state.staged.push(...staged);
347
+ const newKeys = new Set(staged.map(c => `${c.objectType}:${c.objectName}:${c.parentName || ''}`));
348
+ const keptStaged = state.staged.filter(c => !newKeys.has(`${c.objectType}:${c.objectName}:${c.parentName || ''}`));
349
+ state.staged = [...keptStaged, ...staged];
273
350
  state.unstaged = remaining;
274
351
  state.timestamp = new Date().toISOString();
275
352
  saveWorkingState(state, projectRoot);
@@ -310,6 +387,18 @@ export function getStagedChanges(projectRoot = process.cwd()) {
310
387
  const state = loadWorkingState(projectRoot);
311
388
  return state?.staged || [];
312
389
  }
390
+ export function cleanupStagedChanges(validChanges, projectRoot = process.cwd()) {
391
+ const state = getOrCreateWorkingState(projectRoot);
392
+ const validKeys = new Set(validChanges.map(c => `${c.objectType}:${c.objectName}:${c.parentName || ''}`));
393
+ const originalCount = state.staged.length;
394
+ state.staged = state.staged.filter(c => validKeys.has(`${c.objectType}:${c.objectName}:${c.parentName || ''}`));
395
+ const removedCount = originalCount - state.staged.length;
396
+ if (removedCount > 0) {
397
+ state.timestamp = new Date().toISOString();
398
+ saveWorkingState(state, projectRoot);
399
+ }
400
+ return removedCount;
401
+ }
313
402
  export function getUnstagedChanges(projectRoot = process.cwd()) {
314
403
  const state = loadWorkingState(projectRoot);
315
404
  return state?.unstaged || [];
@@ -388,7 +477,7 @@ export async function getRemoteHead(connection) {
388
477
  const commits = await fetchRemoteCommits(connection, 1);
389
478
  return commits.length > 0 ? commits[0].hash : null;
390
479
  }
391
- export async function pushCommit(connection, commit) {
480
+ export async function pushCommit(connection, commit, projectRoot = process.cwd()) {
392
481
  const { Pool } = await import("../../addon/pg/index.js");
393
482
  const pool = new Pool({
394
483
  host: connection.host,
@@ -400,6 +489,27 @@ export async function pushCommit(connection, commit) {
400
489
  ssl: connection.ssl,
401
490
  });
402
491
  try {
492
+ let schemaSnapshot = null;
493
+ if (commit.schema) {
494
+ schemaSnapshot = commit.schema;
495
+ }
496
+ else {
497
+ schemaSnapshot = loadSnapshot(projectRoot);
498
+ }
499
+ if (!schemaSnapshot) {
500
+ throw new Error(`Cannot push commit ${commit.hash}: No schema snapshot available. Run 'relq pull' first.`);
501
+ }
502
+ const stats = commit.stats || {
503
+ tables: 0,
504
+ columns: 0,
505
+ indexes: 0,
506
+ enums: 0,
507
+ domains: 0,
508
+ compositeTypes: 0,
509
+ sequences: 0,
510
+ functions: 0,
511
+ triggers: 0,
512
+ };
403
513
  await pool.query(`
404
514
  INSERT INTO _relq_commits (hash, parent_hash, author, message, schema_snapshot, stats)
405
515
  VALUES ($1, $2, $3, $4, $5, $6)
@@ -409,8 +519,8 @@ export async function pushCommit(connection, commit) {
409
519
  commit.parentHash,
410
520
  commit.author,
411
521
  commit.message,
412
- JSON.stringify(commit.schema),
413
- JSON.stringify(commit.stats),
522
+ JSON.stringify(schemaSnapshot),
523
+ JSON.stringify(stats),
414
524
  ]);
415
525
  }
416
526
  finally {
@@ -191,6 +191,7 @@ function compareTables(before, after) {
191
191
  changes.push(...compareConstraints(beforeTable.constraints || [], table.constraints || [], name));
192
192
  changes.push(...compareTableComments(beforeTable, table, name));
193
193
  changes.push(...compareColumnComments(beforeTable.columns, table.columns, name));
194
+ changes.push(...compareIndexComments(beforeTable.indexes || [], table.indexes || [], name));
194
195
  changes.push(...comparePartitions(beforeTable, table, name));
195
196
  }
196
197
  }
@@ -224,21 +225,46 @@ function tableToChangeData(table) {
224
225
  }
225
226
  function compareColumns(before, after, tableName) {
226
227
  const changes = [];
227
- const beforeMap = new Map(before.map(c => [c.name, c]));
228
- const afterMap = new Map(after.map(c => [c.name, c]));
229
- for (const [name, col] of afterMap) {
230
- if (!beforeMap.has(name)) {
228
+ const beforeByName = new Map(before.map(c => [c.name, c]));
229
+ const afterByName = new Map(after.map(c => [c.name, c]));
230
+ const beforeByTrackingId = new Map(before.filter(c => c.trackingId).map(c => [c.trackingId, c]));
231
+ const afterByTrackingId = new Map(after.filter(c => c.trackingId).map(c => [c.trackingId, c]));
232
+ const processedBefore = new Set();
233
+ const processedAfter = new Set();
234
+ for (const [trackingId, afterCol] of afterByTrackingId) {
235
+ const beforeCol = beforeByTrackingId.get(trackingId);
236
+ if (beforeCol && beforeCol.name !== afterCol.name) {
237
+ changes.push(createChange('RENAME', 'COLUMN', afterCol.name, columnToChangeData(beforeCol), columnToChangeData(afterCol), tableName));
238
+ processedBefore.add(beforeCol.name);
239
+ processedAfter.add(afterCol.name);
240
+ }
241
+ else if (beforeCol && beforeCol.name === afterCol.name) {
242
+ if (hasColumnChanged(beforeCol, afterCol)) {
243
+ changes.push(createChange('ALTER', 'COLUMN', afterCol.name, columnToChangeData(beforeCol), columnToChangeData(afterCol), tableName));
244
+ }
245
+ processedBefore.add(beforeCol.name);
246
+ processedAfter.add(afterCol.name);
247
+ }
248
+ }
249
+ for (const [name, col] of afterByName) {
250
+ if (processedAfter.has(name))
251
+ continue;
252
+ if (!beforeByName.has(name)) {
231
253
  changes.push(createChange('CREATE', 'COLUMN', name, null, columnToChangeData(col), tableName));
232
254
  }
233
255
  else {
234
- const beforeCol = beforeMap.get(name);
235
- if (hasColumnChanged(beforeCol, col)) {
256
+ const beforeCol = beforeByName.get(name);
257
+ if (!processedBefore.has(name) && hasColumnChanged(beforeCol, col)) {
236
258
  changes.push(createChange('ALTER', 'COLUMN', name, columnToChangeData(beforeCol), columnToChangeData(col), tableName));
237
259
  }
260
+ processedBefore.add(name);
238
261
  }
262
+ processedAfter.add(name);
239
263
  }
240
- for (const [name, col] of beforeMap) {
241
- if (!afterMap.has(name)) {
264
+ for (const [name, col] of beforeByName) {
265
+ if (processedBefore.has(name))
266
+ continue;
267
+ if (!afterByName.has(name)) {
242
268
  changes.push(createChange('DROP', 'COLUMN', name, columnToChangeData(col), null, tableName));
243
269
  }
244
270
  }
@@ -255,6 +281,7 @@ function columnToChangeData(col) {
255
281
  maxLength: col.maxLength,
256
282
  precision: col.precision,
257
283
  scale: col.scale,
284
+ trackingId: col.trackingId,
258
285
  };
259
286
  }
260
287
  function normalizeDataType(type) {
@@ -321,10 +348,28 @@ function hasColumnChanged(before, after) {
321
348
  }
322
349
  function compareIndexes(before, after, tableName) {
323
350
  const changes = [];
324
- const beforeMap = new Map(before.map(i => [i.name, i]));
325
- const afterMap = new Map(after.map(i => [i.name, i]));
326
- for (const [name, idx] of afterMap) {
327
- if (!beforeMap.has(name) && !idx.isPrimary) {
351
+ const beforeByName = new Map(before.map(i => [i.name, i]));
352
+ const afterByName = new Map(after.map(i => [i.name, i]));
353
+ const beforeByTrackingId = new Map(before.filter(i => i.trackingId).map(i => [i.trackingId, i]));
354
+ const afterByTrackingId = new Map(after.filter(i => i.trackingId).map(i => [i.trackingId, i]));
355
+ const processedBefore = new Set();
356
+ const processedAfter = new Set();
357
+ for (const [trackingId, afterIdx] of afterByTrackingId) {
358
+ const beforeIdx = beforeByTrackingId.get(trackingId);
359
+ if (beforeIdx && beforeIdx.name !== afterIdx.name && !beforeIdx.isPrimary && !afterIdx.isPrimary) {
360
+ changes.push(createChange('RENAME', 'INDEX', afterIdx.name, { name: beforeIdx.name, tableName, columns: beforeIdx.columns }, { name: afterIdx.name, tableName, columns: afterIdx.columns, isUnique: afterIdx.isUnique, type: afterIdx.type }));
361
+ processedBefore.add(beforeIdx.name);
362
+ processedAfter.add(afterIdx.name);
363
+ }
364
+ else if (beforeIdx && beforeIdx.name === afterIdx.name) {
365
+ processedBefore.add(beforeIdx.name);
366
+ processedAfter.add(afterIdx.name);
367
+ }
368
+ }
369
+ for (const [name, idx] of afterByName) {
370
+ if (processedAfter.has(name) || idx.isPrimary)
371
+ continue;
372
+ if (!beforeByName.has(name)) {
328
373
  changes.push(createChange('CREATE', 'INDEX', name, null, {
329
374
  name: idx.name,
330
375
  tableName,
@@ -333,9 +378,12 @@ function compareIndexes(before, after, tableName) {
333
378
  type: idx.type,
334
379
  }));
335
380
  }
381
+ processedAfter.add(name);
336
382
  }
337
- for (const [name, idx] of beforeMap) {
338
- if (!afterMap.has(name) && !idx.isPrimary) {
383
+ for (const [name, idx] of beforeByName) {
384
+ if (processedBefore.has(name) || idx.isPrimary)
385
+ continue;
386
+ if (!afterByName.has(name)) {
339
387
  changes.push(createChange('DROP', 'INDEX', name, {
340
388
  name: idx.name,
341
389
  tableName,
@@ -501,6 +549,42 @@ function compareColumnComments(before, after, tableName) {
501
549
  }
502
550
  return changes;
503
551
  }
552
+ function compareIndexComments(before, after, tableName) {
553
+ const changes = [];
554
+ const beforeMap = new Map(before.map(i => [i.name, i]));
555
+ const afterMap = new Map(after.map(i => [i.name, i]));
556
+ for (const [name, afterIdx] of afterMap) {
557
+ const beforeIdx = beforeMap.get(name);
558
+ const afterComment = afterIdx.comment || null;
559
+ const beforeComment = beforeIdx ? beforeIdx.comment || null : null;
560
+ if (afterComment && !beforeComment) {
561
+ changes.push(createChange('CREATE', 'INDEX_COMMENT', name, null, {
562
+ tableName,
563
+ indexName: name,
564
+ comment: afterComment,
565
+ }, tableName));
566
+ }
567
+ else if (!afterComment && beforeComment) {
568
+ changes.push(createChange('DROP', 'INDEX_COMMENT', name, {
569
+ tableName,
570
+ indexName: name,
571
+ comment: beforeComment,
572
+ }, null, tableName));
573
+ }
574
+ else if (afterComment && beforeComment && afterComment !== beforeComment) {
575
+ changes.push(createChange('ALTER', 'INDEX_COMMENT', name, {
576
+ tableName,
577
+ indexName: name,
578
+ comment: beforeComment,
579
+ }, {
580
+ tableName,
581
+ indexName: name,
582
+ comment: afterComment,
583
+ }, tableName));
584
+ }
585
+ }
586
+ return changes;
587
+ }
504
588
  function comparePartitions(before, after, tableName) {
505
589
  const changes = [];
506
590
  const beforePartitioned = before.isPartitioned;
@@ -1,3 +1,19 @@
1
+ function mapInternalToFriendlyType(internalType) {
2
+ const typeMap = {
3
+ 'int2': 'smallint',
4
+ 'int4': 'integer',
5
+ 'int8': 'bigint',
6
+ 'float4': 'real',
7
+ 'float8': 'double precision',
8
+ 'bool': 'boolean',
9
+ 'timestamptz': 'timestamp with time zone',
10
+ 'timetz': 'time with time zone',
11
+ 'varchar': 'character varying',
12
+ 'bpchar': 'character',
13
+ 'varbit': 'bit varying',
14
+ };
15
+ return typeMap[internalType] || internalType;
16
+ }
1
17
  export async function introspectDatabase(connection, onProgress, options) {
2
18
  const { includeFunctions = false, includeTriggers = false } = options || {};
3
19
  const { Pool } = await import("../../addon/pg/index.js");
@@ -17,7 +33,8 @@ export async function introspectDatabase(connection, onProgress, options) {
17
33
  SELECT
18
34
  t.table_name,
19
35
  t.table_schema,
20
- (SELECT reltuples::bigint FROM pg_class WHERE relname = t.table_name) as row_count
36
+ (SELECT reltuples::bigint FROM pg_class WHERE relname = t.table_name) as row_count,
37
+ obj_description((quote_ident(t.table_schema) || '.' || quote_ident(t.table_name))::regclass, 'pg_class') as table_comment
21
38
  FROM information_schema.tables t
22
39
  WHERE t.table_schema = 'public'
23
40
  AND t.table_type = 'BASE TABLE'
@@ -74,11 +91,17 @@ export async function introspectDatabase(connection, onProgress, options) {
74
91
  WHERE tc.table_name = $1 AND tc.constraint_type = 'PRIMARY KEY'
75
92
  ) pk ON pk.column_name = c.column_name
76
93
  LEFT JOIN (
94
+ -- Only mark as unique if column is the ONLY column in the unique constraint
95
+ -- (not part of a composite unique constraint)
77
96
  SELECT kcu.column_name, true as is_unique
78
97
  FROM information_schema.table_constraints tc
79
98
  JOIN information_schema.key_column_usage kcu
80
99
  ON tc.constraint_name = kcu.constraint_name
81
100
  WHERE tc.table_name = $1 AND tc.constraint_type = 'UNIQUE'
101
+ AND (
102
+ SELECT COUNT(*) FROM information_schema.key_column_usage kcu2
103
+ WHERE kcu2.constraint_name = tc.constraint_name
104
+ ) = 1
82
105
  ) uq ON uq.column_name = c.column_name
83
106
  LEFT JOIN (
84
107
  SELECT
@@ -95,29 +118,41 @@ export async function introspectDatabase(connection, onProgress, options) {
95
118
  WHERE c.table_name = $1 AND c.table_schema = $2
96
119
  ORDER BY c.ordinal_position;
97
120
  `, [tableName, tableSchema]);
98
- const columns = columnsResult.rows.map(col => ({
99
- name: col.column_name,
100
- dataType: col.udt_name || col.data_type,
101
- isNullable: col.is_nullable === 'YES',
102
- defaultValue: col.column_default,
103
- isPrimaryKey: col.is_primary_key,
104
- isUnique: col.is_unique,
105
- maxLength: col.character_maximum_length,
106
- precision: col.numeric_precision,
107
- scale: col.numeric_scale,
108
- references: col.foreign_table ? {
109
- table: col.foreign_table,
110
- column: col.foreign_column,
111
- } : null,
112
- comment: col.column_comment || undefined,
113
- }));
121
+ const columns = columnsResult.rows.map(col => {
122
+ let dataType = col.data_type;
123
+ if (dataType === 'ARRAY' && col.udt_name) {
124
+ const baseType = col.udt_name.startsWith('_') ? col.udt_name.slice(1) : col.udt_name;
125
+ const friendlyBase = mapInternalToFriendlyType(baseType);
126
+ dataType = `${friendlyBase}[]`;
127
+ }
128
+ else if (dataType === 'USER-DEFINED' && col.udt_name) {
129
+ dataType = col.udt_name;
130
+ }
131
+ return {
132
+ name: col.column_name,
133
+ dataType,
134
+ isNullable: col.is_nullable === 'YES',
135
+ defaultValue: col.column_default,
136
+ isPrimaryKey: col.is_primary_key,
137
+ isUnique: col.is_unique,
138
+ maxLength: col.character_maximum_length,
139
+ precision: col.numeric_precision,
140
+ scale: col.numeric_scale,
141
+ references: col.foreign_table ? {
142
+ table: col.foreign_table,
143
+ column: col.foreign_column,
144
+ } : null,
145
+ comment: col.column_comment || undefined,
146
+ };
147
+ });
114
148
  const indexesResult = await pool.query(`
115
149
  SELECT
116
150
  i.relname as index_name,
117
151
  array_agg(a.attname ORDER BY k.n) as columns,
118
152
  ix.indisunique as is_unique,
119
153
  ix.indisprimary as is_primary,
120
- am.amname as index_type
154
+ am.amname as index_type,
155
+ obj_description(i.oid, 'pg_class') as index_comment
121
156
  FROM pg_index ix
122
157
  JOIN pg_class t ON t.oid = ix.indrelid
123
158
  JOIN pg_class i ON i.oid = ix.indexrelid
@@ -125,7 +160,7 @@ export async function introspectDatabase(connection, onProgress, options) {
125
160
  JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true
126
161
  JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
127
162
  WHERE t.relname = $1
128
- GROUP BY i.relname, ix.indisunique, ix.indisprimary, am.amname
163
+ GROUP BY i.relname, i.oid, ix.indisunique, ix.indisprimary, am.amname
129
164
  ORDER BY i.relname;
130
165
  `, [tableName]);
131
166
  const indexes = indexesResult.rows.map(idx => ({
@@ -134,6 +169,7 @@ export async function introspectDatabase(connection, onProgress, options) {
134
169
  isUnique: idx.is_unique,
135
170
  isPrimary: idx.is_primary,
136
171
  type: idx.index_type,
172
+ comment: idx.index_comment || null,
137
173
  }));
138
174
  const constraintsResult = await pool.query(`
139
175
  SELECT
@@ -201,6 +237,7 @@ export async function introspectDatabase(connection, onProgress, options) {
201
237
  isPartitioned: partitionInfo.is_partitioned || false,
202
238
  partitionType: partitionInfo.partition_type,
203
239
  partitionKey,
240
+ comment: row.table_comment || null,
204
241
  });
205
242
  }
206
243
  onProgress?.('fetching_extensions');
@@ -58,7 +58,6 @@ function tableToSnapshot(table) {
58
58
  maxLength: col.maxLength,
59
59
  precision: col.precision,
60
60
  scale: col.scale,
61
- references: col.references,
62
61
  };
63
62
  }
64
63
  for (const idx of table.indexes || []) {