Skip to content

fix(backup): prevent stale and incomplete database restores - #23

Open
MahdiButcher wants to merge 4 commits into
PasarGuard:mainfrom
MahdiButcher:fix/sqlite-backup-stale-restore
Open

fix(backup): prevent stale and incomplete database restores#23
MahdiButcher wants to merge 4 commits into
PasarGuard:mainfrom
MahdiButcher:fix/sqlite-backup-stale-restore

Conversation

@MahdiButcher

@MahdiButcher MahdiButcher commented Aug 1, 2026

Copy link
Copy Markdown

Summary

  • generate canonical four-slash absolute SQLite URLs while accepting legacy five-slash URLs
  • require a consistent, integrity-checked SQLite snapshot and keep live database/WAL/SHM files out of raw data copies
  • restore the authoritative SQLite snapshot after the data directory so legacy archives cannot overwrite it
  • bind Telegram uploads to artifacts created by the current backup run
  • make PostgreSQL/TimescaleDB multi-database dumps atomic and require the configured application database
  • reject incomplete/truncated SQLite, MySQL, MariaDB, PostgreSQL, and TimescaleDB artifacts before archiving or restoring
  • verify the configured MySQL/MariaDB database exists before accepting an all-database dump

Root causes

  • Legacy five-slash SQLite URLs parsed as double-slash paths, breaking raw-data exclusions; restore then overwrote the consistent snapshot with an older checkpoint and removed its WAL.
  • PostgreSQL/TimescaleDB multi-database backup returned success when any database dumped, even if the configured PasarGuard database failed or was omitted.
  • SQL dump validation accepted non-empty but interrupted output, and Telegram discovery was not scoped to the current backup timestamp.

Tests

  • all shell unit suites (312 assertions)
  • script update-safety suite
  • live WAL-mode SQLite single-archive round trip
  • live WAL-mode SQLite multipart round trip
  • PostgreSQL/TimescaleDB partial-dump and missing-app-database failure simulations
  • syntax and whitespace checks

The existing CI matrix covers SQLite, MySQL, MariaDB, PostgreSQL, and TimescaleDB in single and multipart modes; fork PR workflows require maintainer approval.

Summary by CodeRabbit

  • Bug Fixes

    • Improved backup and restore reliability for PostgreSQL, TimescaleDB, MySQL/MariaDB, and SQLite.
    • Prevented incomplete or non-restorable backups from being archived or restored.
    • Added safer cleanup for failed archives, split files, and temporary SQLite data.
    • Improved handling of active SQLite WAL databases and backup timestamps.
  • Improvements

    • Enhanced SQLite URL support for relative, absolute, and legacy formats.
    • Expanded archive validation to ensure only expected files are included.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds shared SQLite URL helpers and strengthens backup and restore validation. Database artifacts must be complete and restorable before archive creation or destructive restore operations. Failed backups clean up generated artifacts, and tests cover SQLite WAL snapshots and truncated dumps.

Changes

Backup and restore validation

Layer / File(s) Summary
SQLite URL and path helpers
lib/common.sh, pasarguard.sh, lib/pasarguard-backup.sh, lib/pasarguard-restore.sh, tests/unit_lib_common.sh
Added POSIX path normalization, SQLite URL parsing, validated absolute URL generation, and helper integration.
Backup completeness and archive flow
lib/pasarguard-backup.sh
Added restorability checks for PostgreSQL, TimescaleDB, MySQL/MariaDB, and SQLite artifacts. Multi-database backups require complete dumps and the configured database. Failed archives and split parts are removed.
Restore safety and deferred SQLite restore
lib/pasarguard-restore.sh
Rejects incomplete SQL and PostgreSQL artifacts before destructive operations. Validates SQLite snapshots and copies them after data-directory restoration.
Validation coverage
tests/backup_restore_roundtrip.sh, tests/unit_pasarguard.sh, tests/unit_restore_archive_safety.sh
Added coverage for WAL snapshots, exact archive manifests, truncated dumps, missing databases, SQLite integrity, and failure cleanup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: t3st3st3r0n, immohammad20000, m03ed

Sequence Diagram(s)

sequenceDiagram
  participant Backup
  participant Validator
  participant Archive
  participant Restore
  participant SQLiteDatabase
  Backup->>Validator: validate database artifacts
  Validator-->>Backup: return validation result
  Backup->>Archive: create archive when valid
  Restore->>Validator: validate selected backup
  Validator-->>Restore: return validation result
  Restore->>SQLiteDatabase: copy validated snapshot after data restore
