relq 1.0.25 → 1.0.26

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 (52) hide show
  1. package/dist/cjs/cli/commands/commit.cjs +80 -0
  2. package/dist/cjs/cli/commands/import.cjs +1 -0
  3. package/dist/cjs/cli/commands/pull.cjs +8 -25
  4. package/dist/cjs/cli/commands/push.cjs +48 -8
  5. package/dist/cjs/cli/commands/rollback.cjs +205 -84
  6. package/dist/cjs/cli/commands/schema-ast.cjs +219 -0
  7. package/dist/cjs/cli/index.cjs +6 -0
  8. package/dist/cjs/cli/utils/ast-codegen.cjs +95 -3
  9. package/dist/cjs/cli/utils/ast-transformer.cjs +12 -0
  10. package/dist/cjs/cli/utils/change-tracker.cjs +135 -0
  11. package/dist/cjs/cli/utils/commit-manager.cjs +54 -0
  12. package/dist/cjs/cli/utils/migration-generator.cjs +319 -0
  13. package/dist/cjs/cli/utils/repo-manager.cjs +99 -3
  14. package/dist/cjs/cli/utils/schema-diff.cjs +390 -0
  15. package/dist/cjs/cli/utils/schema-hash.cjs +4 -0
  16. package/dist/cjs/cli/utils/schema-to-ast.cjs +477 -0
  17. package/dist/cjs/schema-definition/column-types.cjs +50 -4
  18. package/dist/cjs/schema-definition/pg-enum.cjs +10 -0
  19. package/dist/cjs/schema-definition/pg-function.cjs +19 -0
  20. package/dist/cjs/schema-definition/pg-sequence.cjs +22 -1
  21. package/dist/cjs/schema-definition/pg-trigger.cjs +39 -0
  22. package/dist/cjs/schema-definition/pg-view.cjs +17 -0
  23. package/dist/cjs/schema-definition/sql-expressions.cjs +3 -0
  24. package/dist/cjs/schema-definition/table-definition.cjs +4 -0
  25. package/dist/config.d.ts +98 -0
  26. package/dist/esm/cli/commands/commit.js +83 -3
  27. package/dist/esm/cli/commands/import.js +1 -0
  28. package/dist/esm/cli/commands/pull.js +9 -26
  29. package/dist/esm/cli/commands/push.js +49 -9
  30. package/dist/esm/cli/commands/rollback.js +206 -85
  31. package/dist/esm/cli/commands/schema-ast.js +183 -0
  32. package/dist/esm/cli/index.js +6 -0
  33. package/dist/esm/cli/utils/ast-codegen.js +93 -3
  34. package/dist/esm/cli/utils/ast-transformer.js +12 -0
  35. package/dist/esm/cli/utils/change-tracker.js +134 -0
  36. package/dist/esm/cli/utils/commit-manager.js +51 -0
  37. package/dist/esm/cli/utils/migration-generator.js +318 -0
  38. package/dist/esm/cli/utils/repo-manager.js +96 -3
  39. package/dist/esm/cli/utils/schema-diff.js +389 -0
  40. package/dist/esm/cli/utils/schema-hash.js +4 -0
  41. package/dist/esm/cli/utils/schema-to-ast.js +447 -0
  42. package/dist/esm/schema-definition/column-types.js +50 -4
  43. package/dist/esm/schema-definition/pg-enum.js +10 -0
  44. package/dist/esm/schema-definition/pg-function.js +19 -0
  45. package/dist/esm/schema-definition/pg-sequence.js +22 -1
  46. package/dist/esm/schema-definition/pg-trigger.js +39 -0
  47. package/dist/esm/schema-definition/pg-view.js +17 -0
  48. package/dist/esm/schema-definition/sql-expressions.js +3 -0
  49. package/dist/esm/schema-definition/table-definition.js +4 -0
  50. package/dist/index.d.ts +98 -0
  51. package/dist/schema-builder.d.ts +223 -24
  52. package/package.json +1 -1
