1 # pg_fossil CLI Design Specification
8 `pg_fossil` is a unified command-line interface that orchestrates the underlying PostgreSQL tools (`pg_recvlogical`, `pg_dump`, `pg_dumpall`, event triggers) into a cohesive backup system. It follows Unix principles: simple subcommands, composable operations, clear stdout/stderr separation, and scriptable workflows.
12 **Unix Principles Applied:**
13 - **Composability**: Each subcommand can be used independently or in pipelines
14 - **Minimalism**: One subcommand per logical operation
15 - **Discoverability**: Clear help output, sensible defaults, interactive prompts when needed
16 - **Text-based**: All configuration and metadata in plain text files
17 - **Non-interactive by default**: Suitable for cron jobs and scripts
18 - **Exit codes**: 0 for success, non-zero for failure (scriptable)
23 pg_fossil <subcommand> [options]
26 init Initialize backup system for a database
27 collect Collect incremental changes (continuous or one-shot)
28 snapshot Take a full backup snapshot
29 restore Restore database from backups
30 verify Verify backup integrity
31 status Show replication slot and backup status
32 prune Remove old backups according to retention policy
33 emergency Emergency procedures (drop slot, etc.)
36 ## Subcommand Specifications
40 **Purpose**: Bootstrap the backup system for a database.
44 pg_fossil init [options]
47 -d, --dbname=NAME Database name (required)
48 -h, --host=HOST Database host (default: localhost)
49 -p, --port=PORT Database port (default: 5432)
50 -U, --username=USER Database user (default: current user)
51 -s, --slot=NAME Replication slot name (default: pg_fossil_<dbname>)
52 -D, --backup-dir=PATH Backup directory (default: ./pg_fossil_backups)
53 --plugin-path=PATH Path to decoder_raw.so (auto-detected by default)
54 --skip-full-backup Skip initial full backup (for testing)
55 --skip-event-triggers Skip event trigger setup (not recommended)
56 -v, --verbose Verbose output
57 -q, --quiet Minimal output
62 pg_fossil init --dbname=mydb --backup-dir=/var/backups/mydb
66 1. Checks PostgreSQL configuration (`wal_level = logical`, etc.)
67 2. Verifies decoder_raw plugin is installed
68 3. Creates logical replication slot with decoder_raw
69 4. Exports snapshot from slot
70 5. Takes initial base backup using exported snapshot
71 6. Creates `pg_dumpall --globals-only` backup
72 7. Sets up event triggers for DDL capture
73 8. Configures `REPLICA IDENTITY` on tables without primary keys (with warnings)
74 9. Creates backup directory structure
75 10. Writes configuration file: `$BACKUP_DIR/pg_fossil.conf`
76 11. Writes metadata file: `$BACKUP_DIR/metadata.json`
80 pg_fossil: Checking PostgreSQL configuration... OK
81 pg_fossil: Verifying decoder_raw plugin... OK
82 pg_fossil: Creating replication slot 'pg_fossil_mydb'... OK
83 pg_fossil: Exported snapshot: 00000003-00000001-1
84 pg_fossil: Taking initial base backup... OK (2.3 GB)
85 pg_fossil: Backing up global objects... OK
86 pg_fossil: Setting up event triggers... OK
87 pg_fossil: Checking replica identity...
88 pg_fossil: WARNING: Table 'public.logs' has no replica identity
89 pg_fossil: Set to REPLICA IDENTITY FULL
90 pg_fossil: Creating backup directory: /var/backups/mydb
91 pg_fossil: Writing configuration... OK
93 pg_fossil: Initialization complete!
95 pg_fossil: Next steps:
96 pg_fossil: 1. Start collection: pg_fossil collect -d mydb
97 pg_fossil: 2. Monitor status: pg_fossil status -d mydb
98 pg_fossil: 3. Schedule snapshots (cron): 0 2 * * * pg_fossil snapshot -d mydb
103 - 1: PostgreSQL configuration error (wal_level not logical, etc.)
104 - 2: decoder_raw plugin not found
105 - 3: Permission denied
106 - 4: Slot already exists (use --force to recreate)
107 - 5: Disk space insufficient
112 ├── pg_fossil.conf # Configuration (plain text)
113 ├── metadata.json # PostgreSQL version, extensions, encoding
115 │ └── 2024-01-15_base.sql.gz
117 │ └── 2024-01-15_globals.sql
119 │ └── (empty initially)
124 ### `pg_fossil collect`
126 **Purpose**: Collect incremental changes from logical replication slot.
130 pg_fossil collect [options]
133 -d, --dbname=NAME Database name (required, or read from config)
134 -D, --backup-dir=PATH Backup directory (default: read from config)
135 -f, --foreground Run in foreground (default: daemon mode)
136 --stdout Write changes to stdout instead of files
137 --rotate-interval=DURATION Rotate files every DURATION (default: 1h)
138 --rotate-size=SIZE Rotate when file exceeds SIZE (default: 100MB)
139 --fsync-interval=SEC Fsync interval in seconds (default: 10)
140 --start-lsn=LSN Start from specific LSN (for recovery)
141 -v, --verbose Verbose output
142 --no-daemon Synonym for --foreground
145 **Example (daemon mode)**:
147 pg_fossil collect --dbname=mydb
148 # Runs in background, writes PID to backup-dir/pg_fossil_collect.pid
151 **Example (foreground for testing)**:
153 pg_fossil collect --dbname=mydb --foreground --verbose
156 **Example (stdout for piping)**:
158 pg_fossil collect --dbname=mydb --stdout | tee incremental.sql | grep "DELETE FROM users"
162 1. Reads configuration from `pg_fossil.conf`
163 2. Starts `pg_recvlogical` with decoder_raw plugin
164 3. Manages file rotation (time-based or size-based)
165 4. Handles SIGHUP (rotate), SIGTERM (shutdown), SIGINT (shutdown)
166 5. Writes to timestamped files: `incremental/YYYY-MM-DD_HH-MM-SS.sql`
167 6. Updates position tracking metadata
168 7. Monitors replication lag and logs warnings
170 **Output** (stdout in foreground mode):
172 pg_fossil: Starting collection for database 'mydb'
173 pg_fossil: Replication slot: pg_fossil_mydb
174 pg_fossil: Output directory: /var/backups/mydb/incremental
175 pg_fossil: Rotation: every 1h or 100MB
176 pg_fossil: Connected to PostgreSQL, streaming changes...
177 pg_fossil: [2024-01-15 10:30:15] Wrote 1,234 transactions to 2024-01-15_10-00-00.sql
178 pg_fossil: [2024-01-15 11:00:00] Rotating file (time interval reached)
179 pg_fossil: [2024-01-15 11:00:01] New file: 2024-01-15_11-00-00.sql
183 **Output** (daemon mode):
185 pg_fossil: Collection started in background (PID: 12345)
186 pg_fossil: Logs: /var/backups/mydb/logs/pg_fossil_collect.log
187 pg_fossil: Stop with: kill -TERM 12345
191 - **SIGHUP**: Rotate current incremental file
192 - **SIGTERM**: Graceful shutdown (flush current file, update position)
193 - **SIGINT**: Same as SIGTERM
196 - 0: Success (foreground: clean shutdown; daemon: successfully started)
197 - 1: Configuration error
198 - 2: Cannot connect to database
199 - 3: Replication slot not found (run `pg_fossil init` first)
200 - 4: Already running (PID file exists)
202 ### `pg_fossil snapshot`
204 **Purpose**: Take a full backup snapshot (base + globals).
208 pg_fossil snapshot [options]
211 -d, --dbname=NAME Database name (required, or read from config)
212 -D, --backup-dir=PATH Backup directory (default: read from config)
213 --compress=METHOD Compression method: gzip, zstd, none (default: gzip)
214 --parallel=N Parallel dump jobs (pg_dump -j, default: 1)
215 --exclude-schema=PATTERN Exclude schemas matching pattern
216 --exclude-table=PATTERN Exclude tables matching pattern
217 -v, --verbose Verbose output
222 pg_fossil snapshot --dbname=mydb --compress=zstd --parallel=4
226 1. Runs `pg_dump` with specified options
227 2. Runs `pg_dumpall --globals-only`
228 3. Compresses output files
229 4. Updates metadata with snapshot timestamp and LSN
230 5. Calculates and stores SHA256 checksums
234 pg_fossil: Taking snapshot of database 'mydb'
235 pg_fossil: Starting pg_dump (parallel: 4, compression: zstd)...
236 pg_fossil: Progress: [=========> ] 45% (2.1 GB / 4.7 GB)
237 pg_fossil: Snapshot complete: base/2024-01-15_base.sql.zst (1.8 GB compressed)
238 pg_fossil: Backing up global objects...
239 pg_fossil: Globals saved: globals/2024-01-15_globals.sql
240 pg_fossil: Calculating checksums...
241 pg_fossil: SHA256: a3f2e9d8c4b1... (base)
242 pg_fossil: SHA256: 7b4c1e9a2f3d... (globals)
243 pg_fossil: Snapshot complete!
244 pg_fossil: LSN: 0/1A2B3C4D
245 pg_fossil: Size: 1.8 GB (4.7 GB uncompressed)
246 pg_fossil: Duration: 3m 42s
251 - 1: Configuration error
253 - 3: Disk space insufficient
254 - 4: Checksum calculation failed
256 ### `pg_fossil restore`
258 **Purpose**: Restore database from backups.
262 pg_fossil restore [options]
265 -d, --dbname=NAME Target database name (required)
266 -D, --backup-dir=PATH Backup directory (required)
267 --snapshot=TIMESTAMP Restore specific snapshot (default: latest)
268 --until=TIMESTAMP Point-in-time recovery: apply incrementals until this time
269 --target-lsn=LSN Stop at specific LSN (alternative to --until)
270 --no-globals Skip restoring global objects
271 --no-ddl Skip applying DDL changes from event triggers
272 --no-incremental Restore only snapshot (no incremental changes)
273 --target-db=NAME Database name on target (if different from backup)
274 --dry-run Show restore plan without executing
275 --verify Verify checksums before restore
276 -v, --verbose Verbose output
277 -y, --yes Assume yes to prompts (for scripts)
280 **Example (full restore)**:
282 pg_fossil restore --dbname=mydb_restored --backup-dir=/var/backups/mydb --verify
285 **Example (point-in-time recovery)**:
287 pg_fossil restore --dbname=mydb_restored --backup-dir=/var/backups/mydb \
288 --until="2024-01-15 14:30:00"
291 **Example (dry run)**:
293 pg_fossil restore --dbname=mydb_restored --backup-dir=/var/backups/mydb --dry-run
297 1. Finds latest snapshot (or specified snapshot)
298 2. Identifies all incremental files since snapshot
299 3. If `--until` specified, filters incrementals by timestamp
300 4. Verifies checksums if `--verify` enabled
301 5. Prompts for confirmation (unless `--yes`)
302 6. Creates target database if it doesn't exist
303 7. Restores global objects (unless `--no-globals`)
304 8. Restores base snapshot
305 9. Applies DDL changes from `ddl_history` in chronological order (unless `--no-ddl`)
306 10. Applies incremental SQL files in order (unless `--no-incremental`)
307 11. Synchronizes sequences using `setval()`
308 12. Reports success and statistics
312 pg_fossil: Restore plan for database 'mydb'
313 pg_fossil: Source: /var/backups/mydb
314 pg_fossil: Target: mydb_restored
316 pg_fossil: Restore components:
317 pg_fossil: 1. Snapshot: 2024-01-15_base.sql.zst (4.7 GB uncompressed)
318 pg_fossil: 2. Globals: 2024-01-15_globals.sql
319 pg_fossil: 3. Incremental files: 24 files (2024-01-15 to 2024-01-16)
320 pg_fossil: Total transactions: ~45,230
321 pg_fossil: 4. Point-in-time: 2024-01-15 14:30:00
323 pg_fossil: WARNING: This will create/overwrite database 'mydb_restored'
324 pg_fossil: Continue? [y/N]: y
326 pg_fossil: [1/6] Verifying checksums... OK
327 pg_fossil: [2/6] Creating database 'mydb_restored'... OK
328 pg_fossil: [3/6] Restoring global objects... OK
329 pg_fossil: [4/6] Restoring base snapshot...
330 pg_fossil: Progress: [=================> ] 85%
331 pg_fossil: OK (3m 15s)
332 pg_fossil: [5/6] Applying DDL changes... 12 schema changes applied
333 pg_fossil: [6/6] Applying incremental changes...
334 pg_fossil: Processing: 2024-01-15_10-00-00.sql (3,451 transactions)
335 pg_fossil: Processing: 2024-01-15_11-00-00.sql (2,890 transactions)
337 pg_fossil: Stopped at: 2024-01-15 14:30:00 (--until reached)
338 pg_fossil: Applied: 18 of 24 files, 38,442 transactions
339 pg_fossil: Synchronizing sequences... 15 sequences updated
341 pg_fossil: Restore complete!
342 pg_fossil: Database: mydb_restored
343 pg_fossil: Final LSN: 0/1A2B3C4D
344 pg_fossil: Total duration: 8m 32s
346 pg_fossil: Next steps:
347 pg_fossil: 1. Verify data: psql mydb_restored -c "SELECT count(*) FROM users;"
348 pg_fossil: 2. Test application against restored database
349 pg_fossil: 3. When ready, switch application to mydb_restored
354 - 1: Configuration error
355 - 2: Snapshot not found
356 - 3: Restore failed (SQL error during apply)
357 - 4: Checksum verification failed
358 - 5: User cancelled (answered 'no' to prompt)
360 ### `pg_fossil status`
362 **Purpose**: Show replication slot health and backup status.
366 pg_fossil status [options]
369 -d, --dbname=NAME Database name (required, or read from config)
370 -D, --backup-dir=PATH Backup directory (default: read from config)
371 --json Output in JSON format (for monitoring)
372 --check Exit with error if slot lag is critical
373 --warn-lag=SIZE Warning threshold (default: 1GB)
374 --critical-lag=SIZE Critical threshold (default: 10GB)
375 -v, --verbose Show detailed statistics
380 pg_fossil status --dbname=mydb
383 **Example (monitoring script)**:
385 pg_fossil status --dbname=mydb --check --json | jq '.slot.lag_bytes'
389 1. Queries `pg_replication_slots` for slot status
390 2. Calculates replication lag in bytes and estimated time
391 3. Lists recent snapshots and incremental files
392 4. Shows disk usage statistics
393 5. If `--check` enabled, exits with error code if lag exceeds critical threshold
395 **Output** (stdout, human-readable):
397 pg_fossil: Status for database 'mydb'
403 Current LSN: 0/1A2B3C4D
404 Restart LSN: 0/1A2B3000
405 Confirmed Flush: 0/1A2B3C40
406 Lag: 13 MB (estimated 2 minutes behind)
408 Last Activity: 2024-01-15 15:42:33 (2 seconds ago)
410 Backup Directory: /var/backups/mydb
411 Latest Snapshot: 2024-01-15_base.sql.zst (1.8 GB)
413 Incremental Files: 24 files
414 Range: 2024-01-15 02:00 to 2024-01-15 15:42
418 Total backups: 2.3 GB
419 Available space: 847 GB (99% free)
422 Status: running (PID: 12345)
424 Current file: incremental/2024-01-15_15-00-00.sql (23 MB)
429 **Output** (JSON format):
433 "timestamp": "2024-01-15T15:42:35Z",
435 "name": "pg_fossil_mydb",
437 "plugin": "decoder_raw",
438 "current_lsn": "0/1A2B3C4D",
439 "restart_lsn": "0/1A2B3000",
440 "confirmed_flush_lsn": "0/1A2B3C40",
441 "lag_bytes": 13631488,
442 "lag_estimated_seconds": 120,
443 "wal_retained_bytes": 163577856,
444 "last_activity": "2024-01-15T15:42:33Z"
447 "directory": "/var/backups/mydb",
449 "file": "2024-01-15_base.sql.zst",
450 "size_bytes": 1932735283,
451 "timestamp": "2024-01-15T02:00:00Z",
454 "incremental_count": 24,
455 "incremental_size_bytes": 256901120,
456 "total_size_bytes": 2402338816
459 "available_bytes": 909543923712,
465 "uptime_seconds": 49320
472 - 0: Health OK (or not using `--check`)
473 - 1: Warning threshold exceeded
474 - 2: Critical threshold exceeded
476 - 4: Collection process not running
478 ### `pg_fossil verify`
480 **Purpose**: Verify backup integrity and executability.
484 pg_fossil verify [options]
487 -D, --backup-dir=PATH Backup directory (required)
488 --snapshot=TIMESTAMP Verify specific snapshot (default: all)
489 --incremental Verify incremental files
490 --checksums Verify SHA256 checksums
491 --syntax Verify SQL syntax (parse only, no execution)
492 --deep Deep verification (try restoring to temp database)
493 --parallel=N Parallel verification jobs (default: 1)
494 -v, --verbose Verbose output
499 pg_fossil verify --backup-dir=/var/backups/mydb --checksums --syntax
502 **Example (deep verification)**:
504 pg_fossil verify --backup-dir=/var/backups/mydb --deep
508 1. Verifies files exist and are readable
509 2. If `--checksums`: Validates SHA256 checksums
510 3. If `--syntax`: Parses SQL files for syntax errors (using `psql --no-psqlrc --single-transaction < file`)
511 4. If `--deep`: Creates temporary database, performs full restore, verifies row counts
512 5. Reports any corruption or issues
516 pg_fossil: Verifying backups in /var/backups/mydb
519 [OK] 2024-01-15_base.sql.zst (checksum valid, syntax valid)
520 [OK] 2024-01-14_base.sql.zst (checksum valid, syntax valid)
521 [OK] 2024-01-13_base.sql.zst (checksum valid, syntax valid)
524 [OK] 2024-01-15_globals.sql (checksum valid, syntax valid)
525 [OK] 2024-01-14_globals.sql (checksum valid, syntax valid)
527 Incremental files (24 files):
528 [OK] 2024-01-15_10-00-00.sql (syntax valid)
529 [OK] 2024-01-15_11-00-00.sql (syntax valid)
531 [WARN] 2024-01-15_14-00-00.sql (file is empty, may be rotation artifact)
534 Total files checked: 29
543 - 0: All checks passed
545 - 2: Errors found (corruption detected)
546 - 3: Syntax errors in SQL files
548 ### `pg_fossil prune`
550 **Purpose**: Remove old backups according to retention policy.
554 pg_fossil prune [options]
557 -D, --backup-dir=PATH Backup directory (required)
558 --keep-days=N Keep incremental backups for N days (default: 7)
559 --keep-snapshots=N Keep N most recent snapshots (default: 30)
560 --keep-monthly=N Keep one snapshot per month for N months (default: 12)
561 --dry-run Show what would be deleted without deleting
562 --force Skip confirmation prompt
563 -v, --verbose Verbose output
568 pg_fossil prune --backup-dir=/var/backups/mydb --keep-days=7 --keep-snapshots=30
571 **Example (dry run)**:
573 pg_fossil prune --backup-dir=/var/backups/mydb --dry-run
577 1. Identifies backups outside retention policy
578 2. Shows deletion plan
579 3. Prompts for confirmation (unless `--force` or `--dry-run`)
580 4. Deletes old backups
585 pg_fossil: Pruning backups in /var/backups/mydb
589 Snapshots: 30 most recent
593 Snapshots (3 files, 5.4 GB):
594 2023-11-15_base.sql.zst (1.8 GB)
595 2023-10-15_base.sql.zst (1.8 GB)
596 2023-09-15_base.sql.zst (1.8 GB)
598 Incremental (156 files, 2.1 GB):
599 2024-01-01_*.sql through 2024-01-07_*.sql
601 Total: 159 files, 7.5 GB
606 Progress: [==================>] 100% (159/159)
609 Deleted: 159 files (7.5 GB)
610 Remaining: 87 files (12.3 GB)
615 - 1: Configuration error
617 - 3: Deletion failed (partial deletion)
619 ### `pg_fossil emergency`
621 **Purpose**: Emergency procedures for critical situations.
625 pg_fossil emergency <operation> [options]
628 drop-slot Drop replication slot (stops WAL retention)
629 advance-slot Advance slot to current LSN (loses backup coverage)
630 reset Full reset (drop slot, delete backups, start fresh)
633 -d, --dbname=NAME Database name (required)
634 -s, --slot=NAME Replication slot name (default: from config)
635 -D, --backup-dir=PATH Backup directory (for reset operation)
636 --force Skip all confirmations (DANGEROUS)
637 -v, --verbose Verbose output
640 **Example (drop slot)**:
642 pg_fossil emergency drop-slot --dbname=mydb
646 1. Shows impact assessment (WAL will be cleaned, backups may be incomplete)
647 2. Requires typing "YES" to confirm (unless `--force`)
648 3. Executes emergency operation
649 4. Logs action to audit log
653 pg_fossil: EMERGENCY OPERATION: drop-slot
655 WARNING: This will drop the replication slot 'pg_fossil_mydb'
658 - PostgreSQL will immediately start cleaning old WAL files
659 - Disk space will be freed
660 - Incremental backup collection will STOP
661 - Existing backups will remain but may be incomplete
662 - You will need to run 'pg_fossil init' to restart backups
665 WAL retained: 15.3 GB
666 Lag: 12.5 GB (estimated 3 hours behind)
667 Last activity: 3 hours ago
669 This operation CANNOT be undone.
671 Type 'YES' to confirm: YES
673 Dropping replication slot 'pg_fossil_mydb'... OK
675 Emergency operation complete.
676 Replication slot has been dropped.
679 1. Verify disk space freed: df -h
680 2. To resume backups: pg_fossil init --dbname=mydb
681 3. Review incident and prevent recurrence
687 - 2: Operation failed
690 ## Configuration File Format
692 **Location**: `$BACKUP_DIR/pg_fossil.conf`
694 **Format**: Plain text, INI-style (compatible with PostgreSQL config format)
698 # pg_fossil configuration
699 # Generated: 2024-01-15 02:00:00
708 slot_name = pg_fossil_mydb
712 backup_dir = /var/backups/mydb
731 ## Metadata File Format
733 **Location**: `$BACKUP_DIR/metadata.json`
735 **Format**: JSON (machine-readable)
743 "version": "PostgreSQL 18.0 (Debian 18.0-1.pgdg120+1)",
745 "collation": "en_US.UTF-8",
746 "ctype": "en_US.UTF-8"
749 {"name": "plpgsql", "version": "1.0"},
750 {"name": "pg_stat_statements", "version": "1.10"}
753 "slot_name": "pg_fossil_mydb",
754 "plugin": "decoder_raw",
755 "plugin_path": "/usr/lib/postgresql/18/lib/decoder_raw.so"
758 "timestamp": "2024-01-15T02:00:00Z",
759 "snapshot": "00000003-00000001-1",
764 "timestamp": "2024-01-15T02:00:00Z",
765 "file": "base/2024-01-15_base.sql.zst",
766 "size_bytes": 1932735283,
767 "uncompressed_bytes": 4932735283,
769 "sha256": "a3f2e9d8c4b1..."
772 "last_updated": "2024-01-15T15:42:35Z"
776 ## Exit Code Convention
778 All `pg_fossil` commands follow consistent exit code conventions:
782 1 = Configuration/usage error
783 2 = Database connection error
784 3 = Operation failed (specific to command)
785 4 = Resource error (disk space, permissions, etc.)
787 10+ = Command-specific error codes
790 ## Standard Output Conventions
792 **Stdout**: Progress, results, structured data (e.g., JSON with `--json`)
793 **Stderr**: Warnings, errors, diagnostics
795 **Prefix convention**:
796 - `pg_fossil:` for normal messages
797 - `pg_fossil: WARNING:` for warnings
798 - `pg_fossil: ERROR:` for errors
800 **Enables filtering**:
803 pg_fossil status 2>&1 | grep ERROR
805 # Get JSON output without messages
806 pg_fossil status --json 2>/dev/null | jq '.slot.lag_bytes'
809 ## Composability Examples
811 ### Example 1: Monitor lag and alert
814 lag=$(pg_fossil status --dbname=mydb --json | jq '.slot.lag_bytes')
815 if [ "$lag" -gt 1073741824 ]; then # 1GB
816 echo "CRITICAL: Replication lag is $(($lag / 1024 / 1024)) MB" | mail -s "Backup Alert" admin@example.com
820 ### Example 2: Stream changes to monitoring system
822 pg_fossil collect --dbname=mydb --stdout | \
823 grep "DELETE FROM" | \
825 echo "$line" | send-to-monitoring-system
829 ### Example 3: Automated restore testing
832 # Test restore every night to verify backups work
833 pg_fossil restore --dbname=test_restore --backup-dir=/var/backups/mydb \
834 --yes --no-globals 2>&1 | tee /var/log/restore-test.log
836 if [ $? -eq 0 ]; then
837 echo "Restore test PASSED" | mail -s "Backup Verification" admin@example.com
838 psql test_restore -c "DROP DATABASE test_restore;"
840 echo "Restore test FAILED" | mail -s "URGENT: Backup Failure" admin@example.com
844 ### Example 4: Backup rotation in cron
846 # Daily full snapshot at 2 AM
847 0 2 * * * /usr/local/bin/pg_fossil snapshot --dbname=mydb --quiet
849 # Weekly pruning on Sundays at 3 AM
850 0 3 * * 0 /usr/local/bin/pg_fossil prune --backup-dir=/var/backups/mydb --force --quiet
853 0 * * * * /usr/local/bin/pg_fossil status --dbname=mydb --check || echo "Replication lag critical" | mail -s "Alert" admin@example.com
856 ### Example 5: Point-in-time recovery workflow
858 # User reports: "I accidentally deleted customer records at 2:30 PM today"
860 # 1. Find exact timestamp from application logs
861 # 2. Restore to 5 minutes before the incident
862 pg_fossil restore --dbname=mydb_recovery \
863 --backup-dir=/var/backups/mydb \
864 --until="2024-01-15 14:25:00" \
867 # 3. Extract the deleted records
868 psql mydb_recovery -c "COPY (SELECT * FROM customers WHERE deleted_at IS NULL) TO '/tmp/recovered_customers.csv' CSV HEADER;"
870 # 4. Re-insert into production (or compare and merge manually)
873 ## Implementation Notes
875 ### Language and Dependencies
877 **Language**: Python 3.8+ recommended
878 - Good PostgreSQL library support (`psycopg2` or `psycopg3`)
879 - Easy subprocess management for `pg_recvlogical`, `pg_dump`
880 - Rich CLI library ecosystem (`argparse` or `click`)
881 - JSON handling built-in
882 - Cross-platform compatibility
885 - PostgreSQL client utilities (pg_recvlogical, pg_dump, pg_dumpall, psql)
887 - psycopg2 or psycopg3
888 - Standard library only (no exotic dependencies)
893 pip install pg-fossil
895 apt install pg-fossil # Future package
900 - Unit tests for each subcommand
901 - Integration tests with PostgreSQL in Docker
902 - End-to-end restore tests
903 - Crash recovery tests
904 - Signal handling tests
906 ## Quick Start Example
908 For users who want to try the system immediately:
911 # 1. Setup (one time)
912 pg_fossil init --dbname=mydb --backup-dir=/var/backups/mydb
914 # 2. Start collecting changes (run once, continues in background)
915 pg_fossil collect --dbname=mydb
917 # 3. Take daily snapshots (add to cron)
918 pg_fossil snapshot --dbname=mydb
921 pg_fossil status --dbname=mydb
923 # 5. Restore when needed
924 pg_fossil restore --dbname=mydb_restored --backup-dir=/var/backups/mydb
927 ## Unix Philosophy Alignment
929 This CLI design adheres to the Unix philosophy:
931 1. **Do one thing well**: Each subcommand has a focused purpose
932 2. **Text-based**: All input/output in plain text (SQL, JSON, INI)
933 3. **Composable**: Commands can be piped and scripted
934 4. **Non-interactive by default**: Suitable for automation
935 5. **Clear separation**: stdout for data, stderr for diagnostics
936 6. **Orthogonal**: Subcommands don't overlap in functionality
937 7. **Discoverable**: Clear help, sensible defaults, progress messages
938 8. **Scriptable**: Exit codes, JSON output, quiet modes
942 ### Why a unified CLI?
944 The underlying PostgreSQL tools (`pg_recvlogical`, `pg_dump`, etc.) are powerful but require careful orchestration. A unified CLI:
946 1. **Reduces complexity**: Users learn one tool instead of five
947 2. **Ensures correctness**: Orchestration logic is tested and reliable
948 3. **Improves discoverability**: `pg_fossil --help` shows all capabilities
949 4. **Enables best practices**: Built-in monitoring, verification, rotation
950 5. **Maintains composability**: Each subcommand still usable independently
952 ### Why text-based config?
954 - Human-readable and editable
955 - Version control friendly
956 - Compatible with PostgreSQL conventions
957 - No binary parsing dependencies
959 ### Why both INI and JSON?
961 - **INI** (`pg_fossil.conf`): Human-editable configuration
962 - **JSON** (`metadata.json`): Machine-readable metadata for tooling
966 Potential future enhancements (not in v1.0):
968 1. **Remote backups**: `pg_fossil collect --remote=s3://bucket/path`
969 2. **Encryption**: `pg_fossil snapshot --encrypt=gpg-key-id`
970 3. **Compression streaming**: `pg_fossil collect --compress=zstd`
971 4. **Web UI**: `pg_fossil serve --port=8080` (view status, trigger restores)
972 5. **Plugin system**: Allow custom hooks for monitoring, alerting
973 6. **Multi-database**: `pg_fossil init --all-databases`
974 7. **Differential snapshots**: Faster snapshots between full dumps
978 - [The Art of Unix Programming](http://www.catb.org/~esr/writings/taoup/) by Eric S. Raymond
979 - [PostgreSQL Logical Replication Documentation](https://www.postgresql.org/docs/current/logical-replication.html)
980 - [decoder_raw plugin](https://github.com/michaelpq/pg_plugins/tree/main/decoder_raw)
981 - [12 Factor CLI Apps](https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46)