new release script

This commit is contained in:
Mark Gregory
2026-10-18 16:06:16 +01:00
parent 73e9a3efe2
commit abd8c59e60
4 changed files with 113 additions and 11 deletions
+16 -3
View File
@@ -224,13 +224,14 @@ struct WorkbenchSettingsView: View {
.font(.system(size: 12))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 190)
.padding(.leading, 186)
Divider()
.padding(.vertical, 12)
Text("Examples")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text(
"500 rows: Faster browsing and lower memory usage.\n" +
@@ -284,10 +285,22 @@ struct WorkbenchSettingsView: View {
Text("Light").tag("light")
Text("Dark").tag("dark")
}
.pickerStyle(.segmented)
.labelsHidden()
.frame(width: 170)
.accessibilityLabel("Application appearance")
.frame(width: 260, alignment: .leading)
}
Text(appearance == "system"
? "DBSyn follows your Mac’s appearance setting."
: appearance == "light"
? "DBSyn always uses Light appearance."
: "DBSyn always uses Dark appearance.")
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 186)
settingsRow("Navigator") {
Toggle(
"Automatically expand table folders",
@@ -327,7 +340,7 @@ struct WorkbenchSettingsView: View {
Text(title)
.font(.system(size: 13))
.frame(width: 170, alignment: .trailing)
.frame(width: 170, alignment: .leading)
content()
+86 -8
View File
@@ -455,27 +455,42 @@ final class DatabaseModel: ObservableObject {
}
private func execute(_ statement: String, listingDatabases: Bool) {
guard let profile = selected else { status = "Select a connection"; return }
guard !statement.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
let statements = listingDatabases ? [statement] : SQLStatementSplitter.split(statement)
guard !statements.isEmpty else { return }
isRunning = true
if !listingDatabases { queryDurationText = "Running…" }
let queryStarted = Date()
status = "Connecting…"
let activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
status = "Executing…"
let password = PasswordStore.read(profile.id)
Task {
var activeDB = listingDatabases ? "" : (selectedDatabase.isEmpty ? profile.database : selectedDatabase)
do {
let parsed = try await NativeMySQLRunner.run(profile: profile, database: activeDB, password: password, sql: statement)
var lastResult = QueryResult()
for item in statements {
// Each query is sent separately: COM_QUERY accepts one SQL
// statement, not an entire semicolon-separated editor buffer.
let parsed = try await NativeMySQLRunner.run(
profile: profile, database: activeDB, password: password, sql: item
)
lastResult = parsed
if let usedSchema = SQLStatementSplitter.usedDatabase(item) {
activeDB = usedSchema
selectedDatabase = usedSchema
// Metadata is cached in memory for native SQL completion.
loadTables(database: usedSchema)
}
}
if listingDatabases {
databases = parsed.rows.compactMap { $0.first }
databases = lastResult.rows.compactMap { $0.first }
status = "Connected to \(profile.name)"
} else {
result = parsed
result = lastResult
if let id = activeQueryID,
let index = queryTabs.firstIndex(where: { $0.id == id }) {
queryTabs[index].sql = sql
queryTabs[index].result = parsed
queryTabs[index].result = lastResult
}
status = "Query completed (\(parsed.rows.count) rows)"
status = "Executed \(statements.count) statement(s) (\(lastResult.rows.count) rows)"
recordQuery(statement, profile: profile, database: activeDB, succeeded: true, detail: status)
}
} catch {
@@ -491,9 +506,72 @@ final class DatabaseModel: ObservableObject {
isRunning = false
}
}
}
// Splits editor scripts without splitting semicolons inside quoted values or comments.
private enum SQLStatementSplitter {
static func split(_ source: String) -> [String] {
let chars = Array(source)
var output: [String] = []
var current = ""
var state = 0 // 0 normal, 1 single quote, 2 double quote, 3 identifier, 4 line comment, 5 block comment
var i = 0
while i < chars.count {
let c = chars[i]
let next = i + 1 < chars.count ? chars[i + 1] : "\0"
if state == 4 {
if c == "\n" || c == "\r" { state = 0; current.append(" ") }
i += 1; continue
}
if state == 5 {
if c == "*" && next == "/" { state = 0; i += 2; current.append(" ") }
else { i += 1 }
continue
}
if state == 0 {
if c == "#" || (c == "-" && next == "-" && (i + 2 == chars.count || chars[i + 2].isWhitespace)) {
state = 4; i += c == "#" ? 1 : 2; continue
}
if c == "/" && next == "*" { state = 5; i += 2; continue }
if c == "'" { state = 1 }
else if c == "\"" { state = 2 }
else if c == "`" { state = 3 }
else if c == ";" {
let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty { output.append(trimmed) }
current = ""; i += 1; continue
}
} else {
if c == "\\" && state != 3 && i + 1 < chars.count {
current.append(c); current.append(next); i += 2; continue
}
if (state == 1 && c == "'") || (state == 2 && c == "\"") || (state == 3 && c == "`") {
if next == c { current.append(c); current.append(next); i += 2; continue }
state = 0
}
}
current.append(c); i += 1
}
let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty { output.append(trimmed) }
return output
}
static func usedDatabase(_ statement: String) -> String? {
let pattern = #"(?is)^\s*USE\s+(?:`((?:``|[^`])+)`|([A-Za-z0-9_$]+))\s*$"#
guard let regex = try? NSRegularExpression(pattern: pattern),
let match = regex.firstMatch(in: statement, range: NSRange(statement.startIndex..., in: statement)) else { return nil }
for index in [1, 2] where match.range(at: index).location != NSNotFound {
if let range = Range(match.range(at: index), in: statement) {
return String(statement[range]).replacingOccurrences(of: "``", with: "`")
}
}
return nil
}
}
// MARK: - SQL Keyword Formatter
enum SQLFormatter {
+8
View File
@@ -1546,6 +1546,14 @@ struct SQLTextView: NSViewRepresentable {
}
}
// MySQL # comments are muted, including the entire line.
if let comments = try? NSRegularExpression(pattern: #"(?m)^[ \t]*#.*$"#) {
for match in comments.matches(in: editor.string, range: range) {
storage.addAttribute(.foregroundColor,
value: NSColor.secondaryLabelColor,
range: match.range)
}
}
storage.endEditing()
}
}
+3
View File
@@ -0,0 +1,3 @@
# Phase 61.2 — Appearance selector
Settings → General now has a prominent segmented control for System, Light, and Dark. Changes apply immediately and persist through `preferences.appearance`.