@@ -10,6 +10,14 @@ function pgView(name, definition) {
10
10
  name,
11
11
  definition: definition.trim(),
12
12
  isMaterialized: false,
13
+ toAST() {
14
+ return {
15
+ name: this.name,
16
+ definition: this.definition,
17
+ isMaterialized: false,
18
+ trackingId: this.$trackingId,
19
+ };
20
+ },
13
21
  };
14
22
  }
15
23
  function pgMaterializedView(name, definition, options) {
@@ -19,6 +27,15 @@ function pgMaterializedView(name, definition, options) {
19
27
  definition: definition.trim(),
20
28
  isMaterialized: true,
21
29
  withData: options?.withData,
30
+ toAST() {
31
+ return {
32
+ name: this.name,
33
+ definition: this.definition,
34
+ isMaterialized: true,
35
+ withData: this.withData,
36
+ trackingId: this.$trackingId,
37
+ };
38
+ },
22
39
  };
23
40
  }
24
41
  function viewToSQL(view) {
@@ -188,6 +188,9 @@ function pgExtensions(...extensions) {
188
188
  toSQL() {
189
189
  return extensions.map(ext => `CREATE EXTENSION IF NOT EXISTS "${ext}";`);
190
190
  },
191
+ toAST() {
192
+ return [...extensions];
193
+ },
191
194
  };
192
195
  }
