connection updates

This commit is contained in:
Mark Gregory
2026-10-19 12:46:18 +01:00
parent b06a605f38
commit 682132e7ac
4 changed files with 223 additions and 8 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ enum LaravelMigrationGenerator {
"""
}
private static func backtickNames(_ text: String) -> [String] {
let regex = try! NSRegularExpression(pattern: "`([^`]+)`")
guard let regex = try? NSRegularExpression(pattern: "`([^`]+)`") else { return [] }
let ns = text as NSString
return regex.matches(in: text, range: NSRange(location: 0, length: ns.length)).map { ns.substring(with: $0.range(at: 1)) }
}
+207 -6
View File
@@ -379,10 +379,21 @@ struct MainView: View {
.padding(.horizontal, 12)
.frame(height: 34)
TextField("Filter schemas", text: $schemaFilter)
.textFieldStyle(.roundedBorder)
.padding(.horizontal, 9)
.padding(.bottom, 5)
HStack(spacing: 5) {
TextField("Filter schemas", text: $schemaFilter)
.textFieldStyle(.roundedBorder)
if !schemaFilter.isEmpty {
Button { schemaFilter = "" } label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Clear schema filter")
.accessibilityLabel("Clear schema filter")
}
}
.padding(.horizontal, 9)
.padding(.bottom, 5)
Divider()
@@ -6352,6 +6363,9 @@ private struct WorkbenchSchemaEditorView: View {
@State private var message: String?
@State private var confirmExecution = false
@State private var successful = false
@State private var editedColumns: [[String]] = []
@State private var originalColumns: [[String]] = []
@State private var showSQLReview = false
private func identifier(_ value: String) -> String {
"`" + value.replacingOccurrences(of: "`", with: "``") + "`"
@@ -6389,7 +6403,7 @@ private struct WorkbenchSchemaEditorView: View {
}
if target.table != nil {
TabView {
schemaGrid(columns).tabItem { Text("Columns") }
editableColumnsGrid.tabItem { Text("Columns") }
schemaGrid(indexes).tabItem { Text("Indexes") }
ScrollView {
Text(originalDDL).font(.system(.body, design: .monospaced))
@@ -6401,6 +6415,10 @@ private struct WorkbenchSchemaEditorView: View {
}
HStack {
Text("SQL schema changes").font(.headline)
if target.table != nil {
Button("Generate SQL from Edits") { generateColumnSQL() }
.disabled(busy || editedColumns == originalColumns)
}
Spacer()
Text("Review before execution").font(.caption).foregroundStyle(.orange)
}
@@ -6412,6 +6430,14 @@ private struct WorkbenchSchemaEditorView: View {
Text("DDL may implicitly commit and cannot reliably be rolled back.")
.font(.caption).foregroundStyle(.secondary)
Spacer()
Button("Copy SQL") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(proposedSQL, forType: .string)
}
.disabled(proposedSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
.help("Copy generated SQL to paste into a DBSyn query editor")
Button("Review SQL…") { showSQLReview = true }
.disabled(busy || proposedSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
Button("Reload") { Task { await load() } }.disabled(busy)
Button("Execute DDL…") { confirmExecution = true }
.disabled(busy || proposedSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
@@ -6421,6 +6447,26 @@ private struct WorkbenchSchemaEditorView: View {
.padding(20)
.frame(minWidth: 820, minHeight: 620)
.task { await load() }
.sheet(isPresented: $showSQLReview) {
VStack(alignment: .leading, spacing: 12) {
Text("Review Schema Changes").font(.title2.bold())
Text("Copy this SQL into any DBSyn query editor, or execute it here after review.")
.foregroundStyle(.secondary)
TextEditor(text: $proposedSQL)
.font(.system(.body, design: .monospaced))
.frame(minWidth: 720, minHeight: 320)
HStack {
Button("Copy SQL") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(proposedSQL, forType: .string)
}
Spacer()
Button("Close") { showSQLReview = false }
Button("Apply…") { showSQLReview = false; confirmExecution = true }
.buttonStyle(.borderedProminent)
}
}.padding(20)
}
.confirmationDialog("Execute schema modification?", isPresented: $confirmExecution,
titleVisibility: .visible) {
Button("Execute SQL", role: .destructive) { Task { await executeDDL() } }
@@ -6430,6 +6476,124 @@ private struct WorkbenchSchemaEditorView: View {
}
}
// Only editable column properties are offered. Keys and indexes require separate DDL.
private var editableColumnsGrid: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Edit Type, Null, Default or Extra in place, then generate and review the ALTER TABLE SQL.")
.font(.caption).foregroundStyle(.secondary)
ScrollView([.horizontal, .vertical]) {
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 6) {
GridRow {
ForEach(columns.columns, id: \.self) { heading in
Text(heading).fontWeight(.semibold).frame(minWidth: 100, alignment: .leading)
}
}
Divider()
ForEach(editedColumns.indices, id: \.self) { row in
GridRow {
ForEach(editedColumns[row].indices, id: \.self) { col in
if [1, 3, 5, 6].contains(col) {
TextField(columns.columns[col], text: $editedColumns[row][col])
.textFieldStyle(.roundedBorder)
.frame(minWidth: 115)
} else {
Text(editedColumns[row][col])
.font(.system(.caption, design: .monospaced))
.frame(minWidth: 100, alignment: .leading)
.textSelection(.enabled)
}
}
}
}
}.padding(8)
}
HStack {
if editedColumns != originalColumns {
Text("Unsaved column edits").foregroundStyle(.orange)
Button("Revert Edits") { editedColumns = originalColumns }
} else {
Text("No pending column edits").foregroundStyle(.secondary)
}
Spacer()
Button("Generate ALTER TABLE SQL") { generateColumnSQL() }
.disabled(editedColumns == originalColumns)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private func generateColumnSQL() {
guard target.table != nil else { return }
let names = columns.columns
func value(_ row: [String], _ name: String) -> String {
guard let index = names.firstIndex(of: name), index < row.count else { return "" }
return row[index]
}
guard originalColumns.count == editedColumns.count else {
message = "Column metadata changed. Reload before generating DDL."
successful = false
return
}
var statements: [String] = []
for (old, updated) in zip(originalColumns, editedColumns) where old != updated {
let field = value(old, "Field")
guard field == value(updated, "Field"),
value(old, "Key") == value(updated, "Key"),
value(old, "Collation") == value(updated, "Collation"),
value(old, "Comment") == value(updated, "Comment") else {
message = "Only Type, Null, Default and Extra can be modified. Reload and retry."
successful = false
return
}
let type = value(updated, "Type").trimmingCharacters(in: .whitespacesAndNewlines)
let null = value(updated, "Null").uppercased()
let defaultValue = value(updated, "Default")
let extra = value(updated, "Extra").trimmingCharacters(in: .whitespacesAndNewlines)
// SHOW FULL COLUMNS does not contain generated-column expressions or
// every attribute from SHOW CREATE TABLE. Never reconstruct those columns.
if value(old, "Extra").uppercased().contains("GENERATED") ||
extra.uppercased().contains("GENERATED") ||
value(old, "Type").lowercased().contains("vector(") {
message = "Complex/generated columns cannot be safely modified in place. Use Original DDL and the SQL editor."
successful = false
return
}
guard !field.isEmpty, !type.isEmpty, (null == "YES" || null == "NO"),
!type.contains(";"), !type.contains("`"), !extra.contains(";"), !extra.contains("`"),
!extra.contains("--"), !extra.contains("/*") else {
message = "Invalid column definition for \(field). Check Type, Null and Extra."
successful = false
return
}
// SHOW FULL COLUMNS reports SQL NULL as the literal string NULL.
// Preserve the existing default when unchanged; generated SQL must be reviewed.
let defaultSQL: String
if defaultValue.uppercased() == "NULL" {
defaultSQL = null == "YES" ? " DEFAULT NULL" : ""
} else if defaultValue.uppercased() == "CURRENT_TIMESTAMP" || defaultValue.uppercased().hasPrefix("CURRENT_TIMESTAMP(") {
defaultSQL = " DEFAULT " + defaultValue
} else {
defaultSQL = " DEFAULT '" + defaultValue.replacingOccurrences(of: "'", with: "''") + "'"
}
let collation = value(updated, "Collation")
let collationSQL = collation != "NULL" && !collation.isEmpty &&
collation.range(of: "^[A-Za-z0-9_]+$", options: .regularExpression) != nil
? " COLLATE " + collation : ""
let comment = value(old, "Comment")
let commentSQL = comment.isEmpty || comment == "NULL" ? "" :
" COMMENT '" + comment.replacingOccurrences(of: "'", with: "''") + "'"
let clause = "MODIFY COLUMN " + identifier(field) + " " + type + collationSQL +
(null == "YES" ? " NULL" : " NOT NULL") + defaultSQL +
(extra.isEmpty || extra == "NULL" ? "" : " " + extra) + commentSQL
statements.append(clause)
}
if !statements.isEmpty {
proposedSQL = "ALTER TABLE " + qualifiedName + "\n " + statements.joined(separator: ",\n ") + ";"
message = "Generated \(statements.count) statement(s). Review SQL before applying."
successful = true
showSQLReview = true
}
}
private func schemaGrid(_ data: QueryResult) -> some View {
VStack(alignment: .leading, spacing: 6) {
if data.columns.isEmpty {
@@ -6468,6 +6632,8 @@ private struct WorkbenchSchemaEditorView: View {
defer { busy = false }
do {
columns = try await query("SHOW FULL COLUMNS FROM " + qualifiedName)
originalColumns = columns.rows
editedColumns = columns.rows
indexes = try await query("SHOW INDEX FROM " + qualifiedName)
let ddl = try await query("SHOW CREATE TABLE " + qualifiedName)
originalDDL = ddl.rows.first?.last ?? ""
@@ -6490,10 +6656,45 @@ private struct WorkbenchSchemaEditorView: View {
busy = true
defer { busy = false }
do {
let expected = editedColumns
let wasGenerated = expected != originalColumns
_ = try await query(statement)
successful = true
message = "Schema statement executed successfully."
if target.table != nil { await load() }
if target.table != nil {
await load()
// Verify the server metadata after execution, rather than trusting success alone.
if wasGenerated {
let fieldIndex = columns.columns.firstIndex(of: "Field")
let actualByName: [String: [String]] = fieldIndex.map { index in
Dictionary(columns.rows.compactMap { row -> (String, [String])? in
guard row.indices.contains(index) else { return nil }
return (row[index], row)
}, uniquingKeysWith: { first, _ in first })
} ?? [:]
let expectedNames = expected.compactMap { row -> String? in
guard let index = fieldIndex, row.indices.contains(index) else { return nil }
return row[index]
}
let verified = expectedNames.count == expected.count &&
Set(expectedNames).count == expected.count &&
columns.rows.count == expected.count &&
actualByName.count == expected.count &&
zip(expectedNames, expected).allSatisfy { name, wanted in
guard let actual = actualByName[name], wanted.count == actual.count else { return false }
return [1, 3, 5, 6].allSatisfy { index in
wanted.indices.contains(index) && actual.indices.contains(index) &&
wanted[index].caseInsensitiveCompare(actual[index]) == .orderedSame
}
}
successful = verified
message = verified
? "DDL executed and edited column metadata verified against MySQL."
: "DDL executed, but reloaded metadata differs from the requested values. Review the columns."
} else {
message = "DDL executed. Schema metadata reloaded from MySQL."
}
}
} catch {
successful = false
message = error.localizedDescription
+1 -1
View File
@@ -473,7 +473,7 @@ struct DBSynSchemaDiagramView: View {
private func printDiagram() {
let view = printLayout()
let info = NSPrintInfo.shared.copy() as! NSPrintInfo
guard let info = NSPrintInfo.shared.copy() as? NSPrintInfo else { return }
info.horizontalPagination = .fit
info.verticalPagination = .fit
info.isHorizontallyCentered = true
+14
View File
@@ -0,0 +1,14 @@
# Phase 74.9.1 — schema safety corrections
## Corrected
- Post-DDL verification checks complete column counts, unique column names, and matches properties by column name rather than zip truncation.
- Refuses inline DDL generation if the column list changed or unsupported properties were modified.
- Preserves existing SHOW FULL COLUMNS column comments in generated MODIFY COLUMN clauses.
- Refuses inline reconstruction of generated columns; edit their original DDL instead.
- Replaced a forced regular-expression construction and forced NSPrintInfo cast.
## Still unverified / outstanding
- Full Xcode/macOS compilation and UI/integration tests (Linux environment cannot execute Xcode).
- Full preservation of all possible MySQL column attributes: generated SQL MUST be reviewed; for advanced column definitions prefer Original DDL.
- Live TLS certificate verification, socket deadlines and cancellation, account administration operations, query-grid editing and large-result performance benchmarks.
- Do not execute generated DDL on production without a backup and SQL review.