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.")
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
+1
-1
@@ -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
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
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>
|
||||||
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>
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -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><nil></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><nil></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")
|
||||||
|
|||||||
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-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>
|
||||||
|
|||||||
Reference in New Issue
Block a user