relq 1.0.2 → 1.0.4

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 (92) hide show
  1. package/dist/cjs/cli/commands/add.cjs +403 -27
  2. package/dist/cjs/cli/commands/branch.cjs +13 -23
  3. package/dist/cjs/cli/commands/checkout.cjs +16 -29
  4. package/dist/cjs/cli/commands/cherry-pick.cjs +3 -4
  5. package/dist/cjs/cli/commands/commit.cjs +21 -29
  6. package/dist/cjs/cli/commands/diff.cjs +28 -32
  7. package/dist/cjs/cli/commands/export.cjs +7 -7
  8. package/dist/cjs/cli/commands/fetch.cjs +15 -21
  9. package/dist/cjs/cli/commands/generate.cjs +28 -54
  10. package/dist/cjs/cli/commands/history.cjs +19 -40
  11. package/dist/cjs/cli/commands/import.cjs +34 -41
  12. package/dist/cjs/cli/commands/init.cjs +69 -59
  13. package/dist/cjs/cli/commands/introspect.cjs +4 -8
  14. package/dist/cjs/cli/commands/log.cjs +26 -32
  15. package/dist/cjs/cli/commands/merge.cjs +24 -41
  16. package/dist/cjs/cli/commands/migrate.cjs +12 -25
  17. package/dist/cjs/cli/commands/pull.cjs +216 -106
  18. package/dist/cjs/cli/commands/push.cjs +35 -75
  19. package/dist/cjs/cli/commands/remote.cjs +2 -1
  20. package/dist/cjs/cli/commands/reset.cjs +22 -43
  21. package/dist/cjs/cli/commands/resolve.cjs +12 -14
  22. package/dist/cjs/cli/commands/rollback.cjs +16 -38
  23. package/dist/cjs/cli/commands/stash.cjs +5 -7
  24. package/dist/cjs/cli/commands/status.cjs +5 -10
  25. package/dist/cjs/cli/commands/sync.cjs +30 -50
  26. package/dist/cjs/cli/commands/tag.cjs +3 -4
  27. package/dist/cjs/cli/index.cjs +72 -9
  28. package/dist/cjs/cli/utils/change-tracker.cjs +107 -3
  29. package/dist/cjs/cli/utils/cli-utils.cjs +217 -0
  30. package/dist/cjs/cli/utils/config-loader.cjs +34 -8
  31. package/dist/cjs/cli/utils/fast-introspect.cjs +109 -3
  32. package/dist/cjs/cli/utils/git-utils.cjs +42 -161
  33. package/dist/cjs/cli/utils/pool-manager.cjs +156 -0
  34. package/dist/cjs/cli/utils/project-root.cjs +56 -5
  35. package/dist/cjs/cli/utils/relqignore.cjs +1 -0
  36. package/dist/cjs/cli/utils/repo-manager.cjs +47 -0
  37. package/dist/cjs/cli/utils/schema-comparator.cjs +301 -11
  38. package/dist/cjs/cli/utils/schema-diff.cjs +202 -1
  39. package/dist/cjs/cli/utils/schema-hash.cjs +2 -1
  40. package/dist/cjs/cli/utils/schema-introspect.cjs +7 -3
  41. package/dist/cjs/cli/utils/snapshot-manager.cjs +1 -0
  42. package/dist/cjs/cli/utils/spinner.cjs +14 -106
  43. package/dist/cjs/cli/utils/sql-generator.cjs +10 -2
  44. package/dist/cjs/cli/utils/type-generator.cjs +28 -16
  45. package/dist/config.d.ts +16 -6
  46. package/dist/esm/cli/commands/add.js +372 -29
  47. package/dist/esm/cli/commands/branch.js +14 -24
  48. package/dist/esm/cli/commands/checkout.js +16 -29
  49. package/dist/esm/cli/commands/cherry-pick.js +3 -4
  50. package/dist/esm/cli/commands/commit.js +22 -30
  51. package/dist/esm/cli/commands/diff.js +6 -10
  52. package/dist/esm/cli/commands/export.js +8 -8
  53. package/dist/esm/cli/commands/fetch.js +14 -20
  54. package/dist/esm/cli/commands/generate.js +28 -54
  55. package/dist/esm/cli/commands/history.js +11 -32
  56. package/dist/esm/cli/commands/import.js +35 -42
  57. package/dist/esm/cli/commands/init.js +65 -55
  58. package/dist/esm/cli/commands/introspect.js +4 -8
  59. package/dist/esm/cli/commands/log.js +6 -12
  60. package/dist/esm/cli/commands/merge.js +20 -37
  61. package/dist/esm/cli/commands/migrate.js +12 -25
  62. package/dist/esm/cli/commands/pull.js +204 -94
  63. package/dist/esm/cli/commands/push.js +21 -61
  64. package/dist/esm/cli/commands/remote.js +2 -1
  65. package/dist/esm/cli/commands/reset.js +16 -37
  66. package/dist/esm/cli/commands/resolve.js +13 -15
  67. package/dist/esm/cli/commands/rollback.js +16 -38
  68. package/dist/esm/cli/commands/stash.js +6 -8
  69. package/dist/esm/cli/commands/status.js +6 -11
  70. package/dist/esm/cli/commands/sync.js +30 -50
  71. package/dist/esm/cli/commands/tag.js +3 -4
  72. package/dist/esm/cli/index.js +72 -9
  73. package/dist/esm/cli/utils/change-tracker.js +107 -3
  74. package/dist/esm/cli/utils/cli-utils.js +169 -0
  75. package/dist/esm/cli/utils/config-loader.js +34 -8
  76. package/dist/esm/cli/utils/fast-introspect.js +109 -3
  77. package/dist/esm/cli/utils/git-utils.js +2 -124
  78. package/dist/esm/cli/utils/pool-manager.js +114 -0
  79. package/dist/esm/cli/utils/project-root.js +55 -5
  80. package/dist/esm/cli/utils/relqignore.js +1 -0
  81. package/dist/esm/cli/utils/repo-manager.js +42 -0
  82. package/dist/esm/cli/utils/schema-comparator.js +301 -11
  83. package/dist/esm/cli/utils/schema-diff.js +202 -1
  84. package/dist/esm/cli/utils/schema-hash.js +2 -1
  85. package/dist/esm/cli/utils/schema-introspect.js +7 -3
  86. package/dist/esm/cli/utils/snapshot-manager.js +1 -0
  87. package/dist/esm/cli/utils/spinner.js +1 -101
  88. package/dist/esm/cli/utils/sql-generator.js +10 -2
  89. package/dist/esm/cli/utils/type-generator.js +28 -16
  90. package/dist/index.d.ts +25 -8
  91. package/dist/schema-builder.d.ts +18 -7
  92. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import * as readline from 'readline';
