diff --git a/WorkbenchReplacement/MainView.swift b/WorkbenchReplacement/MainView.swift
index e6e9583..8db8e29 100644
--- a/WorkbenchReplacement/MainView.swift
+++ b/WorkbenchReplacement/MainView.swift
@@ -1589,6 +1589,8 @@ struct WorkbenchDatabaseTransferView: View {
@State private var loadingTables = false
@State private var availableDatabases: [String] = []
@State private var loadingDatabases = false
+ @State private var lastBackupURL: URL?
+ @State private var lastVerificationReport = ""
private var selectedProfile: ConnectionProfile? {
model.selected
@@ -1710,6 +1712,12 @@ struct WorkbenchDatabaseTransferView: View {
.font(.caption)
.foregroundStyle(.secondary)
.textSelection(.enabled)
+ if !lastVerificationReport.isEmpty {
+ Text(lastVerificationReport)
+ .font(.system(.caption, design: .monospaced))
+ .textSelection(.enabled)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
if isWorking && selectedMode == 0 && currentTableExpected > 0 {
ProgressView(value: Double(currentTableRows), total: Double(currentTableExpected))
Text("Current table: \(currentTableRows.formatted()) / \(currentTableExpected.formatted()) rows · Total exported: \(transferredRows.formatted())")
@@ -1738,6 +1746,11 @@ struct WorkbenchDatabaseTransferView: View {
chooseBackupToVerify()
}
.help("Verify an SQL file against its SHA-256 checksum sidecar")
+ Button("Verify Restored Database…") {
+ chooseBackupToValidate()
+ }
+ .disabled(selectedProfile == nil || database.isEmpty)
+ .help("Compare restored table row counts and object names with the export manifest")
}
if isWorking {
@@ -1864,6 +1877,91 @@ struct WorkbenchDatabaseTransferView: View {
}
}
+ private func chooseBackupToValidate() {
+ let panel = NSOpenPanel()
+ panel.title = "Select Exported SQL Backup to Validate"
+ panel.canChooseDirectories = false
+ panel.canChooseFiles = true
+ guard panel.runModal() == .OK, let url = panel.url else { return }
+ operationTask = Task { await validateRestoredDatabase(from: url) }
+ }
+
+ @MainActor
+ private func validateRestoredDatabase(from url: URL) async {
+ isWorking = true
+ progress = 0
+ lastVerificationReport = ""
+ defer { isWorking = false; operationTask = nil }
+ do {
+ let manifest = try WorkbenchBackupManifest.read(for: url)
+ guard manifest.database == database else {
+ throw WorkbenchTransferError.message(
+ "Manifest is for database \(manifest.database), but \(database) is selected."
+ )
+ }
+ if WorkbenchBackupIntegrity.hasSidecar(for: url) {
+ status = "Verifying SQL checksum…"
+ try await Task.detached(priority: .utility) {
+ try WorkbenchBackupIntegrity.verify(url)
+ }.value
+ }
+ var failures: [String] = []
+ var checked = 0
+ let total = manifest.tables.count + manifest.views.count + manifest.triggers.count + manifest.routines.count
+ for table in manifest.tables {
+ try Task.checkCancellation()
+ status = "Validating table \(table.name)"
+ do {
+ let rows = try await execute("SELECT COUNT(*) FROM \(quoteIdentifier(database)).\(quoteIdentifier(table.name))")
+ guard let actual = rows.rows.first?.first.flatMap(Int64.init) else {
+ failures.append("\(table.name): count unavailable")
+ continue
+ }
+ if let expected = table.rows, actual != expected {
+ failures.append("\(table.name): expected \(expected), found \(actual) rows")
+ }
+ } catch {
+ failures.append("\(table.name): \(error.localizedDescription)")
+ }
+ checked += 1
+ progress = Double(checked) / Double(max(total, 1))
+ }
+ for view in manifest.views {
+ try Task.checkCancellation()
+ status = "Validating view \(view)"
+ do { _ = try await execute("SHOW CREATE VIEW \(quoteIdentifier(database)).\(quoteIdentifier(view))") }
+ catch { failures.append("View \(view): \(error.localizedDescription)") }
+ checked += 1
+ progress = Double(checked) / Double(max(total, 1))
+ }
+ for trigger in manifest.triggers {
+ try Task.checkCancellation()
+ status = "Validating trigger \(trigger)"
+ do { _ = try await execute("SHOW CREATE TRIGGER \(quoteIdentifier(database)).\(quoteIdentifier(trigger))") }
+ catch { failures.append("Trigger \(trigger): \(error.localizedDescription)") }
+ checked += 1
+ progress = Double(checked) / Double(max(total, 1))
+ }
+ for routine in manifest.routines {
+ try Task.checkCancellation()
+ status = "Validating \(routine.kind) \(routine.name)"
+ do { _ = try await execute("SHOW CREATE \(routine.kind) \(quoteIdentifier(database)).\(quoteIdentifier(routine.name))") }
+ catch { failures.append("\(routine.kind) \(routine.name): \(error.localizedDescription)") }
+ checked += 1
+ progress = Double(checked) / Double(max(total, 1))
+ }
+ progress = 1
+ lastVerificationReport = failures.isEmpty
+ ? "PASS: \(checked) objects checked. Counts and object existence match the manifest."
+ : "FAILED: \(failures.count) mismatches:\n" + failures.joined(separator: "\n")
+ status = failures.isEmpty ? "Restore validation passed" : "Restore validation found \(failures.count) problems"
+ } catch is CancellationError {
+ status = "Validation cancelled"
+ } catch {
+ status = "Validation failed: \(error.localizedDescription)"
+ }
+ }
+
private func chooseImportSource() {
let panel = NSOpenPanel()
panel.title = "Import MySQL SQL File"
@@ -1985,6 +2083,10 @@ struct WorkbenchDatabaseTransferView: View {
isWorking = true
progress = 0
transferredRows = 0
+ lastVerificationReport = ""
+ var manifestTables: [WorkbenchBackupManifest.Table] = []
+ var manifestRoutines: [WorkbenchBackupManifest.Routine] = []
+ var manifestTriggers: [String] = []
currentTableRows = 0
currentTableExpected = 0
let tables = availableTables.filter { selectedTables.contains($0) }
@@ -2143,8 +2245,12 @@ struct WorkbenchDatabaseTransferView: View {
"Table \(table): expected \(expectedCount) rows, streamed \(written). Backup discarded."
)
}
+ manifestTables.append(.init(name: table, rows: written))
status = "Exported \(table): \(written) rows"
}
+ if !exportData {
+ manifestTables.append(.init(name: table, rows: nil))
+ }
currentTableExpected = 0
currentTableRows = 0
progress = Double(index + 1) / Double(max(tables.count + views.count, 1))
@@ -2192,6 +2298,7 @@ struct WorkbenchDatabaseTransferView: View {
try write("DELIMITER $$\n")
if includeDrop { try write("DROP \(kind) IF EXISTS \(qualified)$$\n") }
try write(row[2] + "$$\nDELIMITER ;\n")
+ manifestRoutines.append(.init(name: name, kind: kind))
}
}
if exportStructure && includeTriggers {
@@ -2216,6 +2323,7 @@ struct WorkbenchDatabaseTransferView: View {
try write("DELIMITER $$\n")
if includeDrop { try write("DROP TRIGGER IF EXISTS \(qualified)$$\n") }
try write(row[2] + "$$\nDELIMITER ;\n")
+ manifestTriggers.append(name)
}
}
try Task.checkCancellation()
@@ -2232,6 +2340,15 @@ struct WorkbenchDatabaseTransferView: View {
// Sidecar failure is reported; it does not delete a completed export.
do {
try WorkbenchBackupIntegrity.createSidecar(for: url)
+ let manifest = WorkbenchBackupManifest(
+ database: database,
+ tables: manifestTables,
+ views: views,
+ triggers: manifestTriggers,
+ routines: manifestRoutines
+ )
+ try manifest.write(for: url)
+ lastBackupURL = url
progress = 1
status = "Export complete and SHA-256 verified: \(tables.count) tables, \(views.count) views, \(transferredRows.formatted()) rows"
} catch {
@@ -2294,7 +2411,8 @@ struct WorkbenchDatabaseTransferView: View {
}
}
progress = 1
- status = "Import complete: \(statementNumber) statements"
+ status = "Import complete: \(statementNumber) statements. Use Verify Restored Database to compare with the export manifest."
+ lastBackupURL = url
model.loadDatabases()
@@ -2627,3 +2745,53 @@ final class WorkbenchStreamingSQLReader {
if !statement.isEmpty { statement.append("\n") }
}
}
+
+// MARK: - Phase 12 restore validation manifest
+// This is a post-restore comparison, not a proof of byte-for-byte data equality.
+struct WorkbenchBackupManifest: Codable {
+ struct Table: Codable {
+ let name: String
+ let rows: Int64? // nil when the export contains no table data
+ }
+ struct Routine: Codable {
+ let name: String
+ let kind: String
+ }
+ let version: Int
+ let database: String
+ let tables: [Table]
+ let views: [String]
+ let triggers: [String]
+ let routines: [Routine]
+
+ init(database: String, tables: [Table], views: [String], triggers: [String], routines: [Routine]) {
+ self.version = 1
+ self.database = database
+ self.tables = tables
+ self.views = views
+ self.triggers = triggers
+ self.routines = routines
+ }
+
+ static func sidecarURL(for sql: URL) -> URL {
+ URL(fileURLWithPath: sql.path + ".manifest.json")
+ }
+
+ func write(for sql: URL) throws {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ try encoder.encode(self).write(to: Self.sidecarURL(for: sql), options: .atomic)
+ }
+
+ static func read(for sql: URL) throws -> Self {
+ let url = sidecarURL(for: sql)
+ guard FileManager.default.fileExists(atPath: url.path) else {
+ throw WorkbenchTransferError.message("No .manifest.json sidecar found. Export a backup with Phase 12 first.")
+ }
+ let manifest = try JSONDecoder().decode(Self.self, from: Data(contentsOf: url))
+ guard manifest.version == 1 else {
+ throw WorkbenchTransferError.message("Unsupported backup manifest version \(manifest.version)")
+ }
+ return manifest
+ }
+}
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors
index 3efe3db..a7b8cc3 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement-primary.priors differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule
index a23de6c..bf1efd1 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftmodule differ
diff --git a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo
index ccb2d20..12be0c5 100644
Binary files a/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo and b/build/Build/Intermediates.noindex/WorkbenchReplacement.build/Debug/WorkbenchReplacement.build/Objects-normal/arm64/WorkbenchReplacement.swiftsourceinfo differ
diff --git a/build/Logs/Build/LogStoreManifest.plist b/build/Logs/Build/LogStoreManifest.plist
index 05d847f..021c10a 100644
--- a/build/Logs/Build/LogStoreManifest.plist
+++ b/build/Logs/Build/LogStoreManifest.plist
@@ -48,48 +48,6 @@
uniqueIdentifier
2FC9B29D-EE21-4701-8634-B8C65E3A79A5
- 68955E3A-0B2E-4614-AC89-290DCD28B6D0
-
- className
- IDECommandLineBuildLog
- documentTypeString
- <nil>
- domainType
- Xcode.IDEActivityLogDomainType.BuildLog
- fileName
- 68955E3A-0B2E-4614-AC89-290DCD28B6D0.xcactivitylog
- hasPrimaryLog
-
- primaryObservable
-
- highLevelStatus
- S
- totalNumberOfAnalyzerIssues
- 0
- totalNumberOfErrors
- 0
- totalNumberOfTestFailures
- 0
- totalNumberOfWarnings
- 0
-
- schemeIdentifier-containerName
- WorkbenchReplacement project
- schemeIdentifier-schemeName
- WorkbenchReplacement
- schemeIdentifier-sharedScheme
- 1
- signature
- Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug
- timeStartedRecording
- 813877338.21300805
- timeStoppedRecording
- 813877351.33591294
- title
- Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug
- uniqueIdentifier
- 68955E3A-0B2E-4614-AC89-290DCD28B6D0
-
B2C949DC-0503-4157-BC15-F74641625B50
className
@@ -132,6 +90,48 @@
uniqueIdentifier
B2C949DC-0503-4157-BC15-F74641625B50
+ D705A532-F8F7-4B72-94B4-3464B760E3E3
+
+ className
+ IDECommandLineBuildLog
+ documentTypeString
+ <nil>
+ domainType
+ Xcode.IDEActivityLogDomainType.BuildLog
+ fileName
+ D705A532-F8F7-4B72-94B4-3464B760E3E3.xcactivitylog
+ hasPrimaryLog
+
+ primaryObservable
+
+ highLevelStatus
+ S
+ totalNumberOfAnalyzerIssues
+ 0
+ totalNumberOfErrors
+ 0
+ totalNumberOfTestFailures
+ 0
+ totalNumberOfWarnings
+ 0
+
+ schemeIdentifier-containerName
+ WorkbenchReplacement project
+ schemeIdentifier-schemeName
+ WorkbenchReplacement
+ schemeIdentifier-sharedScheme
+ 1
+ signature
+ Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug
+ timeStartedRecording
+ 813878073.32850504
+ timeStoppedRecording
+ 813878087.86567605
+ title
+ Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug
+ uniqueIdentifier
+ D705A532-F8F7-4B72-94B4-3464B760E3E3
+
EB994874-BDA8-4BAC-BAF2-6EB6F20E2468
className
diff --git a/build/ModuleCache.noindex/Session.modulevalidation b/build/ModuleCache.noindex/Session.modulevalidation
index a8e1ffc..98b29e1 100644
--- a/build/ModuleCache.noindex/Session.modulevalidation
+++ b/build/ModuleCache.noindex/Session.modulevalidation
@@ -1 +1 @@
-1792185059.890316: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
+1792185273.801373: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
diff --git a/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache b/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache
index d1d401a..5542a21 100644
Binary files a/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache and b/build/SDKStatCaches.noindex/macosx27.0-26A425-e829b21a6f8c9b70876ba0858053c0447231f6df4def33e022ae2eb8f782df1a.sdkstatcache differ
diff --git a/build/info.plist b/build/info.plist
index ba19501..7e44b95 100644
--- a/build/info.plist
+++ b/build/info.plist
@@ -3,7 +3,7 @@
LastAccessedDate
- 2026-10-16T21:10:59Z
+ 2026-10-16T21:14:33Z
WorkspacePath
/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj
diff --git a/PHASE10-NOTES.md b/docs/PHASE10-NOTES.md
similarity index 100%
rename from PHASE10-NOTES.md
rename to docs/PHASE10-NOTES.md
diff --git a/PHASE11-NOTES.md b/docs/PHASE11-NOTES.md
similarity index 100%
rename from PHASE11-NOTES.md
rename to docs/PHASE11-NOTES.md
diff --git a/docs/PHASE12-NOTES.md b/docs/PHASE12-NOTES.md
new file mode 100644
index 0000000..4abf3c2
--- /dev/null
+++ b/docs/PHASE12-NOTES.md
@@ -0,0 +1,10 @@
+# Phase 12 — Restore Validation
+
+- Exports now create `.sql.manifest.json` alongside SQL and SHA-256 files.
+- The manifest records the selected database, table names and snapshot row counts, view names, trigger names, and routine names/types.
+- **Verify Restored Database…** selects an SQL backup and checks its manifest against the selected live database. It optionally verifies the SQL SHA-256 sidecar before checking.
+- Reports mismatched table row counts, missing or inaccessible views/triggers/routines, and successful checks. Validation is read-only.
+- Structure-only exports record a `null` row count; row-count comparison is skipped for those tables.
+- This does **not** validate row content, schema definitions, or guarantee a complete/restorable backup. Do not use as a substitute for a test restore and independent backups.
+- Requires appropriate MySQL privileges for SHOW CREATE operations. No external utilities.
+- Full Xcode build and live-server tests have not been performed.