frg-data-diff 2.0.0 → 2.1.0

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > It compares row data between two PostgreSQL databases and safely applies changes.
4
4
  >
5
- > It also produces a review-only schema diff based on `information_schema`, with JSON/YAML artifacts plus SQL for manual execution.
5
+ > It also produces review-only schema, trigger/function, and view SQL artifacts for manual execution.
6
6
 
7
7
  Safe PostgreSQL **data** diff and apply tooling, with companion PostgreSQL **schema** diff output for manual review. Compare data between two PostgreSQL databases and apply changes with strong safety guards.
8
8
 
@@ -23,6 +23,8 @@ npx frg-data-diff
23
23
  npx frg-data-diff generate
24
24
  npx frg-data-diff apply
25
25
  npx frg-data-diff sql
26
+ npx frg-data-diff pg-triggers
27
+ npx frg-data-diff pg-views
26
28
  ```
27
29
 
28
30
  ---
@@ -62,12 +64,30 @@ Reads a JSON diff file and writes a plain SQL script for manual review and execu
62
64
  npx frg-data-diff sql --input frg-data-diff.json --output frg-data-diff.sql
63
65
  ```
64
66
 
67
+ ### `frg-data-diff pg-triggers`
68
+
69
+ Compares PostgreSQL triggers and functions and writes a SQL diff script.
70
+
71
+ ```bash
72
+ npx frg-data-diff pg-triggers --output frg-triggers-diff.sql
73
+ ```
74
+
75
+ ### `frg-data-diff pg-views`
76
+
77
+ Compares regular and materialized view definitions and writes a SQL diff script.
78
+
79
+ ```bash
80
+ npx frg-data-diff pg-views --output frg-views-diff.sql
81
+ ```
82
+
65
83
  ---
66
84
 
67
85
  ## Configuration File: `.frg-data-diff.config.json`
68
86
 
69
87
  Commands look for this file in the current working directory.
70
88
 
89
+ The config file is JSON with comments: `// line comments` and `/* block comments */` are accepted. Other JSON rules still apply, including no trailing commas.
90
+
71
91
  ### Committing the config file
72
92
 
73
93
  **Do commit** `.frg-data-diff.config.json` to version control.
@@ -86,11 +106,13 @@ Generator list fields also support `$ENV_VAR` entries:
86
106
  - `schemaDiffExcludeTables`
87
107
  - `pgTriggersTables`
88
108
  - `pgTriggersExcludeTables`
109
+ - `pgViews`
110
+ - `pgViewsExclude`
89
111
  - `ignoreColumns`
90
112
 
91
113
  When used in those list fields, the environment variable value is parsed as a comma-separated list at runtime.
92
114
 
93
- Data, schema diff, and PostgreSQL trigger table filters are independent. If a schema or trigger table/exclude field is omitted, it defaults to an empty list and does not inherit from `tables` or `excludeTables`.
115
+ Data, schema diff, PostgreSQL trigger, and PostgreSQL view filters are independent. Schema and trigger include filters inherit `tables` when omitted. View include filters default to all views when omitted.
94
116
 
95
117
  In the interactive wizard, pressing Enter keeps the displayed default. For optional list fields, type `none` to clear the value; the config stores that explicit clear as an empty array.
96
118
 
@@ -121,6 +143,9 @@ In the interactive wizard, pressing Enter keeps the displayed default. For optio
121
143
  "pgTriggersTables": ["my_table"],
122
144
  "pgTriggersExcludeTables": [],
123
145
  "pgTriggersOutput": "frg-triggers-diff.sql",
146
+ "pgViews": ["*"],
147
+ "pgViewsExclude": [],
148
+ "pgViewsOutput": "frg-views-diff.sql",
124
149
  "ignoreColumns": ["created_at", "updated_at"],
125
150
  "includeDeletes": true,
126
151
  "skipMissingPk": false,
@@ -256,6 +281,13 @@ Key options:
256
281
  | `--table` | Table(s) or `*` patterns to include (repeatable) |
257
282
  | `--exclude-table` | Table(s) or `*` patterns to skip (repeatable) |
258
283
  | `--ignore-column` | Column(s) to ignore during comparison (repeatable) |
284
+ | `--pg-triggers-table` | Table(s) for trigger/function diff |
285
+ | `--pg-triggers-output` | Output path for PostgreSQL trigger/function diff SQL |
286
+ | `--generate-pg-triggers` | Generate PostgreSQL trigger/function diff SQL |
287
+ | `--pg-view` | View(s) or `*` patterns for view DDL diff |
288
+ | `--pg-view-exclude` | View(s) or `*` patterns to skip in view DDL diff |
289
+ | `--pg-views-output` | Output path for PostgreSQL view diff SQL |
290
+ | `--generate-pg-views` | Generate PostgreSQL view diff SQL from `generate` |
259
291
  | `--include-deletes` | Generate delete entries for rows only in dest |
260
292
  | `--skip-missing-pk` | Skip tables without a primary key instead of failing |
261
293
  | `--output` | Output diff file path (default: `frg-data-diff.json`) |