4
3
  import { requireValidConfig } from "../utils/config-loader.js";
5
4
  import { getConnectionDescription } from "../utils/env-loader.js";
5
+ import { confirm, fatal, colors, warning } from "../utils/cli-utils.js";
6
6
  function parseMigration(content) {
7
7
  const upMatch = content.match(/--\s*UP\s*\n([\s\S]*?)(?=--\s*DOWN|$)/i);
8
8
  const downMatch = content.match(/--\s*DOWN\s*\n([\s\S]*?)$/i);
@@ -11,31 +11,19 @@ function parseMigration(content) {
11
11
  down: downMatch?.[1]?.trim() || '',
12
12
  };
13
13
  }
14
- function askConfirm(question) {
15
- const rl = readline.createInterface({
16
- input: process.stdin,
17
- output: process.stdout,
18
- });
19
- return new Promise((resolve) => {
20
- rl.question(`${question} [y/N]: `, (answer) => {
21
- rl.close();
22
- resolve(answer.trim().toLowerCase() === 'y');
23
- });
24
- });
25
- }
26
14
  export async function migrateCommand(context) {
27
15
  const { config, flags } = context;
28
16
  if (!config) {
29
- console.error('Error: No configuration found.');
30
- process.exit(1);
17
+ fatal('No configuration found', `run ${colors.cyan('relq init')} to create a configuration file`);
18
+ return;
31
19
  }
32
- requireValidConfig(config);
20
+ await requireValidConfig(config, { calledFrom: 'migrate' });
33
21
  const dryRun = flags['dry-run'] === true;
34
22
  const force = flags['force'] === true;
35
23
  const connection = config.connection;
36
24
  const migrationsDir = config.migrations?.directory || './migrations';
37
25
  const tableName = config.migrations?.tableName || '_relq_migrations';
38
- console.log('🔄 Running migrations...');
26
+ console.log('Running migrations...');
39
27
  console.log(` Connection: ${getConnectionDescription(connection)}`);
40
28
  console.log(` Migrations: ${migrationsDir}\n`);
41
29
  try {
@@ -69,16 +57,16 @@ export async function migrateCommand(context) {
69
57
  .sort();
70
58
  const pending = migrationFiles.filter(f => !appliedMigrations.has(f));
71
59
  if (pending.length === 0) {
72
- console.log('No pending migrations.');
60
+ console.log('No pending migrations.');
73
61
  return;
74
62
  }
75
- console.log(`📋 Pending migrations (${pending.length}):`);
63
+ console.log(`Pending migrations (${pending.length}):`);
76
64
  for (const file of pending) {
77
65
  console.log(` • ${file}`);
78
66
  }
79
67
  console.log('');
80
68
  if (!force && !dryRun) {
81
- if (!await askConfirm('Apply these migrations?')) {
69
+ if (!await confirm('Apply these migrations?', false)) {
82
70
  console.log('Cancelled.');
83
71
  return;
84
72
  }
@@ -88,7 +76,7 @@ export async function migrateCommand(context) {
88
76
  const content = fs.readFileSync(filePath, 'utf-8');
89
77
  const { up } = parseMigration(content);
90
78
  if (!up) {
91
- console.log(`⚠️ Skipping ${file} (no UP section)`);
79
+ warning(`Skipping ${file} (no UP section)`);
92
80
  continue;
93
81
  }
94
82
  if (dryRun) {
@@ -105,7 +93,7 @@ export async function migrateCommand(context) {
105
93
  await client.query(up);
106
94
  await client.query(`INSERT INTO "${tableName}" (name) VALUES ($1)`, [file]);
107
95
  await client.query('COMMIT');
108
- console.log(` Applied`);
96
+ console.log(' Applied');
109
97
  }
110
98
  catch (error) {
111
99
  await client.query('ROLLBACK');
@@ -117,7 +105,7 @@ export async function migrateCommand(context) {
117
105
  }
118
106
  }
119
107
  if (!dryRun) {
120
- console.log(`\n✅ Applied ${pending.length} migration(s).`);
108
+ console.log(`\nApplied ${pending.length} migration(s).`);
121
109
  }
122
110
  }
123
111
  finally {
@@ -125,7 +113,6 @@ export async function migrateCommand(context) {
125
113
  }
126
114
  }
127
115
  catch (error) {
128
- console.error('Error:', error instanceof Error ? error.message : error);
129
- process.exit(1);
116
+ fatal('Migration failed', error instanceof Error ? error.message : String(error));
130
117
  }
131
118
  }
@@ -1,16 +1,31 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import * as readline from 'readline';
4
3
  import { requireValidConfig } from "../utils/config-loader.js";
5
4
  import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
6
5
  import { generateTypeScript } from "../utils/type-generator.js";
7
6
  import { getConnectionDescription } from "../utils/env-loader.js";
8
- import { createSpinner, colors, formatBytes } from "../utils/spinner.js";
7
+ import { createSpinner, colors, formatBytes, formatDuration, fatal, confirm, warning } from "../utils/cli-utils.js";
9
8
  import { loadRelqignore, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isCompositeTypeIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
10
- import { isInitialized, initRepository, getHead, saveSnapshot, loadSnapshot, createCommit, shortHash, fetchRemoteCommits, ensureRemoteTable, setFetchHead, getStagedChanges, getUnstagedChanges, } from "../utils/repo-manager.js";
9
+ import { isInitialized, initRepository, getHead, saveSnapshot, loadSnapshot, createCommit, shortHash, fetchRemoteCommits, ensureRemoteTable, setFetchHead, addUnstagedChanges, getStagedChanges, getUnstagedChanges, clearWorkingState, hashFileContent, saveFileHash, } from "../utils/repo-manager.js";
10
+ import { compareSchemas } from "../utils/schema-comparator.js";
11
11
  function toCamelCase(str) {
12
12
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
13
13
  }
14
+ function normalizePartitionKey(partitionKey) {
15
+ if (!partitionKey)
16
+ return undefined;
17
+ if (Array.isArray(partitionKey)) {
18
+ return partitionKey;
19
+ }
20
+ if (typeof partitionKey === 'string') {
21
+ return partitionKey
22
+ .replace(/^\{|\}$/g, '')
23
+ .split(',')
24
+ .map(k => k.trim())
25
+ .filter(Boolean);
26
+ }
27
+ return undefined;
28
+ }
14
29
  function parseSchemaFileForSnapshot(schemaPath) {
15
30
  if (!fs.existsSync(schemaPath)) {
16
31
  return null;
@@ -116,36 +131,20 @@ function parseSchemaFileForSnapshot(schemaPath) {
116
131
  extensions: [],
117
132
  };
118
133
  }
119
- function askConfirm(question, defaultYes = true) {
120
- const suffix = defaultYes ? colors.muted('[Y/n]') : colors.muted('[y/N]');
121
- const rl = readline.createInterface({
122
- input: process.stdin,
123
- output: process.stdout,
124
- });
125
- return new Promise((resolve) => {
126
- rl.question(`${question} ${suffix}: `, (answer) => {
127
- rl.close();
128
- const a = answer.trim().toLowerCase();
129
- if (!a)
130
- resolve(defaultYes);
131
- else
132
- resolve(a === 'y' || a === 'yes');
133
- });
134
- });
135
- }
136
134
  export async function pullCommand(context) {
137
- const { config, flags } = context;
135
+ const { config, flags, projectRoot } = context;
138
136
  if (!config) {
139
- console.error('Error: No configuration found.');
140
- process.exit(1);
137
+ fatal('No configuration found', `Run ${colors.cyan('relq init')} to create one.`);
141
138
  }
142
- requireValidConfig(config);
139
+ await requireValidConfig(config, { calledFrom: 'pull' });
143
140
  const connection = config.connection;
144
- const projectRoot = process.cwd();
145
141
  const force = flags['force'] === true;
142
+ const merge = flags['merge'] === true;
143
+ const noCommit = flags['no-commit'] === true;
146
144
  const dryRun = flags['dry-run'] === true;
147
145
  const author = config.author || 'Relq CLI';
148
- const schemaPath = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
146
+ const schemaPathRaw = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
147
+ const schemaPath = path.resolve(projectRoot, schemaPathRaw);
149
148
  const includeFunctions = config.generate?.includeFunctions ?? false;
150
149
  const includeTriggers = config.generate?.includeTriggers ?? false;
151
150
  const spinner = createSpinner();
@@ -153,23 +152,27 @@ export async function pullCommand(context) {
153
152
  console.log('');
154
153
  if (!isInitialized(projectRoot)) {
155
154
  initRepository(projectRoot);
156
- console.log(`${colors.cyan('ℹ')} Initialized .relq folder`);
155
+ console.log('Initialized .relq folder');
157
156
  }
158
- if (!force) {
159
- const stagedChanges = getStagedChanges(projectRoot);
160
- const unstagedChanges = getUnstagedChanges(projectRoot);
161
- if (stagedChanges.length > 0 || unstagedChanges.length > 0) {
157
+ const stagedChanges = getStagedChanges(projectRoot);
158
+ const unstagedChanges = getUnstagedChanges(projectRoot);
159
+ const hasLocalChanges = stagedChanges.length > 0 || unstagedChanges.length > 0;
160
+ let stashedSchemaContent = null;
161
+ const schemaFileExists = fs.existsSync(schemaPath);
162
+ if (hasLocalChanges) {
163
+ if (force) {
164
+ clearWorkingState(projectRoot);
165
+ }
166
+ else if (merge) {
167
+ if (schemaFileExists) {
168
+ stashedSchemaContent = fs.readFileSync(schemaPath, 'utf-8');
169
+ }
170
+ clearWorkingState(projectRoot);
171
+ console.log(`Stashed local file content for restore after pull`);
172
+ }
173
+ else {
162
174
  const hasUnstaged = unstagedChanges.length > 0;
163
175
  const hasStaged = stagedChanges.length > 0;
164
- if (hasUnstaged && hasStaged) {
165
- console.error(colors.red('Error: You have uncommitted and unstaged changes.'));
166
- }
167
- else if (hasStaged) {
168
- console.error(colors.red('Error: You have uncommitted changes.'));
169
- }
170
- else {
171
- console.error(colors.red('Error: You have unstaged changes.'));
172
- }
173
176
  console.log('');
174
177
  if (hasStaged) {
175
178
  console.log(` ${colors.green('Staged (uncommitted):')} ${stagedChanges.length} change(s)`);
@@ -177,19 +180,15 @@ export async function pullCommand(context) {
177
180
  if (hasUnstaged) {
178
181
  console.log(` ${colors.red('Unstaged:')} ${unstagedChanges.length} change(s)`);
179
182
  }
180
- console.log('');
181
- if (hasUnstaged && !hasStaged) {
182
- console.log('Please stage and commit, or reset your changes:');
183
- console.log(` ${colors.cyan('relq add .')} - stage all changes`);
184
- console.log(` ${colors.cyan('relq commit -m "message"')} - then commit`);
183
+ let errorMsg = 'You have uncommitted changes';
184
+ if (hasUnstaged && hasStaged) {
185
+ errorMsg = 'You have uncommitted and unstaged changes';
185
186
  }
186
- else {
187
- console.log('Please commit or reset your changes:');
188
- console.log(` ${colors.cyan('relq commit -m "message"')} - commit staged changes`);
187
+ else if (hasUnstaged) {
188
+ errorMsg = 'You have unstaged changes';
189
189
  }
190
- console.log(` ${colors.cyan('relq reset')} - discard all changes`);
191
- console.log(` ${colors.cyan('relq pull --force')} - force pull (overwrites local changes)`);
192
- return;
190
+ const hint = `Commit first: ${colors.cyan('relq add . && relq commit -m "message"')}\nOr merge: ${colors.cyan('relq pull --merge')} (preserve local edits)\nOr force: ${colors.cyan('relq pull --force')} (discard & pull)`;
191
+ fatal(errorMsg, hint);
193
192
  }
194
193
  }
195
194
  try {
@@ -254,6 +253,9 @@ export async function pullCommand(context) {
254
253
  type: c.type,
255
254
  definition: c.definition,
256
255
  })),
256
+ isPartitioned: t.isPartitioned,
257
+ partitionType: t.partitionType,
258
+ partitionKey: normalizePartitionKey(t.partitionKey),
257
259
  })),
258
260
  enums: filteredEnums.map(e => ({
259
261
  name: e.name,
@@ -294,12 +296,7 @@ export async function pullCommand(context) {
294
296
  console.log('');
295
297
  const mergeStatePath = path.join(projectRoot, '.relq', 'MERGE_STATE');
296
298
  if (fs.existsSync(mergeStatePath) && !force) {
297
- console.log(`${colors.red('error:')} You have unresolved merge conflicts`);
298
- console.log('');
299
- console.log(`${colors.muted('Use')} ${colors.cyan('relq resolve')} ${colors.muted('to see and resolve conflicts')}`);
300
- console.log(`${colors.muted('Or use')} ${colors.cyan('relq pull --force')} ${colors.muted('to overwrite local')}`);
301
- console.log('');
302
- return;
299
+ fatal('You have unresolved merge conflicts', `Use ${colors.cyan('relq resolve')} to see and resolve conflicts\nOr use ${colors.cyan('relq pull --force')} to overwrite local`);
303
300
  }
304
301
  if (schemaExists && localSnapshot && !force) {
305
302
  const localTables = new Set(localSnapshot.tables.map(t => t.name));
@@ -314,7 +311,6 @@ export async function pullCommand(context) {
314
311
  createdAt: new Date().toISOString(),
315
312
  };
316
313
  fs.writeFileSync(mergeStatePath, JSON.stringify(mergeState, null, 2));
317
- console.log(`${colors.red('error:')} Merge conflict detected`);
318
314
  console.log('');
319
315
  console.log(`Both local and remote have modified the same objects:`);
320
316
  console.log('');
@@ -326,17 +322,10 @@ export async function pullCommand(context) {
326
322
  if (conflicts.length > 10) {
327
323
  console.log(` ${colors.muted(`... and ${conflicts.length - 10} more`)}`);
328
324
  }
329
- console.log('');
330
- console.log('To resolve:');
331
- console.log(` ${colors.cyan('relq resolve --theirs <name>')} Take remote version`);
332
- console.log(` ${colors.cyan('relq resolve --ours <name>')} Keep local version`);
333
- console.log(` ${colors.cyan('relq resolve --all-theirs')} Take all remote`);
334
- console.log(` ${colors.cyan('relq pull --force')} Force overwrite local`);
335
- console.log('');
336
- return;
325
+ fatal('Automatic merge failed; fix conflicts and then commit', `${colors.cyan('relq resolve --theirs <name>')} Take remote version\n${colors.cyan('relq resolve --all-theirs')} Take all remote\n${colors.cyan('relq pull --force')} Force overwrite local`);
337
326
  }
338
327
  if (added.length === 0 && removed.length === 0) {
339
- console.log(`${colors.green('✓')} Already up to date with remote`);
328
+ console.log('Already up to date with remote');
340
329
  console.log('');
341
330
  return;
342
331
  }
@@ -349,40 +338,49 @@ export async function pullCommand(context) {
349
338
  }
350
339
  console.log('');
351
340
  if (!dryRun) {
352
- const proceed = await askConfirm(`${colors.bold('Pull these changes?')}`, true);
341
+ const proceed = await confirm(`${colors.bold('Pull these changes?')}`, true);
353
342
  if (!proceed) {
354
- console.log('');
355
- console.log(`${colors.muted('Cancelled.')}`);
356
- return;
343
+ fatal('Operation cancelled by user');
357
344
  }
358
345
  console.log('');
359
346
  }
360
347
  }
361
348
  else if (schemaExists && !force) {
362
- console.log(`${colors.yellow('⚠')} ${colors.bold('Local schema exists but not tracked')}`);
349
+ warning('Local schema exists but not tracked');
363
350
  console.log('');
364
351
  console.log(` ${colors.cyan(schemaPath)}`);
365
352
  console.log('');
366
353
  if (!dryRun) {
367
- const proceed = await askConfirm(`${colors.bold('Overwrite local schema?')}`, false);
354
+ const proceed = await confirm(`${colors.bold('Overwrite local schema?')}`, false);
368
355
  if (!proceed) {
369
- console.log('');
370
- console.log(`${colors.muted('Cancelled. Run')} ${colors.cyan('relq status')} ${colors.muted('to see current state.')}`);
371
- return;
356
+ fatal('Operation cancelled by user', `Run ${colors.cyan('relq status')} to see current state.`);
372
357
  }
373
358
  console.log('');
374
359
  }
375
360
  }
376
361
  else if (!schemaExists) {
377
- console.log(`${colors.cyan('ℹ')} ${colors.bold('First pull - creating schema')}`);
362
+ console.log('First pull - creating schema');
378
363
  console.log('');
379
- console.log(`📊 ${colors.bold('Schema Summary')}`);
364
+ const indexCount = filteredTables.reduce((sum, t) => sum + (t.indexes?.filter(i => !i.isPrimary).length || 0), 0);
365
+ const partitionCount = filteredTables.filter(t => t.isPartitioned).length;
366
+ const tableCommentCount = filteredTables.filter(t => t.comment).length;
367
+ const columnCommentCount = filteredTables.reduce((sum, t) => sum + t.columns.filter(c => c.comment).length, 0);
368
+ console.log('Schema Summary:');
380
369
  console.log(` ${colors.green(String(filteredTables.length))} tables`);
381
- console.log(` ${colors.green(String(dbSchema.extensions.length))} extensions`);
370
+ if (indexCount > 0)
371
+ console.log(` ${colors.green(String(indexCount))} indexes`);
372
+ if (partitionCount > 0)
373
+ console.log(` ${colors.green(String(partitionCount))} partitioned tables`);
374
+ if (tableCommentCount > 0)
375
+ console.log(` ${colors.green(String(tableCommentCount))} table comments`);
376
+ if (columnCommentCount > 0)
377
+ console.log(` ${colors.green(String(columnCommentCount))} column comments`);
378
+ if (dbSchema.extensions.length > 0)
379
+ console.log(` ${colors.green(String(dbSchema.extensions.length))} extensions`);
382
380
  console.log('');
383
381
  }
384
382
  if (dryRun) {
385
- console.log(`${colors.yellow('⚠')} Dry run - no files written`);
383
+ console.log('Dry run - no files written');
386
384
  console.log('');
387
385
  return;
388
386
  }
@@ -404,26 +402,138 @@ export async function pullCommand(context) {
404
402
  fs.writeFileSync(schemaPath, typescript, 'utf-8');
405
403
  const fileSize = Buffer.byteLength(typescript, 'utf8');
406
404
  spinner.succeed(`Written ${colors.cyan(schemaPath)} ${colors.muted(`(${formatBytes(fileSize)})`)}`);
407
- const snapshotFromFile = parseSchemaFileForSnapshot(schemaPath);
408
- if (snapshotFromFile) {
409
- saveSnapshot(snapshotFromFile, projectRoot);
405
+ const fileHash = hashFileContent(typescript);
406
+ saveFileHash(fileHash, projectRoot);
407
+ const oldSnapshot = loadSnapshot(projectRoot);
408
+ const beforeSchema = oldSnapshot ? {
409
+ extensions: oldSnapshot.extensions?.map(e => e.name) || [],
410
+ enums: oldSnapshot.enums || [],
411
+ domains: oldSnapshot.domains?.map(d => ({
412
+ name: d.name,
413
+ baseType: d.baseType,
414
+ isNotNull: d.notNull,
415
+ defaultValue: d.default,
416
+ checkExpression: d.check,
417
+ })) || [],
418
+ compositeTypes: oldSnapshot.compositeTypes || [],
419
+ sequences: oldSnapshot.sequences || [],
420
+ tables: oldSnapshot.tables.map(t => ({
421
+ name: t.name,
422
+ schema: t.schema,
423
+ columns: t.columns.map(c => ({
424
+ name: c.name,
425
+ dataType: c.type,
426
+ isNullable: c.nullable,
427
+ defaultValue: c.default,
428
+ isPrimaryKey: c.primaryKey,
429
+ isUnique: c.unique,
430
+ comment: c.comment,
431
+ })),
432
+ indexes: t.indexes.map(i => ({
433
+ name: i.name,
434
+ columns: i.columns,
435
+ isUnique: i.unique,
436
+ type: i.type,
437
+ })),
438
+ constraints: t.constraints || [],
439
+ isPartitioned: t.isPartitioned,
440
+ partitionType: t.partitionType,
441
+ partitionKey: t.partitionKey,
442
+ comment: t.comment,
443
+ })),
444
+ functions: oldSnapshot.functions || [],
445
+ triggers: oldSnapshot.triggers || [],
446
+ } : {
447
+ extensions: [],
448
+ enums: [],
449
+ domains: [],
450
+ compositeTypes: [],
451
+ sequences: [],
452
+ tables: [],
453
+ functions: [],
454
+ triggers: [],
455
+ };
456
+ const afterSchema = {
457
+ extensions: dbSchema.extensions || [],
458
+ enums: filteredEnums || [],
459
+ domains: filteredDomains?.map(d => ({
460
+ name: d.name,
461
+ baseType: d.baseType,
462
+ isNotNull: d.isNotNull,
463
+ defaultValue: d.defaultValue,
464
+ checkExpression: d.checkExpression,
465
+ })) || [],
466
+ compositeTypes: filteredCompositeTypes || [],
467
+ sequences: [],
468
+ tables: filteredTables.map(t => ({
469
+ name: t.name,
470
+ schema: t.schema,
471
+ columns: t.columns.map(c => ({
472
+ name: c.name,
473
+ dataType: c.dataType,
474
+ isNullable: c.isNullable,
475
+ defaultValue: c.defaultValue,
476
+ isPrimaryKey: c.isPrimaryKey,
477
+ isUnique: c.isUnique,
478
+ comment: c.comment,
479
+ })),
480
+ indexes: t.indexes.map(i => ({
481
+ name: i.name,
482
+ columns: i.columns,
483
+ isUnique: i.isUnique,
484
+ type: i.type,
485
+ })),
486
+ constraints: t.constraints || [],
487
+ isPartitioned: t.isPartitioned,
488
+ partitionType: t.partitionType,
489
+ partitionKey: t.partitionKey,
490
+ childPartitions: t.childPartitions,
491
+ comment: t.comment,
492
+ })),
493
+ functions: filteredFunctions || [],
494
+ triggers: filteredTriggers || [],
495
+ };
496
+ const schemaChanges = compareSchemas(beforeSchema, afterSchema);
497
+ saveSnapshot(currentSchema, projectRoot);
498
+ const duration = Date.now() - startTime;
499
+ if (noCommit) {
500
+ if (schemaChanges.length > 0) {
501
+ addUnstagedChanges(schemaChanges, projectRoot);
502
+ spinner.succeed(`Detected ${schemaChanges.length} schema change(s)`);
503
+ }
504
+ console.log('');
505
+ console.log(`Pull completed in ${formatDuration(duration)}`);
506
+ if (schemaChanges.length > 0) {
507
+ console.log('');
508
+ console.log(`${colors.green(String(schemaChanges.length))} change(s) ready to stage`);
509
+ console.log(`hint: run ${colors.cyan("'relq add .'")} to stage all changes`);
510
+ }
511
+ else {
512
+ console.log('Already up to date');
513
+ }
410
514
  }
411
515
  else {
412
- saveSnapshot(currentSchema, projectRoot);
516
+ const connectionDesc = getConnectionDescription(connection);
517
+ const commitMessage = `pull: sync from ${connectionDesc}`;
518
+ const commit = createCommit(currentSchema, author, commitMessage, projectRoot);
519
+ clearWorkingState(projectRoot);
520
+ console.log('');
521
+ console.log(`Pull completed in ${formatDuration(duration)}`);
522
+ console.log('');
523
+ console.log(`${colors.yellow('→')} ${shortHash(commit.hash)} ${commitMessage}`);
524
+ console.log(` ${colors.green(String(commit.stats.tables))} tables, ${colors.green(String(commit.stats.columns))} columns`);
525
+ if (stashedSchemaContent) {
526
+ fs.writeFileSync(schemaPath, stashedSchemaContent, 'utf-8');
527
+ console.log('');
528
+ console.log(`${colors.yellow('Restored')} local schema file content`);
529
+ console.log(`hint: run ${colors.cyan("'relq add .'")} to detect changes against the new snapshot`);
530
+ }
413
531
  }
414
- spinner.start('Creating commit...');
415
- const message = localHead ? 'Pulled schema from database' : 'Initial schema pull';
416
- const commit = createCommit(currentSchema, author, message, projectRoot);
417
- spinner.succeed(`Created commit ${colors.yellow(shortHash(commit.hash))}`);
418
- const duration = Date.now() - startTime;
419
- console.log('');
420
- console.log(`${colors.green('✓')} Pull completed in ${colors.cyan(`${duration}ms`)}`);
421
532
  console.log('');
422
533
  }
423
- catch (error) {
534
+ catch (err) {
424
535
  spinner.fail('Pull failed');
425
- console.error(colors.red(`Error: ${error instanceof Error ? error.message : error}`));
426
- process.exit(1);
536
+ fatal(err instanceof Error ? err.message : String(err));
427
537
  }
428
538
  }
429
539
  function detectObjectConflicts(local, remote) {