relq 1.0.46 → 1.0.48

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.
@@ -178,7 +178,7 @@ async function pullCommand(context) {
178
178
  const connection = config.connection;
179
179
  const force = flags['force'] === true;
180
180
  const merge = flags['merge'] === true;
181
- const noCommit = flags['no-commit'] === true;
181
+ const autoCommit = flags['commit'] === true;
182
182
  const dryRun = flags['dry-run'] === true;
183
183
  const author = config.author || 'Relq CLI';
184
184
  const schemaPathRaw = (0, config_loader_1.getSchemaPath)(config);
@@ -880,7 +880,7 @@ async function pullCommand(context) {
880
880
  (0, ast_codegen_1.copyTrackingIdsToNormalized)(parsedSchema, currentSchema);
881
881
  (0, repo_manager_1.saveSnapshot)(currentSchema, projectRoot);
882
882
  const duration = Date.now() - startTime;
883
- if (noCommit) {
883
+ if (!autoCommit) {
884
884
  if (schemaChanges.length > 0) {
885
885
  (0, repo_manager_1.addUnstagedChanges)(schemaChanges, projectRoot);
886
886
  spinner.succeed(`Detected ${schemaChanges.length} schema change(s)`);
@@ -380,7 +380,18 @@ async function pushCommand(context) {
380
380
  catch {
381
381
  spinner.fail('SQL execution failed');
382
382
  }
383
- throw error;
383
+ const dbError = error?.message || String(error);
384
+ const enhancedError = new Error(`${dbError}\n\n` +
385
+ `${cli_utils_1.colors.muted('This usually means:')}\n` +
386
+ ` • The SQL statements were generated in the wrong order\n` +
387
+ ` • A column/table is referenced before it's created\n` +
388
+ ` • The schema is out of sync with the database\n\n` +
389
+ `${cli_utils_1.colors.muted('To fix:')}\n` +
390
+ ` 1. Run ${cli_utils_1.colors.cyan('relq reset --hard HEAD~1')} to undo the commit\n` +
391
+ ` 2. Run ${cli_utils_1.colors.cyan('relq pull')} to sync with database\n` +
392
+ ` 3. Run ${cli_utils_1.colors.cyan('relq add')} and ${cli_utils_1.colors.cyan('relq commit')} again\n\n` +
393
+ `${cli_utils_1.colors.muted('To debug, run:')} ${cli_utils_1.colors.cyan('relq push --dry-run')} ${cli_utils_1.colors.muted('to see the SQL')}`);
394
+ throw enhancedError;
384
395
  }
385
396
  finally {
386
397
  await client.end();
@@ -114,7 +114,7 @@ async function resetCommand(context) {
114
114
  (0, cli_utils_1.fatal)('Cannot find commit data - repository may be corrupt');
115
115
  }
116
116
  const commitData = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
117
- const targetSnapshot = commitData.snapshot;
117
+ const targetSnapshot = (commitData.snapshot || commitData.schema);
118
118
  if (!targetSnapshot) {
119
119
  spinner.stop();
120
120
  (0, cli_utils_1.fatal)('Commit has no snapshot data - repository may be corrupt');
@@ -122,6 +122,18 @@ async function resetCommand(context) {
122
122
  if (hard) {
123
123
  (0, repo_manager_1.saveSnapshot)(targetSnapshot, projectRoot);
124
124
  (0, repo_manager_1.setHead)(targetHash, projectRoot);
125
+ const commitsDir = path.join(projectRoot, '.relq', 'commits');
126
+ const headIndex = allCommits.findIndex(c => c.hash === currentHead);
127
+ const targetIndex = allCommits.findIndex(c => c.hash === targetHash);
128
+ if (headIndex >= 0 && targetIndex > headIndex) {
129
+ for (let i = headIndex; i < targetIndex; i++) {
130
+ const commitToDelete = allCommits[i];
131
+ const commitFile = path.join(commitsDir, `${commitToDelete.hash}.json`);
132
+ if (fs.existsSync(commitFile)) {
133
+ fs.unlinkSync(commitFile);
134
+ }
135
+ }
136
+ }
125
137
  const stagedPath = path.join(projectRoot, '.relq', 'staged.json');
126
138
  const unstagedPath = path.join(projectRoot, '.relq', 'unstaged.json');
127
139
  if (fs.existsSync(stagedPath))
@@ -131,6 +143,9 @@ async function resetCommand(context) {
131
143
  const mergeStatePath = path.join(projectRoot, '.relq', 'MERGE_STATE');
132
144
  if (fs.existsSync(mergeStatePath))
133
145
  fs.unlinkSync(mergeStatePath);
146
+ const fileHashPath = path.join(projectRoot, '.relq', 'file_hash');
147
+ if (fs.existsSync(fileHashPath))
148
+ fs.unlinkSync(fileHashPath);
134
149
  }
135
150
  spinner.succeed(`Reset to ${cli_utils_1.colors.yellow((0, repo_manager_1.shortHash)(targetHash))}`);
136
151
  console.log('');
@@ -92,6 +92,11 @@ function parseArgs(argv) {
92
92
  const args = [];
93
93
  const flags = {};
94
94
  let command = '';
95
+ const booleanFlags = new Set([
96
+ 'hard', 'soft', 'force', 'yes', 'y', 'dry-run', 'verbose', 'quiet', 'q',
97
+ 'help', 'h', 'version', 'v', 'all', 'a', 'cached', 'staged', 'no-verify',
98
+ 'metadata-only', 'skip-prompt', 'interactive', 'i'
99
+ ]);
95
100
  for (let i = 2; i < argv.length; i++) {
96
101
  const arg = argv[i];
97
102
  if (arg.startsWith('--')) {
@@ -100,25 +105,36 @@ function parseArgs(argv) {
100
105
  flags[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1);
101
106
  }
102
107
  else {
103
- const nextArg = argv[i + 1];
104
- if (nextArg && !nextArg.startsWith('-')) {
105
- flags[arg.slice(2)] = nextArg;
106
- i++;
108
+ const flagName = arg.slice(2);
109
+ if (booleanFlags.has(flagName)) {
110
+ flags[flagName] = true;
107
111
  }
108
112
  else {
109
- flags[arg.slice(2)] = true;
113
+ const nextArg = argv[i + 1];
114
+ if (nextArg && !nextArg.startsWith('-')) {
115
+ flags[flagName] = nextArg;
116
+ i++;
117
+ }
118
+ else {
119
+ flags[flagName] = true;
120
+ }
110
121
  }
111
122
  }
112
123
  }
113
124
  else if (arg.startsWith('-') && arg.length === 2) {
114
125
  const flag = arg.slice(1);
115
- const nextArg = argv[i + 1];
116
- if (nextArg && !nextArg.startsWith('-')) {
117
- flags[flag] = nextArg;
118
- i++;
126
+ if (booleanFlags.has(flag)) {
127
+ flags[flag] = true;
119
128
  }
120
129
  else {
121
- flags[flag] = true;
130
+ const nextArg = argv[i + 1];
131
+ if (nextArg && !nextArg.startsWith('-')) {
132
+ flags[flag] = nextArg;
133
+ i++;
134
+ }
135
+ else {
136
+ flags[flag] = true;
137
+ }
122
138
  }
123
139
  }
124
140
  else if (arg.startsWith('-') && arg.length > 2) {
@@ -559,14 +559,14 @@ function sortChangesByDependency(changes) {
559
559
  'PARTITION': 11,
560
560
  'PARTITION_CHILD': 12,
561
561
  'COLUMN': 13,
562
- 'COLUMN_COMMENT': 12,
563
- 'INDEX': 13,
564
- 'INDEX_COMMENT': 13,
565
- 'CONSTRAINT': 14,
566
- 'PRIMARY_KEY': 14,
567
- 'FOREIGN_KEY': 15,
568
- 'CHECK': 14,
569
- 'EXCLUSION': 14,
562
+ 'COLUMN_COMMENT': 14,
563
+ 'INDEX': 15,
564
+ 'INDEX_COMMENT': 16,
565
+ 'CONSTRAINT': 17,
566
+ 'PRIMARY_KEY': 17,
567
+ 'FOREIGN_KEY': 18,
568
+ 'CHECK': 17,
569
+ 'EXCLUSION': 17,
570
570
  'VIEW': 20,
571
571
  'MATERIALIZED_VIEW': 21,
572
572
  'FUNCTION': 30,
@@ -77,6 +77,18 @@ function getLocalCommits(baseDir = process.cwd()) {
77
77
  return [];
78
78
  }
79
79
  }
80
+ function getLocalCommitByHash(hash, baseDir = process.cwd()) {
81
+ const commitPath = path.join(baseDir, '.relq', 'commits', `${hash}.json`);
82
+ if (!fs.existsSync(commitPath)) {
83
+ return null;
84
+ }
85
+ try {
86
+ return JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
80
92
  function saveLocalCommits(commits, baseDir = process.cwd()) {
81
93
  ensureRelqDir(baseDir);
82
94
  const filePath = path.join(baseDir, RELQ_DIR, COMMITS_FILE);
@@ -263,7 +275,21 @@ function generateASTHash(ast) {
263
275
  function createCommitFromSchema(schema, author, message, commitLimit = 1000, baseDir = process.cwd()) {
264
276
  const parsedSchema = (0, schema_to_ast_1.schemaToAST)(schema);
265
277
  const hash = generateASTHash(parsedSchema);
266
- const parentHash = getLocalHead(baseDir);
278
+ const currentHead = getLocalHead(baseDir);
279
+ let parentHash = currentHead;
280
+ if (currentHead) {
281
+ const headCommit = getLocalCommitByHash(currentHead, baseDir);
282
+ const isUnpushed = headCommit && (!headCommit.remotes?.pushed || headCommit.remotes.pushed.length === 0);
283
+ const isPullCommit = headCommit?.message?.startsWith('pull: sync from ');
284
+ if (isUnpushed && !isPullCommit) {
285
+ parentHash = headCommit?.parentHash || null;
286
+ const commitsDir = path.join(baseDir, '.relq', 'commits');
287
+ const oldCommitFile = path.join(commitsDir, `${currentHead}.json`);
288
+ if (fs.existsSync(oldCommitFile)) {
289
+ fs.unlinkSync(oldCommitFile);
290
+ }
291
+ }
292
+ }
267
293
  const commit = {
268
294
  hash,
269
295
  parentHash,
@@ -404,9 +404,21 @@ function detectFileChanges(schemaPath, projectRoot = process.cwd()) {
404
404
  const currentContent = fs.readFileSync(schemaPath, 'utf-8');
405
405
  const currentHash = hashFileContent(currentContent);
406
406
  const savedHash = getSavedFileHash(projectRoot);
407
- if (!savedHash || currentHash === savedHash) {
407
+ if (savedHash && currentHash === savedHash) {
408
408
  return null;
409
409
  }
410
+ if (!savedHash) {
411
+ return {
412
+ id: `file_${currentHash.substring(0, 8)}`,
413
+ type: 'ALTER',
414
+ objectType: 'SCHEMA_FILE',
415
+ objectName: path.basename(schemaPath),
416
+ before: null,
417
+ after: { hash: currentHash },
418
+ sql: '-- Schema file needs comparison (no previous hash)',
419
+ detectedAt: new Date().toISOString(),
420
+ };
421
+ }
410
422
  return {
411
423
  id: `file_${currentHash.substring(0, 8)}`,
412
424
  type: 'ALTER',
@@ -142,7 +142,7 @@ export async function pullCommand(context) {
142
142
  const connection = config.connection;
143
143
  const force = flags['force'] === true;
144
144
  const merge = flags['merge'] === true;
145
- const noCommit = flags['no-commit'] === true;
145
+ const autoCommit = flags['commit'] === true;
146
146
  const dryRun = flags['dry-run'] === true;
147
147
  const author = config.author || 'Relq CLI';
148
148
  const schemaPathRaw = getSchemaPath(config);
@@ -844,7 +844,7 @@ export async function pullCommand(context) {
844
844
  copyTrackingIdsToNormalized(parsedSchema, currentSchema);
845
845
  saveSnapshot(currentSchema, projectRoot);
846
846
  const duration = Date.now() - startTime;
847
- if (noCommit) {
847
+ if (!autoCommit) {
848
848
  if (schemaChanges.length > 0) {
849
849
  addUnstagedChanges(schemaChanges, projectRoot);
850
850
  spinner.succeed(`Detected ${schemaChanges.length} schema change(s)`);
@@ -344,7 +344,18 @@ export async function pushCommand(context) {
344
344
  catch {
345
345
  spinner.fail('SQL execution failed');
346
346
  }
347
- throw error;
347
+ const dbError = error?.message || String(error);
348
+ const enhancedError = new Error(`${dbError}\n\n` +
349
+ `${colors.muted('This usually means:')}\n` +
350
+ ` • The SQL statements were generated in the wrong order\n` +
351
+ ` • A column/table is referenced before it's created\n` +
352
+ ` • The schema is out of sync with the database\n\n` +
353
+ `${colors.muted('To fix:')}\n` +
354
+ ` 1. Run ${colors.cyan('relq reset --hard HEAD~1')} to undo the commit\n` +
355
+ ` 2. Run ${colors.cyan('relq pull')} to sync with database\n` +
356
+ ` 3. Run ${colors.cyan('relq add')} and ${colors.cyan('relq commit')} again\n\n` +
357
+ `${colors.muted('To debug, run:')} ${colors.cyan('relq push --dry-run')} ${colors.muted('to see the SQL')}`);
358
+ throw enhancedError;
348
359
  }
349
360
  finally {
350
361
  await client.end();
@@ -78,7 +78,7 @@ export async function resetCommand(context) {
78
78
  fatal('Cannot find commit data - repository may be corrupt');
79
79
  }
80
80
  const commitData = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
81
- const targetSnapshot = commitData.snapshot;
81
+ const targetSnapshot = (commitData.snapshot || commitData.schema);
82
82
  if (!targetSnapshot) {
83
83
  spinner.stop();
84
84
  fatal('Commit has no snapshot data - repository may be corrupt');
@@ -86,6 +86,18 @@ export async function resetCommand(context) {
86
86
  if (hard) {
87
87
  saveSnapshot(targetSnapshot, projectRoot);
88
88
  setHead(targetHash, projectRoot);
89
+ const commitsDir = path.join(projectRoot, '.relq', 'commits');
90
+ const headIndex = allCommits.findIndex(c => c.hash === currentHead);
91
+ const targetIndex = allCommits.findIndex(c => c.hash === targetHash);
92
+ if (headIndex >= 0 && targetIndex > headIndex) {
93
+ for (let i = headIndex; i < targetIndex; i++) {
94
+ const commitToDelete = allCommits[i];
95
+ const commitFile = path.join(commitsDir, `${commitToDelete.hash}.json`);
96
+ if (fs.existsSync(commitFile)) {
97
+ fs.unlinkSync(commitFile);
98
+ }
99
+ }
100
+ }
89
101
  const stagedPath = path.join(projectRoot, '.relq', 'staged.json');
90
102
  const unstagedPath = path.join(projectRoot, '.relq', 'unstaged.json');
91
103
  if (fs.existsSync(stagedPath))
@@ -95,6 +107,9 @@ export async function resetCommand(context) {
95
107
  const mergeStatePath = path.join(projectRoot, '.relq', 'MERGE_STATE');
96
108
  if (fs.existsSync(mergeStatePath))
97
109
  fs.unlinkSync(mergeStatePath);
110
+ const fileHashPath = path.join(projectRoot, '.relq', 'file_hash');
111
+ if (fs.existsSync(fileHashPath))
112
+ fs.unlinkSync(fileHashPath);
98
113
  }
99
114
  spinner.succeed(`Reset to ${colors.yellow(shortHash(targetHash))}`);
100
115
  console.log('');
@@ -56,6 +56,11 @@ function parseArgs(argv) {
56
56
  const args = [];
57
57
  const flags = {};
58
58
  let command = '';
59
+ const booleanFlags = new Set([
60
+ 'hard', 'soft', 'force', 'yes', 'y', 'dry-run', 'verbose', 'quiet', 'q',
61
+ 'help', 'h', 'version', 'v', 'all', 'a', 'cached', 'staged', 'no-verify',
62
+ 'metadata-only', 'skip-prompt', 'interactive', 'i'
63
+ ]);
59
64
  for (let i = 2; i < argv.length; i++) {
60
65
  const arg = argv[i];
61
66
  if (arg.startsWith('--')) {
@@ -64,25 +69,36 @@ function parseArgs(argv) {
64
69
  flags[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1);
65
70
  }
66
71
  else {
67
- const nextArg = argv[i + 1];
68
- if (nextArg && !nextArg.startsWith('-')) {
69
- flags[arg.slice(2)] = nextArg;
70
- i++;
72
+ const flagName = arg.slice(2);
73
+ if (booleanFlags.has(flagName)) {
74
+ flags[flagName] = true;
71
75
  }
72
76
  else {
73
- flags[arg.slice(2)] = true;
77
+ const nextArg = argv[i + 1];
78
+ if (nextArg && !nextArg.startsWith('-')) {
79
+ flags[flagName] = nextArg;
80
+ i++;
81
+ }
82
+ else {
83
+ flags[flagName] = true;
84
+ }
74
85
  }
75
86
  }
76
87
  }
77
88
  else if (arg.startsWith('-') && arg.length === 2) {
78
89
  const flag = arg.slice(1);
79
- const nextArg = argv[i + 1];
80
- if (nextArg && !nextArg.startsWith('-')) {
81
- flags[flag] = nextArg;
82
- i++;
90
+ if (booleanFlags.has(flag)) {
91
+ flags[flag] = true;
83
92
  }
84
93
  else {
85
- flags[flag] = true;
94
+ const nextArg = argv[i + 1];
95
+ if (nextArg && !nextArg.startsWith('-')) {
96
+ flags[flag] = nextArg;
97
+ i++;
98
+ }
99
+ else {
100
+ flags[flag] = true;
101
+ }
86
102
  }
87
103
  }
88
104
  else if (arg.startsWith('-') && arg.length > 2) {
@@ -517,14 +517,14 @@ export function sortChangesByDependency(changes) {
517
517
  'PARTITION': 11,
518
518
  'PARTITION_CHILD': 12,
519
519
  'COLUMN': 13,
520
- 'COLUMN_COMMENT': 12,
521
- 'INDEX': 13,
522
- 'INDEX_COMMENT': 13,
523
- 'CONSTRAINT': 14,
524
- 'PRIMARY_KEY': 14,
525
- 'FOREIGN_KEY': 15,
526
- 'CHECK': 14,
527
- 'EXCLUSION': 14,
520
+ 'COLUMN_COMMENT': 14,
521
+ 'INDEX': 15,
522
+ 'INDEX_COMMENT': 16,
523
+ 'CONSTRAINT': 17,
524
+ 'PRIMARY_KEY': 17,
525
+ 'FOREIGN_KEY': 18,
526
+ 'CHECK': 17,
527
+ 'EXCLUSION': 17,
528
528
  'VIEW': 20,
529
529
  'MATERIALIZED_VIEW': 21,
530
530
  'FUNCTION': 30,
@@ -26,6 +26,18 @@ export function getLocalCommits(baseDir = process.cwd()) {
26
26
  return [];
27
27
  }
28
28
  }
29
+ function getLocalCommitByHash(hash, baseDir = process.cwd()) {
30
+ const commitPath = path.join(baseDir, '.relq', 'commits', `${hash}.json`);
31
+ if (!fs.existsSync(commitPath)) {
32
+ return null;
33
+ }
34
+ try {
35
+ return JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
29
41
  export function saveLocalCommits(commits, baseDir = process.cwd()) {
30
42
  ensureRelqDir(baseDir);
31
43
  const filePath = path.join(baseDir, RELQ_DIR, COMMITS_FILE);
@@ -212,7 +224,21 @@ export function generateASTHash(ast) {
212
224
  export function createCommitFromSchema(schema, author, message, commitLimit = 1000, baseDir = process.cwd()) {
213
225
  const parsedSchema = schemaToAST(schema);
214
226
  const hash = generateASTHash(parsedSchema);
215
- const parentHash = getLocalHead(baseDir);
227
+ const currentHead = getLocalHead(baseDir);
228
+ let parentHash = currentHead;
229
+ if (currentHead) {
230
+ const headCommit = getLocalCommitByHash(currentHead, baseDir);
231
+ const isUnpushed = headCommit && (!headCommit.remotes?.pushed || headCommit.remotes.pushed.length === 0);
232
+ const isPullCommit = headCommit?.message?.startsWith('pull: sync from ');
233
+ if (isUnpushed && !isPullCommit) {
234
+ parentHash = headCommit?.parentHash || null;
235
+ const commitsDir = path.join(baseDir, '.relq', 'commits');
236
+ const oldCommitFile = path.join(commitsDir, `${currentHead}.json`);
237
+ if (fs.existsSync(oldCommitFile)) {
238
+ fs.unlinkSync(oldCommitFile);
239
+ }
240
+ }
241
+ }
216
242
  const commit = {
217
243
  hash,
218
244
  parentHash,
@@ -313,9 +313,21 @@ export function detectFileChanges(schemaPath, projectRoot = process.cwd()) {
313
313
  const currentContent = fs.readFileSync(schemaPath, 'utf-8');
314
314
  const currentHash = hashFileContent(currentContent);
315
315
  const savedHash = getSavedFileHash(projectRoot);
316
- if (!savedHash || currentHash === savedHash) {
316
+ if (savedHash && currentHash === savedHash) {
317
317
  return null;
318
318
  }
319
+ if (!savedHash) {
320
+ return {
321
+ id: `file_${currentHash.substring(0, 8)}`,
322
+ type: 'ALTER',
323
+ objectType: 'SCHEMA_FILE',
324
+ objectName: path.basename(schemaPath),
325
+ before: null,
326
+ after: { hash: currentHash },
327
+ sql: '-- Schema file needs comparison (no previous hash)',
328
+ detectedAt: new Date().toISOString(),
329
+ };
330
+ }
319
331
  return {
320
332
  id: `file_${currentHash.substring(0, 8)}`,
321
333
  type: 'ALTER',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relq",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "description": "The Fully-Typed PostgreSQL ORM for TypeScript",
5
5
  "author": "Olajide Mathew O. <olajide.mathew@yuniq.solutions>",
6
6
  "license": "MIT",