Loading

Poem

A rabbit checks each dump at night,
WAL values stay in proper flight.
Complete files pass the gate,
Truncated ones must wait.
Clean archives hop away,
Safe restores start the day.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes that prevent stale and incomplete database backup restores.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/common.sh`:
- Around line 47-49: Update the path-normalization logic around the path
variable to remove all trailing slashes while preserving the root path "/"
unchanged. Ensure values such as DATA_DIR ending with multiple slashes normalize
to a single canonical non-root path without a trailing slash, so subsequent
backup-prefix checks behave correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d35b7964-db7a-4d32-a5df-95acc185c17a

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa4eac and e26bd71.

📒 Files selected for processing (6)
  • lib/common.sh
  • lib/pasarguard-backup.sh
  • lib/pasarguard-restore.sh
  • pasarguard.sh
  • tests/backup_restore_roundtrip.sh
  • tests/unit_lib_common.sh

Comment thread lib/common.sh Outdated
@MahdiButcher
MahdiButcher force-pushed the fix/sqlite-backup-stale-restore branch from e26bd71 to f075feb Compare August 1, 2026 08:50
@MahdiButcher MahdiButcher changed the title fix(backup): prevent stale SQLite restores fix(backup): prevent stale and incomplete database restores Aug 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
lib/pasarguard-restore.sh (1)

1188-1191: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Also remove a stale -journal sidecar.

This block removes -wal and -shm only. If the previous database used rollback-journal mode and a hot ${sqlite_file}-journal file remains, SQLite treats it as a journal for the newly copied file and attempts a rollback on the next open. Remove it together with the other sidecars.

🛡️ Proposed fix
-        rm -f "${sqlite_file}-wal" "${sqlite_file}-shm" 2>>"$log_file" || true
+        rm -f "${sqlite_file}-wal" "${sqlite_file}-shm" "${sqlite_file}-journal" 2>>"$log_file" || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pasarguard-restore.sh` around lines 1188 - 1191, Update the sqlite
restore cleanup block to remove the stale "${sqlite_file}-journal" sidecar
alongside the existing -wal and -shm files before copying the backup, preserving
the current logging and failure-tolerant behavior.
tests/unit_pasarguard.sh (1)

286-294: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a manifest case with an empty ts_version.

pg_manifest_encode emits a trailing tab when ts_version is empty, which is the normal case for a non-TimescaleDB database. postgres_backup_looks_restorable counts fields with awk -F '\t' '{ print NF }' and requires exactly 5. Only the populated-ts_version shape is covered here. A case with has_ts=0 and an empty ts_version would lock in the field-count behavior for the common layout.

💚 Proposed additional assertion
 assert_false "backup validation: configured PostgreSQL database must be present" \
     database_backup_looks_restorable postgresql "$DB_VALIDATION_DIR" missingdb ""
+
+printf '%s\n' "$(pg_manifest_encode appdb appuser 0 db-001.sql '')" >"$DB_VALIDATION_DIR/pg_dump/manifest.tsv"
+assert_true "backup validation: manifest with empty ts_version accepted" \
+    database_backup_looks_restorable postgresql "$DB_VALIDATION_DIR" appdb ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit_pasarguard.sh` around lines 286 - 294, Extend the manifest
validation tests near the existing pg_manifest_encode case to add a
non-TimescaleDB entry using has_ts=0 and an empty ts_version. Use the resulting
manifest with database_backup_looks_restorable and assert it is accepted,
covering the trailing-tab five-field layout.
lib/pasarguard-backup.sh (2)

1014-1016: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale comment above this branch.

The comment at Lines 1007-1009 still states that an undetermined owner is recorded as empty and that restore falls back to the admin role. This branch now aborts the multi-database backup instead. Align the comment with the new behavior.

♻️ Proposed comment update
-        # Owner of this database (empty if it can't be determined; restore
-        # falls back to the admin role).
+        # Owner of this database. An undetermined owner means the manifest would
+        # be incomplete, so abort the multi-DB backup and let the caller fall
+        # back to a validated single-database dump.
         local owner=""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pasarguard-backup.sh` around lines 1014 - 1016, Update the comment
immediately above the owner-determination failure branch to state that an
undetermined database owner causes the multi-database backup to abort and the
output directory to be removed, rather than describing empty-owner recording or
restore fallback behavior. Keep the branch logic unchanged.

