#!/usr/bin/env bash set -euo pipefail # Restores a Teleo SQLite backup into a disposable local Postgres container and # compares source/target table row counts. It does not touch GCP or production. SQLITE_BACKUP="${SQLITE_BACKUP:-}" POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:16-alpine}" OUTPUT_ROOT="${OUTPUT_ROOT:-./outputs/gcp-infra-hardening-20260707/private-restore-canaries}" PROOF_ROOT="${PROOF_ROOT:-./outputs/gcp-infra-hardening-20260707/proofs}" TS="${TS:-$(date -u +%Y%m%dT%H%M%SZ)}" SCHEMA="${SCHEMA:-teleo_restore}" CONTAINER_NAME="${CONTAINER_NAME:-teleo-pg-restore-canary-${TS}}" if [[ -z "$SQLITE_BACKUP" ]]; then echo "SQLITE_BACKUP is required, for example SQLITE_BACKUP=outputs/.../teleo-pipeline-sqlite-...db.gz" >&2 exit 2 fi if [[ ! -f "$SQLITE_BACKUP" ]]; then echo "SQLite backup not found: $SQLITE_BACKUP" >&2 exit 2 fi mkdir -p "$OUTPUT_ROOT" "$PROOF_ROOT" chmod 700 "$OUTPUT_ROOT" WORK_DIR="$OUTPUT_ROOT/sqlite-postgres-restore-canary-${TS}" mkdir -p "$WORK_DIR" chmod 700 "$WORK_DIR" IMPORT_SQL="$WORK_DIR/teleo-pipeline-postgres-import.sql" SOURCE_SUMMARY="$WORK_DIR/source-summary.json" TARGET_COUNTS_SQL="$WORK_DIR/target-counts.sql" TARGET_COUNTS_CSV="$WORK_DIR/target-counts.csv" IMPORT_LOG="$WORK_DIR/postgres-import.log" PROOF_JSON="$PROOF_ROOT/sqlite-postgres-restore-canary-${TS}.json" cleanup() { if [[ "${KEEP_CONTAINER:-0}" != "1" ]]; then docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true fi } trap cleanup EXIT python3 ops/sqlite_to_postgres_dump.py \ --sqlite-backup "$SQLITE_BACKUP" \ --output-sql "$IMPORT_SQL" \ --summary-json "$SOURCE_SUMMARY" \ --schema "$SCHEMA" \ --target-counts-sql "$TARGET_COUNTS_SQL" docker run --rm -d \ --name "$CONTAINER_NAME" \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=teleo_restore_canary \ "$POSTGRES_IMAGE" >/dev/null for _ in $(seq 1 60); do if docker exec "$CONTAINER_NAME" pg_isready -U postgres -d teleo_restore_canary >/dev/null 2>&1; then break fi sleep 1 done docker exec "$CONTAINER_NAME" pg_isready -U postgres -d teleo_restore_canary >/dev/null docker exec -i "$CONTAINER_NAME" \ psql -v ON_ERROR_STOP=1 -U postgres -d teleo_restore_canary \ < "$IMPORT_SQL" > "$IMPORT_LOG" docker exec -i "$CONTAINER_NAME" \ psql -v ON_ERROR_STOP=1 -U postgres -d teleo_restore_canary \ < "$TARGET_COUNTS_SQL" > "$TARGET_COUNTS_CSV" python3 - "$SOURCE_SUMMARY" "$TARGET_COUNTS_CSV" "$PROOF_JSON" "$SQLITE_BACKUP" "$IMPORT_SQL" "$POSTGRES_IMAGE" <<'PY' import csv import hashlib import json import sys from datetime import UTC, datetime from pathlib import Path source_summary_path = Path(sys.argv[1]) target_counts_path = Path(sys.argv[2]) proof_path = Path(sys.argv[3]) sqlite_backup = Path(sys.argv[4]) import_sql = Path(sys.argv[5]) postgres_image = sys.argv[6] source_summary = json.loads(source_summary_path.read_text()) source_counts = {table["name"]: int(table["row_count"]) for table in source_summary["tables"]} with target_counts_path.open(newline="") as handle: target_counts = {row["table_name"]: int(row["row_count"]) for row in csv.DictReader(handle)} all_tables = sorted(set(source_counts) | set(target_counts)) mismatches = [ { "table": table, "source_rows": source_counts.get(table), "target_rows": target_counts.get(table), } for table in all_tables if source_counts.get(table) != target_counts.get(table) ] def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() proof = { "artifact": "sqlite_postgres_restore_canary", "generated_at_utc": datetime.now(UTC).isoformat(), "status": "pass" if not mismatches and source_summary["source_integrity_check"] == "ok" else "fail", "source_sqlite_backup": str(sqlite_backup), "source_sqlite_backup_sha256": sha256(sqlite_backup), "postgres_image": postgres_image, "source_integrity_check": source_summary["source_integrity_check"], "postgres_schema": source_summary["schema"], "source_table_count": len(source_counts), "target_table_count": len(target_counts), "source_total_rows": sum(source_counts.values()), "target_total_rows": sum(target_counts.values()), "mismatched_tables": mismatches, "conversion_notes": source_summary["conversion_notes"], "conversion_stats": source_summary["conversion_stats"], "private_artifacts": { "source_summary_json": str(source_summary_path), "postgres_import_sql": str(import_sql), "target_counts_csv": str(target_counts_path), }, } proof_path.write_text(json.dumps(proof, indent=2, sort_keys=True) + "\n") print(json.dumps(proof, indent=2, sort_keys=True)) sys.exit(0 if proof["status"] == "pass" else 1) PY