Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e177b1d1ea | ||
|
|
b1bce0c166 |
@@ -0,0 +1,13 @@
|
||||
# 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.
|
||||
@@ -2271,30 +2271,30 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
}.value
|
||||
}
|
||||
try Task.checkCancellation()
|
||||
let sql = try String(
|
||||
contentsOf: url,
|
||||
encoding: .utf8
|
||||
)
|
||||
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 statements = try WorkbenchSQLImportParser.parse(sql)
|
||||
|
||||
guard !statements.isEmpty else {
|
||||
status = "No SQL statements found"
|
||||
return
|
||||
}
|
||||
|
||||
for (index, statement) in statements.enumerated() {
|
||||
while let entry = try reader.nextStatement() {
|
||||
try Task.checkCancellation()
|
||||
|
||||
status = "Executing statement \(index + 1) of \(statements.count)"
|
||||
|
||||
_ = try await execute(statement)
|
||||
|
||||
progress = Double(index + 1) /
|
||||
Double(statements.count)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
status = "Import complete: \(statements.count) statements"
|
||||
progress = 1
|
||||
status = "Import complete: \(statementNumber) statements"
|
||||
|
||||
model.loadDatabases()
|
||||
|
||||
@@ -2501,3 +2501,129 @@ 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") }
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -16,3 +16,4 @@
|
||||
/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
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+1
-1
@@ -19,7 +19,7 @@
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
6S/w6SxP3rEIpQZE9rYcyy4Jl84=
|
||||
NSM7hOCoNX+DMsteke6Yge2XHGs=
|
||||
</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>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,48 @@
|
||||
<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><nil></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>
|
||||
@@ -132,48 +174,6 @@
|
||||
<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><nil></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 @@
|
||||
1792184872.486644: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
|
||||
1792185059.890316: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex")
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastAccessedDate</key>
|
||||
<date>2026-10-16T21:07:52Z</date>
|
||||
<date>2026-10-16T21:10:59Z</date>
|
||||
<key>WorkspacePath</key>
|
||||
<string>/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj</string>
|
||||
</dict>
|
||||
|
||||
Reference in New Issue
Block a user