1352-1363: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Skip empty lines when you match the configured database.

A here-string of an empty variable still produces one empty line. If $db_name is empty, [ "$_db_line" = "$db_name" ] matches that empty line and configured_db_found becomes true. The root --all-databases dump then runs without any real presence check. The fallback branch at Line 1371 already treats an empty $db_name as possible, so add the guard here too.

🛡️ Proposed guard
                     while IFS= read -r _db_line; do
+                        [ -n "$_db_line" ] || continue
                         if [ "$_db_line" = "$db_name" ]; then
                             configured_db_found=true
                             break
                         fi
                     done <<<"$mariadb_databases"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pasarguard-backup.sh` around lines 1352 - 1363, Update the
database-matching loop around mariadb_databases and configured_db_found to skip
empty lines and prevent an empty db_name from matching the here-string’s
synthetic empty line. Require a non-empty db_name and non-empty _db_line before
setting configured_db_found=true, preserving the existing match and dump
behavior for valid database names.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/pasarguard-backup.sh`:
- Around line 1679-1689: Move the SQLite completeness check in the database
backup flow to immediately before archive creation, after the rsync copy into
temp_dir has completed. Ensure database_backup_looks_restorable validates the
final $sqlite_file artifact that will be included in the archive, preventing an
APP_DIR file with the same basename from replacing the snapshot after
validation.

In `@lib/pasarguard-restore.sh`:
- Around line 708-709: The callers of sqlite_database_path_from_url do not
validate its result. In lib/pasarguard-restore.sh lines 708-709, check both the
command status and sqlite_file emptiness; on failure, remove temp_restore_dir,
print a clear message, and exit 1. In lib/pasarguard-backup.sh lines 1250-1251,
perform the same validation, append a diagnostic to log_file, and add it to
error_messages so the backup fails clearly.
- Around line 843-858: Update the SQLite restore block to handle a missing
sqlite3 executable separately from an invalid snapshot: after the best-effort
try_install_package call, recheck command -v sqlite3 and report the missing
dependency with the existing cleanup/abort flow before invoking
sqlite_snapshot_looks_restorable. Also move the safety copy created for
sqlite_file outside DATA_DIR, using the established DATA_DIR backup location or
an equivalent path that survives the later rsync --delete before the snapshot is
applied.

---

Nitpick comments:
In `@lib/pasarguard-backup.sh`:
- Around line 1014-1016: Update the comment immediately above the
owner-determination failure branch to state that an undetermined database owner
causes the multi-database backup to abort and the output directory to be
removed, rather than describing empty-owner recording or restore fallback
behavior. Keep the branch logic unchanged.
- Around line 1352-1363: Update the database-matching loop around
mariadb_databases and configured_db_found to skip empty lines and prevent an
empty db_name from matching the here-string’s synthetic empty line. Require a
non-empty db_name and non-empty _db_line before setting
configured_db_found=true, preserving the existing match and dump behavior for
valid database names.

In `@lib/pasarguard-restore.sh`:
- Around line 1188-1191: Update the sqlite restore cleanup block to remove the
stale "${sqlite_file}-journal" sidecar alongside the existing -wal and -shm
files before copying the backup, preserving the current logging and
failure-tolerant behavior.

In `@tests/unit_pasarguard.sh`:
- Around line 286-294: Extend the manifest validation tests near the existing
pg_manifest_encode case to add a non-TimescaleDB entry using has_ts=0 and an
empty ts_version. Use the resulting manifest with
database_backup_looks_restorable and assert it is accepted, covering the
trailing-tab five-field layout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 65480e73-9e3c-4832-9ab6-ada5b02c77f4

📥 Commits

Reviewing files that changed from the base of the PR and between e26bd71 and 76df1a9.

📒 Files selected for processing (8)
  • lib/common.sh
  • lib/pasarguard-backup.sh
  • lib/pasarguard-restore.sh
  • pasarguard.sh
  • tests/backup_restore_roundtrip.sh
  • tests/unit_lib_common.sh
  • tests/unit_pasarguard.sh
  • tests/unit_restore_archive_safety.sh
🚧 Files skipped from review as they are similar to previous changes (4)
  • pasarguard.sh
  • tests/unit_lib_common.sh
  • lib/common.sh
  • tests/backup_restore_roundtrip.sh

Comment thread lib/pasarguard-backup.sh Outdated
Comment thread lib/pasarguard-restore.sh Outdated
Comment thread lib/pasarguard-restore.sh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant