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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user