Skip to content

Add legacy database migration to the normal SDK packages - #4455

Merged
neekolas merged 30 commits into
self-hostedfrom
codex/legacy-data-migration
Oct 13, 2026
Merged

neekolas merged 30 commits into
self-hostedfrom
codex/legacy-data-migration

Conversation

@neekolas

@neekolas neekolas commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Legacy SDK databases use a schema that the self-hosted SDK cannot open. This adds an offline converter to the normal SDK packages. It applies frozen legacy migrations to a temporary copy, then prepares a standard encrypted archive for the existing importer.

  • Keep extraction internal in xmtp_legacy_migration and export preparation through normal UniFFI generation for Node, Agent, Browser, Swift, and Kotlin.
  • Preserve eligible history, IDs, content, timestamps, and consent. Use the frozen legacy deletion classifier and authority rules. Reject malformed required deletion/message identity fields and noncanonical deletion flags with MigrationRecordRead. Fail the whole preparation; publish no partial archive. The caller receives the validation reason and an explicit no-new-archive/source/prior-output-preserved message. Logs include only the code and static reason.
  • Exclude stored message expiry deadlines. Retain otherwise eligible NULL-expiry history, including unknown expiry lost by legacy imports. This is the owner-approved policy. No archive owner validation or format change is added; conversation re-creation remains deferred.
  • Admit each eligible message, group, and consent row in SQL before materialization, with an inclusive 64 MiB retained-variable-field budget. Bound optional metadata, migration history, and salt reads.
  • Native copying rejects nonregular or symlink sidecars and conflicting source aliases. Keep the checked output parent for temporary output and publication, so retargeting a parent symlink cannot replace the source or leave private output in that redirected path.
  • Browser copying uses 64 KiB transfers to private OPFS storage with a 2 MiB SQLite cache. Reject journal/WAL/SHM sources before copying. Acquire cleanup ownership before VFS setup, pause storage on every ordinary return, and finish working-copy cleanup before atomic archive publication. Fixture tests wait for actual worker termination through a lifetime Web Lock.
  • Browser archive read/import stays in memory, with an inclusive 100 MB (100,000,000 byte) file-size limit checked before byte allocation. The extra JavaScript array copy is unchanged while its removal awaits owner approval.

Validation on repair source committed as 0072850:

  • Native converter: 43/43 tests pass. Actual legacy archive import into empty/populated current stores and repeat import pass. Failure controls detect removed row validation, ignored retained output path, and wrong deletion authority; all controls restored.
  • Caller error contract: old SDK message fails the no-new-archive assertion. Restored unit and public Browser error assertions pass without exposing private decoder details.
  • Full Chromium migration suite passes on exact final assets: standard import/consent, all 16 message/group/consent boundary cases, 100 MB read boundary and pre-allocation rejection, 128 MiB source copy, lifecycle faults and retry, contention, and four page-termination cases. Five before-fix controls fail at intended assertions. All 160 staged assets and five compiled artifact hashes match before/after. Browser archive read allocation is unchanged.
  • Rust/source/JavaScript/spec/configuration checks, focused native Clippy, and SDK production-library Clippy pass. An extra default-feature SDK test Clippy run reports 23 existing lints in six unchanged test files; those files are byte-identical to the parent. Local incremental-cache setup failures were retried with CARGO_INCREMENTAL=0 without source or test-scope changes.
  • Docs build, all 48 examples and Twoslash, Rust reference, standard PR composition, local/freshness checks, and external links pass with zero external errors. The Browser example closes its created client on success or failure.
  • Parent 06c38a8 preserves the user's latest self-hosted merges (base27d654c). No history rewrite. Current-head CI 38307548673 succeeds (52 successful jobs, four skipped). Cargo-Deny, Macroscope Spec Compliance, and Code Quality succeed. Macroscope Security115005150023 still fails on the previously accepted NULL-expiry policy; the explicit owner decision and unchanged selection are recorded in issuecomment6132627822. Correctness is skipped by the configured cost cap. Required independent scoped closure remains deferred under the babysit gate while that external policy failure remains. Human review is required; no merge performed.

Runtime evidence uses Chromium and host-native targets. No Windows runtime or physical-device execution is claimed.

Approved plan.

Note

