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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user