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
|
}.value
|
||||||
}
|
}
|
||||||
try Task.checkCancellation()
|
try Task.checkCancellation()
|
||||||
let sql = try String(
|
let fileSize = (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?.int64Value ?? 0
|
||||||
contentsOf: url,
|
var statementNumber = 0
|
||||||
encoding: .utf8
|
var lastLine = 1
|
||||||
)
|
let reader = try WorkbenchStreamingSQLReader(url: url)
|
||||||
|
defer { reader.close() }
|
||||||
|
|
||||||
let statements = try WorkbenchSQLImportParser.parse(sql)
|
while let entry = try reader.nextStatement() {
|
||||||
|
|
||||||
guard !statements.isEmpty else {
|
|
||||||
status = "No SQL statements found"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, statement) in statements.enumerated() {
|
|
||||||
try Task.checkCancellation()
|
try Task.checkCancellation()
|
||||||
|
statementNumber += 1
|
||||||
status = "Executing statement \(index + 1) of \(statements.count)"
|
lastLine = entry.line
|
||||||
|
status = "Importing statement \(statementNumber) · line \(lastLine)"
|
||||||
_ = try await execute(statement)
|
do {
|
||||||
|
_ = try await execute(entry.sql)
|
||||||
progress = Double(index + 1) /
|
} catch {
|
||||||
Double(statements.count)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
progress = 1
|
||||||
status = "Import complete: \(statements.count) statements"
|
status = "Import complete: \(statementNumber) statements"
|
||||||
|
|
||||||
model.loadDatabases()
|
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
|
||||||
/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>
|
<dict>
|
||||||
<key>cdhash</key>
|
<key>cdhash</key>
|
||||||
<data>
|
<data>
|
||||||
6S/w6SxP3rEIpQZE9rYcyy4Jl84=
|
NSM7hOCoNX+DMsteke6Yge2XHGs=
|
||||||
</data>
|
</data>
|
||||||
<key>requirement</key>
|
<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>
|
<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>
|
<integer>13</integer>
|
||||||
<key>logs</key>
|
<key>logs</key>
|
||||||
<dict>
|
<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>
|
<key>68955E3A-0B2E-4614-AC89-290DCD28B6D0</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>className</key>
|
<key>className</key>
|
||||||
@@ -132,48 +174,6 @@
|
|||||||
<key>uniqueIdentifier</key>
|
<key>uniqueIdentifier</key>
|
||||||
<string>EB994874-BDA8-4BAC-BAF2-6EB6F20E2468</string>
|
<string>EB994874-BDA8-4BAC-BAF2-6EB6F20E2468</string>
|
||||||
</dict>
|
</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>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</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">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>LastAccessedDate</key>
|
<key>LastAccessedDate</key>
|
||||||
<date>2026-10-16T21:07:52Z</date>
|
<date>2026-10-16T21:10:59Z</date>
|
||||||
<key>WorkspacePath</key>
|
<key>WorkspacePath</key>
|
||||||
<string>/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj</string>
|
<string>/Volumes/Developer/Applications/WorkbenchReplacement/WorkbenchReplacement.xcodeproj</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Reference in New Issue
Block a user