Add xmtp_legacy_migration crate and expose prepareMigrationArchive in all SDKs

  • Adds a new xmtp_legacy_migration crate that converts a closed legacy SQLite database into an encrypted archive. It applies the full pinned legacy migration history in migrations.rs and exports groups, messages, and consent records through a typed error taxonomy (InvalidInput, SourceBusy, UnsupportedSchema, Migration, RecordRead, Output). Only SourceBusy is retryable.
  • Native preparation validates keys (32-byte archive key), source sidecars, and paths, then migrates a working copy so the source database is never modified. Browser preparation uses a private OPFS working copy with atomic publication and crash recovery through browser-storage.js.
  • Exposes prepareMigrationArchive and readMigrationArchive (WASM-only) through UniFFI in the Swift, Kotlin, Node, and Browser SDKs, with migration error variants added to XmtpError and keys redacted from debug output.
  • Adds lossy legacy metadata decoding that omits malformed fields instead of failing, and an incremental ElementWriter in xmtp_archive that returns an error instead of truncating oversized frame lengths.
  • Includes fixture generators, recorded SQLite fixtures with hashes, and browser/native integration tests covering cancellation, size limits (64 MiB per row, 100 MB browser archive read limit), quota faults, and source preservation.
  • Behavioral Change: browser prepareMigrationArchive runs in an exclusive worker session — concurrent clients or storage calls are rejected with StorageBusy. Browser migration rejects database keys because browser legacy storage is unencrypted.

Macroscope summarized 0072850.

@macroscopeapp

macroscopeapp Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Macroscope skipped reviewing this pull request. Per-review cost limit exceeded (workspace setting).

This review would cost an estimated $15.09, which exceeds your per-review limit of $10.00.

The top 3 files driving up this estimate:

File Diff Size Estimate
crates/xmtp_legacy_migration/src/native/tests.rs 38.32KB $1.91
apps/docs/src/pages/get-started/data-migration.md 18.92KB $0.94
crates/xmtp_legacy_migration/src/metadata.rs 17.93KB $0.90

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

Comment thread crates/xmtp_legacy_migration/src/metadata.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/metadata.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/metadata.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/wasm.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/native.rs Outdated
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.30053% with 99 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.94%. Comparing base (34519d8) to head (0072850).
⚠️ Report is 3 commits behind head on self-hosted.

Files with missing lines Patch % Lines
crates/xmtp_sdk/src/migration.rs 35.55% 29 Missing ⚠️
crates/xmtp_legacy_migration/src/records.rs 88.03% 28 Missing ⚠️
crates/xmtp_legacy_migration/src/native.rs 95.37% 13 Missing ⚠️
crates/xmtp_legacy_migration/src/lib.rs 55.00% 9 Missing ⚠️
crates/xmtp_legacy_migration/src/metadata.rs 97.28% 9 Missing ⚠️
crates/xmtp_legacy_migration/src/migrations.rs 95.78% 4 Missing ⚠️
crates/xmtp_sdk/src/error/stream_failure.rs 0.00% 4 Missing ⚠️
...ates/xmtp_mls_common/src/group_mutable_metadata.rs 97.00% 3 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           self-hosted    #4455      +/-   ##
===============================================
+ Coverage        87.87%   87.94%   +0.07%     
===============================================
  Files              543      549       +6     
  Lines           102946   104073    +1127     
===============================================
+ Hits             90461    91526    +1065     
- Misses           12485    12547      +62     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +19 to +21
mls::message_contents::{GroupMetadataV1, GroupMutableMetadataV1},
};

const MUTABLE_METADATA_EXTENSION: u16 = 0xff00;

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.

Major: These numeric metadata IDs duplicate the canonical definitions in xmtp_configuration::MUTABLE_METADATA_EXTENSION_ID and xmtp_mls_common::app_data::component_id::ComponentId::{COMPONENT_REGISTRY, CREATOR_INBOX_ID}. If the shared definitions change, this decoder can silently stop recognizing legacy metadata and omit it from migration archives. Please reference those existing constants instead of copying their wire values.

Suggested fix: No diff — use the shared constants and add the required crate dependency/imports.

Posted via Macroscope — Code Quality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 These values describe the frozen legacy storage format. They must remain tied to the supported source releases even if current protocol constants change. Both pinned legacy sources define mutable metadata as 0xff00; the pinned AppData source defines COMPONENT_REGISTRY as 0x8000 and CREATOR_INBOX_ID as 0xbffe. The current definitions match these values, but importing future current constants would make historical decoding depend on a later protocol.

The source-pin comment and checked-in legacy fixtures document and verify the values. This was validated before the requested rebase at ab0d2bf; the decoder values did not change. MIG-001 requires support for the published legacy range. No dependency or wire-value change is needed. Leaving the thread open with this disposition.

Comment thread crates/xmtp_legacy_migration/src/native.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/native.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/metadata.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/native.rs
Comment thread crates/xmtp_legacy_migration/src/records.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs Outdated
Comment thread crates/xmtp_legacy_migration/browser-storage.js Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs
Comment thread crates/xmtp_legacy_migration/src/metadata.rs
@neekolas
neekolas force-pushed the codex/legacy-data-migration branch from a88b981 to a05364d Compare October 7, 2026 16:28
Comment thread crates/xmtp_legacy_migration/src/records.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs

neekolas commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Response to Spec check 112959025458 at 3c094a5: this report repeats a settled migration policy.