@@ -372,6 +404,8 @@ export DIRECTUS_TABLES="directus_*, custom_table"
372
404
  export DIRECTUS_EXCLUDES="directus_activity, directus_sessions"
373
405
  export DIRECTUS_PG_TRIGGER_TABLES="directus_flows, directus_operations"
374
406
  export DIRECTUS_PG_TRIGGER_EXCLUDES="directus_sessions"
407
+ export DIRECTUS_PG_VIEWS="directus_*_view, custom_view"
408
+ export DIRECTUS_PG_VIEW_EXCLUDES="legacy_*"
375
409
  export DIRECTUS_IGNORED_COLUMNS="created_at, updated_at"
376
410
  ```
377
411
 
@@ -383,6 +417,9 @@ export DIRECTUS_IGNORED_COLUMNS="created_at, updated_at"
383
417
  "pgTriggersTables": ["$DIRECTUS_PG_TRIGGER_TABLES"],
384
418
  "pgTriggersExcludeTables": ["$DIRECTUS_PG_TRIGGER_EXCLUDES"],
385
419
  "pgTriggersOutput": "frg-triggers-diff.sql",
420
+ "pgViews": ["$DIRECTUS_PG_VIEWS"],
421
+ "pgViewsExclude": ["$DIRECTUS_PG_VIEW_EXCLUDES"],
422
+ "pgViewsOutput": "frg-views-diff.sql",
386
423
  "ignoreColumns": ["$DIRECTUS_IGNORED_COLUMNS"]
387
424
  }
388
425
  }
@@ -400,7 +437,7 @@ export DIRECTUS_IGNORED_COLUMNS="created_at, updated_at"
400
437
  - **dryRun defaults to true** — no mutations without `--execute`.
401
438
  - **Transaction by default** — all changes in a single transaction; rolled back on failure in `abort` mode.
402
439
  - **Generated columns are never written**.
403
- - **Schema is never mutated**.
440
+ - **Data apply never mutates schema** — generated schema, trigger, and view SQL is review-only and must be executed manually.
404
441
 
405
442
  ---
406
443
 
@@ -479,6 +516,9 @@ For publishing Directus configuration data from development to production:
479
516
  "pgTriggersTables": ["directus_flows", "directus_operations"],
480
517
  "pgTriggersExcludeTables": [],
481
518
  "pgTriggersOutput": "frg-triggers-diff.sql",
519
+ "pgViews": ["directus_*_view"],
520
+ "pgViewsExclude": [],
521
+ "pgViewsOutput": "frg-views-diff.sql",
482
522
  "ignoreColumns": ["created_at", "updated_at"],
483
523
  "includeDeletes": true,
484
524
  "skipMissingPk": false,
@@ -546,7 +586,7 @@ npx frg-data-diff apply --execute --apply-deletes
546
586
  - Schema diff (table creation, column changes, index changes) — use a dedicated schema migration tool.
547
587
  - Cross-schema comparisons within a single call (one schema per run).
548
588
  - Tables without primary keys (use `--skip-missing-pk` to skip them).
549
- - Views, materialized views, foreign tables.
589
+ - Foreign tables.
550
590
  - Streaming of very large tables — v1 loads each table into memory. For huge tables, process in smaller table batches.
551
591
  - PostgreSQL pseudo-types and internal-only types as table columns.
552
592
  - Multiple databases in a single run — run the tool once per database pair.
@@ -617,6 +657,7 @@ frg-data-diff sql --help
617
657
  4. **No schema migration**: This tool does not create tables, add columns, or change indexes.
618
658
  5. **Single schema per run**: All tables must be in the same schema.
619
659
  6. **Env var references use shell-style names**: `$ENV_VAR`, `$envVar`, and `$env_var` are all valid.
660
+ 7. **View SQL scope**: View diffs compare definitions from PostgreSQL catalog metadata. Ownership, grants, comments, and materialized-view indexes are not recreated.
620
661
 
621
662
  ---
622
663
 
@@ -49,6 +49,7 @@ const resolve_options_1 = require("../config/resolve-options");
49
49
  const write_config_1 = require("../config/write-config");
50
50
  const connection_1 = require("../db/connection");
51
51
  const metadata_1 = require("../db/metadata");
52
+ const pg_views_1 = require("../db/pg-views");
52
53
  const generate_diff_1 = require("../diff/generate-diff");
53
54
  const write_diff_yaml_1 = require("../diff/write-diff-yaml");
54
55
  const prompts_1 = require("../shared/prompts");
@@ -82,15 +83,20 @@ program
82
83
  .option("--schema-diff-exclude-table <table...>", "Table(s) to exclude from schema diff")
83
84
  .option("--pg-triggers-table <table...>", "Table(s) to include for PostgreSQL triggers diff")
84
85
  .option("--pg-triggers-exclude-table <table...>", "Table(s) to exclude from PostgreSQL triggers diff")
86
+ .option("--pg-view <view...>", "View(s) to include for PostgreSQL views diff")
87
+ .option("--pg-view-exclude <view...>", "View(s) to exclude from PostgreSQL views diff")
85
88
  .option("--ignore-column <column...>", "Column(s) to ignore during comparison")
86
89
  .option("--include-deletes", "Include deletes in the diff (rows in dest not in source)")
87
90
  .option("--skip-missing-pk", "Skip tables that have no primary key instead of failing")
88
91
  .option("--output <file>", "Output diff file path")
89
92
  .option("--schema-diff-output <file>", "Output schema diff file path")
90
93
  .option("--pg-triggers-output <file>", "Output PostgreSQL triggers diff file path")
94
+ .option("--pg-views-output <file>", "Output PostgreSQL views diff file path")
91
95
  .option("--pretty", "Pretty-print the output JSON")
92
96
  .option("--generate-pg-triggers", "Generate a PostgreSQL triggers and functions diff")
93
97
  .option("--no-generate-pg-triggers", "Do not generate a PostgreSQL triggers and functions diff")
98
+ .option("--generate-pg-views", "Generate a PostgreSQL views diff")
99
+ .option("--no-generate-pg-views", "Do not generate a PostgreSQL views diff")
94
100
  .option("--verbose", "Enable verbose logging")
95
101
  .option("--config <file>", "Path to config file", load_config_1.DEFAULT_CONFIG_FILENAME)
96
102
  .option("--wizard <value>", "Force the interactive wizard (use true or 1)")
@@ -136,14 +142,18 @@ async function main() {
136
142
  schemaDiffExcludeTables: opts["schemaDiffExcludeTable"],
137
143
  pgTriggersTables: opts["pgTriggersTable"],
138
144
  pgTriggersExcludeTables: opts["pgTriggersExcludeTable"],
145
+ pgViews: opts["pgView"],
146
+ pgViewsExclude: opts["pgViewExclude"],
139
147
  ignoreColumns: opts["ignoreColumn"],
140
148
  includeDeletes: opts["includeDeletes"] ? true : undefined,
141
149
  skipMissingPk: opts["skipMissingPk"] ? true : undefined,
142
150
  output: opts["output"],
143
151
  schemaDiffOutput: opts["schemaDiffOutput"],
144
152
  pgTriggersOutput: opts["pgTriggersOutput"],
153
+ pgViewsOutput: opts["pgViewsOutput"],
145
154
  pretty: opts["pretty"] ? true : undefined,
146
155
  generatePgTriggers: normalizeOptionalBoolean(opts["generatePgTriggers"]),
156
+ generatePgViews: normalizeOptionalBoolean(opts["generatePgViews"]),
147
157
  verbose: opts["verbose"] ? true : undefined,
148
158
  };
149
159
  // Remove undefined values
@@ -173,6 +183,14 @@ async function main() {
173
183
  const requestedPgTriggersExcludeTables = cleanCliArgs.pgTriggersExcludeTables === null
174
184
  ? null
175
185
  : [...resolved.pgTriggersExcludeTables];
186
+ const requestedPgViews = [...resolved.pgViews];
187
+ let requestedPgViewsExclude;
188
+ if (cleanCliArgs.pgViewsExclude === null) {
189
+ requestedPgViewsExclude = null;
190
+ }
191
+ else {
192
+ requestedPgViewsExclude = [...resolved.pgViewsExclude];
193
+ }
176
194
  const requestedIgnoreColumns = cleanCliArgs.ignoreColumns === null ? null : [...resolved.ignoreColumns];
177
195
  const runtimeBaseResolved = (0, resolve_options_1.resolveRuntimeGeneratorOptions)({
178
196
  ...resolved,
@@ -182,6 +200,8 @@ async function main() {
182
200
  schemaDiffExcludeTables: resolved.schemaDiffExcludeTables,
183
201
  pgTriggersTables: requestedPgTriggersTables,
184
202
  pgTriggersExcludeTables: resolved.pgTriggersExcludeTables,
203
+ pgViews: requestedPgViews,
204
+ pgViewsExclude: resolved.pgViewsExclude,
185
205
  ignoreColumns: resolved.ignoreColumns,
186
206
  });
187
207
  // Validate required values
@@ -215,6 +235,8 @@ async function main() {
215
235
  schemaDiffExcludeTables: requestedSchemaDiffExcludeTables,
216
236
  pgTriggersTables: requestedPgTriggersTables,
217
237
  pgTriggersExcludeTables: requestedPgTriggersExcludeTables,
238
+ pgViews: requestedPgViews,
239
+ pgViewsExclude: requestedPgViewsExclude,
218
240
  ignoreColumns: requestedIgnoreColumns,
219
241
  }, (0, resolve_options_1.resolveApplyOptions)(loadedConfig?.apply, {
220
242
  destPgHost: resolved.destPgHost,
@@ -228,6 +250,16 @@ async function main() {
228
250
  if (configExists) {
229
251
  const shouldUpdate = await (0, prompts_1.confirmUpdateConfig)();
230
252
  if (shouldUpdate) {
253
+ const date = new Date();
254
+ const yyyy = date.getFullYear();
255
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
256
+ const dd = String(date.getDate()).padStart(2, "0");
257
+ const hh = String(date.getHours()).padStart(2, "0");
258
+ const min = String(date.getMinutes()).padStart(2, "0");
259
+ const ss = String(date.getSeconds()).padStart(2, "0");
260
+ const backupPath = `${configFilePath}~${yyyy}-${mm}-${dd}-${hh}${min}${ss}`;
261
+ fs.copyFileSync(configFilePath, backupPath);
262
+ console.log(`Backup of previous config saved to: ${backupPath}`);
231
263
  (0, write_config_1.writeConfig)(configFilePath, configToWrite);
232
264
  configWritten = true;
233
265
  configUpdatedThisRun = true;
@@ -304,7 +336,8 @@ async function main() {
304
336
  const expandedTables = resolvedTablePatterns.data;
305
337
  const expandedSchemaDiffTables = resolvedTablePatterns.schema;
306
338
  const expandedPgTriggersTables = resolvedTablePatterns.pgTriggers;
307
- console.log(`Resolved ${expandedTables.tables.length} data table(s), ${expandedSchemaDiffTables.tables.length} schema table(s), and ${expandedPgTriggersTables.tables.length} PostgreSQL trigger table(s).`);
339
+ const expandedPgViews = resolvedTablePatterns.pgViews;
340
+ console.log(`Resolved ${expandedTables.tables.length} data table(s), ${expandedSchemaDiffTables.tables.length} schema table(s), ${expandedPgTriggersTables.tables.length} PostgreSQL trigger table(s), and ${expandedPgViews.views.length} PostgreSQL view(s).`);
308
341
  const runtimeResolved = {
309
342
  ...runtimeBaseResolved,
310
343
  tables: expandedTables.tables,
@@ -313,6 +346,8 @@ async function main() {
313
346
  schemaDiffExcludeTables: expandedSchemaDiffTables.excludedTables,
314
347
  pgTriggersTables: expandedPgTriggersTables.tables,
315
348
  pgTriggersExcludeTables: expandedPgTriggersTables.excludedTables,
349
+ pgViews: expandedPgViews.views,
350
+ pgViewsExclude: expandedPgViews.excludedViews,
316
351
  };
317
352
  const logProgress = (message) => {
318
353
  console.log(message);
@@ -323,9 +358,9 @@ async function main() {
323
358
  }
324
359
  : undefined;
325
360
  // Print resolved plan
326
- printResolvedPlan(resolved, runtimeResolved.tables, runtimeResolved.excludeTables, runtimeResolved.schemaDiffTables, runtimeResolved.schemaDiffExcludeTables, runtimeResolved.pgTriggersTables, runtimeResolved.pgTriggersExcludeTables, sourceConnection, destConnection);
361
+ printResolvedPlan(resolved, runtimeResolved.tables, runtimeResolved.excludeTables, runtimeResolved.schemaDiffTables, runtimeResolved.schemaDiffExcludeTables, runtimeResolved.pgTriggersTables, runtimeResolved.pgTriggersExcludeTables, runtimeResolved.pgViews, runtimeResolved.pgViewsExclude, sourceConnection, destConnection);
327
362
  console.log("Connecting to databases...");
328
- console.log(`Comparing ${runtimeResolved.tables.length} data table(s), ${runtimeResolved.schemaDiffTables.length} schema table(s), and ${runtimeResolved.pgTriggersTables.length} PostgreSQL trigger table(s)...`);
363
+ console.log(`Comparing ${runtimeResolved.tables.length} data table(s), ${runtimeResolved.schemaDiffTables.length} schema table(s), ${runtimeResolved.pgTriggersTables.length} PostgreSQL trigger table(s), and ${runtimeResolved.pgViews.length} PostgreSQL view(s)...`);
329
364
  console.log("Starting data and schema diff generation...");
330
365
  const [diff, schemaDiff] = await Promise.all([
331
366
  (0, generate_diff_1.generateDiff)(sourcePool, destPool, {
@@ -356,6 +391,7 @@ async function main() {
356
391
  const schemaDiffYamlOutputPath = path.resolve((0, write_diff_yaml_1.buildYamlOutputPath)(runtimeResolved.schemaDiffOutput));
357
392
  const schemaDiffSqlOutputPath = path.resolve((0, generate_schema_sql_1.buildSchemaSqlOutputPath)(runtimeResolved.schemaDiffOutput));
358
393
  const pgTriggersOutputPath = path.resolve(runtimeResolved.pgTriggersOutput);
394
+ const pgViewsOutputPath = path.resolve(runtimeResolved.pgViewsOutput);
359
395
  console.log("Writing JSON and YAML diff artifacts...");
360
396
  const dataContent = runtimeResolved.pretty
361
397
  ? JSON.stringify(diff, null, 2) + "\n"
@@ -376,6 +412,7 @@ async function main() {
376
412
  fs.writeFileSync(schemaDiffSqlOutputPath, schemaSqlResult.sql, "utf-8");
377
413
  let sqlGenerated = false;
378
414
  let pgTriggersSqlGenerated = false;
415
+ let pgViewsSqlGenerated = false;
379
416
  console.log("\nData diff summary:");
380
417
  console.log(` Tables compared: ${diff.summary.tablesCompared}`);
381
418
  console.log(` Inserts: ${diff.summary.inserts}`);
@@ -471,6 +508,72 @@ async function main() {
471
508
  pgTriggersSqlGenerated = true;
472
509
  }
473
510
  }
511
+ let shouldGeneratePgViews = false;
512
+ if (runtimeResolved.generatePgViews !== undefined) {
513
+ shouldGeneratePgViews = runtimeResolved.generatePgViews;
514
+ }
515
+ else if (shouldAskInteractiveQuestion(opts["yes"])) {
516
+ shouldGeneratePgViews = await (0, prompts_1.confirmProceed)("\nGenerate a PostgreSQL views diff? (SQL script) [yes]: ", true);
517
+ }
518
+ if (shouldGeneratePgViews) {
519
+ console.log("\nGenerating PostgreSQL views diff...");
520
+ const pgViewsArgs = [];
521
+ if (configWritten) {
522
+ pgViewsArgs.push("--config", path.relative(process.cwd(), configFilePath) ||
523
+ load_config_1.DEFAULT_CONFIG_FILENAME);
524
+ }
525
+ else {
526
+ // Pass all resolved args down manually if no config is available.
527
+ // This is a rare edge case, usually we write the config.
528
+ pgViewsArgs.push("--source-pg-host", resolved.sourcePgHost);
529
+ pgViewsArgs.push("--source-pg-port", String(resolved.sourcePgPort));
530
+ pgViewsArgs.push("--source-pg-database", resolved.sourcePgDatabase);
531
+ pgViewsArgs.push("--source-pg-user", resolved.sourcePgUser);
532
+ if ((0, env_values_1.isEnvReference)(resolved.sourcePgPassword)) {
533
+ pgViewsArgs.push("--source-pg-password-env", resolved.sourcePgPassword);
534
+ }
535
+ if (resolved.sourcePgSsl) {
536
+ pgViewsArgs.push("--source-pg-ssl");
537
+ }
538
+ else {
539
+ pgViewsArgs.push("--no-source-pg-ssl");
540
+ }
541
+ pgViewsArgs.push("--dest-pg-host", resolved.destPgHost);
542
+ pgViewsArgs.push("--dest-pg-port", String(resolved.destPgPort));
543
+ pgViewsArgs.push("--dest-pg-database", resolved.destPgDatabase);
544
+ pgViewsArgs.push("--dest-pg-user", resolved.destPgUser);
545
+ if ((0, env_values_1.isEnvReference)(resolved.destPgPassword)) {
546
+ pgViewsArgs.push("--dest-pg-password-env", resolved.destPgPassword);
547
+ }
548
+ if (resolved.destPgSsl) {
549
+ pgViewsArgs.push("--dest-pg-ssl");
550
+ }
551
+ else {
552
+ pgViewsArgs.push("--no-dest-pg-ssl");
553
+ }
554
+ pgViewsArgs.push("--schema", resolved.schema);
555
+ for (const viewName of runtimeResolved.pgViews) {
556
+ pgViewsArgs.push("--view", viewName);
557
+ }
558
+ for (const viewName of runtimeResolved.pgViewsExclude) {
559
+ pgViewsArgs.push("--exclude-view", viewName);
560
+ }
561
+ pgViewsArgs.push("--output", runtimeResolved.pgViewsOutput);
562
+ }
563
+ if (runtimeResolved.verbose) {
564
+ pgViewsArgs.push("--verbose");
565
+ }
566
+ const pgViewsCommandPath = path.join(__dirname, "pg-views.js");
567
+ const res = (0, child_process_1.spawnSync)(process.execPath, [pgViewsCommandPath, ...pgViewsArgs], {
568
+ stdio: "inherit",
569
+ });
570
+ if (res.status !== 0) {
571
+ console.error("Warning: pg-views diff generation failed.");
572
+ }
573
+ else {
574
+ pgViewsSqlGenerated = true;
575
+ }
576
+ }
474
577
  console.log("\nReview the data diff and schema SQL carefully before applying anything.");
475
578
  if (!sqlGenerated) {
476
579
  console.log("\nGenerate SQL Diff:");
@@ -490,6 +593,9 @@ async function main() {
490
593
  if (pgTriggersSqlGenerated) {
491
594
  console.log(`PostgreSQL triggers SQL written to: ${pgTriggersOutputPath}`);
492
595
  }
596
+ if (pgViewsSqlGenerated) {
597
+ console.log(`PostgreSQL views SQL written to: ${pgViewsOutputPath}`);
598
+ }
493
599
  console.log("");
494
600
  }
495
601
  finally {
@@ -569,10 +675,13 @@ async function resolveGeneratorTablePatterns(sourcePool, destPool, resolved, log
569
675
  destClient = await runLoggedAsyncStep("connecting to destination database for table resolution", () => destPool.connect(), logProgress);
570
676
  const sourceTables = await listTablesWithProgress(sourceClient, resolved.schema, "source", logProgress);
571
677
  const destTables = await listTablesWithProgress(destClient, resolved.schema, "destination", logProgress);
678
+ const sourceViews = await listPgViewsWithProgress(sourceClient, resolved.schema, "source", logProgress);
679
+ const destViews = await listPgViewsWithProgress(destClient, resolved.schema, "destination", logProgress);
572
680
  const data = resolveTablePatternGroup("data", sourceTables, destTables, resolved.tables, resolved.excludeTables, "common", logProgress);
573
681
  const schema = resolveTablePatternGroup("schema diff", sourceTables, destTables, resolved.schemaDiffTables, resolved.schemaDiffExcludeTables, "either", logProgress);
574
682
  const pgTriggers = resolveTablePatternGroup("PostgreSQL trigger", sourceTables, destTables, resolved.pgTriggersTables, resolved.pgTriggersExcludeTables, "common", logProgress);
575
- return { data, schema, pgTriggers };
683
+ const pgViews = resolveViewPatternGroup("PostgreSQL view", sourceViews, destViews, resolved.pgViews, resolved.pgViewsExclude, logProgress);
684
+ return { data, schema, pgTriggers, pgViews };
576
685
  }
577
686
  finally {
578
687
  if (destClient !== undefined) {
@@ -591,6 +700,12 @@ async function listTablesWithProgress(client, schema, databaseLabel, logProgress
591
700
  logProgress(`[table resolution] ${databaseLabel} table preview: ${formatStringList(tables)}`);
592
701
  return tables;
593
702
  }
703
+ async function listPgViewsWithProgress(client, schema, databaseLabel, logProgress) {
704
+ const views = await runLoggedAsyncStep(`scanning ${databaseLabel} views in schema "${schema}"`, () => (0, pg_views_1.listPgViews)(client, schema), logProgress);
705
+ logProgress(`[view resolution] ${databaseLabel} database returned ${views.length} view(s).`);
706
+ logProgress(`[view resolution] ${databaseLabel} view preview: ${formatStringList(views)}`);
707
+ return views;
708
+ }
594
709
  function resolveTablePatternGroup(label, sourceTables, destTables, includePatterns, excludePatterns, availability, logProgress) {
595
710
  const startedAt = Date.now();
596
711
  logProgress(`[table resolution] resolving ${label} patterns against ${formatAvailability(availability)}.`);
@@ -616,6 +731,25 @@ function resolveTablePatternGroup(label, sourceTables, destTables, includePatter
616
731
  throw error;
617
732
  }
618
733
  }
734
+ function resolveViewPatternGroup(label, sourceViews, destViews, includePatterns, excludePatterns, logProgress) {
735
+ const startedAt = Date.now();
736
+ logProgress(`[view resolution] resolving ${label} patterns.`);
737
+ logProgress(`[view resolution] ${label} include patterns: ${formatStringList(includePatterns)}`);
738
+ logProgress(`[view resolution] ${label} exclude patterns: ${formatStringList(excludePatterns)}`);
739
+ try {
740
+ const result = (0, pg_views_1.resolvePgViewPatternsFromViewLists)(sourceViews, destViews, includePatterns, excludePatterns);
741
+ logProgress(`[view resolution] resolved ${label} patterns in ${formatDuration(Date.now() - startedAt)}.`);
742
+ logProgress(`[view resolution] ${label} views: ${formatStringList(result.views)}`);
743
+ if (result.excludedViews.length > 0) {
744
+ logProgress(`[view resolution] ${label} excluded views: ${formatStringList(result.excludedViews)}`);
745
+ }
746
+ return result;
747
+ }
748
+ catch (error) {
749
+ console.error(`[view resolution] failed to resolve ${label} patterns after ${formatDuration(Date.now() - startedAt)}.`);
750
+ throw error;
751
+ }
752
+ }
619
753
  async function runLoggedAsyncStep(label, action, logProgress) {
620
754
  const startedAt = Date.now();
621
755
  logProgress(`[table resolution] ${label}...`);
@@ -676,7 +810,7 @@ function shellEscapeArg(value) {
676
810
  }
677
811
  return `'${value.replace(/'/g, `'\\''`)}'`;
