The second file contains a SHA-256 checksum of the SQL backup.

This allows the application to detect whether the backup file has changed or become corrupted.

2. Verify Backup button

The Import/Export window now includes Verify Backup….

Select an exported SQL file, and the application calculates its checksum and compares it with the accompanying checksum file.

3. Automatic verification before import

When importing an SQL file, WorkbenchReplacement checks for an accompanying .sha256 file.

If one exists, its checksum must match before SQL execution begins.

Third-party SQL files without checksum files can still be imported.

4. Large-file handling

Checksum calculation reads files in 1 MB blocks rather than loading the entire file into memory.

This reduces memory usage when verifying large backups.
This commit is contained in:
Mark Gregory
2026-10-16 22:06:06 +01:00
parent 5948387b4e
commit 0d0b46761f
23 changed files with 251 additions and 88 deletions
+60
View File
@@ -287,6 +287,35 @@ final class MySQLWireConnection {
private var hostIsLoopback = false
func markHost(_ host: String) { hostIsLoopback = ["localhost", "127.0.0.1", "::1"].contains(host.lowercased()) }
// Streams result rows without the query editor's 100,000-row limit.
// The callback is invoked on the session's serial background queue.
func streamFirstColumn(_ sql: String, onRow: (String) throws -> Void) throws -> Int64 {
sequence = 0
try send([0x03] + Array(sql.utf8))
let first = try readPacket()
try checkError(first)
guard first.first != 0x00 else { return 0 }
var cursor = ByteCursor(bytes: first)
guard let columnCount = try cursor.lenInt(), columnCount == 1 else {
throw MySQLWireError(message: "Export query must return exactly one column")
}
_ = try readPacket() // Column metadata
_ = try readPacket() // Column EOF
var count: Int64 = 0
while true {
let packet = try readPacket()
try checkError(packet)
if packet.first == 0xfe && packet.count < 9 { break }
var row = ByteCursor(bytes: packet)
guard let value = try row.lenString() else {
throw MySQLWireError(message: "Unexpected NULL export row")
}
try onRow(value)
count += 1
}
return count
}
func query(_ sql: String) throws -> QueryResult {
sequence = 0
try send([0x03] + Array(sql.utf8))
@@ -367,6 +396,23 @@ private final class PersistentMySQLSessions {
}
}
func stream(profile: ConnectionProfile, database: String,
password: String, sql: String,
onRow: (String) throws -> Void) throws -> Int64 {
try queue.sync {
guard let entry = connections[profile.id],
entry.profile == profile, entry.password == password else {
throw MySQLWireError(message: "Export session is no longer connected")
}
do {
return try entry.connection.streamFirstColumn(sql, onRow: onRow)
} catch {
connections.removeValue(forKey: profile.id)
throw error
}
}
}
func disconnect(profileID: UUID) {
queue.sync { _ = connections.removeValue(forKey: profileID) }
}
@@ -382,6 +428,20 @@ enum NativeMySQLRunner {
}.value
}
static func stream(profile: ConnectionProfile, database: String,
password: String, sql: String,
onRow: @escaping (String) throws -> Void) async throws -> Int64 {
try await Task.detached(priority: .userInitiated) {
try PersistentMySQLSessions.shared.stream(
profile: profile, database: database, password: password,
sql: sql, onRow: { row in
try Task<Never, Never>.checkCancellation()
try onRow(row)
}
)
}.value
}
static func disconnect(profileID: UUID) {
PersistentMySQLSessions.shared.disconnect(profileID: profileID)
}