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.")
.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)
.foregroundStyle(.secondary)
}
@@ -2091,145 +2091,135 @@ enum WorkbenchTransferError: LocalizedError {
}
// MARK: - SQL Import Parser
//
// Handles ordinary SQL statements with quoted strings,
// quoted identifiers, line comments and block comments.
// Stored routines and custom DELIMITER directives are
// deliberately rejected rather than executed incorrectly.
// Supports MySQL DELIMITER directives, quoted text, executable /*! */
// comments, and routine bodies. Parsing does not execute SQL.
enum WorkbenchSQLImportParser {
static func parse(_ sql: String) throws -> [String] {
let chars = Array(sql)
var statements: [String] = []
var current = ""
var quote: Character?
var escaped = false
var delimiter = ";"
var quote: Character? = nil
var lineComment = false
var blockComment = false
var escaped = false
var lineStart = true
var i = 0
let characters = Array(sql)
var index = 0
func matches(_ value: String, at index: Int) -> Bool {
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 {
let char = characters[index]
let next: Character? = index + 1 < characters.count
? characters[index + 1]
: nil
func emit() {
let value = current.trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty { statements.append(value) }
current = ""
}
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 char == "\n" {
if ch == "\n" || ch == "\r" {
lineComment = false
current.append("\n")
lineStart = true
}
index += 1
i += 1
continue
}
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
index += 2
i += 2
} else {
index += 1
current.append(ch)
i += 1
}
continue
}
if let activeQuote = quote {
current.append(char)
if escaped {
escaped = false
} else if char == "\\" && activeQuote != "`" {
escaped = true
} else if char == activeQuote {
if next == activeQuote {
if let active = quote {
current.append(ch)
if escaped { escaped = false }
else if ch == "\\" && active != "`" { escaped = true }
else if ch == active {
if next == active {
current.append(next!)
index += 1
} else {
quote = nil
}
i += 1
} else { quote = nil }
}
index += 1
i += 1
continue
}
if char == "'" || char == "\"" || char == "`" {
quote = char
current.append(char)
index += 1
if ch == "'" || ch == "\"" || ch == "`" {
quote = ch
current.append(ch)
lineStart = false
i += 1
continue
}
if char == "#"
|| (char == "-" && next == "-"
&& (index + 2 >= characters.count
|| characters[index + 2].isWhitespace)) {
if ch == "#" || (ch == "-" && next == "-" &&
(i + 2 == chars.count || chars[i + 2].isWhitespace)) {
lineComment = true
index += char == "#" ? 1 : 2
i += ch == "#" ? 1 : 2
continue
}
if char == "/", next == "*" {
// Executable MySQL comments are not safe to discard.
if index + 2 < characters.count,
characters[index + 2] == "!" {
throw WorkbenchTransferError.message(
"This SQL file contains MySQL executable comments, which are not supported by this importer yet."
)
}
if ch == "/" && next == "*" {
// Keep executable comments; ordinary comments may also contain
// SQL hints, so preserve all block comments verbatim.
current.append("/*")
blockComment = true
index += 2
i += 2
continue
}
if char == ";" {
let statement = current.trimmingCharacters(
in: .whitespacesAndNewlines
)
if !statement.isEmpty {
statements.append(statement)
}
current = ""
index += 1
if matches(delimiter, at: i) {
emit()
i += delimiter.count
lineStart = false
continue
}
current.append(char)
index += 1
current.append(ch)
if ch == "\n" || ch == "\r" { lineStart = true }
else if !ch.isWhitespace { lineStart = false }
i += 1
}
if quote != nil || blockComment {
throw WorkbenchTransferError.message(
"SQL file contains an unterminated quote or comment."
)
guard quote == nil && !blockComment else {
throw WorkbenchTransferError.message("SQL file ends inside a quoted string or block comment")
}
let remaining = current.trimmingCharacters(
in: .whitespacesAndNewlines
)
if !remaining.isEmpty {
statements.append(remaining)
guard delimiter == ";" else {
throw WorkbenchTransferError.message("SQL file ends without resetting DELIMITER to ;")
}
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."
)
}
}
emit()
return statements
}
}