Skip to content
36 changes: 20 additions & 16 deletions packages/swift-sdk/SCHEMA_RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ distribution. TestFlight uploads capture provenance and a synthetic SQLite
fixture, but do not by themselves register a released schema. The accepted frozen V1 remains unchanged. Historical V2 is now reconstructed
from `52e8d4ec68f0c772313fa1bbef223fb1eabbf1cc`; all 35 entity hashes and the
model checksum match the observed App Store 9.0.2 database. Active models are
V3. Other intermediate development shapes remain unsupported.
V4. The released V3 graph is preserved by `DashSchemaSnapshotV3`. Other
intermediate development shapes remain unsupported.

The old V2 fixture was generated from September 8 sources containing 13
properties added on August 28, after the August 27 App Store release. The
Expand All @@ -15,25 +16,28 @@ separate from archive-captured releases. This identifies a matching model
source, not the confirmed build commit of Apple's binary. V2 is reserved:
automated release capture must not register another shape under that number.

The main migration plan is historical V2 → V3. Accepted V1 has a separate
V1 → V3 plan: V1 already contains the 13 properties missing from historical
V2, so a V1 → V2 → V3 chain could discard values. Routing uses model metadata
The main migration plan is historical V2 → V3 → V4. Accepted V1 has a separate
V1 → V4 plan: V1 already contains the 13 properties missing from historical
V2, so a chain through V2 could discard values. Routing uses model metadata
and runs after recovery; a version label alone never selects an unknown beta
schema. The former live V2 is accepted only when its complete graph matches
current V3 exactly, entity hashes and checksum alike. That alias therefore
lasts only until the next live-graph change: after it, every store still
labelled `2.0.0` with the former live shape becomes `unsupported-v2` and fails
closed. Check internal devices still carrying that label before the next
shape change and migrate or deliberately reset them then, rather than
discovering them afterwards as failed opens.
frozen V3 exactly, entity hashes and checksum alike. The alias remains supported
when the live graph changes; other stores labelled `2.0.0` fail closed.

V4 adds the optional `PersistentTransaction.netAmountUnavailable` marker through
a lightweight migration. Existing rows receive `nil`, preserving their stored
accounting. When deleting one participant of a shared transaction, the remaining
wallet's amount and direction are saved before the ownership links disappear.
If its amount was unresolved, the marker keeps it unavailable across restart;
an authoritative wallet record or complete accounting reconciliation clears it.

The route decision is logged as `store_migration_route` with the validated
`source_version`, `source_checksum` and one of `new-store`,
`accepted-v1-to-v3`, `historical-v2-to-v3`, `previous-live-v2-current-shape`,
`unsupported-v2`, `labelled-current-v3`, `ordinary-current-plan` or, from the
`accepted-v1-to-v4`, `historical-v2-to-v4`, `previous-live-v2-v3-shape`,
`published-v3-to-v4`, `unsupported-v2`, `labelled-current-v4`, `ordinary-current-plan` or, from the
bridge, `legacy-v1-bridge-to-v3`. Resolving a frozen schema's identity builds
a temporary store, so it is memoized per process and consulted only where a
label is undecidable without it (`1.0.0`, `2.0.0`); a `3.0.0` label takes the
label is undecidable without it (`1.0.0`, `2.0.0`); a `3.0.0` or `4.0.0` label takes the
default plan without any probe, and a probe failure on the undecidable labels
refuses the open with the probe's own error while leaving the store untouched.

Expand Down Expand Up @@ -137,9 +141,9 @@ The run failed before upload, so this provenance establishes a tested source
layout rather than proof of publication. Regression tests exercise the public
factory, data/default preservation, writes, reopen, and failure recovery.

Keep the bridge for installations that skip the V3 app release. When advancing
to V4, bind `DashSchemaV3` to its released snapshot and retain the legacy-to-V3
step before the normal V3-to-current plan. The bridge must never automatically
Keep the bridge for installations that skip the V3 app release. `DashSchemaV3`
stays bound to its released snapshot, retaining the legacy-to-V3 step before
the normal V3-to-current plan. The bridge must never automatically
follow the latest live model graph. The release observer's one-time `bootstrap`
only records its observation baseline; it neither runs this migration nor
proves V1's App Store provenance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public struct DecodedTransaction: Sendable, Equatable {
public let valueDuffs: UInt64
/// Raw scriptPubKey bytes.
public let scriptPubkey: Data

/// `true` for an `OP_RETURN` (0x6a) data-carrier / burn output.
var isOpReturn: Bool { scriptPubkey.first == 0x6a }
}

/// Transaction id in consensus (internal) byte order — reverse for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public enum DashModelContainer {

/// Create the schema for all Dash Platform models
public static var schema: Schema {
Schema(versionedSchema: DashSchemaV3.self)
Schema(versionedSchema: DashSchemaV4.self)
}