678
812
  }
679
- function printResolvedPlan(resolved, expandedTables, expandedExcludeTables, expandedSchemaDiffTables, expandedSchemaDiffExcludeTables, expandedPgTriggersTables, expandedPgTriggersExcludeTables, sourceConnection, destConnection) {
813
+ function printResolvedPlan(resolved, expandedTables, expandedExcludeTables, expandedSchemaDiffTables, expandedSchemaDiffExcludeTables, expandedPgTriggersTables, expandedPgTriggersExcludeTables, expandedPgViews, expandedPgViewsExclude, sourceConnection, destConnection) {
680
814
  console.log("\ntool:");
681
815
  console.log(" frg-data-diff generate");
682
816
  console.log("\nsource:");
@@ -741,10 +875,22 @@ function printResolvedPlan(resolved, expandedTables, expandedExcludeTables, expa
741
875
  console.log("expanded pg triggers exclude tables: " +
742
876
  expandedPgTriggersExcludeTables.join(", "));
743
877
  }
878
+ console.log("pg views output: " + resolved.pgViewsOutput);
879
+ console.log("pg views: " + resolved.pgViews.join(", "));
880
+ if (expandedPgViews.join(",") !== resolved.pgViews.join(",")) {
881
+ console.log("expanded pg views: " + expandedPgViews.join(", "));
882
+ }
883
+ if (resolved.pgViewsExclude.length > 0) {
884
+ console.log("pg views exclude: " + resolved.pgViewsExclude.join(", "));
885
+ }
886
+ if (expandedPgViewsExclude.join(",") !== resolved.pgViewsExclude.join(",")) {
887
+ console.log("expanded pg views exclude: " + expandedPgViewsExclude.join(", "));
888
+ }
744
889
  console.log("include deletes: " + resolved.includeDeletes);
745
890
  console.log("pretty: " + resolved.pretty);
746
891
  console.log("generate sql: " + (resolved.generateSql ?? "interactive"));
747
892
  console.log("generate pg triggers: " + (resolved.generatePgTriggers ?? "interactive"));
893
+ console.log("generate pg views: " + (resolved.generatePgViews ?? "interactive"));
748
894
  }
749
895
  function hasAnyGeneratorArgs(argv) {
750
896
  const argKeys = [
@@ -770,14 +916,19 @@ function hasAnyGeneratorArgs(argv) {
770
916
  "--schema-diff-exclude-table",
771
917
  "--pg-triggers-table",
772
918
  "--pg-triggers-exclude-table",
919
+ "--pg-view",
920
+ "--pg-view-exclude",
773
921
  "--include-deletes",
774
922
  "--skip-missing-pk",
775
923
  "--output",
776
924
  "--schema-diff-output",
777
925
  "--pg-triggers-output",
926
+ "--pg-views-output",
778
927
  "--pretty",
779
928
  "--generate-pg-triggers",
780
929
  "--no-generate-pg-triggers",
930
+ "--generate-pg-views",
931
+ "--no-generate-pg-views",
781
932
  "--verbose",
782
933
  "--config",
783
934
  "--wizard",
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pg-views.d.ts.map
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const path = __importStar(require("path"));
38
+ const commander_1 = require("commander");
39
+ const load_config_1 = require("../config/load-config");
40
+ const resolve_options_1 = require("../config/resolve-options");
41
+ const connection_1 = require("../db/connection");
42
+ const pg_views_diff_1 = require("../diff/pg-views-diff");
43
+ const program = new commander_1.Command();
44
+ program
45
+ .name("frg-data-diff pg-views")
46
+ .description("Compare PostgreSQL view definitions and write a SQL diff script.")
47
+ .option("--source-pg-host <host>", "Source database host")
48
+ .option("--source-pg-port <port>", "Source database port", (value) => parseInt(value, 10))
49
+ .option("--source-pg-database <db>", "Source database name")
50
+ .option("--source-pg-user <user>", "Source database user")
51
+ .option("--source-pg-password-env <value>", "Source DB password or $ENV_VAR reference")
52
+ .option("--source-pg-ssl", "Use SSL for source database connection")
53
+ .option("--no-source-pg-ssl", "Do not use SSL for source database connection")
54
+ .option("--dest-pg-host <host>", "Destination database host")
55
+ .option("--dest-pg-port <port>", "Destination database port", (value) => parseInt(value, 10))
56
+ .option("--dest-pg-database <db>", "Destination database name")
57
+ .option("--dest-pg-user <user>", "Destination database user")
58
+ .option("--dest-pg-password-env <value>", "Destination DB password or $ENV_VAR reference")
59
+ .option("--dest-pg-ssl", "Use SSL for destination database connection")
60
+ .option("--no-dest-pg-ssl", "Do not use SSL for destination database connection")
61
+ .option("--schema <schema>", "PostgreSQL schema to compare")
62
+ .option("--view <view...>", "View(s) to include")
63
+ .option("--exclude-view <view...>", "View(s) to exclude")
64
+ .option("--output <file>", "Output diff file path", "frg-views-diff.sql")
65
+ .option("--verbose", "Enable verbose logging")
66
+ .option("--config <file>", "Path to config file", load_config_1.DEFAULT_CONFIG_FILENAME);
67
+ program.parse(process.argv);
68
+ const opts = program.opts();
69
+ function normalizeOptionalBoolean(value) {
70
+ if (value === true) {
71
+ return true;
72
+ }
73
+ if (value === false) {
74
+ return false;
75
+ }
76
+ return undefined;
77
+ }
78
+ async function main() {
79
+ const configFilePath = path.resolve(opts["config"] || load_config_1.DEFAULT_CONFIG_FILENAME);
80
+ const configExists = fs.existsSync(configFilePath);
81
+ let generatorConfig = undefined;
82
+ if (configExists) {
83
+ const loadedConfig = (0, load_config_1.loadConfig)(configFilePath);
84
+ generatorConfig = loadedConfig.generator;
85
+ }
86
+ let verbose;
87
+ if (opts["verbose"]) {
88
+ verbose = true;
89
+ }
90
+ const cliArgs = {
91
+ sourcePgHost: opts["sourcePgHost"],
92
+ sourcePgPort: opts["sourcePgPort"],
93
+ sourcePgDatabase: opts["sourcePgDatabase"],
94
+ sourcePgUser: opts["sourcePgUser"],
95
+ sourcePgPassword: opts["sourcePgPasswordEnv"],
96
+ sourcePgSsl: normalizeOptionalBoolean(opts["sourcePgSsl"]),
97
+ destPgHost: opts["destPgHost"],
98
+ destPgPort: opts["destPgPort"],
99
+ destPgDatabase: opts["destPgDatabase"],
100
+ destPgUser: opts["destPgUser"],
101
+ destPgPassword: opts["destPgPasswordEnv"],
102
+ destPgSsl: normalizeOptionalBoolean(opts["destPgSsl"]),
103
+ schema: opts["schema"],
104
+ pgViews: opts["view"],
105
+ pgViewsExclude: opts["excludeView"],
106
+ pgViewsOutput: opts["output"],
107
+ verbose,
108
+ };
109
+ const cleanCliArgs = Object.fromEntries(Object.entries(cliArgs).filter(([, value]) => value !== undefined));
110
+ const resolved = (0, resolve_options_1.resolveGeneratorOptions)(generatorConfig, cleanCliArgs);
111
+ const runtimeResolved = (0, resolve_options_1.resolveRuntimeGeneratorOptions)({
112
+ ...resolved,
113
+ tables: [...resolved.tables],
114
+ excludeTables: [...resolved.excludeTables],
115
+ schemaDiffTables: [...resolved.schemaDiffTables],
116
+ schemaDiffExcludeTables: [...resolved.schemaDiffExcludeTables],
117
+ pgTriggersTables: [...resolved.pgTriggersTables],
118
+ pgTriggersExcludeTables: [...resolved.pgTriggersExcludeTables],
119
+ pgViews: [...resolved.pgViews],
120
+ pgViewsExclude: [...resolved.pgViewsExclude],
121
+ ignoreColumns: [...resolved.ignoreColumns],
122
+ });
123
+ console.log("frg-data-diff: generate pg-views");
124
+ if (!resolved.sourcePgHost || !resolved.destPgHost) {
125
+ console.error("Missing generator configuration. Please run with full arguments or define config.");
126
+ process.exit(1);
127
+ }
128
+ const sourceConnection = (0, connection_1.resolveConnectionParams)({
129
+ host: resolved.sourcePgHost,
130
+ port: resolved.sourcePgPort,
131
+ database: resolved.sourcePgDatabase,
132
+ user: resolved.sourcePgUser,
133
+ password: resolved.sourcePgPassword,
134
+ ssl: runtimeResolved.sourcePgSsl,
135
+ }, {
136
+ host: "source host",
137
+ port: "source port",
138
+ database: "source database",
139
+ user: "source user",
140
+ password: "source password",
141
+ });
142
+ const destConnection = (0, connection_1.resolveConnectionParams)({
143
+ host: resolved.destPgHost,
144
+ port: resolved.destPgPort,
145
+ database: resolved.destPgDatabase,
146
+ user: resolved.destPgUser,
147
+ password: resolved.destPgPassword,
148
+ ssl: runtimeResolved.destPgSsl,
149
+ }, {
150
+ host: "dest host",
151
+ port: "dest port",
152
+ database: "dest database",
153
+ user: "dest user",
154
+ password: "dest password",
155
+ });
156
+ const sourcePool = (0, connection_1.createPool)(sourceConnection);
157
+ const destPool = (0, connection_1.createPool)(destConnection);
158
+ try {
159
+ console.log("Preparing pg-views diff...");
160
+ let onVerboseProgress;
161
+ if (runtimeResolved.verbose) {
162
+ onVerboseProgress = (message) => {
163
+ console.log(message);
164
+ };
165
+ }
166
+ const diffOptions = {
167
+ schema: runtimeResolved.schema,
168
+ views: runtimeResolved.pgViews,
169
+ excludeViews: runtimeResolved.pgViewsExclude,
170
+ verbose: runtimeResolved.verbose,
171
+ onProgress: (message) => {
172
+ console.log(message);
173
+ },
174
+ onVerboseProgress,
175
+ };
176
+ const sql = await (0, pg_views_diff_1.generatePgViewsDiffSql)(sourcePool, destPool, diffOptions);
177
+ const output = runtimeResolved.pgViewsOutput;
178
+ fs.writeFileSync(output, sql, "utf-8");
179
+ console.log(`Wrote views diff SQL to ${output}`);
180
+ }
181
+ catch (error) {
182
+ console.error("Failed to generate pg-views diff.");
183
+ console.error(error.message);
184
+ process.exit(1);
185
+ }
186
+ finally {
187
+ await sourcePool.end();
188
+ await destPool.end();
189
+ }
190
+ }
191
+ main().catch((err) => {
192
+ console.error("Unhandled error:", err);
193
+ process.exit(1);
194
+ });
195
+ //# sourceMappingURL=pg-views.js.map