193
196
  function getSql(expr) {
@@ -7,6 +7,7 @@ exports.defineTable = defineTable;
7
7
  const pg_format_1 = __importDefault(require("../addon/pg-format/index.cjs"));
8
8
  const partitions_1 = require("./partitions.cjs");
9
9
  const sql_expressions_1 = require("./sql-expressions.cjs");
10
+ const schema_to_ast_1 = require("../cli/utils/schema-to-ast.cjs");
10
11
  function formatWhereValue(val) {
11
12
  if (val === null)
12
13
  return 'NULL';
@@ -481,6 +482,9 @@ function defineTable(name, columns, options) {
481
482
  },
482
483
  toCreateIndexSQL() {
483
484
  return generateIndexSQL(this);
485
+ },
486
+ toAST() {
487
+ return (0, schema_to_ast_1.tableToAST)(this);
484
488
  }
485
489
  };
486
490
  return definition;
package/dist/config.d.ts CHANGED
@@ -261,6 +261,102 @@ export interface DefaultValue {
261
261
  readonly $sql: string;
262
262
  readonly $isDefault: true;
263
263
  }
264
+ /**
265
+ * AST Type Definitions
266
+ *
267
+ * Type definitions for parsed PostgreSQL schema objects.
268
+ * These are intermediate representations between pgsql-parser AST and TypeScript code generation.
269
+ */
270
+ export interface ParsedColumn {
271
+ name: string;
272
+ type: string;
273
+ typeParams?: {
274
+ precision?: number;
275
+ scale?: number;
276
+ length?: number;
277
+ };
278
+ isNullable: boolean;
279
+ isPrimaryKey: boolean;
280
+ isUnique: boolean;
281
+ hasDefault: boolean;
282
+ defaultValue?: string;
283
+ isGenerated: boolean;
284
+ generatedExpression?: string;
285
+ generatedExpressionAst?: any;
286
+ checkConstraint?: {
287
+ name: string;
288
+ expression: string;
289
+ expressionAst?: any;
290
+ };
291
+ references?: {
292
+ table: string;
293
+ column: string;
294
+ onDelete?: string;
295
+ onUpdate?: string;
296
+ match?: "SIMPLE" | "FULL";
297
+ deferrable?: boolean;
298
+ initiallyDeferred?: boolean;
299
+ };
300
+ isArray: boolean;
301
+ arrayDimensions?: number;
302
+ comment?: string;
303
+ /** Tracking ID for rename detection in versioning */
304
+ trackingId?: string;
305
+ }
306
+ export interface ParsedConstraint {
307
+ name: string;
308
+ type: "PRIMARY KEY" | "UNIQUE" | "FOREIGN KEY" | "CHECK" | "EXCLUDE";
309
+ columns: string[];
310
+ expression?: string;
311
+ expressionAst?: any;
312
+ comment?: string;
313
+ references?: {
314
+ table: string;
315
+ columns: string[];
316
+ onDelete?: string;
317
+ onUpdate?: string;
318
+ match?: "SIMPLE" | "FULL";
319
+ deferrable?: boolean;
320
+ initiallyDeferred?: boolean;
321
+ };
322
+ /** Tracking ID for rename detection in versioning */
323
+ trackingId?: string;
324
+ }
325
+ export interface ParsedIndex {
326
+ name: string;
327
+ columns: string[];
328
+ isUnique: boolean;
329
+ method?: string;
330
+ whereClause?: string;
331
+ whereClauseAst?: any;
332
+ includeColumns?: string[];
333
+ opclass?: string;
334
+ isExpression?: boolean;
335
+ expressions?: string[];
336
+ comment?: string;
337
+ /** Tracking ID for rename detection in versioning */
338
+ trackingId?: string;
339
+ }
340
+ export interface ParsedTable {
341
+ name: string;
342
+ schema?: string;
343
+ columns: ParsedColumn[];
344
+ constraints: ParsedConstraint[];
345
+ indexes: ParsedIndex[];
346
+ isPartitioned: boolean;
347
+ partitionType?: "RANGE" | "LIST" | "HASH";
348
+ partitionKey?: string[];
349
+ partitionOf?: string;
350
+ partitionBound?: string;
351
+ inherits?: string[];
352
+ comment?: string;
353
+ childPartitions?: {
354
+ name: string;
355
+ partitionBound: string;
356
+ }[];
357
+ /** Tracking ID for rename detection in versioning */
358
+ trackingId?: string;
359
+ }
264
360
  declare const EMPTY_OBJECT: unique symbol;
265
361
  declare const EMPTY_ARRAY: unique symbol;
266
362
  export interface ColumnConfig<T = unknown> {
@@ -505,6 +601,8 @@ export interface TableDefinition<T extends Record<string, ColumnConfig>> {
505
601
  $inferInsert: BuildInsertType<T>;
506
602
  toSQL(): string;
507
603
  toCreateIndexSQL(): string[];
604
+ /** Returns AST for schema diffing and migration generation */
605
+ toAST(): ParsedTable;
508
606
  }
509
607
  /**
510
608
  * AWS regions with autocomplete support
@@ -1,11 +1,14 @@
1
1
  import * as crypto from 'crypto';
2
- import { fatal, hint } from "../utils/cli-utils.js";
3
- import { isInitialized, getHead, getStagedChanges, getUnstagedChanges, shortHash, hashFileContent, saveFileHash, } from "../utils/repo-manager.js";
2
+ import { colors, createSpinner, fatal, hint } from "../utils/cli-utils.js";
3
+ import { isInitialized, getHead, getStagedChanges, getUnstagedChanges, loadSnapshot, shortHash, hashFileContent, saveFileHash, } from "../utils/repo-manager.js";
4
4
  import { loadConfig } from "../../config/config.js";
5
5
  import { getSchemaPath } from "../utils/config-loader.js";
6
- import { sortChangesByDependency, generateCombinedSQL, } from "../utils/change-tracker.js";
6
+ import { sortChangesByDependency, generateCombinedSQL, generateDownSQL, } from "../utils/change-tracker.js";
7
+ import { createCommitFromSchema, generateASTHash, } from "../utils/commit-manager.js";
8
+ import { schemaToAST } from "../utils/schema-to-ast.js";
7
9
  import * as fs from 'fs';
8
10
  import * as path from 'path';
11
+ import { createJiti } from 'jiti';
9
12
  export async function commitCommand(context) {
10
13
  const { config, flags, args, projectRoot } = context;
11
14
  const author = config?.author || 'Developer <dev@example.com>';
@@ -16,6 +19,9 @@ export async function commitCommand(context) {
16
19
  const schemaPath = path.resolve(projectRoot, getSchemaPath(config ?? undefined));
17
20
  const { requireValidSchema } = await import("../utils/config-loader.js");
18
21
  await requireValidSchema(schemaPath, flags);
22
+ if (flags['from-schema']) {
23
+ return commitFromSchema(context, schemaPath, author);
24
+ }
19
25
  const staged = getStagedChanges(projectRoot);
20
26
  if (staged.length === 0) {
21
27
  console.log('nothing to commit, working tree clean');
@@ -40,6 +46,8 @@ export async function commitCommand(context) {
40
46
  }
41
47
  const sortedChanges = sortChangesByDependency(staged);
42
48
  const sql = generateCombinedSQL(sortedChanges);
49
+ const downSQL = generateDownSQL(sortedChanges);
50
+ const snapshot = loadSnapshot(projectRoot);
43
51
  const creates = staged.filter(c => c.type === 'CREATE').length;
44
52
  const alters = staged.filter(c => c.type === 'ALTER').length;
45
53
  const drops = staged.filter(c => c.type === 'DROP').length;
@@ -60,6 +68,8 @@ export async function commitCommand(context) {
60
68
  timestamp: new Date().toISOString(),
61
69
  changes: sortedChanges,
62
70
  sql,
71
+ downSQL,
72
+ schema: snapshot,
63
73
  snapshotHash: hash.substring(0, 12),
64
74
  stats: {
65
75
  creates,
@@ -113,4 +123,74 @@ export async function commitCommand(context) {
113
123
  hint("run 'relq export' to export as SQL file");
114
124
  console.log('');
115
125
  }
126
+ async function commitFromSchema(context, schemaPath, author) {
127
+ const { config, flags, args, projectRoot } = context;
128
+ const spinner = createSpinner();
129
+ let message = flags['m'] || flags['message'];
130
+ if (!message) {
131
+ if (args.length > 0) {
132
+ message = args.join(' ');
133
+ }
134
+ else {
135
+ fatal('commit message required', "usage: relq commit --from-schema -m '<message>'");
136
+ }
137
+ }
138
+ spinner.start('Loading schema file');
139
+ let schemaModule;
140
+ try {
141
+ const jiti = createJiti(path.dirname(schemaPath), { interopDefault: true });
142
+ const module = await jiti.import(schemaPath);
143
+ if (module && module.default && typeof module.default === 'object') {
144
+ schemaModule = module.default;
145
+ }
146
+ else if (module && typeof module === 'object') {
147
+ schemaModule = module;
148
+ }
149
+ else {
150
+ throw new Error('Schema file must export an object with table/enum definitions');
151
+ }
152
+ spinner.succeed('Loaded schema file');
153
+ }
154
+ catch (err) {
155
+ spinner.fail('Failed to load schema');
156
+ fatal(`Could not load schema: ${err instanceof Error ? err.message : String(err)}`);
157
+ }
158
+ spinner.start('Converting schema to AST');
159
+ const ast = schemaToAST(schemaModule);
160
+ spinner.succeed('Converted schema to AST');
161
+ const schemaHash = generateASTHash(ast);
162
+ spinner.start('Creating commit');
163
+ try {
164
+ const commit = createCommitFromSchema(schemaModule, author, message, config?.commitLimit ?? 1000, projectRoot);
165
+ spinner.succeed('Created commit');
166
+ const tableCount = ast.tables.length;
167
+ const enumCount = ast.enums.length;
168
+ const functionCount = ast.functions.length;
169
+ const viewCount = ast.views.length;
170
+ const triggerCount = ast.triggers.length;
171
+ console.log('');
172
+ console.log(`[${shortHash(commit.hash)}] ${message}`);
173
+ const statsParts = [];
174
+ if (tableCount > 0)
175
+ statsParts.push(`${tableCount} table(s)`);
176
+ if (enumCount > 0)
177
+ statsParts.push(`${enumCount} enum(s)`);
178
+ if (functionCount > 0)
179
+ statsParts.push(`${functionCount} function(s)`);
180
+ if (viewCount > 0)
181
+ statsParts.push(`${viewCount} view(s)`);
182
+ if (triggerCount > 0)
183
+ statsParts.push(`${triggerCount} trigger(s)`);
184
+ console.log(` ${statsParts.length > 0 ? statsParts.join(', ') : 'empty schema'}`);
185
+ console.log('');
186
+ console.log(colors.muted(`Schema hash: ${schemaHash.substring(0, 12)}`));
187
+ console.log('');
188
+ hint("run 'relq push' to apply changes to database");
189
+ hint("run 'relq log' to view commit history");
190
+ }
191
+ catch (err) {
192
+ spinner.fail('Failed to create commit');
193
+ fatal(`Could not create commit: ${err instanceof Error ? err.message : String(err)}`);
194
+ }
195
+ }
116
196
  export default commitCommand;
@@ -175,6 +175,7 @@ export async function importCommand(sqlFilePath, options = {}, projectRoot = pro
175
175
  tables: filteredTables,
176
176
  enums: filteredEnums,
177
177
  domains: filteredDomains,
178
+ compositeTypes: parsedSchema.compositeTypes || [],
178
179
  sequences: filteredSequences,
179
180
  views: parsedSchema.views,
180
181
  functions: parsedSchema.functions,
@@ -3,7 +3,7 @@ import * as path from 'path';
3
3
  import { requireValidConfig, getSchemaPath } from "../utils/config-loader.js";
4
4
  import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
5
5
  import { introspectedToParsedSchema } from "../utils/ast-transformer.js";
6
- import { generateTypeScriptFromAST } from "../utils/ast-codegen.js";
6
+ import { generateTypeScriptFromAST, assignTrackingIds, copyTrackingIdsToNormalized } from "../utils/ast-codegen.js";
7
7
  import { getConnectionDescription } from "../utils/env-loader.js";
8
8
  import { createSpinner, colors, formatBytes, formatDuration, fatal, confirm, warning, createMultiProgress } from "../utils/cli-utils.js";
9
9
  import { loadRelqignore, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isCompositeTypeIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
@@ -377,13 +377,18 @@ export async function pullCommand(context) {
377
377
  console.log(` ${colors.red(`-${removed.length}`)} tables removed`);
378
378
  }
379
379
  console.log('');
380
- if (!dryRun) {
380
+ const noAutoMerge = flags['no-auto-merge'] === true;
381
+ if (!dryRun && noAutoMerge) {
381
382
  const proceed = await confirm(`${colors.bold('Pull these changes?')}`, true);
382
383
  if (!proceed) {
383
384
  fatal('Operation cancelled by user');
384
385
  }
385
386
  console.log('');
386
387
  }
388
+ else if (!dryRun) {
389
+ console.log(`${colors.green('Auto-merging')} (no conflicts detected)`);
390
+ console.log('');
391
+ }
387
392
  }
388
393
  else if (schemaExists && !force) {
389
394
  warning('Local schema exists but not tracked');
@@ -445,6 +450,7 @@ export async function pullCommand(context) {
445
450
  }
446
451
  spinner.start('Generating TypeScript schema...');
447
452
  const parsedSchema = await introspectedToParsedSchema(dbSchema);
453
+ assignTrackingIds(parsedSchema);
448
454
  const typescript = generateTypeScriptFromAST(parsedSchema, {
449
455
  camelCase: config.generate?.camelCase ?? true,
450
456
  importPath: 'relq/schema-builder',
@@ -550,7 +556,7 @@ export async function pullCommand(context) {
550
556
  triggers: filteredTriggers || [],
551
557
  };
552
558
  const schemaChanges = compareSchemas(beforeSchema, afterSchema);
553
- applyTrackingIdsToSnapshot(typescript, currentSchema);
559
+ copyTrackingIdsToNormalized(parsedSchema, currentSchema);
554
560
  saveSnapshot(currentSchema, projectRoot);
555
561
  const duration = Date.now() - startTime;
556
562
  if (noCommit) {
@@ -656,26 +662,3 @@ function detectObjectConflicts(local, remote) {
656
662
  }
657
663
  return conflicts;
658
664
  }
659
- function applyTrackingIdsToSnapshot(typescript, snapshot) {
660
- for (const table of snapshot.tables) {
661
- const tablePattern = new RegExp(`defineTable\\s*\\(\\s*['"]${table.name}['"]\\s*,\\s*\\{[^}]+\\}\\s*,\\s*\\{[^}]*\\$trackingId:\\s*['"]([^'"]+)['"]`, 's');
662
- const tableMatch = typescript.match(tablePattern);
663
- if (tableMatch) {
664
- table.trackingId = tableMatch[1];
665
- }
666
- for (const col of table.columns) {
667
- const colPattern = new RegExp(`(?:${col.tsName}|${col.name}):\\s*\\w+\\([^)]*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
668
- const colMatch = typescript.match(colPattern);
669
- if (colMatch) {
670
- col.trackingId = colMatch[1];
671
- }
672
- }
673
- for (const idx of table.indexes) {
674
- const idxPattern = new RegExp(`index\\s*\\(\\s*['"]${idx.name}['"]\\s*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
675
- const idxMatch = typescript.match(idxPattern);
676
- if (idxMatch) {
677
- idx.trackingId = idxMatch[1];
678
- }
679
- }
680
- }
681
- }
@@ -5,7 +5,7 @@ import { getConnectionDescription } from "../utils/env-loader.js";
5
5
  import { colors, createSpinner, fatal, confirm, warning } from "../utils/cli-utils.js";
6
6
  import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
7
7
  import { loadRelqignore, isTableIgnored, isColumnIgnored, isEnumIgnored, isDomainIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
8
- import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, isCommitSyncedWith, markCommitAsPushed, getConnectionLabel, } from "../utils/repo-manager.js";
8
+ import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, isCommitSyncedWith, markCommitAsPushed, markCommitAsApplied, getConnectionLabel, } from "../utils/repo-manager.js";
9
9
  export async function pushCommand(context) {
10
10
  const { config, flags } = context;
11
11
  if (!config) {
@@ -16,7 +16,7 @@ export async function pushCommand(context) {
16
16
  const { projectRoot } = context;
17
17
  const force = flags['force'] === true;
18
18
  const dryRun = flags['dry-run'] === true;
19
- const applySQL = flags['apply'] === true;
19
+ const metadataOnly = flags['metadata-only'] === true;
20
20
  const noVerify = flags['no-verify'] === true;
21
21
  const skipPrompt = flags['yes'] === true || flags['y'] === true;
22
22
  const includeFunctions = config.includeFunctions ?? false;
@@ -129,9 +129,46 @@ export async function pushCommand(context) {
129
129
  console.log('');
130
130
  }
131
131
  if (dryRun) {
132
- console.log(`${colors.yellow('Dry run')} - no changes applied`);
132
+ console.log(`${colors.yellow('Dry run')} - showing changes that would be applied`);
133
133
  console.log('');
134
- console.log(`${colors.muted('Use')} ${colors.cyan('relq push --apply')} ${colors.muted('to execute.')}`);
134
+ if (hasObjectsToDrop && force) {
135
+ console.log(`${colors.red('DROP statements:')}`);
136
+ for (const obj of analysis.objectsToDrop.slice(0, 5)) {
137
+ console.log(` ${generateDropSQL(obj)}`);
138
+ }
139
+ if (analysis.objectsToDrop.length > 5) {
140
+ console.log(` ${colors.muted(`... and ${analysis.objectsToDrop.length - 5} more`)}`);
141
+ }
142
+ console.log('');
143
+ }
144
+ const commitsToProcess = [...toPush].reverse();
145
+ let totalStatements = 0;
146
+ for (const commit of commitsToProcess) {
147
+ const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
148
+ if (fs.existsSync(commitPath)) {
149
+ const enhancedCommit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
150
+ if (enhancedCommit.sql && enhancedCommit.sql.trim()) {
151
+ const statements = enhancedCommit.sql.split(';').filter(s => s.trim());
152
+ totalStatements += statements.length;
153
+ console.log(`${colors.cyan(`Commit ${shortHash(commit.hash)}:`)} ${commit.message}`);
154
+ for (const stmt of statements.slice(0, 3)) {
155
+ console.log(` ${stmt.trim().substring(0, 80)}${stmt.trim().length > 80 ? '...' : ''};`);
156
+ }
157
+ if (statements.length > 3) {
158
+ console.log(` ${colors.muted(`... and ${statements.length - 3} more statements`)}`);
159
+ }
160
+ console.log('');
161
+ }
162
+ }
163
+ }
164
+ if (totalStatements === 0 && !hasObjectsToDrop) {
165
+ console.log(`${colors.muted('No SQL changes to apply')}`);
166
+ }
167
+ else {
168
+ console.log(`${colors.muted('Total:')} ${totalStatements + (hasObjectsToDrop ? analysis.objectsToDrop.length : 0)} statements`);
169
+ }
170
+ console.log('');
171
+ console.log(`${colors.muted('Remove')} ${colors.cyan('--dry-run')} ${colors.muted('to execute these changes.')}`);
135
172
  console.log('');
136
173
  return;
137
174
  }
@@ -144,8 +181,8 @@ export async function pushCommand(context) {
144
181
  }
145
182
  spinner.succeed(`Pushed ${toPush.length} commit(s) to ${getConnectionLabel(connection)}`);
146
183
  }
147
- if (applySQL) {
148
- spinner.start('Applying SQL changes...');
184
+ if (!metadataOnly && !dryRun) {
185
+ spinner.start('Applying schema changes...');
149
186
  const pg = await import("../../addon/pg/index.js");
150
187
  const client = new pg.Client({
151
188
  host: connection.host,
@@ -182,6 +219,9 @@ export async function pushCommand(context) {
182
219
  }
183
220
  await client.query('COMMIT');
184
221
  spinner.succeed(`Applied ${statementsRun} statement(s) from ${sqlExecuted} commit(s)`);
222
+ for (const commit of commitsToProcess) {
223
+ await markCommitAsApplied(connection, commit.hash);
224
+ }
185
225
  let hasRenameOperations = false;
186
226
  for (const commit of commitsToProcess) {
187
227
  const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
@@ -226,13 +266,13 @@ export async function pushCommand(context) {
226
266
  const oldHash = remoteHead ? shortHash(remoteHead) : '(none)';
227
267
  const newHash = shortHash(localHead);
228
268
  console.log(` ${oldHash}..${newHash} ${colors.muted('main -> main')}`);
229
- if (hasObjectsToDrop && force && applySQL) {
269
+ if (hasObjectsToDrop && force && !metadataOnly && !dryRun) {
230
270
  console.log('');
231
271
  warning(`Dropped ${analysis.objectsToDrop.length} object(s) from remote`);
232
272
  }
233
- if (!applySQL) {
273
+ if (metadataOnly) {
234
274
  console.log('');
235
- console.log(`${colors.muted('Use')} ${colors.cyan('relq push --apply')} ${colors.muted('to execute SQL.')}`);
275
+ console.log(`${colors.muted('Metadata only - SQL not executed. Remove')} ${colors.cyan('--metadata-only')} ${colors.muted('to apply changes.')}`);
236
276
  }
237
277
  console.log('');
238
278
  }