Skip to content

fix(backup): close hollow-gate path where RestoreDrill.Success ignored zero rows - #414

Merged
acamarata merged 2 commits into
mainfrom
fix/backup-drill-zero-row-success
Sep 12, 2026
Merged

acamarata merged 2 commits into
mainfrom
fix/backup-drill-zero-row-success

Conversation

@acamarata

Copy link
Copy Markdown
Collaborator

Summary

  • `RestoreDrill()` set `RestoreDrillResult.Success = true` unconditionally, never checking `RowsVerified`. Only the separate `Drill()`/`smokeCheck` wrapper enforced the zero-row hard-fail (added in bddfc1c). Anything calling `RestoreDrill()` directly — chiefly `nself db restore-drill` (`cmd/commands/db_pitr_ops.go`) — could restore genuinely nothing on a multi-schema stack and still print "Drill status: PASS" / "Rows sampled: 0". This is the same hollow-gate shape proven live on staging 2026-08-31, one call-frame lower than where it was previously fixed.
  • Moved the smoke-gate assertion into RestoreDrill() itself so Success=true is now structurally impossible without RowsVerified > 0 and the table-count floor, regardless of caller.
  • Added Docker/Postgres integration tests reproducing a multi-schema fixture (tables split across public + a second schema, matching nself-web's shape): one proving real multi-schema rows are counted correctly, one proving a zero-row restore can never report success (fails against pre-fix code, passes against the fix).
  • Same hollow-gate shape found and fixed elsewhere on the backup path: verify.go's restore-test gate query silently continued past its own error instead of failing; create.go derived a docker container name from unset PROJECT_NAME/POSTGRES_DB instead of naming the missing variable; BACKUP_ACCESS_KEY/BACKUP_SECRET_KEY (used by ntask/backend/.env.example) were recognized as known env vars but never read into config, silently disabling remote backup upload.
  • Two files split to stay under the 300-line file-size cap after the additions (pure moves).

Test plan

  • go build ./...
  • go vet ./...
  • gofmt -l clean
  • golangci-lint run clean on touched packages
  • go test ./... — full suite green, including internal/repoqa (file-size + gofmt gates)
  • INTEGRATION=1 go test -tags integration ./internal/database/... -run TestRestoreDrill_Integration — both new tests pass against the fix
  • Confirmed the zero-row regression test fails against the pre-fix code (reverted the fix locally, reran: Success = true for tables=8 rows=0) and passes against the fix
  • New unit tests for the S3 credential alias/half-config guard and the PROJECT_NAME/POSTGRES_DB guard (no Docker required)
  • Never ran anything against nself-prod; all reproduction is against local Docker Postgres fixtures

…d zero rows

RestoreDrill() unconditionally set RestoreDrillResult.Success = true once
restore/verify/critical-table steps returned no Go error, never consulting
RowsVerified itself — only the separate Drill()/smokeCheck wrapper enforced
the zero-row hard-fail (added in bddfc1c). Anything calling RestoreDrill()
directly, chiefly `nself db restore-drill` (cmd/commands/db_pitr_ops.go),
could restore genuinely nothing on a multi-schema stack and still print
"Drill status: PASS" with "Rows sampled: 0" — the same hollow-gate shape
proven live on staging 2026-08-31, one call-frame lower.

Move the smoke-gate assertion (smokeCheck, already covered by
TestSmokeCheck_ZeroRowsFails et al.) into RestoreDrill() itself so
Success=true is impossible to produce without RowsVerified > 0 and the
table-count floor, regardless of caller. Drill()'s own smokeCheck call
stays as a second, now largely redundant, layer.

Added multi-schema Docker/Postgres integration tests
(restore_drill_integration_test.go) reproducing nself-web's shape — tables
split across `public` + a second schema:
  - MultiSchemaRowsCounted: real rows in both schemas restore and count
    correctly (RowsVerified sums across every non-system schema, not just
    public).
  - ZeroRowsNeverSucceeds: same table count, zero rows in all of them —
    proven to fail against the pre-fix code (Success=true, tables=8 rows=0)
    and pass against the fix.

Also, same hollow-gate shape found elsewhere on the backup path:
  - internal/backup/verify.go: the restore-test's user-table gate query
    silently continued past its own psql/docker error instead of failing —
    only a returned "0" was treated as failure, an error running the check
    at all was not. Now a hard fail for the gate query specifically.
  - internal/backup/create.go: `nself backup create` derived a docker
    container name from PROJECT_NAME/POSTGRES_DB without checking they were
    actually configured (both silently default via ApplyDefaults), so an
    unset .env produced a confusing "no such container" error instead of
    naming the missing variable. Added requireBackupContainerConfig().
  - internal/config: BACKUP_ACCESS_KEY/BACKUP_SECRET_KEY (used verbatim by
    ntask/backend/.env.example) were listed as "known" env vars but never
    read into BackupConfig — only BACKUP_S3_ACCESS_KEY_ID/
    BACKUP_S3_SECRET_ACCESS_KEY were. Remote backup upload was silently
    non-functional. Both names now accepted (canonical wins if both set),
    wired into the actual rclone upload's environment, and
    requireCompleteS3Credentials() refuses a half-configured pair instead of
    silently proceeding.

internal/database/restore_drill.go and internal/backup/create_targets.go
split (restore_drill_verify.go, create_remote.go) to stay under the
300-line file cap after the above additions — pure moves, no behavior
change beyond what's described above.
@acamarata
acamarata enabled auto-merge (squash) September 12, 2026 14:39
The doc-sync gate flagged this PR for changing env vars without updating
the env-var inventory, and it was right: BACKUP_ACCESS_KEY and
BACKUP_SECRET_KEY are now read as aliases for BACKUP_S3_ACCESS_KEY_ID and
BACKUP_S3_SECRET_ACCESS_KEY, and none of the BACKUP_S3_* family was
documented at all.

Adds a Backup and Restore section covering the whole family, the alias
precedence (canonical wins when both are set), why the aliases exist, and
the half-configured-pair rejection. Notes that both-empty is valid because
the remote may be configured via rclone.conf, and that BACKUP_S3_BUCKET
and BACKUP_S3_PREFIX are app-level rather than CLI config-struct fields.
@acamarata
acamarata merged commit faa6cff into main Sep 12, 2026
31 checks passed
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