frg-data-diff 2.0.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.
Files changed (62) hide show
  1. package/README.md +628 -0
  2. package/dist/apply/apply-diff.d.ts +24 -0
  3. package/dist/apply/apply-diff.js +205 -0
  4. package/dist/apply/conflict.d.ts +20 -0
  5. package/dist/apply/conflict.js +14 -0
  6. package/dist/apply/plan.d.ts +13 -0
  7. package/dist/apply/plan.js +37 -0
  8. package/dist/cli/apply.d.ts +8 -0
  9. package/dist/cli/apply.js +270 -0
  10. package/dist/cli/generator.d.ts +9 -0
  11. package/dist/cli/generator.js +804 -0
  12. package/dist/cli/pg-triggers.d.ts +2 -0
  13. package/dist/cli/pg-triggers.js +185 -0
  14. package/dist/cli/root.d.ts +8 -0
  15. package/dist/cli/root.js +231 -0
  16. package/dist/cli/sql.d.ts +9 -0
  17. package/dist/cli/sql.js +158 -0
  18. package/dist/config/config-schema.d.ts +380 -0
  19. package/dist/config/config-schema.js +95 -0
  20. package/dist/config/load-config.d.ts +12 -0
  21. package/dist/config/load-config.js +87 -0
  22. package/dist/config/resolve-options.d.ts +95 -0
  23. package/dist/config/resolve-options.js +183 -0
  24. package/dist/config/write-config.d.ts +18 -0
  25. package/dist/config/write-config.js +103 -0
  26. package/dist/db/connection.d.ts +28 -0
  27. package/dist/db/connection.js +34 -0
  28. package/dist/db/metadata.d.ts +70 -0
  29. package/dist/db/metadata.js +238 -0
  30. package/dist/db/pg-triggers.d.ts +27 -0
  31. package/dist/db/pg-triggers.js +59 -0
  32. package/dist/db/sql.d.ts +72 -0
  33. package/dist/db/sql.js +250 -0
  34. package/dist/diff/diff-schema.d.ts +380 -0
  35. package/dist/diff/diff-schema.js +67 -0
  36. package/dist/diff/generate-diff.d.ts +20 -0
  37. package/dist/diff/generate-diff.js +224 -0
  38. package/dist/diff/pg-triggers-diff.d.ts +11 -0
  39. package/dist/diff/pg-triggers-diff.js +161 -0
  40. package/dist/diff/serialize-value.d.ts +35 -0
  41. package/dist/diff/serialize-value.js +242 -0
  42. package/dist/diff/write-diff-yaml.d.ts +3 -0
  43. package/dist/diff/write-diff-yaml.js +48 -0
  44. package/dist/schema-diff/generate-schema-diff.d.ts +12 -0
  45. package/dist/schema-diff/generate-schema-diff.js +355 -0
  46. package/dist/schema-diff/generate-schema-sql.d.ts +20 -0
  47. package/dist/schema-diff/generate-schema-sql.js +187 -0
  48. package/dist/schema-diff/schema-diff-schema.d.ts +1088 -0
  49. package/dist/schema-diff/schema-diff-schema.js +70 -0
  50. package/dist/shared/env-values.d.ts +11 -0
  51. package/dist/shared/env-values.js +100 -0
  52. package/dist/shared/generator-wizard.d.ts +10 -0
  53. package/dist/shared/generator-wizard.js +337 -0
  54. package/dist/shared/identifiers.d.ts +24 -0
  55. package/dist/shared/identifiers.js +45 -0
  56. package/dist/shared/prompts.d.ts +26 -0
  57. package/dist/shared/prompts.js +104 -0
  58. package/dist/shared/summary.d.ts +33 -0
  59. package/dist/shared/summary.js +41 -0
  60. package/dist/sql/generate-sql.d.ts +19 -0
  61. package/dist/sql/generate-sql.js +223 -0
  62. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,628 @@
1
+ # frg-data-diff
2
+
3
+ > It compares row data between two PostgreSQL databases and safely applies changes.
4
+ >
5
+ > It also produces a review-only schema diff based on `information_schema`, with JSON/YAML artifacts plus SQL for manual execution.
6
+
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
+
9
+ Primary use case: You have a production database copied to development. You change configuration data in development (e.g., Directus rows). You want to publish only those data changes back to production in a reviewable, auditable way.
10
+
11
+ ---
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g frg-data-diff
17
+ ```
18
+
19
+ Or use directly with `npx` (no install required):
20
+
21
+ ```bash
22
+ npx frg-data-diff
23
+ npx frg-data-diff generate
24
+ npx frg-data-diff apply
25
+ npx frg-data-diff sql
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Commands
31
+
32
+ ### `frg-data-diff`
33
+
34
+ Root CLI. Prints usage information when run without a command.
35
+
36
+ ```bash
37
+ npx frg-data-diff
38
+ ```
39
+
40
+ ### `frg-data-diff generate`
41
+
42
+ Compares a source PostgreSQL database against a destination PostgreSQL database and writes data diff plus schema diff files.
43
+
44
+ ```bash
45
+ npx frg-data-diff generate
46
+ ```
47
+
48
+ ### `frg-data-diff apply`
49
+
50
+ Reads a JSON diff file and safely applies it to a destination PostgreSQL database.
51
+
52
+ ```bash
53
+ npx frg-data-diff apply --dry-run
54
+ npx frg-data-diff apply --execute
55
+ ```
56
+
57
+ ### `frg-data-diff sql`
58
+
59
+ Reads a JSON diff file and writes a plain SQL script for manual review and execution.
60
+
61
+ ```bash
62
+ npx frg-data-diff sql --input frg-data-diff.json --output frg-data-diff.sql
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Configuration File: `.frg-data-diff.config.json`
68
+
69
+ Commands look for this file in the current working directory.
70
+
71
+ ### Committing the config file
72
+
73
+ **Do commit** `.frg-data-diff.config.json` to version control.
74
+
75
+ **Do not commit** raw passwords or connection strings.
76
+
77
+ Connection values may be plain text or `$ENV_VAR` references.
78
+
79
+ Passwords may be plain text too, but `$ENV_VAR` is recommended.
80
+
81
+ Generator list fields also support `$ENV_VAR` entries:
82
+
83
+ - `tables`
84
+ - `excludeTables`
85
+ - `schemaDiffTables`
86
+ - `schemaDiffExcludeTables`
87
+ - `pgTriggersTables`
88
+ - `pgTriggersExcludeTables`
89
+ - `ignoreColumns`
90
+
91
+ When used in those list fields, the environment variable value is parsed as a comma-separated list at runtime.
92
+
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`.
94
+
95
+ 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
+
97
+ ### Example config
98
+
99
+ ```json
100
+ {
101
+ "format": "frg-data-diff-config/v1",
102
+ "generator": {
103
+ "sourcePgHost": "dev-db.example.com",
104
+ "sourcePgPort": 5432,
105
+ "sourcePgDatabase": "app",
106
+ "sourcePgUser": "app_user",
107
+ "sourcePgPassword": "$PG_PASSWORD_DEV",
108
+
109
+ "destPgHost": "prod-db.example.com",
110
+ "destPgPort": 5432,
111
+ "destPgDatabase": "app",
112
+ "destPgUser": "app_user",
113
+ "destPgPassword": "$PG_PASSWORD_PROD",
114
+
115
+ "schema": "public",
116
+ "tables": ["my_table"],
117
+ "tablesWhereDataFilters": {},
118
+ "excludeTables": [],
119
+ "schemaDiffTables": ["my_table"],
120
+ "schemaDiffExcludeTables": [],
121
+ "pgTriggersTables": ["my_table"],
122
+ "pgTriggersExcludeTables": [],
123
+ "pgTriggersOutput": "frg-triggers-diff.sql",
124
+ "ignoreColumns": ["created_at", "updated_at"],
125
+ "includeDeletes": true,
126
+ "skipMissingPk": false,
127
+ "output": "frg-data-diff.json",
128
+ "pretty": true
129
+ },
130
+ "apply": {
131
+ "destPgHost": "prod-db.example.com",
132
+ "destPgPort": 5432,
133
+ "destPgDatabase": "app",
134
+ "destPgUser": "app_user",
135
+ "destPgPassword": "$PG_PASSWORD_PROD",
136
+
137
+ "input": "frg-data-diff.json",
138
+ "dryRun": true,
139
+ "applyInserts": true,
140
+ "applyUpdates": true,
141
+ "applyDeletes": false,
142
+ "conflictMode": "abort",
143
+ "insertMode": "strict",
144
+ "transaction": true
145
+ }
146
+ }
147
+ ```
148
+
149
+ ### Row-level data filters
150
+
151
+ `tablesWhereDataFilters` is an optional generator config object. Keys are table names, and values are SQL `WHERE` fragments applied when reading table data for diff generation.
152
+
153
+ The filter is applied equally to source and destination reads. Rows outside the filter are ignored completely and are not treated as missing, so generated data SQL will not insert, update, or delete rows excluded by the filter. This affects data diff only, not schema diff, and is configured only in JSON.
154
+
155
+ Example:
156
+
157
+ ```json
158
+ {
159
+ "generator": {
160
+ "tables": ["directus_presets"],
161
+ "tablesWhereDataFilters": {
162
+ "directus_presets": "\"user\" IS NULL"
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ For Directus, this compares only global/role presets and ignores personal Studio UI presets.
169
+
170
+ ### Environment variable references
171
+
172
+ At runtime, any value stored as `$ENV_VAR` is read from the environment:
173
+
174
+ ```bash
175
+ export PG_PASSWORD_DEV="actual-dev-password"
176
+ export PG_PASSWORD_PROD="actual-prod-password"
177
+ ```
178
+
179
+ If the environment variable is missing:
180
+
181
+ ```
182
+ Missing required environment variable for destination password: PG_PASSWORD_PROD
183
+ ```
184
+
185
+ The actual value is never printed.
186
+
187
+ ---
188
+
189
+ ## No-args behavior
190
+
191
+ When invoked with **no arguments**:
192
+
193
+ 1. Looks for `.frg-data-diff.config.json` in the current directory.
194
+ 2. If not found: prints usage and exits with non-zero code.
195
+ 3. If found: loads and validates config, prints the resolved execution plan, and asks:
196
+
197
+ ```
198
+ Proceed? Type "yes" to continue:
199
+ ```
200
+
201
+ Only the exact string `yes` proceeds. Anything else aborts without modifying anything.
202
+
203
+ ---
204
+
205
+ ## First-run config creation
206
+
207
+ When invoked with CLI arguments and no config file exists:
208
+
209
+ ```
210
+ No .frg-data-diff.config.json file was found.
211
+ Create one from these options? Type "yes" to create:
212
+ ```
213
+
214
+ Only `yes` creates the file. If declined, the operation continues without creating a config.
215
+
216
+ If you choose plain-text passwords, they are written to the config file as plain text.
217
+
218
+ ---
219
+
220
+ ## Confirmation behavior
221
+
222
+ - Interactive: you must type `yes` to proceed.
223
+ - Non-interactive / CI/CD: use `--yes` to skip confirmation.
224
+ - `--yes` does not bypass missing required parameters.
225
+
226
+ ---
227
+
228
+ ## Generator usage
229
+
230
+ ```bash
231
+ npx frg-data-diff generate \
232
+ --source-pg-host dev-db.example.com \
233
+ --source-pg-port 5432 \
234
+ --source-pg-database app \
235
+ --source-pg-user app_user \
236
+ --source-pg-password-env '$PG_PASSWORD_DEV' \
237
+ --dest-pg-host prod-db.example.com \
238
+ --dest-pg-port 5432 \
239
+ --dest-pg-database app \
240
+ --dest-pg-user app_user \
241
+ --dest-pg-password-env '$PG_PASSWORD_PROD' \
242
+ --schema public \
243
+ --table my_table \
244
+ --output frg-data-diff.json \
245
+ --include-deletes \
246
+ --pretty \
247
+ --yes
248
+ ```
249
+
250
+ Key options:
251
+
252
+ | Option | Description |
253
+ | -------------------------- | ----------------------------------------------------- |
254
+ | `--source-pg-password-env` | Source DB password or `$ENV_VAR` reference |
255
+ | `--dest-pg-password-env` | Destination DB password or `$ENV_VAR` reference |
256
+ | `--table` | Table(s) or `*` patterns to include (repeatable) |
257
+ | `--exclude-table` | Table(s) or `*` patterns to skip (repeatable) |
258
+ | `--ignore-column` | Column(s) to ignore during comparison (repeatable) |
259
+ | `--include-deletes` | Generate delete entries for rows only in dest |
260
+ | `--skip-missing-pk` | Skip tables without a primary key instead of failing |
261
+ | `--output` | Output diff file path (default: `frg-data-diff.json`) |
262
+ | `--pretty` | Pretty-print the output JSON |
263
+ | `--yes` | Skip confirmation |
264
+ | `--verbose` | Enable verbose logging |
265
+
266
+ ---
267
+
268
+ ## Apply usage
269
+
270
+ ### Dry-run (default — safe, no DB changes)
271
+
272
+ ```bash
273
+ npx frg-data-diff apply \
274
+ --dest-pg-host prod-db.example.com \
275
+ --dest-pg-port 5432 \
276
+ --dest-pg-database app \
277
+ --dest-pg-user app_user \
278
+ --dest-pg-password-env '$PG_PASSWORD_PROD' \
279
+ --input frg-data-diff.json \
280
+ --dry-run
281
+ ```
282
+
283
+ ### Real execution
284
+
285
+ ```bash
286
+ npx frg-data-diff apply \
287
+ --dest-pg-host prod-db.example.com \
288
+ --dest-pg-database app \
289
+ --dest-pg-user app_user \
290
+ --dest-pg-password-env '$PG_PASSWORD_PROD' \
291
+ --input frg-data-diff.json \
292
+ --execute
293
+ ```
294
+
295
+ Key options:
296
+
297
+ | Option | Description |
298
+ | -------------------- | -------------------------------------------------------- |
299
+ | `--dry-run` | Simulate apply, make no DB changes (default) |
300
+ | `--execute` | Actually apply changes to the DB |
301
+ | `--apply-deletes` | Apply deletes (default: false, requires explicit opt-in) |
302
+ | `--no-apply-deletes` | Disable deletes (default) |
303
+ | `--conflict-mode` | `abort` \| `skip` \| `overwrite` |
304
+ | `--insert-mode` | `strict` \| `upsert` |
305
+ | `--transaction` | Wrap all changes in a transaction (default: true) |
306
+ | `--no-transaction` | Do not use a transaction |
307
+ | `--yes` | Skip confirmation |
308
+ | `--verbose` | Enable verbose logging |
309
+
310
+ ---
311
+
312
+ ## Dry-run behavior
313
+
314
+ `--dry-run` never mutates the destination database. It simulates the apply and prints what would happen.
315
+
316
+ `--execute` is required to make real changes. If both are passed, an error is shown.
317
+
318
+ ---
319
+
320
+ ## JSON Diff Format
321
+
322
+ The diff file (`frg-data-diff.json`) uses a versioned format:
323
+
324
+ ```json
325
+ {
326
+ "format": "postgres-data-diff-json/v1",
327
+ "generatedAt": "2026-05-11T21:00:00.000Z",
328
+ "source": { "schema": "public" },
329
+ "dest": { "schema": "public" },
330
+ "options": {
331
+ "includeDeletes": true,
332
+ "ignoredColumns": ["updated_at"]
333
+ },
334
+ "tables": [
335
+ {
336
+ "schema": "public",
337
+ "table": "example_table",
338
+ "primaryKey": ["id"],
339
+ "updates": [
340
+ {
341
+ "pk": { "id": 1 },
342
+ "changes": {
343
+ "name": { "from": "Old value", "to": "New value" }
344
+ }
345
+ }
346
+ ],
347
+ "inserts": [{ "row": { "id": 2, "name": "Inserted value" } }],
348
+ "deletes": [
349
+ {
350
+ "pk": { "id": 3 },
351
+ "guard": { "id": 3, "name": "Deleted value" }
352
+ }
353
+ ]
354
+ }
355
+ ],
356
+ "summary": {
357
+ "tablesCompared": 1,
358
+ "updates": 1,
359
+ "inserts": 1,
360
+ "deletes": 1,
361
+ "skippedTables": []
362
+ }
363
+ }
364
+ ```
365
+
366
+ The diff file is designed to be **reviewed before applying**.
367
+
368
+ Example list env vars:
369
+
370
+ ```bash
371
+ export DIRECTUS_TABLES="directus_*, custom_table"
372
+ export DIRECTUS_EXCLUDES="directus_activity, directus_sessions"
373
+ export DIRECTUS_PG_TRIGGER_TABLES="directus_flows, directus_operations"
374
+ export DIRECTUS_PG_TRIGGER_EXCLUDES="directus_sessions"
375
+ export DIRECTUS_IGNORED_COLUMNS="created_at, updated_at"
376
+ ```
377
+
378
+ ```json
379
+ {
380
+ "generator": {
381
+ "tables": ["$DIRECTUS_TABLES"],
382
+ "excludeTables": ["$DIRECTUS_EXCLUDES"],
383
+ "pgTriggersTables": ["$DIRECTUS_PG_TRIGGER_TABLES"],
384
+ "pgTriggersExcludeTables": ["$DIRECTUS_PG_TRIGGER_EXCLUDES"],
385
+ "pgTriggersOutput": "frg-triggers-diff.sql",
386
+ "ignoreColumns": ["$DIRECTUS_IGNORED_COLUMNS"]
387
+ }
388
+ }
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Safety Model
394
+
395
+ - **Parameterized queries only** — values are never string-concatenated into SQL.
396
+ - **Identifier quoting** — all table and column names are properly quoted.
397
+ - **Update guards** — updates check that the destination `"from"` value still matches before applying.
398
+ - **Delete guards** — deletes check the full destination row still matches the stored guard before deleting.
399
+ - **applyDeletes defaults to false** — deletes never run without explicit opt-in.
400
+ - **dryRun defaults to true** — no mutations without `--execute`.
401
+ - **Transaction by default** — all changes in a single transaction; rolled back on failure in `abort` mode.
402
+ - **Generated columns are never written**.
403
+ - **Schema is never mutated**.
404
+
405
+ ---
406
+
407
+ ## Conflict Modes
408
+
409
+ | Mode | Behavior |
410
+ | ----------- | --------------------------------------------------------- |
411
+ | `abort` | Roll back transaction on first conflict (default, safest) |
412
+ | `skip` | Skip conflicting row, continue, record in summary |
413
+ | `overwrite` | Force apply changes, ignore from-value guards for updates |
414
+
415
+ Note: `overwrite` does **not** disable guarded deletes. Delete guards always apply.
416
+
417
+ ---
418
+
419
+ ## Delete Behavior
420
+
421
+ 1. Deletes are **generated** only when `includeDeletes: true` in the generator config.
422
+ 2. Deletes are **applied** only when `applyDeletes: true` in the apply config.
423
+ 3. When applied, deletes use a guard check using `IS NOT DISTINCT FROM` semantics.
424
+ 4. If the destination row changed after the diff was generated, the guarded delete will:
425
+ - In `abort` mode: throw and roll back.
426
+ - In `skip` mode: skip the delete, record it in summary.
427
+ - In `overwrite` mode: the delete guard still applies (overwrite only disables update guards).
428
+
429
+ ---
430
+
431
+ ## Directus Example Config
432
+
433
+ For publishing Directus configuration data from development to production:
434
+
435
+ ```json
436
+ {
437
+ "format": "frg-data-diff-config/v1",
438
+ "generator": {
439
+ "sourcePgHost": "dev-db.example.com",
440
+ "sourcePgPort": 5432,
441
+ "sourcePgDatabase": "directus",
442
+ "sourcePgUser": "directus",
443
+ "sourcePgPassword": "PG_PASSWORD_DEV",
444
+
445
+ "destPgHost": "prod-db.example.com",
446
+ "destPgPort": 5432,
447
+ "destPgDatabase": "directus",
448
+ "destPgUser": "directus",
449
+ "destPgPassword": "PG_PASSWORD_PROD",
450
+
451
+ "schema": "public",
452
+ "tables": [
453
+ "directus_collections",
454
+ "directus_fields",
455
+ "directus_relations",
456
+ "directus_permissions",
457
+ "directus_roles",
458
+ "directus_policies",
459
+ "directus_access",
460
+ "directus_settings",
461
+ "directus_flows",
462
+ "directus_operations",
463
+ "directus_dashboards",
464
+ "directus_panels",
465
+ "directus_translations",
466
+ "directus_presets",
467
+ "directus_webhooks"
468
+ ],
469
+ "tablesWhereDataFilters": {
470
+ "directus_presets": "\"user\" IS NULL"
471
+ },
472
+ "excludeTables": [
473
+ "directus_activity",
474
+ "directus_revisions",
475
+ "directus_sessions",
476
+ "directus_migrations",
477
+ "directus_notifications"
478
+ ],
479
+ "pgTriggersTables": ["directus_flows", "directus_operations"],
480
+ "pgTriggersExcludeTables": [],
481
+ "pgTriggersOutput": "frg-triggers-diff.sql",
482
+ "ignoreColumns": ["created_at", "updated_at"],
483
+ "includeDeletes": true,
484
+ "skipMissingPk": false,
485
+ "output": "frg-data-diff.json",
486
+ "pretty": true
487
+ },
488
+ "apply": {
489
+ "destPgHost": "prod-db.example.com",
490
+ "destPgPort": 5432,
491
+ "destPgDatabase": "directus",
492
+ "destPgUser": "directus",
493
+ "destPgPassword": "PG_PASSWORD_PROD",
494
+
495
+ "input": "frg-data-diff.json",
496
+ "dryRun": true,
497
+ "applyInserts": true,
498
+ "applyUpdates": true,
499
+ "applyDeletes": false,
500
+ "conflictMode": "abort",
501
+ "insertMode": "strict",
502
+ "transaction": true
503
+ }
504
+ }
505
+ ```
506
+
507
+ Notes:
508
+
509
+ - Runtime/audit/session tables (`directus_activity`, `directus_revisions`, etc.) are excluded — they should not be overwritten.
510
+ - `tablesWhereDataFilters.directus_presets` ignores personal Directus Studio UI presets while still comparing global/role presets.
511
+ - `includeDeletes: true` generates delete entries, but `applyDeletes: false` means they are never applied unless you explicitly enable them.
512
+ - To apply deletes: `npx frg-data-diff apply --execute --apply-deletes`
513
+
514
+ ---
515
+
516
+ ## Production Workflow
517
+
518
+ ```
519
+ 1. Copy production DB to development.
520
+ 2. Change data/configuration in development.
521
+ 3. Commit .frg-data-diff.config.json to repo.
522
+ 4. Generate diff:
523
+
524
+ npx frg-data-diff generate
525
+
526
+ 5. Review frg-data-diff.json carefully.
527
+ 6. Dry-run apply:
528
+
529
+ npx frg-data-diff apply --dry-run
530
+
531
+ 7. Real apply:
532
+
533
+ npx frg-data-diff apply --execute
534
+ ```
535
+
536
+ If applying deletes:
537
+
538
+ ```bash
539
+ npx frg-data-diff apply --execute --apply-deletes
540
+ ```
541
+
542
+ ---
543
+
544
+ ## What Is Not Supported
545
+
546
+ - Schema diff (table creation, column changes, index changes) — use a dedicated schema migration tool.
547
+ - Cross-schema comparisons within a single call (one schema per run).
548
+ - Tables without primary keys (use `--skip-missing-pk` to skip them).
549
+ - Views, materialized views, foreign tables.
550
+ - Streaming of very large tables — v1 loads each table into memory. For huge tables, process in smaller table batches.
551
+ - PostgreSQL pseudo-types and internal-only types as table columns.
552
+ - Multiple databases in a single run — run the tool once per database pair.
553
+
554
+ ---
555
+
556
+ ## Running Unit Tests
557
+
558
+ ```bash
559
+ npm run test:unit
560
+ ```
561
+
562
+ ---
563
+
564
+ ## Running Integration Tests
565
+
566
+ Integration tests require Docker and Docker Compose.
567
+
568
+ ```bash
569
+ # Start the two PostgreSQL test instances
570
+ docker compose up -d
571
+
572
+ # Wait for them to be healthy, then run
573
+ npm run test:integration
574
+ ```
575
+
576
+ Environment variables (with defaults):
577
+
578
+ ```
579
+ PG_SOURCE_HOST=localhost PG_SOURCE_PORT=15432 PG_SOURCE_DB=testdb PG_SOURCE_USER=testuser PG_SOURCE_PASSWORD=testpassword
580
+ PG_DEST_HOST=localhost PG_DEST_PORT=15433 PG_DEST_DB=testdb PG_DEST_USER=testuser PG_DEST_PASSWORD=testpassword
581
+ ```
582
+
583
+ Stop when done:
584
+
585
+ ```bash
586
+ docker compose down
587
+ ```
588
+
589
+ ---
590
+
591
+ ## Testing `npx` / Package Binary Behavior
592
+
593
+ ```bash
594
+ # Build
595
+ npm run build
596
+
597
+ # Pack locally
598
+ npm pack
599
+
600
+ # Install from pack
601
+ npm install -g ./frg-data-diff-1.1.0.tgz
602
+
603
+ # Test
604
+ frg-data-diff
605
+ frg-data-diff generate --help
606
+ frg-data-diff apply --help
607
+ frg-data-diff sql --help
608
+ ```
609
+
610
+ ---
611
+
612
+ ## Known Limitations
613
+
614
+ 1. **Memory**: All rows for a table are loaded into memory. Not suitable for tables with millions of rows in v1.
615
+ 2. **No streaming**: Pagination is used within the tool, but the result set is buffered.
616
+ 3. **Schema mismatch**: If source and dest have different columns, only common columns are compared.
617
+ 4. **No schema migration**: This tool does not create tables, add columns, or change indexes.
618
+ 5. **Single schema per run**: All tables must be in the same schema.
619
+ 6. **Env var references use shell-style names**: `$ENV_VAR`, `$envVar`, and `$env_var` are all valid.
620
+
621
+ ---
622
+
623
+ ## Building
624
+
625
+ ```bash
626
+ npm run build # Compile TypeScript to dist/
627
+ npm run typecheck # Type-check without emitting
628
+ ```
@@ -0,0 +1,24 @@
1
+ import { type PoolClient } from "pg";
2
+ import { type TableDiff } from "../diff/diff-schema";
3
+ import { type ApplySummary } from "../shared/summary";
4
+ export interface ApplyTableOptions {
5
+ dryRun: boolean;
6
+ applyInserts: boolean;
7
+ applyUpdates: boolean;
8
+ applyDeletes: boolean;
9
+ conflictMode: "abort" | "skip" | "overwrite";
10
+ insertMode: "strict" | "upsert";
11
+ verbose: boolean;
12
+ }
13
+ /**
14
+ * Applies a single table diff to the destination database.
15
+ * Modifies summary in-place.
16
+ */
17
+ export declare function applyTableDiff(client: PoolClient, tableDiff: TableDiff, options: ApplyTableOptions, summary: ApplySummary): Promise<void>;
18
+ /**
19
+ * Thrown when a conflict is detected in abort mode.
20
+ */
21
+ export declare class ConflictError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ //# sourceMappingURL=apply-diff.d.ts.map