Previously, the importer rejected these scripts because it couldn’t handle custom statement delimiters.

The updated parser identifies the complete procedure as a single statement before passing it to the native MySQL connection.

I tested the parser with Swift, including a stored procedure containing multiple SQL statements. The parser test passed. However, I haven’t compiled the complete macOS application or tested importing against a live MySQL server.
This commit is contained in:
Mark Gregory
2026-10-16 21:55:18 +01:00
parent 819e5cf46e
commit 2b6eb249ce
18 changed files with 138 additions and 147 deletions
+91 -101
View File
@@ -1684,7 +1684,7 @@ struct WorkbenchDatabaseTransferView: View {
Text("Import SQL statements into the selected database.") Text("Import SQL statements into the selected database.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text("Supports ordinary SQL statements. Dumps using custom DELIMITER directives or stored routines are not supported yet.") Text("Supports MySQL DELIMITER directives, triggers and stored routines. Import is not transactional; use a verified backup before restoring.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@@ -2091,145 +2091,135 @@ enum WorkbenchTransferError: LocalizedError {
} }
// MARK: - SQL Import Parser // MARK: - SQL Import Parser
// // Supports MySQL DELIMITER directives, quoted text, executable /*! */
// Handles ordinary SQL statements with quoted strings, // comments, and routine bodies. Parsing does not execute SQL.
// quoted identifiers, line comments and block comments.
// Stored routines and custom DELIMITER directives are
// deliberately rejected rather than executed incorrectly.
enum WorkbenchSQLImportParser { enum WorkbenchSQLImportParser {
static func parse(_ sql: String) throws -> [String] { static func parse(_ sql: String) throws -> [String] {
let chars = Array(sql)
var statements: [String] = [] var statements: [String] = []
var current = "" var current = ""
var quote: Character? var delimiter = ";"
var escaped = false var quote: Character? = nil
var lineComment = false var lineComment = false
var blockComment = false var blockComment = false
var escaped = false
var lineStart = true
var i = 0
let characters = Array(sql) func matches(_ value: String, at index: Int) -> Bool {
var index = 0 let pattern = Array(value)
guard index + pattern.count <= chars.count else { return false }
return Array(chars[index..<(index + pattern.count)]) == pattern
}
while index < characters.count { func emit() {
let char = characters[index] let value = current.trimmingCharacters(in: .whitespacesAndNewlines)
let next: Character? = index + 1 < characters.count if !value.isEmpty { statements.append(value) }
? characters[index + 1] current = ""
: nil }
while i < chars.count {
let ch = chars[i]
let next: Character? = i + 1 < chars.count ? chars[i + 1] : nil
// DELIMITER is a client-side directive, never sent to MySQL.
if lineStart && quote == nil && !lineComment && !blockComment {
var j = i
while j < chars.count && (chars[j] == " " || chars[j] == "\t") { j += 1 }
if j + 9 <= chars.count && String(chars[j..<min(j + 9, chars.count)]).uppercased() == "DELIMITER" &&
(j + 9 == chars.count || chars[j + 9].isWhitespace) {
let pending = current.trimmingCharacters(in: .whitespacesAndNewlines)
if !pending.isEmpty {
throw WorkbenchTransferError.message("DELIMITER directive encountered inside an unfinished statement")
}
var k = j + 9
while k < chars.count && chars[k] != "\n" && chars[k] != "\r" { k += 1 }
let token = String(chars[(j + 9)..<k]).trimmingCharacters(in: .whitespacesAndNewlines)
guard !token.isEmpty && !token.contains(where: { $0.isWhitespace }) else {
throw WorkbenchTransferError.message("Invalid DELIMITER directive")
}
delimiter = token
i = k
lineStart = true
continue
}
}
if lineComment { if lineComment {
if char == "\n" { if ch == "\n" || ch == "\r" {
lineComment = false lineComment = false
current.append("\n") current.append("\n")
lineStart = true
} }
index += 1 i += 1
continue continue
} }
if blockComment { if blockComment {
if char == "*", next == "/" { // Preserve MySQL executable and optimizer-hint comments in SQL.
if ch == "*" && next == "/" {
if current.hasSuffix("*") { /* no-op */ }
current.append("*/")
blockComment = false blockComment = false
index += 2 i += 2
} else { } else {
index += 1 current.append(ch)
i += 1
} }
continue continue
} }
if let active = quote {
if let activeQuote = quote { current.append(ch)
current.append(char) if escaped { escaped = false }
else if ch == "\\" && active != "`" { escaped = true }
if escaped { else if ch == active {
escaped = false if next == active {
} else if char == "\\" && activeQuote != "`" {
escaped = true
} else if char == activeQuote {
if next == activeQuote {
current.append(next!) current.append(next!)
index += 1 i += 1
} else { } else { quote = nil }
quote = nil
}
} }
i += 1
index += 1
continue continue
} }
if ch == "'" || ch == "\"" || ch == "`" {
if char == "'" || char == "\"" || char == "`" { quote = ch
quote = char current.append(ch)
current.append(char) lineStart = false
index += 1 i += 1
continue continue
} }
if ch == "#" || (ch == "-" && next == "-" &&
if char == "#" (i + 2 == chars.count || chars[i + 2].isWhitespace)) {
|| (char == "-" && next == "-"
&& (index + 2 >= characters.count
|| characters[index + 2].isWhitespace)) {
lineComment = true lineComment = true
index += char == "#" ? 1 : 2 i += ch == "#" ? 1 : 2
continue continue
} }
if ch == "/" && next == "*" {
if char == "/", next == "*" { // Keep executable comments; ordinary comments may also contain
// Executable MySQL comments are not safe to discard. // SQL hints, so preserve all block comments verbatim.
if index + 2 < characters.count, current.append("/*")
characters[index + 2] == "!" {
throw WorkbenchTransferError.message(
"This SQL file contains MySQL executable comments, which are not supported by this importer yet."
)
}
blockComment = true blockComment = true
index += 2 i += 2
continue continue
} }
if matches(delimiter, at: i) {
if char == ";" { emit()
let statement = current.trimmingCharacters( i += delimiter.count
in: .whitespacesAndNewlines lineStart = false
)
if !statement.isEmpty {
statements.append(statement)
}
current = ""
index += 1
continue continue
} }
current.append(ch)
current.append(char) if ch == "\n" || ch == "\r" { lineStart = true }
index += 1 else if !ch.isWhitespace { lineStart = false }
i += 1
} }
guard quote == nil && !blockComment else {
if quote != nil || blockComment { throw WorkbenchTransferError.message("SQL file ends inside a quoted string or block comment")
throw WorkbenchTransferError.message(
"SQL file contains an unterminated quote or comment."
)
} }
guard delimiter == ";" else {
let remaining = current.trimmingCharacters( throw WorkbenchTransferError.message("SQL file ends without resetting DELIMITER to ;")
in: .whitespacesAndNewlines
)
if !remaining.isEmpty {
statements.append(remaining)
} }
emit()
for statement in statements {
let upper = statement.uppercased()
if upper.hasPrefix("DELIMITER ")
|| upper.hasPrefix("CREATE PROCEDURE")
|| upper.hasPrefix("CREATE FUNCTION")
|| upper.hasPrefix("CREATE TRIGGER")
|| upper.hasPrefix("CREATE EVENT") {
throw WorkbenchTransferError.message(
"Stored routines, triggers, events and DELIMITER directives require the advanced SQL importer."
)
}
}
return statements return statements
} }
} }
@@ -540,7 +540,7 @@
}, },
{ {
"comment": "", "comment": "",
"key": "Supports ordinary SQL statements. Dumps using custom DELIMITER directives or stored routines are not supported yet.", "key": "Supports MySQL DELIMITER directives, triggers and stored routines. Import is not transactional; use a verified backup before restoring.",
"location": { "location": {
"startingColumn": 30, "startingColumn": 30,
"startingLine": 1687 "startingLine": 1687
@@ -9,3 +9,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
@@ -19,7 +19,7 @@
<dict> <dict>
<key>cdhash</key> <key>cdhash</key>
<data> <data>
Iv3+eGn5ABjizzuIQDv3FNnVrWE= cNkEGkXY0XL0979Pg1Y/5+wqs/g=
</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>
+42 -42
View File
@@ -6,48 +6,6 @@
<integer>13</integer> <integer>13</integer>
<key>logs</key> <key>logs</key>
<dict> <dict>
<key>087CD1E0-7E94-4934-A62E-C6C070CD1577</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>087CD1E0-7E94-4934-A62E-C6C070CD1577.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>813876132.17963398</real>
<key>timeStoppedRecording</key>
<real>813876144.18060398</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>087CD1E0-7E94-4934-A62E-C6C070CD1577</string>
</dict>
<key>20B4B0A0-41A0-44A0-BA02-3E5707BABE60</key> <key>20B4B0A0-41A0-44A0-BA02-3E5707BABE60</key>
<dict> <dict>
<key>className</key> <key>className</key>
@@ -90,6 +48,48 @@
<key>uniqueIdentifier</key> <key>uniqueIdentifier</key>
<string>20B4B0A0-41A0-44A0-BA02-3E5707BABE60</string> <string>20B4B0A0-41A0-44A0-BA02-3E5707BABE60</string>
</dict> </dict>
<key>3389C95F-527F-43D4-BFD7-803CFE84AD6A</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>3389C95F-527F-43D4-BFD7-803CFE84AD6A.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>813876842.83179998</real>
<key>timeStoppedRecording</key>
<real>813876855.72915101</real>
<key>title</key>
<string>Building project WorkbenchReplacement with scheme WorkbenchReplacement and configuration Debug</string>
<key>uniqueIdentifier</key>
<string>3389C95F-527F-43D4-BFD7-803CFE84AD6A</string>
</dict>
<key>705B952B-2A37-40D9-B731-8E2290C15462</key> <key>705B952B-2A37-40D9-B731-8E2290C15462</key>
<dict> <dict>
<key>className</key> <key>className</key>
@@ -1 +1 @@
1792183792.223736: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/WorkbenchReplacement/build/ModuleCache.noindex") 1792184043.218933: 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"> <plist version="1.0">
<dict> <dict>
<key>LastAccessedDate</key> <key>LastAccessedDate</key>
<date>2026-10-16T20:49:52Z</date> <date>2026-10-16T20:54:03Z</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>