/// Create a persistent model container for storing data.
Expand Down Expand Up @@ -172,7 +172,7 @@ public enum DashModelContainer {
bridgeLegacyStore: Bool = true
) throws -> ModelContainer {
SDKLogger.event("store_open_started", category: .persistence,
fields: ["target_version": .publicText("3.0.0")])
fields: ["target_version": .publicText("4.0.0")])
do {
let container: ModelContainer
if bridgeLegacyStore {
Expand All @@ -185,7 +185,7 @@ public enum DashModelContainer {
configurations: [configuration])
}
SDKLogger.event("store_open_succeeded", category: .persistence,
fields: ["target_version": .publicText("3.0.0")])
fields: ["target_version": .publicText("4.0.0")])
return container
} catch {
logMigrationFailure(error)
Expand All @@ -201,7 +201,7 @@ public enum DashModelContainer {
let domain = systemDomains.contains(nsError.domain) ? nsError.domain : String(reflecting: type(of: error))
SDKLogger.event("store_open_failed", category: .persistence, severity: .error,
fields: ["error_domain": .publicText(domain), "error_code": .integer(Int64(nsError.code)),
"target_version": .publicText("3.0.0")])
"target_version": .publicText("4.0.0")])
}

/// Select by the complete stored model identity, after journal recovery.
Expand All @@ -218,7 +218,7 @@ public enum DashModelContainer {
guard ObjectIdentifier(defaultPlan) == ObjectIdentifier(DashMigrationPlan.self) else { return defaultPlan }
guard FileManager.default.fileExists(atPath: url.path) else {
SDKLogger.event("store_migration_route", category: .persistence, fields: [
"route": .publicText("new-store"), "target_version": .publicText("3.0.0")])
"route": .publicText("new-store"), "target_version": .publicText("4.0.0")])
return defaultPlan
}
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(type: .sqlite, at: url)
Expand All @@ -236,7 +236,7 @@ public enum DashModelContainer {
func logRoute(_ route: String) {
SDKLogger.event("store_migration_route", category: .persistence, fields: [
"source_version": .publicText(safeVersions), "source_checksum": .publicText(safeChecksum),
"route": .publicText(route), "target_version": .publicText("3.0.0")])
"route": .publicText(route), "target_version": .publicText("4.0.0")])
}
func matches(_ type: any VersionedSchema.Type) throws -> Bool {
let expected = try identity(type)
Expand All @@ -247,7 +247,7 @@ public enum DashModelContainer {
// probe stays fatal here: the open fails with the probe's own error
// instead of an inapplicable plan, and the store is untouched.
if versions == ["1.0.0"], try matches(DashSchemaV1.self) {
logRoute("accepted-v1-to-v3")
logRoute("accepted-v1-to-v4")
return DashAcceptedV1MigrationPlan.self
}
if versions == ["2.0.0"] {
Expand All @@ -261,12 +261,14 @@ public enum DashModelContainer {
throw DashLegacyStoreSQLite.Failure.unsupported(
"The database identifies itself as schema 2.0.0 but its model does not match the supported historical or current schema. The original database has not been replaced. Contact support; do not delete the app.")
}
logRoute(historical ? "historical-v2-to-v3" : "previous-live-v2-current-shape")
logRoute(historical ? "historical-v2-to-v4" : "previous-live-v2-v3-shape")
} else if versions == ["3.0.0"] {
logRoute("published-v3-to-v4")
} else if versions == ["4.0.0"] {
// Same plan either way. Opening a current store must neither wait
// on nor fail with a schema probe that could only refine this line;
// `source_checksum` above already identifies the exact graph.
logRoute("labelled-current-v3")
logRoute("labelled-current-v4")
} else {
logRoute("ordinary-current-plan")
}
Expand All @@ -287,22 +289,23 @@ public enum DashModelContainer {
/// SwiftData migration plan for Dash Platform model updates
public enum DashMigrationPlan: SchemaMigrationPlan {
public static var schemas: [any VersionedSchema.Type] {
[DashSchemaV2.self, DashSchemaV3.self]
[DashSchemaV2.self, DashSchemaV3.self, DashSchemaV4.self]
}

public static var stages: [MigrationStage] {
[
.lightweight(fromVersion: DashSchemaV2.self, toVersion: DashSchemaV3.self)
.lightweight(fromVersion: DashSchemaV2.self, toVersion: DashSchemaV3.self),
.lightweight(fromVersion: DashSchemaV3.self, toVersion: DashSchemaV4.self)
]
}
}

/// Separate compatibility route: V1 has fields missing from historical V2.
/// Never insert V2 between this baseline and the current schema.
enum DashAcceptedV1MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] { [DashSchemaV1.self, DashSchemaV3.self] }
static var schemas: [any VersionedSchema.Type] { [DashSchemaV1.self, DashSchemaV4.self] }
static var stages: [MigrationStage] {
[.lightweight(fromVersion: DashSchemaV1.self, toVersion: DashSchemaV3.self)]
[.lightweight(fromVersion: DashSchemaV1.self, toVersion: DashSchemaV4.self)]
}
}

Expand Down Expand Up @@ -372,9 +375,14 @@ public enum DashSchemaV2: VersionedSchema {
}
}

/// Current working schema. A later shape change must preserve this graph as
/// the fixed legacy-bridge target before introducing another live version.
/// Frozen released graph and fixed target of the legacy-store bridge.
public enum DashSchemaV3: VersionedSchema {
public static var versionIdentifier: Schema.Version { Schema.Version(3, 0, 0) }
public static var models: [any PersistentModel.Type] { DashSchemaSnapshotV3.models }
}

/// Live schema adds an optional accounting-availability marker; existing rows keep nil.
public enum DashSchemaV4: VersionedSchema {
public static var versionIdentifier: Schema.Version { Schema.Version(4, 0, 0) }
public static var models: [any PersistentModel.Type] { DashModelContainer.modelTypes }
}
Loading
Loading