Compare commits

1 Commits
Author SHA1 Message Date
Mark Gregory 3c0b2b2ae5 Improved progress display
Previously, the progress bar updated only after completing an entire table.

For large tables such as LIBARC, this could make the application appear to have stopped responding.

Phase 10 updates the interface while rows are being exported.
2026-10-16 22:09:43 +01:00
20 changed files with 67 additions and 207 deletions
-13
View File
@@ -1,13 +0,0 @@
# Phase 11 — Streaming SQL import
- Reads SQL dumps in 64 KiB file chunks, buffering only a line and the current SQL statement.
- Supports DELIMITER directives, SQL quoting, line/block comments and routine bodies.
- Reports statement number and starting line on SQL execution errors.
- Reports approximate byte progress and allows cancellation between statements.
- Verifies SHA-256 sidecar before import if present.
## Limitations
- A single huge SQL statement or line is still buffered in memory.
- Import is nontransactional and cancellation can leave partial changes.
- Parser assumes UTF-8 input and standard MySQL SQL mode (not NO_BACKSLASH_ESCAPES).
- This has not been built with Xcode or integration-tested against MySQL.
+22 -148
View File
@@ -2271,30 +2271,30 @@ struct WorkbenchDatabaseTransferView: View {
}.value
}
try Task.checkCancellation()
let fileSize = (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?.int64Value ?? 0
var statementNumber = 0
var lastLine = 1
let reader = try WorkbenchStreamingSQLReader(url: url)
defer { reader.close() }
let sql = try String(
contentsOf: url,
encoding: .utf8
)
while let entry = try reader.nextStatement() {
try Task.checkCancellation()
statementNumber += 1
lastLine = entry.line
status = "Importing statement \(statementNumber) · line \(lastLine)"
do {
_ = try await execute(entry.sql)
} catch {
throw WorkbenchTransferError.message(
"Statement \(statementNumber), line \(lastLine): \(error.localizedDescription)"
)
}
if statementNumber % 10 == 0 || reader.bytesRead >= fileSize {
progress = fileSize > 0 ? min(1, Double(reader.bytesRead) / Double(fileSize)) : 1
}
let statements = try WorkbenchSQLImportParser.parse(sql)
guard !statements.isEmpty else {
status = "No SQL statements found"
return
}
progress = 1
status = "Import complete: \(statementNumber) statements"
for (index, statement) in statements.enumerated() {
try Task.checkCancellation()
status = "Executing statement \(index + 1) of \(statements.count)"
_ = try await execute(statement)
progress = Double(index + 1) /
Double(statements.count)
}
status = "Import complete: \(statements.count) statements"
model.loadDatabases()
@@ -2501,129 +2501,3 @@ private enum WorkbenchBackupIntegrity {
}
}
}
// MARK: - Phase 11: bounded-memory SQL dump reader
// Keeps only the current SQL statement in memory. A single very large
// INSERT still requires enough memory to hold that statement.
final class WorkbenchStreamingSQLReader {
struct Entry { let sql: String; let line: Int }
private let handle: FileHandle
private var bytes = Data()
private var reachedEOF = false
private(set) var bytesRead: Int64 = 0
private var lineNumber = 0
private var statementStart = 1
private var statement = ""
private var delimiter = ";"
private var quote: Character?
private var escaped = false
private var blockComment = false
private var lineComment = false
private var pending: [Entry] = []
init(url: URL) throws { handle = try FileHandle(forReadingFrom: url) }
func close() { try? handle.close() }
func nextStatement() throws -> Entry? {
while pending.isEmpty {
guard let line = try readLine() else {
if quote != nil || blockComment {
throw WorkbenchTransferError.message("Unterminated quote or comment near line \(lineNumber)")
}
let tail = statement.trimmingCharacters(in: .whitespacesAndNewlines)
statement = ""
if !tail.isEmpty { return Entry(sql: tail, line: statementStart) }
return nil
}
lineNumber += 1
try consume(line)
}
return pending.removeFirst()
}
private func readLine() throws -> String? {
while true {
if let end = bytes.firstIndex(of: 10) {
let data = bytes.prefix(through: end)
bytes.removeSubrange(...end)
guard let line = String(data: data, encoding: .utf8) else {
throw WorkbenchTransferError.message("Invalid UTF-8 at line \(lineNumber + 1)")
}
return line
}
if reachedEOF {
guard !bytes.isEmpty else { return nil }
let data = bytes
bytes.removeAll(keepingCapacity: true)
guard let line = String(data: data, encoding: .utf8) else {
throw WorkbenchTransferError.message("Invalid UTF-8 at end of SQL file")
}
return line
}
let chunk = try handle.read(upToCount: 64 * 1024) ?? Data()
bytesRead += Int64(chunk.count)
if chunk.isEmpty { reachedEOF = true }
else { bytes.append(chunk) }
}
}
private func consume(_ line: String) throws {
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
if quote == nil && !blockComment && statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
trimmed.uppercased().hasPrefix("DELIMITER ") {
let token = String(trimmed.dropFirst(10)).trimmingCharacters(in: .whitespaces)
guard !token.isEmpty && !token.contains(where: { $0.isWhitespace }) else {
throw WorkbenchTransferError.message("Invalid DELIMITER at line \(lineNumber)")
}
delimiter = token
return
}
let chars = Array(line)
let delim = Array(delimiter)
var i = 0
lineComment = false
while i < chars.count {
let ch = chars[i]
let next: Character? = i + 1 < chars.count ? chars[i + 1] : nil
if lineComment { break }
if blockComment {
statement.append(ch)
if ch == "*" && next == "/" {
statement.append("/"); i += 2; blockComment = false
} else { i += 1 }
continue
}
if let q = quote {
statement.append(ch)
if escaped { escaped = false }
else if ch == "\\" && q != "`" { escaped = true }
else if ch == q {
if next == q { statement.append(ch); i += 1 }
else { quote = nil }
}
i += 1
continue
}
if ch == "'" || ch == "\"" || ch == "`" {
quote = ch; statement.append(ch); i += 1; continue
}
if ch == "#" || (ch == "-" && next == "-" &&
(i + 2 >= chars.count || chars[i + 2].isWhitespace)) {
lineComment = true; break
}
if ch == "/" && next == "*" {
statement.append("/*"); blockComment = true; i += 2; continue
}
if i + delim.count <= chars.count && Array(chars[i..<(i + delim.count)]) == delim {
let sql = statement.trimmingCharacters(in: .whitespacesAndNewlines)
if !sql.isEmpty { pending.append(Entry(sql: sql, line: statementStart)) }
statement = ""; i += delim.count; statementStart = lineNumber; continue
}
if statement.isEmpty && !ch.isWhitespace { statementStart = lineNumber }
statement.append(ch)
i += 1
}
if !statement.isEmpty { statement.append("\n") }
}
}
@@ -16,4 +16,3 @@
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
/Volumes/Developer/Applications/WorkbenchReplacement/build/Build/Products/Debug/WorkbenchReplacement.app
@@ -19,7 +19,7 @@
<dict>
<key>cdhash</key>
<data>
NSM7hOCoNX+DMsteke6Yge2XHGs=
6S/w6SxP3rEIpQZE9rYcyy4Jl84=
</data>
<key>requirement</key>
<string>identifier "WorkbenchReplacement.debug" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: Mark Gregory (FLBRJ3BYDD)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */</string>
+42 -42
View File
@@ -6,48 +6,6 @@
<integer>13</integer>
<key>logs</key>
<dict>
<key>2FC9B29D-EE21-4701-8634-B8C65E3A79A5</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>2FC9B29D-EE21-4701-8634-B8C65E3A79A5.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>S</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>0</integer>
</dict>
<key>schemeIdentifier-containerName</key>
<string>WorkbenchReplacement project</string>
<key>schemeIdentifier-schemeName</key>
<string>WorkbenchReplacement</string>
<key>schemeIdentifier-sharedScheme</key>
<integer>1</integer>
<key>signature</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>timeStartedRecording</key>
<real>813877859.46306098</real>
<key>timeStoppedRecording</key>
<real>813877874.00724101</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>2FC9B29D-EE21-4701-8634-B8C65E3A79A5</string>
</dict>
<key>68955E3A-0B2E-4614-AC89-290DCD28B6D0</key>
<dict>
<key>className</key>
@@ -174,6 +132,48 @@
<key>uniqueIdentifier</key>
<string>EB994874-BDA8-4BAC-BAF2-6EB6F20E2468</string>
</dict>
<key>EDDF2132-184C-41A2-A915-0EC31843BAD5</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>EDDF2132-184C-41A2-A915-0EC31843BAD5.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>S</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>0</integer>
</dict>
<key>schemeIdentifier-containerName</key>
<string>WorkbenchReplacement project</string>
<key>schemeIdentifier-schemeName</key>
<string>WorkbenchReplacement</string>
<key>schemeIdentifier-sharedScheme</key>
<integer>1</integer>
<key>signature</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>timeStartedRecording</key>
<real>813877252.80588198</real>
<key>timeStoppedRecording</key>
<real>813877266.32472801</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>EDDF2132-184C-41A2-A915-0EC31843BAD5</string>
</dict>
</dict>
</dict>
</plist>
@@ -1 +1 @@
1792185059.890316: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
1792184872.486644: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastAccessedDate</key>
<date>2026-10-16T21:10:59Z</date>
<date>2026-10-16T21:07:52Z</date>
<key>WorkspacePath</key>
<string>/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj</string>
</dict>