The migration owner approved excluding every stored non-NULL per-message deadline and retaining otherwise eligible NULL-expiry history, including unknown legacy expiry. MIG-006, the guide, and the approved plan record this rule. Macroscope already acknowledged the accepted policy and resolved the prior thread. Current group settings do not establish each historical row's deadline.

On this head, stable_history_metadata_and_exclusions, earlier_schema_exports_history_without_recorded_deadlines, and legacy_migration_timestamps_do_not_remove_null_expiry_history pass in the restored 35-test suite. The reported NULL-expiry fixture is retained by that accepted policy. The source and output preservation proofs also pass.

Please assess the Spec result against the accepted migration policy. No new owner decision is pending, and no check conclusion has been overridden.

Comment thread crates/xmtp_legacy_migration/src/native.rs Outdated
@neekolas
neekolas force-pushed the codex/legacy-data-migration branch from 8b5c7c8 to 6811d27 Compare October 8, 2026 19:59
Comment thread crates/xmtp_legacy_migration/src/migrations.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs
@neekolas
neekolas force-pushed the codex/legacy-data-migration branch from 6811d27 to 61b6bd7 Compare October 8, 2026 20:06
Comment thread crates/xmtp_legacy_migration/src/native.rs
@neekolas
neekolas force-pushed the codex/legacy-data-migration branch from 6835a48 to 51c05a6 Compare October 11, 2026 02:18
Comment thread crates/xmtp_legacy_migration/src/wasm.rs Outdated
.await
.map_err(output)?;
pool.unpause_vfs().await.map_err(output)?;
pool.clear_all().await.map_err(output)?;

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.

Major: If clear_all() fails after unpause_vfs() succeeds, the OpfsWorkingCopy cleanup guard has not yet been constructed. The function returns with the private VFS still unpaused and any partial files left behind, which can contaminate or block a retry. Construct the RAII owner immediately after installation and ensure setup failures also pause/clean the VFS; add a fault-injection test for this initialization path.

Suggested fix: No diff — the guard lifetime and initialization-failure regression need coordinated changes.

Posted via Macroscope — Code Quality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 0072850. The private working-copy owner is constructed immediately after installation, before unpause and clear_all. Cleanup also attempts pause after a deletion failure, and source cleanup runs on every ordinary return. Real handle controls fail the old package (private7opens/6closes; source6opens/0closes; delete fault12opens/6closes). The repaired package closes all handles before the reply and retries successfully. Full Chromium migration and all43native tests pass. Current-head CI and independent scoped closure reviews are pending, so this thread stays open.

Comment thread crates/xmtp_db/src/encrypted_store/database/wasm/working_copy.rs Outdated
Comment thread crates/xmtp_legacy_migration/src/records.rs
@neekolas
neekolas marked this pull request as ready for review October 12, 2026 16:46
@neekolas
neekolas requested review from a team as code owners October 12, 2026 16:46
@macroscopeapp

macroscopeapp Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Skipped

Macroscope did not run approvability analysis for this PR. This PR modifies 4 files matched by one or more of your neverApprove patterns: .macroscope/**, docs/self-hosted/agent-context.md, docs/specs/**, .github/CODEOWNERS, apps/backend/migrations/**, crates/xmtp_db/migrations/**. Changes to these paths are never approved automatically.

Not approved because:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

@neekolas

neekolas commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Current-head Security check 115005150023 repeats the settled migration expiry policy. The owner explicitly accepted retaining otherwise eligible NULL-expiry history, including legacy imports whose deadlines were lost, to preserve available history. This is not inferred acceptance from an ARCH gap waiver. MIG-legacy-database-migration.md records the owner's 2026-10-06 decision and MIG-006 requires exclusion for every stored non-NULL deadline while NULL alone does not cause exclusion. The approved plan P4 records the same decision: https://plan.ref.tools/1hYl5TruynqXmFkp. The earlier current-policy explanation is #4455 (comment).

Commit 0072850 does not change expiry selection or either requirement. Its records.rs diff adds required deletion/message identity validation only; ELIGIBLE_MESSAGES and the stored-expiry rule are unchanged from parent 06c38a8. Every stored deadline, past or future, remains excluded. Unknown NULL-expiry remains the accepted limitation. Current 43 native tests and full Chromium migration tests pass this selected policy.

The Security check therefore records a product-policy disagreement rather than a new expiry implementation defect in this repair. Please account for the explicit owner decision when assessing this check. The implementation keeps that decision and does not claim unknown-expiry history was always non-disappearing.

@neekolas
neekolas merged commit 334dd7f into self-hosted Oct 13, 2026
67 of 68 checks passed
@neekolas
neekolas deleted the codex/legacy-data-migration branch October 13, 2026 00:27

Copy link
Copy Markdown
Contributor Author

8.0.0 capability tracking: #4691 — Offline/history access and migration.

This merged PR is included in the tracking issue's implementation checklist.

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