Skip to content

fix(dr): make D1 restore imports FK-safe with foreign_keys=OFF prelude - #943

Merged
kody-bot merged 1 commit into
mainfrom
cursor/dr-import-foreign-keys-off-50d7
Jul 25, 2026
Merged

kody-bot merged 1 commit into
mainfrom
cursor/dr-import-foreign-keys-off-50d7

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Isolated restore drills were failing with import-failed / no such table: main.users because D1 remote import enforces foreign keys during CREATE TABLE, while Cloudflare D1 exports are not topologically ordered.

This change verifies the unmodified backup SQL MD5 against the signed manifest R2 ETag, then prefixes PRAGMA foreign_keys=OFF; and uses the prepared body's MD5 for D1 import init/ingest. Stream bodies are loaded twice via loadSqlBody so large backups are not fully buffered in Worker memory.

Operator follow-up after merge

  1. Redeploy the DR control-plane Worker (packages/backup-control-plane) to the KCD account (not app CI).
  2. Re-run Run isolated restore drill for 2026-07-24 (or 2026-07-23) at https://kody-dr.kentcdodds.com
  3. Expect PRAGMA quick_check ok and cleanup of the drill DB.

Test plan

  • packages/backup-control-plane unit tests (64) + typecheck
  • Pre-push / husky validate (unit, Playwright, etc.)
  • Live drill after control-plane redeploy
System recap — extends the backup control plane (medium risk)

Mode: recap · Base: main @ 0a57498b · Head: 15818d8e

Classification: extends — changes restore/drill D1 import behavior (FK-safe prepare + etag contract) inside the existing backup-control-plane primitive.

Primitives touched

Primitive Group Impact
backup-control-plane storage extends — D1 import verifies source MD5, prefixes PRAGMA foreign_keys=OFF;, uploads prepared MD5; drill + production restore call sites updated; nodejs_compat enabled for streaming MD5

System map

flowchart LR
  manifest["signed D1 manifest<br/>sql.r2Etag / objectKey"]:::untouched
  r2["BACKUP_BUCKET SQL object"]:::untouched
  prepare["prepareD1ImportUpload<br/>verify source MD5 + FK-off prefix"]:::extended
  d1import["Cloudflare D1 import API"]:::untouched
  drill["isolated restore drill"]:::extended
  prodRestore["production restore Workflow"]:::extended
  manifest -->|"sourceMd5Etag"| prepare
  r2 -->|"loadSqlBody x2"| prepare
  prepare -->|"upload prepared SQL + MD5"| d1import
  drill --> prepare
  prodRestore --> prepare
  classDef touched fill:#1a7f37,color:#fff
  classDef extended fill:#9a6700,color:#fff
  classDef added fill:#cf222e,color:#fff
  classDef untouched fill:#57606a,color:#fff
Loading

Change flow

sequenceDiagram
  participant Drill as restore drill / production restore
  participant Prep as prepareD1ImportUpload
  participant R2 as BACKUP_BUCKET
  participant D1 as D1 import API
  Drill->>Prep: sourceMd5Etag + loadSqlBody
  Prep->>R2: load SQL hash pass
  Prep->>Prep: verify source MD5 equals manifest r2Etag
  Prep->>R2: load SQL upload pass
  Prep->>D1: init with prepared MD5
  Prep->>D1: PUT prepared body
  Prep->>D1: ingest and poll to complete
Loading

Invariants

  • Signed-manifest provenance still gates restore: unmodified SQL MD5 must match payload.sql.r2Etag before any transform.
  • Upload etag is the prepared body digest, not the raw backup etag.
  • Large backups stay stream-friendly (loadSqlBody twice; no full-buffer requirement for streams).
  • Drill remains isolated to DRILL_ACCOUNT_ID; production restore path shares the same import prepare logic.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • Improvements

    • D1 restore imports now safely handle foreign-key dependencies by temporarily disabling foreign-key enforcement during import.
    • Restore workflows validate SQL backups before loading and retrieve SQL data only when needed.
    • Import integrity checks now distinguish the original backup checksum from the prepared upload checksum.
    • Restore logs include the SQL backup location for improved visibility.
  • Documentation

    • Updated disaster recovery guidance with the revised isolated restore import process and related failure prevention.

D1 remote import enforces FKs during CREATE TABLE, but Cloudflare exports
are not topologically ordered, so drills failed with no such table: main.users.
Verify the unmodified SQL MD5 against the signed manifest etag, then upload a
prefixed body and use that prepared MD5 for import init/ingest.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The D1 import API now validates the original SQL digest, prepends PRAGMA foreign_keys=OFF;, and uses the prepared digest for uploads. Restore flows check SQL existence with head and lazily load bodies. Tests and recovery documentation reflect the prepared import flow.

Changes

D1 import flow

Layer / File(s) Summary
Prepare and validate D1 uploads
packages/backup-control-plane/d1-import-api.ts, packages/backup-control-plane/d1-import-api.node.test.ts, packages/backup-control-plane/wrangler.jsonc, docs/contributing/disaster-recovery.md
The import contract now accepts sourceMd5Etag and loadSqlBody(), validates source SQL, prepends the foreign-key pragma, computes the prepared MD5, and uploads the prepared body. Tests and runbook instructions cover the updated flow.
Lazy SQL loading in restore flows
packages/backup-control-plane/production-restore.ts, packages/backup-control-plane/restore-drill.ts
Restore paths use head for existence checks and defer SQL body retrieval through loaders that preserve the existing missing-object errors.
Validate prepared restore imports
packages/backup-control-plane/production-restore.node.test.ts
Production restore fixtures calculate the prefixed SQL digest and use it as the mocked upload ETag.

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

