relq 1.0.90 → 1.0.91

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.
@@ -156,21 +156,11 @@ exports.default = (0, citty_1.defineCommand)({
156
156
  console.log('');
157
157
  return;
158
158
  }
159
- const s = filteredDiff.summary;
160
159
  console.log('');
161
160
  console.log(`${colors_1.colors.bold('Changes to include:')}`);
162
- if (s.tablesAdded > 0)
163
- console.log(` ${colors_1.colors.green('+')} ${s.tablesAdded} table(s) to create`);
164
- if (s.tablesRemoved > 0)
165
- console.log(` ${colors_1.colors.red('-')} ${s.tablesRemoved} table(s) to drop`);
166
- if (s.tablesModified > 0)
167
- console.log(` ${colors_1.colors.yellow('~')} ${s.tablesModified} table(s) modified`);
168
- if (s.columnsAdded > 0)
169
- console.log(` ${colors_1.colors.green('+')} ${s.columnsAdded} column(s) to add`);
170
- if (s.columnsRemoved > 0)
171
- console.log(` ${colors_1.colors.red('-')} ${s.columnsRemoved} column(s) to drop`);
172
- if (s.columnsModified > 0)
173
- console.log(` ${colors_1.colors.yellow('~')} ${s.columnsModified} column(s) modified`);
161
+ const changeSummaryLines = (0, schema_diff_1.formatCategorizedSummary)(filteredDiff);
162
+ for (const line of changeSummaryLines)
163
+ console.log(line);
174
164
  console.log('');
175
165
  if ((0, schema_diff_1.hasDestructiveChanges)(filteredDiff)) {
176
166
  const tables = (0, schema_diff_1.getDestructiveTables)(filteredDiff);
@@ -205,7 +195,7 @@ exports.default = (0, citty_1.defineCommand)({
205
195
  includeDown: !noDown,
206
196
  includeComments: true,
207
197
  });
208
- spin.stop('Migration generated');
198
+ spin.stop(`Migration generated — ${migration.upSQL.length} statement(s)`);
209
199
  let fileName;
210
200
  if (format === 'timestamp') {
211
201
  fileName = (0, migration_generator_1.generateTimestampedName)(migrationName) + '.sql';
@@ -153,9 +153,15 @@ exports.default = (0, citty_1.defineCommand)({
153
153
  const executeQuery = tx
154
154
  ? (sql, params) => tx.query(sql, params)
155
155
  : (sql, params) => dbClient.query(sql, params);
156
- await executeQuery(up);
157
- const placeholder = await (0, migration_helpers_1.getParamPlaceholder)(dialect);
158
- await executeQuery(`INSERT INTO ${quote}${tableName}${quote} (name) VALUES (${placeholder})`, [file]);
156
+ const statements = (0, migration_helpers_1.splitStatements)(up);
157
+ for (const stmt of statements) {
158
+ await executeQuery(stmt);
159
+ }
160
+ const p1 = await (0, migration_helpers_1.getParamPlaceholder)(dialect, 1);
161
+ const p2 = await (0, migration_helpers_1.getParamPlaceholder)(dialect, 2);
162
+ const p3 = await (0, migration_helpers_1.getParamPlaceholder)(dialect, 3);
163
+ const p4 = await (0, migration_helpers_1.getParamPlaceholder)(dialect, 4);
164
+ await executeQuery(`INSERT INTO ${quote}${tableName}${quote} (name, filename, hash, batch) VALUES (${p1}, ${p2}, ${p3}, ${p4})`, [file.replace(/\.sql$/, ''), file, file, applied + 1]);
159
165
  if (tx)
160
166
  await tx.commit();
161
167
  spin.stop(`Applied ${file}`);
@@ -236,6 +236,12 @@ async function runPush(config, projectRoot, opts = {}) {
236
236
  });
237
237
  const upStatements = migration.upSQL;
238
238
  spin.stop(`Generated ${upStatements.length} statement(s)`);
239
+ const summaryLines = (0, schema_diff_1.formatCategorizedSummary)(filteredDiff);
240
+ if (summaryLines.length > 0) {
241
+ for (const line of summaryLines)
242
+ console.log(line);
243
+ console.log('');
244
+ }
239
245
  const dialect = (0, dialect_router_1.detectDialect)(config);
240
246
  if (dialect !== 'postgres' && upStatements.length > 0) {
241
247
  const upSQL = upStatements.join('\n');
@@ -277,6 +283,7 @@ async function runPush(config, projectRoot, opts = {}) {
277
283
  spin.start('Applying schema changes...');
278
284
  const execClient = await (0, database_client_1.createDatabaseClient)(config);
279
285
  let tx = null;
286
+ let statementsRun = 0;
280
287
  try {
281
288
  try {
282
289
  tx = await execClient.beginTransaction();
@@ -288,7 +295,6 @@ async function runPush(config, projectRoot, opts = {}) {
288
295
  const executeQuery = tx
289
296
  ? (sql) => tx.query(sql)
290
297
  : (sql) => execClient.query(sql);
291
- let statementsRun = 0;
292
298
  for (const stmt of upStatements) {
293
299
  try {
294
300
  await executeQuery(stmt);
@@ -303,7 +309,6 @@ async function runPush(config, projectRoot, opts = {}) {
303
309
  }
304
310
  if (tx)
305
311
  await tx.commit();
306
- spin.stop(`Applied ${statementsRun} statement(s)`);
307
312
  try {
308
313
  const tableName = config.migrations?.tableName || '_relq_migrations';
309
314
  const tableDDL = await (0, migration_helpers_1.getMigrationTableDDL)(config, tableName);
@@ -319,10 +324,11 @@ async function runPush(config, projectRoot, opts = {}) {
319
324
  const hash = (0, migration_generator_1.generateTimestampedName)(migrationName);
320
325
  await execClient.query(`INSERT INTO "${tableName}" (name, filename, hash, batch, sql_up, sql_down, source) ` +
321
326
  `VALUES ($1, $2, $3, 0, $4, $5, 'push')`, [migrationName, '', hash, upSQL, downSQL]);
322
- spin.stop(`Applied ${statementsRun} statement(s) — rollback saved`);
327
+ spin.stop(`Applied ${statementsRun} statement(s) — restore point saved`);
323
328
  }
324
329
  catch (recordError) {
325
- (0, ui_1.warning)(`Schema applied but failed to save rollback point: ${recordError?.message || String(recordError)}`);
330
+ spin.stop(`Applied ${statementsRun} statement(s)`);
331
+ (0, ui_1.warning)(`Failed to save restore point: ${recordError?.message || String(recordError)}`);
326
332
  }
327
333
  }
328
334
  catch (error) {
@@ -340,7 +346,12 @@ async function runPush(config, projectRoot, opts = {}) {
340
346
  if (error.statementIndex) {
341
347
  errorMsg += `${colors_1.colors.yellow('Statement #:')} ${error.statementIndex}\n`;
342
348
  }
343
- errorMsg += `\n${colors_1.colors.muted('All changes rolled back.')}\n`;
349
+ if (tx) {
350
+ errorMsg += `\n${colors_1.colors.muted('All changes rolled back.')}\n`;
351
+ }
352
+ else {
353
+ errorMsg += `\n${colors_1.colors.yellow('Warning:')} ${statementsRun} statement(s) were already applied (no transaction support).\n`;
354
+ }
344
355
  spin.error('SQL execution failed');
345
356
  throw new Error(errorMsg);
346
357
  }
@@ -336,6 +336,39 @@ function generateColumnModification(tableName, columnName, changes) {
336
336
  downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT;`);
337
337
  }
338
338
  break;
339
+ case 'length': {
340
+ const newLen = change.to;
341
+ const oldLen = change.from;
342
+ const newFullType = newLen ? `VARCHAR(${newLen})` : 'VARCHAR';
343
+ const oldFullType = oldLen ? `VARCHAR(${oldLen})` : 'VARCHAR';
344
+ upSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${newFullType};`);
345
+ downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${oldFullType};`);
346
+ break;
347
+ }
348
+ case 'unique': {
349
+ const constraintName = `${tableName}_${columnName}_key`;
350
+ if (change.to === true) {
351
+ upSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${columnName}");`);
352
+ downSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
353
+ }
354
+ else {
355
+ upSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
356
+ downSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${columnName}");`);
357
+ }
358
+ break;
359
+ }
360
+ case 'primaryKey': {
361
+ const pkName = `${tableName}_pkey`;
362
+ if (change.to === true) {
363
+ upSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${columnName}");`);
364
+ downSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
365
+ }
366
+ else {
367
+ upSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
368
+ downSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${columnName}");`);
369
+ }
370
+ break;
371
+ }
339
372
  }
340
373
  }
341
374
  return { upSQL, downSQL };
@@ -549,6 +582,41 @@ function generateColumnChange(tableName, col) {
549
582
  }
550
583
  break;
551
584
  }
585
+ case 'length': {
586
+ const colType = col.after?.dataType ?? col.before?.dataType ?? 'VARCHAR';
587
+ const baseType = colType.toUpperCase().replace(/\(.*\)/, '');
588
+ const newLen = change.to;
589
+ const oldLen = change.from;
590
+ const newFullType = newLen ? `${baseType}(${newLen})` : baseType;
591
+ const oldFullType = oldLen ? `${baseType}(${oldLen})` : baseType;
592
+ up.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${col.name}" TYPE ${newFullType};`);
593
+ down.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${col.name}" TYPE ${oldFullType};`);
594
+ break;
595
+ }
596
+ case 'unique': {
597
+ const constraintName = `${tableName}_${col.name}_key`;
598
+ if (change.to === true) {
599
+ up.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${col.name}");`);
600
+ down.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
601
+ }
602
+ else {
603
+ up.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
604
+ down.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${col.name}");`);
605
+ }
606
+ break;
607
+ }
608
+ case 'primaryKey': {
609
+ const pkName = `${tableName}_pkey`;
610
+ if (change.to === true) {
611
+ up.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${col.name}");`);
612
+ down.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
613
+ }
614
+ else {
615
+ up.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
616
+ down.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${col.name}");`);
617
+ }
618
+ break;
619
+ }
552
620
  }
553
621
  }
554
622
  }
@@ -283,8 +283,13 @@ async function introspectPostgres(connection, options) {
283
283
  continue;
284
284
  const normalizedColName = rawColName.replace(/^"|"$/g, '').toLowerCase();
285
285
  const col = columns.find(c => c.name.toLowerCase() === normalizedColName);
286
- if (col && con.constraint_type === 'PRIMARY KEY') {
287
- col.isPrimaryKey = true;
286
+ if (col) {
287
+ if (con.constraint_type === 'PRIMARY KEY') {
288
+ col.isPrimaryKey = true;
289
+ }
290
+ if (con.constraint_type === 'UNIQUE' && constraintColumns.length === 1) {
291
+ col.isUnique = true;
292
+ }
288
293
  }
289
294
  }
290
295
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.diffSchemas = diffSchemas;
4
4
  exports.formatDiff = formatDiff;
5
5
  exports.formatSummary = formatSummary;
6
+ exports.formatCategorizedSummary = formatCategorizedSummary;
6
7
  exports.filterDiff = filterDiff;
7
8
  exports.hasDestructiveChanges = hasDestructiveChanges;
8
9
  exports.stripDestructiveChanges = stripDestructiveChanges;
@@ -266,12 +267,14 @@ function diffTables(local, remote) {
266
267
  if (remoteTable) {
267
268
  const columnDiffs = diffColumns(localTable.columns, remoteTable.columns);
268
269
  const indexDiffs = diffIndexes(localTable.indexes, remoteTable.indexes);
269
- if (columnDiffs.length > 0 || indexDiffs.length > 0) {
270
+ const constraintDiffs = diffConstraints(localTable.constraints, remoteTable.constraints);
271
+ if (columnDiffs.length > 0 || indexDiffs.length > 0 || constraintDiffs.length > 0) {
270
272
  diffs.push({
271
273
  name,
272
274
  type: 'modified',
273
275
  columns: columnDiffs,
274
276
  indexes: indexDiffs,
277
+ constraints: constraintDiffs,
275
278
  });
276
279
  }
277
280
  }
@@ -340,6 +343,37 @@ function diffIndexes(local, remote) {
340
343
  }
341
344
  return diffs;
342
345
  }
346
+ function diffConstraints(local, remote) {
347
+ const diffs = [];
348
+ const sig = (c) => {
349
+ const def = (c.definition || '').replace(/"/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
350
+ return `${c.type}:${def}`;
351
+ };
352
+ const localSigs = new Map();
353
+ const remoteSigs = new Map();
354
+ for (const c of local)
355
+ localSigs.set(sig(c), c);
356
+ for (const c of remote)
357
+ remoteSigs.set(sig(c), c);
358
+ const toConstraintInfo = (c) => ({
359
+ name: c.name,
360
+ type: (c.type || 'CHECK'),
361
+ columns: [],
362
+ definition: c.definition || '',
363
+ trackingId: c.trackingId,
364
+ });
365
+ for (const [s, c] of remoteSigs) {
366
+ if (!localSigs.has(s)) {
367
+ diffs.push({ name: c.name || c.definition || 'unnamed', type: 'added', after: toConstraintInfo(c) });
368
+ }
369
+ }
370
+ for (const [s, c] of localSigs) {
371
+ if (!remoteSigs.has(s)) {
372
+ diffs.push({ name: c.name || c.definition || 'unnamed', type: 'removed', before: toConstraintInfo(c) });
373
+ }
374
+ }
375
+ return diffs;
376
+ }
343
377
  function diffExtensions(local, remote) {
344
378
  const diffs = [];
345
379
  const localSet = new Set(local);
@@ -426,6 +460,11 @@ function formatDiff(diff) {
426
460
  colors_1.colors.red(' -');
427
461
  lines.push(`${idxIcon} index: ${idx.name}`);
428
462
  }
463
+ for (const con of table.constraints || []) {
464
+ const conIcon = con.type === 'added' ? colors_1.colors.green(' +') :
465
+ colors_1.colors.red(' -');
466
+ lines.push(`${conIcon} constraint: ${con.name}`);
467
+ }
429
468
  }
430
469
  for (const ext of diff.extensions) {
431
470
  const icon = ext.type === 'added' ? colors_1.colors.green('+') : colors_1.colors.red('-');
@@ -472,6 +511,105 @@ function formatSummary(diff) {
472
511
  }
473
512
  return parts.join(', ');
474
513
  }
514
+ function formatCategorizedSummary(diff) {
515
+ let tablesAdded = 0, tablesRemoved = 0, tablesModified = 0;
516
+ let colsAdded = 0, colsRemoved = 0, colsModified = 0;
517
+ let idxAdded = 0, idxRemoved = 0;
518
+ let fkAdded = 0, fkRemoved = 0;
519
+ let uniqueAdded = 0, uniqueRemoved = 0;
520
+ let pkAdded = 0, pkRemoved = 0;
521
+ let checkAdded = 0, checkRemoved = 0;
522
+ let extAdded = 0, extRemoved = 0;
523
+ for (const table of diff.tables) {
524
+ if (table.type === 'added')
525
+ tablesAdded++;
526
+ else if (table.type === 'removed')
527
+ tablesRemoved++;
528
+ else if (table.type === 'modified')
529
+ tablesModified++;
530
+ for (const col of table.columns || []) {
531
+ if (col.type === 'added')
532
+ colsAdded++;
533
+ else if (col.type === 'removed')
534
+ colsRemoved++;
535
+ else if (col.type === 'modified')
536
+ colsModified++;
537
+ }
538
+ for (const idx of table.indexes || []) {
539
+ if (idx.type === 'added')
540
+ idxAdded++;
541
+ else if (idx.type === 'removed')
542
+ idxRemoved++;
543
+ }
544
+ for (const con of table.constraints || []) {
545
+ const name = (con.name || '').toLowerCase();
546
+ const isFk = name.includes('fkey') || name.includes('foreign');
547
+ const isUq = name.includes('unique') || name.includes('_key');
548
+ const isPk = name.includes('pkey') || name.includes('primary');
549
+ const before = con.before;
550
+ const after = con.after;
551
+ const conType = (before?.type || after?.type || '').toUpperCase();
552
+ if (conType === 'FOREIGN KEY' || isFk) {
553
+ if (con.type === 'added')
554
+ fkAdded++;
555
+ else if (con.type === 'removed')
556
+ fkRemoved++;
557
+ }
558
+ else if (conType === 'UNIQUE' || isUq) {
559
+ if (con.type === 'added')
560
+ uniqueAdded++;
561
+ else if (con.type === 'removed')
562
+ uniqueRemoved++;
563
+ }
564
+ else if (conType === 'PRIMARY KEY' || isPk) {
565
+ if (con.type === 'added')
566
+ pkAdded++;
567
+ else if (con.type === 'removed')
568
+ pkRemoved++;
569
+ }
570
+ else if (conType === 'CHECK') {
571
+ if (con.type === 'added')
572
+ checkAdded++;
573
+ else if (con.type === 'removed')
574
+ checkRemoved++;
575
+ }
576
+ else {
577
+ if (con.type === 'added')
578
+ checkAdded++;
579
+ else if (con.type === 'removed')
580
+ checkRemoved++;
581
+ }
582
+ }
583
+ }
584
+ for (const ext of diff.extensions) {
585
+ if (ext.type === 'added')
586
+ extAdded++;
587
+ else if (ext.type === 'removed')
588
+ extRemoved++;
589
+ }
590
+ const lines = [];
591
+ const fmt = (label, added, removed, modified = 0) => {
592
+ const parts = [];
593
+ if (added)
594
+ parts.push(colors_1.colors.green(`${added} added`));
595
+ if (modified)
596
+ parts.push(colors_1.colors.yellow(`${modified} modified`));
597
+ if (removed)
598
+ parts.push(colors_1.colors.red(`${removed} removed`));
599
+ if (parts.length > 0) {
600
+ lines.push(` ${colors_1.colors.muted(label + ':')} ${parts.join(', ')}`);
601
+ }
602
+ };
603
+ fmt('tables', tablesAdded, tablesRemoved, tablesModified);
604
+ fmt('columns', colsAdded, colsRemoved, colsModified);
605
+ fmt('indexes', idxAdded, idxRemoved);
606
+ fmt('primary keys', pkAdded, pkRemoved);
607
+ fmt('unique constraints', uniqueAdded, uniqueRemoved);
608
+ fmt('foreign keys', fkAdded, fkRemoved);
609
+ fmt('check constraints', checkAdded, checkRemoved);
610
+ fmt('extensions', extAdded, extRemoved);
611
+ return lines;
612
+ }
475
613
  function filterDiff(diff, ignorePatterns) {
476
614
  const patterns = ignorePatterns.map(p => {
477
615
  const regexStr = p.replace(/\*/g, '.*').replace(/\?/g, '.');
@@ -405,6 +405,20 @@ function tableToAST(table) {
405
405
  return { name: p.$name, partitionBound };
406
406
  });
407
407
  }
408
+ for (const con of constraints) {
409
+ if (con.type === 'PRIMARY KEY' && con.columns.length > 0) {
410
+ for (const colName of con.columns) {
411
+ const col = columns.find(c => c.name === colName || c.tsName === colName);
412
+ if (col)
413
+ col.isPrimaryKey = true;
414
+ }
415
+ }
416
+ if (con.type === 'UNIQUE' && con.columns.length === 1) {
417
+ const col = columns.find(c => c.name === con.columns[0] || c.tsName === con.columns[0]);
418
+ if (col)
419
+ col.isUnique = true;
420
+ }
421
+ }
408
422
  return {
409
423
  name: table.$name,
410
424
  schema: table.$schema || 'public',
@@ -11,7 +11,7 @@ import { getConnectionDescription } from "../utils/env-loader.js";
11
11
  import { isInitialized } from "../utils/repo-manager.js";
12
12
  import { validateSchemaFile, formatValidationErrors } from "../utils/schema-validator.js";
13
13
  import { loadSchemaFile } from "../utils/schema-loader.js";
14
- import { diffSchemas, filterDiff, hasDestructiveChanges, getDestructiveTables, stripDestructiveChanges } from "../utils/schema-diff.js";
14
+ import { diffSchemas, filterDiff, hasDestructiveChanges, getDestructiveTables, stripDestructiveChanges, formatCategorizedSummary } from "../utils/schema-diff.js";
15
15
  import { normalizeSchema } from "../utils/schema-hash.js";
16
16
  import { generateMigrationFile, generateMigrationName, getNextMigrationNumber, generateTimestampedName } from "../utils/migration-generator.js";
17
17
  import { loadRelqignore } from "../utils/relqignore.js";
@@ -121,21 +121,11 @@ export default defineCommand({
121
121
  console.log('');
122
122
  return;
123
123
  }
124
- const s = filteredDiff.summary;
125
124
  console.log('');
126
125
  console.log(`${colors.bold('Changes to include:')}`);
127
- if (s.tablesAdded > 0)
128
- console.log(` ${colors.green('+')} ${s.tablesAdded} table(s) to create`);
129
- if (s.tablesRemoved > 0)
130
- console.log(` ${colors.red('-')} ${s.tablesRemoved} table(s) to drop`);
131
- if (s.tablesModified > 0)
132
- console.log(` ${colors.yellow('~')} ${s.tablesModified} table(s) modified`);
133
- if (s.columnsAdded > 0)
134
- console.log(` ${colors.green('+')} ${s.columnsAdded} column(s) to add`);
135
- if (s.columnsRemoved > 0)
136
- console.log(` ${colors.red('-')} ${s.columnsRemoved} column(s) to drop`);
137
- if (s.columnsModified > 0)
138
- console.log(` ${colors.yellow('~')} ${s.columnsModified} column(s) modified`);
126
+ const changeSummaryLines = formatCategorizedSummary(filteredDiff);
127
+ for (const line of changeSummaryLines)
128
+ console.log(line);
139
129
  console.log('');
140
130
  if (hasDestructiveChanges(filteredDiff)) {
141
131
  const tables = getDestructiveTables(filteredDiff);
@@ -170,7 +160,7 @@ export default defineCommand({
170
160
  includeDown: !noDown,
171
161
  includeComments: true,
172
162
  });
173
- spin.stop('Migration generated');
163
+ spin.stop(`Migration generated — ${migration.upSQL.length} statement(s)`);
174
164
  let fileName;
175
165
  if (format === 'timestamp') {
176
166
  fileName = generateTimestampedName(migrationName) + '.sql';
@@ -10,7 +10,7 @@ import { requireValidConfig } from "../utils/config-loader.js";
10
10
  import { getConnectionDescription } from "../utils/env-loader.js";
11
11
  import { createDatabaseClient } from "../utils/database-client.js";
12
12
  import { detectDialect } from "../utils/dialect-router.js";
13
- import { getQuoteChar, getParamPlaceholder, getMigrationTableDDL, parseMigration, } from "../utils/migration-helpers.js";
13
+ import { getQuoteChar, getParamPlaceholder, getMigrationTableDDL, parseMigration, splitStatements, } from "../utils/migration-helpers.js";
14
14
  export default defineCommand({
15
15
  meta: { name: 'migrate', description: 'Apply pending migrations' },
16
16
  args: {
@@ -118,9 +118,15 @@ export default defineCommand({
118
118
  const executeQuery = tx
119
119
  ? (sql, params) => tx.query(sql, params)
120
120
  : (sql, params) => dbClient.query(sql, params);
121
- await executeQuery(up);
122
- const placeholder = await getParamPlaceholder(dialect);
123
- await executeQuery(`INSERT INTO ${quote}${tableName}${quote} (name) VALUES (${placeholder})`, [file]);
121
+ const statements = splitStatements(up);
122
+ for (const stmt of statements) {
123
+ await executeQuery(stmt);
124
+ }
125
+ const p1 = await getParamPlaceholder(dialect, 1);
126
+ const p2 = await getParamPlaceholder(dialect, 2);
127
+ const p3 = await getParamPlaceholder(dialect, 3);
128
+ const p4 = await getParamPlaceholder(dialect, 4);
129
+ await executeQuery(`INSERT INTO ${quote}${tableName}${quote} (name, filename, hash, batch) VALUES (${p1}, ${p2}, ${p3}, ${p4})`, [file.replace(/\.sql$/, ''), file, file, applied + 1]);
124
130
  if (tx)
125
131
  await tx.commit();
126
132
  spin.stop(`Applied ${file}`);
@@ -17,7 +17,7 @@ import { loadRelqignore } from "../utils/relqignore.js";
17
17
  import { isInitialized } from "../utils/repo-manager.js";
18
18
  import { saveSnapshot } from "../utils/snapshot-manager.js";
19
19
  import { loadSchemaFile } from "../utils/schema-loader.js";
20
- import { diffSchemas, filterDiff, hasDestructiveChanges, getDestructiveTables, stripDestructiveChanges } from "../utils/schema-diff.js";
20
+ import { diffSchemas, filterDiff, hasDestructiveChanges, getDestructiveTables, stripDestructiveChanges, formatCategorizedSummary } from "../utils/schema-diff.js";
21
21
  import { normalizeSchema } from "../utils/schema-hash.js";
22
22
  import { generateMigrationFile, generateMigrationName, generateTimestampedName } from "../utils/migration-generator.js";
23
23
  import { getMigrationTableDDL } from "../utils/migration-helpers.js";
@@ -200,6 +200,12 @@ export async function runPush(config, projectRoot, opts = {}) {
200
200
  });
201
201
  const upStatements = migration.upSQL;
202
202
  spin.stop(`Generated ${upStatements.length} statement(s)`);
203
+ const summaryLines = formatCategorizedSummary(filteredDiff);
204
+ if (summaryLines.length > 0) {
205
+ for (const line of summaryLines)
206
+ console.log(line);
207
+ console.log('');
208
+ }
203
209
  const dialect = detectDialect(config);
204
210
  if (dialect !== 'postgres' && upStatements.length > 0) {
205
211
  const upSQL = upStatements.join('\n');
@@ -241,6 +247,7 @@ export async function runPush(config, projectRoot, opts = {}) {
241
247
  spin.start('Applying schema changes...');
242
248
  const execClient = await createDatabaseClient(config);
243
249
  let tx = null;
250
+ let statementsRun = 0;
244
251
  try {
245
252
  try {
246
253
  tx = await execClient.beginTransaction();
@@ -252,7 +259,6 @@ export async function runPush(config, projectRoot, opts = {}) {
252
259
  const executeQuery = tx
253
260
  ? (sql) => tx.query(sql)
254
261
  : (sql) => execClient.query(sql);
255
- let statementsRun = 0;
256
262
  for (const stmt of upStatements) {
257
263
  try {
258
264
  await executeQuery(stmt);
@@ -267,7 +273,6 @@ export async function runPush(config, projectRoot, opts = {}) {
267
273
  }
268
274
  if (tx)
269
275
  await tx.commit();
270
- spin.stop(`Applied ${statementsRun} statement(s)`);
271
276
  try {
272
277
  const tableName = config.migrations?.tableName || '_relq_migrations';
273
278
  const tableDDL = await getMigrationTableDDL(config, tableName);
@@ -283,10 +288,11 @@ export async function runPush(config, projectRoot, opts = {}) {
283
288
  const hash = generateTimestampedName(migrationName);
284
289
  await execClient.query(`INSERT INTO "${tableName}" (name, filename, hash, batch, sql_up, sql_down, source) ` +
285
290
  `VALUES ($1, $2, $3, 0, $4, $5, 'push')`, [migrationName, '', hash, upSQL, downSQL]);
286
- spin.stop(`Applied ${statementsRun} statement(s) — rollback saved`);
291
+ spin.stop(`Applied ${statementsRun} statement(s) — restore point saved`);
287
292
  }
288
293
  catch (recordError) {
289
- warning(`Schema applied but failed to save rollback point: ${recordError?.message || String(recordError)}`);
294
+ spin.stop(`Applied ${statementsRun} statement(s)`);
295
+ warning(`Failed to save restore point: ${recordError?.message || String(recordError)}`);
290
296
  }
291
297
  }
292
298
  catch (error) {
@@ -304,7 +310,12 @@ export async function runPush(config, projectRoot, opts = {}) {
304
310
  if (error.statementIndex) {
305
311
  errorMsg += `${colors.yellow('Statement #:')} ${error.statementIndex}\n`;
306
312
  }
307
- errorMsg += `\n${colors.muted('All changes rolled back.')}\n`;
313
+ if (tx) {
314
+ errorMsg += `\n${colors.muted('All changes rolled back.')}\n`;
315
+ }
316
+ else {
317
+ errorMsg += `\n${colors.yellow('Warning:')} ${statementsRun} statement(s) were already applied (no transaction support).\n`;
318
+ }
308
319
  spin.error('SQL execution failed');
309
320
  throw new Error(errorMsg);
310
321
  }
@@ -295,6 +295,39 @@ function generateColumnModification(tableName, columnName, changes) {
295
295
  downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT;`);
296
296
  }
297
297
  break;
298
+ case 'length': {
299
+ const newLen = change.to;
300
+ const oldLen = change.from;
301
+ const newFullType = newLen ? `VARCHAR(${newLen})` : 'VARCHAR';
302
+ const oldFullType = oldLen ? `VARCHAR(${oldLen})` : 'VARCHAR';
303
+ upSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${newFullType};`);
304
+ downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${oldFullType};`);
305
+ break;
306
+ }
307
+ case 'unique': {
308
+ const constraintName = `${tableName}_${columnName}_key`;
309
+ if (change.to === true) {
310
+ upSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${columnName}");`);
311
+ downSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
312
+ }
313
+ else {
314
+ upSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
315
+ downSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${columnName}");`);
316
+ }
317
+ break;
318
+ }
319
+ case 'primaryKey': {
320
+ const pkName = `${tableName}_pkey`;
321
+ if (change.to === true) {
322
+ upSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${columnName}");`);
323
+ downSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
324
+ }
325
+ else {
326
+ upSQL.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
327
+ downSQL.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${columnName}");`);
328
+ }
329
+ break;
330
+ }
298
331
  }
299
332
  }
300
333
  return { upSQL, downSQL };
@@ -508,6 +541,41 @@ function generateColumnChange(tableName, col) {
508
541
  }
509
542
  break;
510
543
  }
544
+ case 'length': {
545
+ const colType = col.after?.dataType ?? col.before?.dataType ?? 'VARCHAR';
546
+ const baseType = colType.toUpperCase().replace(/\(.*\)/, '');
547
+ const newLen = change.to;
548
+ const oldLen = change.from;
549
+ const newFullType = newLen ? `${baseType}(${newLen})` : baseType;
550
+ const oldFullType = oldLen ? `${baseType}(${oldLen})` : baseType;
551
+ up.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${col.name}" TYPE ${newFullType};`);
552
+ down.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${col.name}" TYPE ${oldFullType};`);
553
+ break;
554
+ }
555
+ case 'unique': {
556
+ const constraintName = `${tableName}_${col.name}_key`;
557
+ if (change.to === true) {
558
+ up.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${col.name}");`);
559
+ down.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
560
+ }
561
+ else {
562
+ up.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${constraintName}";`);
563
+ down.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" UNIQUE ("${col.name}");`);
564
+ }
565
+ break;
566
+ }
567
+ case 'primaryKey': {
568
+ const pkName = `${tableName}_pkey`;
569
+ if (change.to === true) {
570
+ up.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${col.name}");`);
571
+ down.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
572
+ }
573
+ else {
574
+ up.push(`ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${pkName}";`);
575
+ down.push(`ALTER TABLE "${tableName}" ADD CONSTRAINT "${pkName}" PRIMARY KEY ("${col.name}");`);
576
+ }
577
+ break;
578
+ }
511
579
  }
512
580
  }
513
581
  }
@@ -247,8 +247,13 @@ export async function introspectPostgres(connection, options) {
247
247
  continue;
248
248
  const normalizedColName = rawColName.replace(/^"|"$/g, '').toLowerCase();
249
249
  const col = columns.find(c => c.name.toLowerCase() === normalizedColName);
250
- if (col && con.constraint_type === 'PRIMARY KEY') {
251
- col.isPrimaryKey = true;
250
+ if (col) {
251
+ if (con.constraint_type === 'PRIMARY KEY') {
252
+ col.isPrimaryKey = true;
253
+ }
254
+ if (con.constraint_type === 'UNIQUE' && constraintColumns.length === 1) {
255
+ col.isUnique = true;
256
+ }
252
257
  }
253
258
  }
254
259
  }
@@ -256,12 +256,14 @@ function diffTables(local, remote) {
256
256
  if (remoteTable) {
257
257
  const columnDiffs = diffColumns(localTable.columns, remoteTable.columns);
258
258
  const indexDiffs = diffIndexes(localTable.indexes, remoteTable.indexes);
259
- if (columnDiffs.length > 0 || indexDiffs.length > 0) {
259
+ const constraintDiffs = diffConstraints(localTable.constraints, remoteTable.constraints);
260
+ if (columnDiffs.length > 0 || indexDiffs.length > 0 || constraintDiffs.length > 0) {
260
261
  diffs.push({
261
262
  name,
262
263
  type: 'modified',
263
264
  columns: columnDiffs,
264
265
  indexes: indexDiffs,
266
+ constraints: constraintDiffs,
265
267
  });
266
268
  }
267
269
  }
@@ -330,6 +332,37 @@ function diffIndexes(local, remote) {
330
332
  }
331
333
  return diffs;
332
334
  }
335
+ function diffConstraints(local, remote) {
336
+ const diffs = [];
337
+ const sig = (c) => {
338
+ const def = (c.definition || '').replace(/"/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
339
+ return `${c.type}:${def}`;
340
+ };
341
+ const localSigs = new Map();
342
+ const remoteSigs = new Map();
343
+ for (const c of local)
344
+ localSigs.set(sig(c), c);
345
+ for (const c of remote)
346
+ remoteSigs.set(sig(c), c);
347
+ const toConstraintInfo = (c) => ({
348
+ name: c.name,
349
+ type: (c.type || 'CHECK'),
350
+ columns: [],
351
+ definition: c.definition || '',
352
+ trackingId: c.trackingId,
353
+ });
354
+ for (const [s, c] of remoteSigs) {
355
+ if (!localSigs.has(s)) {
356
+ diffs.push({ name: c.name || c.definition || 'unnamed', type: 'added', after: toConstraintInfo(c) });
357
+ }
358
+ }
359
+ for (const [s, c] of localSigs) {
360
+ if (!remoteSigs.has(s)) {
361
+ diffs.push({ name: c.name || c.definition || 'unnamed', type: 'removed', before: toConstraintInfo(c) });
362
+ }
363
+ }
364
+ return diffs;
365
+ }
333
366
  function diffExtensions(local, remote) {
334
367
  const diffs = [];
335
368
  const localSet = new Set(local);
@@ -416,6 +449,11 @@ export function formatDiff(diff) {
416
449
  colors.red(' -');
417
450
  lines.push(`${idxIcon} index: ${idx.name}`);
418
451
  }
452
+ for (const con of table.constraints || []) {
453
+ const conIcon = con.type === 'added' ? colors.green(' +') :
454
+ colors.red(' -');
455
+ lines.push(`${conIcon} constraint: ${con.name}`);
456
+ }
419
457
  }
420
458
  for (const ext of diff.extensions) {
421
459
  const icon = ext.type === 'added' ? colors.green('+') : colors.red('-');
@@ -462,6 +500,105 @@ export function formatSummary(diff) {
462
500
  }
463
501
  return parts.join(', ');
464
502
  }
503
+ export function formatCategorizedSummary(diff) {
504
+ let tablesAdded = 0, tablesRemoved = 0, tablesModified = 0;
505
+ let colsAdded = 0, colsRemoved = 0, colsModified = 0;
506
+ let idxAdded = 0, idxRemoved = 0;
507
+ let fkAdded = 0, fkRemoved = 0;
508
+ let uniqueAdded = 0, uniqueRemoved = 0;
509
+ let pkAdded = 0, pkRemoved = 0;
510
+ let checkAdded = 0, checkRemoved = 0;
511
+ let extAdded = 0, extRemoved = 0;
512
+ for (const table of diff.tables) {
513
+ if (table.type === 'added')
514
+ tablesAdded++;
515
+ else if (table.type === 'removed')
516
+ tablesRemoved++;
517
+ else if (table.type === 'modified')
518
+ tablesModified++;
519
+ for (const col of table.columns || []) {
520
+ if (col.type === 'added')
521
+ colsAdded++;
522
+ else if (col.type === 'removed')
523
+ colsRemoved++;
524
+ else if (col.type === 'modified')
525
+ colsModified++;
526
+ }
527
+ for (const idx of table.indexes || []) {
528
+ if (idx.type === 'added')
529
+ idxAdded++;
530
+ else if (idx.type === 'removed')
531
+ idxRemoved++;
532
+ }
533
+ for (const con of table.constraints || []) {
534
+ const name = (con.name || '').toLowerCase();
535
+ const isFk = name.includes('fkey') || name.includes('foreign');
536
+ const isUq = name.includes('unique') || name.includes('_key');
537
+ const isPk = name.includes('pkey') || name.includes('primary');
538
+ const before = con.before;
539
+ const after = con.after;
540
+ const conType = (before?.type || after?.type || '').toUpperCase();
541
+ if (conType === 'FOREIGN KEY' || isFk) {
542
+ if (con.type === 'added')
543
+ fkAdded++;
544
+ else if (con.type === 'removed')
545
+ fkRemoved++;
546
+ }
547
+ else if (conType === 'UNIQUE' || isUq) {
548
+ if (con.type === 'added')
549
+ uniqueAdded++;
550
+ else if (con.type === 'removed')
551
+ uniqueRemoved++;
552
+ }
553
+ else if (conType === 'PRIMARY KEY' || isPk) {
554
+ if (con.type === 'added')
555
+ pkAdded++;
556
+ else if (con.type === 'removed')
557
+ pkRemoved++;
558
+ }
559
+ else if (conType === 'CHECK') {
560
+ if (con.type === 'added')
561
+ checkAdded++;
562
+ else if (con.type === 'removed')
563
+ checkRemoved++;
564
+ }
565
+ else {
566
+ if (con.type === 'added')
567
+ checkAdded++;
568
+ else if (con.type === 'removed')
569
+ checkRemoved++;
570
+ }
571
+ }
572
+ }
573
+ for (const ext of diff.extensions) {
574
+ if (ext.type === 'added')
575
+ extAdded++;
576
+ else if (ext.type === 'removed')
577
+ extRemoved++;
578
+ }
579
+ const lines = [];
580
+ const fmt = (label, added, removed, modified = 0) => {
581
+ const parts = [];
582
+ if (added)
583
+ parts.push(colors.green(`${added} added`));
584
+ if (modified)
585
+ parts.push(colors.yellow(`${modified} modified`));
586
+ if (removed)
587
+ parts.push(colors.red(`${removed} removed`));
588
+ if (parts.length > 0) {
589
+ lines.push(` ${colors.muted(label + ':')} ${parts.join(', ')}`);
590
+ }
591
+ };
592
+ fmt('tables', tablesAdded, tablesRemoved, tablesModified);
593
+ fmt('columns', colsAdded, colsRemoved, colsModified);
594
+ fmt('indexes', idxAdded, idxRemoved);
595
+ fmt('primary keys', pkAdded, pkRemoved);
596
+ fmt('unique constraints', uniqueAdded, uniqueRemoved);
597
+ fmt('foreign keys', fkAdded, fkRemoved);
598
+ fmt('check constraints', checkAdded, checkRemoved);
599
+ fmt('extensions', extAdded, extRemoved);
600
+ return lines;
601
+ }
465
602
  export function filterDiff(diff, ignorePatterns) {
466
603
  const patterns = ignorePatterns.map(p => {
467
604
  const regexStr = p.replace(/\*/g, '.*').replace(/\?/g, '.');
@@ -371,6 +371,20 @@ export function tableToAST(table) {
371
371
  return { name: p.$name, partitionBound };
372
372
  });
373
373
  }
374
+ for (const con of constraints) {
375
+ if (con.type === 'PRIMARY KEY' && con.columns.length > 0) {
376
+ for (const colName of con.columns) {
377
+ const col = columns.find(c => c.name === colName || c.tsName === colName);
378
+ if (col)
379
+ col.isPrimaryKey = true;
380
+ }
381
+ }
382
+ if (con.type === 'UNIQUE' && con.columns.length === 1) {
383
+ const col = columns.find(c => c.name === con.columns[0] || c.tsName === con.columns[0]);
384
+ if (col)
385
+ col.isUnique = true;
386
+ }
387
+ }
374
388
  return {
375
389
  name: table.$name,
376
390
  schema: table.$schema || 'public',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relq",
3
- "version": "1.0.90",
3
+ "version": "1.0.91",
4
4
  "description": "The Fully-Typed PostgreSQL ORM for TypeScript",
5
5
  "author": "Olajide Mathew O. <olajide.mathew@yuniq.solutions>",
6
6
  "license": "MIT",