Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/swift-sdk/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ let package = Package(
.testTarget(
name: "SwiftDashSDKTests",
dependencies: ["SwiftDashSDK"],
path: "SwiftTests/SwiftDashSDKTests"
path: "SwiftTests/SwiftDashSDKTests",
resources: [.copy("Fixtures")]
),

// Integration tests against a local dashmate devnet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,87 @@ import SwiftData

/// Factory for creating SwiftData model containers for Dash Platform persistence
public enum DashModelContainer {
private struct StoreFileSizes {
let main: UInt64
let wal: UInt64
let shm: UInt64

var total: UInt64 {

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.

StoreFileSizes.total re-implements diagnosticSaturatingSum, which this same PR adds as an internal module-level function.

The reduce with addingReportingOverflow and the UInt64.max sentinel is byte-for-byte the body of diagnosticSaturatingSum (PlatformWalletManagerCoreDiagnostics.swift:110-116), which is internal to the same SwiftDashSDK module and therefore callable here. Two copies of the same saturating-sum rule mean a future fix to one — e.g. switching to a sentinel other than UInt64.max — leaves the store-size totals on the old semantics.

🤖 AI-assisted review (Claude Code / Opus 5), relayed by @romchornyi.

[main, wal, shm].reduce(0) { partial, value in
let (sum, overflow) = partial.addingReportingOverflow(value)
return overflow ? UInt64.max : sum
}
}
}

/// Builds the common payload for both sides of the container open. The
/// outcome deliberately describes only what SwiftData tells us: opening an
/// existing store may have included a migration, but this API does not
/// expose whether one actually ran.
private static func storeOpenFields(
succeeded: Bool,
existedBefore: Bool,
startedAt: CFAbsoluteTime,
sizeBefore: StoreFileSizes,
sizeAfter: StoreFileSizes
) -> [String: SDKLogValue] {
let elapsed = max(0, (CFAbsoluteTimeGetCurrent() - startedAt) * 1_000)
let duration: UInt64
if !elapsed.isFinite {
duration = 0
} else if elapsed >= Double(UInt64.max) {
duration = UInt64.max
} else {
duration = UInt64(elapsed)
}
let openOutcome: String
switch (succeeded, existedBefore) {
case (true, true):
openOutcome = "existing_store_opened"
case (true, false):
openOutcome = "new_store_created"
case (false, true):
openOutcome = "existing_store_open_or_migration_failed"
case (false, false):
openOutcome = "new_store_creation_failed"
}

return [
"container_result": .publicText(succeeded ? "opened" : "open_failed"),
"container_reused": .boolean(false),

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.

container_reused is hardcoded to false on both branches, so the field carries no information and would silently lie if container caching were ever added.

storeOpenFields always emits "container_reused": .boolean(false), and DashModelContainer.create has no cache — every call constructs a fresh ModelContainer. The field reads like a measured fact in the exported log, so an analyst can wrongly conclude the SDK checked for reuse. If a future change memoizes the container (a natural optimization given the new open-duration instrumentation), this constant becomes an actively wrong claim with no compiler or test signal. Either drop the field or derive it.

🤖 AI-assisted review (Claude Code / Opus 5), relayed by @romchornyi.

"duration_ms": .unsignedInteger(duration),
"result": .publicText(succeeded ? "success" : "failure"),
"store_existed_before_open": .boolean(existedBefore),
"store_main_size_bytes_after": .unsignedInteger(sizeAfter.main),
"store_main_size_bytes_before": .unsignedInteger(sizeBefore.main),
"store_open_outcome": .publicText(openOutcome),
"store_shm_size_bytes_after": .unsignedInteger(sizeAfter.shm),
"store_shm_size_bytes_before": .unsignedInteger(sizeBefore.shm),
"store_size_bytes_after": .unsignedInteger(sizeAfter.total),
"store_size_bytes_before": .unsignedInteger(sizeBefore.total),
"store_wal_size_bytes_after": .unsignedInteger(sizeAfter.wal),
"store_wal_size_bytes_before": .unsignedInteger(sizeBefore.wal),
]
}

/// SQLite's durable state can be mostly in the WAL immediately after an
/// app kill, so the main file alone is not a useful corruption signal.
/// Read only sizes and never include any component of the device path.
private static func storeFileSizes(at storeURL: URL) -> StoreFileSizes {
func fileSize(at url: URL) -> UInt64 {
guard let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize,
size >= 0
else { return 0 }
return UInt64(size)
}

return StoreFileSizes(
main: fileSize(at: storeURL),
wal: fileSize(at: URL(fileURLWithPath: storeURL.path + "-wal")),
shm: fileSize(at: URL(fileURLWithPath: storeURL.path + "-shm"))
)
}

/// Every registered schema version's model list, parameterised on the
/// one model whose shape differs between versions.
///
Expand Down Expand Up @@ -97,12 +178,49 @@ public enum DashModelContainer {
)

// Always wire the migration plan so stores created by an older SDK
// advance through the registered versioned schemas.
return try ModelContainer(
for: schema,
migrationPlan: DashMigrationPlan.self,
configurations: [modelConfiguration]
)
// advance through the registered versioned schemas. Record only
// metadata about the store — never its device path.
let storeURL = modelConfiguration.url
let existedBefore = FileManager.default.fileExists(atPath: storeURL.path)
let sizeBefore = storeFileSizes(at: storeURL)
let started = CFAbsoluteTimeGetCurrent()
do {
let container = try ModelContainer(
for: schema,
migrationPlan: DashMigrationPlan.self,
configurations: [modelConfiguration]
)
let sizeAfter = storeFileSizes(at: storeURL)
SDKLogger.event(

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.

core_store_open_result is emitted before any log file sink is installed, so it never reaches the exported swift/run.log.

SDKLogger.event writes only to state.destination(for:), which is nil until LoggingPreferences.configure() calls installFileSink. In SwiftExampleApp, DashModelContainer.create() runs in SwiftExampleAppApp.init() (SwiftExampleAppApp.swift:107) while LoggingPreferences.configure() runs later inside bootstrap() (SwiftExampleAppApp.swift:426).

The event is mirrored to NSLog/print only. The support artifact this PR is built to produce contains no core_store_open_result line — and the failure path's sizes, duration and outcome are lost exactly when the store fails to open.

🤖 AI-assisted review (Claude Code / Opus 5), relayed by @romchornyi.

"core_store_open_result",
category: .persistence,
fields: storeOpenFields(
succeeded: true,
existedBefore: existedBefore,
startedAt: started,
sizeBefore: sizeBefore,
sizeAfter: sizeAfter
)
)
return container
} catch {
let sizeAfter = storeFileSizes(at: storeURL)
SDKLogger.event(
"core_store_open_result",
category: .persistence,
severity: .error,
fields: storeOpenFields(
succeeded: false,
existedBefore: existedBefore,
startedAt: started,
sizeBefore: sizeBefore,
sizeAfter: sizeAfter
),
error: error,
redacting: [storeURL.path]
)
throw error
}
}

/// Create an in-memory model container for testing
Expand Down
Loading
Loading