Sequence Diagram(s)

sequenceDiagram
  participant RestoreFlow
  participant R2Bucket
  participant D1ImportAPI
  participant D1
  RestoreFlow->>R2Bucket: head SQL object
  R2Bucket-->>RestoreFlow: object metadata or missing
  RestoreFlow->>D1ImportAPI: sourceMd5Etag and loadSqlBody
  D1ImportAPI->>R2Bucket: get SQL body
  R2Bucket-->>D1ImportAPI: SQL stream or body
  D1ImportAPI->>D1ImportAPI: prepend foreign-key pragma and compute upload MD5
  D1ImportAPI->>D1: initialize and upload prepared SQL
Loading
🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: making D1 restore imports foreign-key safe with a foreign_keys=OFF prelude.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/dr-import-foreign-keys-off-50d7

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.

@kody-bot
kody-bot marked this pull request as ready for review July 25, 2026 20:26
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-943.kody-a99.workers.dev

Worker: kody-pr-943
D1: kody-pr-943-db
KV: kody-pr-943-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/backup-control-plane/d1-import-api.ts (1)

340-379: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Redundant second loadSqlBody() call for non-stream bodies.

For the string/Uint8Array branch, first already holds the fully materialized bytes (not consumed by consumeSqlBodyForHashes, unlike a stream reader). Calling loadSqlBody() again to get second just to rebuild the same bytes is unnecessary work; first can be reused directly.

♻️ Proposed refactor to avoid the redundant reload
 	const expectedSourceMd5 = hexMd5FromR2Etag(input.sourceMd5Etag)
 	const prefix = new TextEncoder().encode(d1ImportForeignKeysOffPrefix)
 	const first = await input.loadSqlBody()
 	const hashes = await consumeSqlBodyForHashes(first, prefix)
 	if (hashes.sourceMd5Hex !== expectedSourceMd5) {
 		throw new BackupError(
 			'import-source-etag-mismatch',
 			'Backup SQL MD5 did not match the signed manifest R2 ETag',
 		)
 	}
 
-	const second = await input.loadSqlBody()
-	if (typeof second === 'string' || second instanceof Uint8Array) {
-		const sourceBytes = toUint8Array(second)
+	if (typeof first === 'string' || first instanceof Uint8Array) {
+		const sourceBytes = toUint8Array(first)
 		const uploadBytes = new Uint8Array(
 			prefix.byteLength + sourceBytes.byteLength,
 		)
 		uploadBytes.set(prefix, 0)
 		uploadBytes.set(sourceBytes, prefix.byteLength)
 		return {
 			uploadBody: uploadBytes,
 			uploadMd5Hex: hashes.uploadMd5Hex,
 			sourceBytes: hashes.sourceBytes,
 		}
 	}
 
+	const second = await input.loadSqlBody()
+	if (typeof second === 'string' || second instanceof Uint8Array) {
+		throw new BackupError(
+			'import-sql-body-inconsistent',
+			'loadSqlBody() returned different body types on repeated invocation',
+		)
+	}
 	return {
 		uploadBody: prependForeignKeysOffStream(prefix, second),
 		uploadMd5Hex: hashes.uploadMd5Hex,
 		sourceBytes: hashes.sourceBytes,
 	}

Note: this branch is currently only exercised by the buffer/string test cases, since both real callers (production-restore.ts, restore-drill.ts) supply loadSqlBody functions returning R2Object.body, which is always a ReadableStream in the Workers runtime, so real-world impact is limited.

🤖 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 `@packages/backup-control-plane/d1-import-api.ts` around lines 340 - 379, Reuse
the already loaded first body in prepareD1ImportUpload for non-stream inputs
instead of calling input.loadSqlBody() a second time. Branch on first being a
string or Uint8Array, preserve the existing prefixing and returned metadata, and
only load the body again for stream inputs that are consumed by
consumeSqlBodyForHashes.
🤖 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.

Nitpick comments:
In `@packages/backup-control-plane/d1-import-api.ts`:
- Around line 340-379: Reuse the already loaded first body in
prepareD1ImportUpload for non-stream inputs instead of calling
input.loadSqlBody() a second time. Branch on first being a string or Uint8Array,
preserve the existing prefixing and returned metadata, and only load the body
again for stream inputs that are consumed by consumeSqlBodyForHashes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a756fbb3-601f-45c0-abac-14ec27f66997

📥 Commits

Reviewing files that changed from the base of the PR and between b88d025 and 15818d8.

📒 Files selected for processing (7)
  • docs/contributing/disaster-recovery.md
  • packages/backup-control-plane/d1-import-api.node.test.ts
  • packages/backup-control-plane/d1-import-api.ts
  • packages/backup-control-plane/production-restore.node.test.ts
  • packages/backup-control-plane/production-restore.ts
  • packages/backup-control-plane/restore-drill.ts
  • packages/backup-control-plane/wrangler.jsonc

@kody-bot
kody-bot merged commit 5598393 into main Jul 25, 2026
7 of 8 checks passed
@kody-bot
kody-bot deleted the cursor/dr-import-foreign-keys-off-50d7 branch July 25, 2026 20:41
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.

3 participants