Improved progress display

Previously, the progress bar updated only after completing an entire table.

For large tables such as LIBARC, this could make the application appear to have stopped responding.

Phase 10 updates the interface while rows are being exported.
This commit is contained in:
Mark Gregory
2026-10-18 11:53:06 +01:00
parent c1bfb36de5
commit d0d7ff257e
2 changed files with 191 additions and 0 deletions
+180
View File
@@ -37,6 +37,7 @@ struct MainView: View {
@State private var showingEditor = false
@State private var showingDatabaseTransfer = false
@State private var schemaEditorTarget: SchemaEditorTarget? = nil
@State private var objectEditorTarget: WorkbenchObjectEditorTarget? = nil
@State private var showingQueryPlan = false
@State private var showingQueryHistory = false
@State private var showingSavedQueries = false
@@ -117,6 +118,11 @@ struct MainView: View {
}) { target in
WorkbenchSchemaEditorView(profile: model.selected, target: target)
}
.sheet(item: $objectEditorTarget, onDismiss: {
if !model.selectedDatabase.isEmpty { model.loadTables(database: model.selectedDatabase) }
}) { target in
WorkbenchObjectEditorView(profile: model.selected, target: target)
}
.sheet(isPresented: $showingEditor) {
ConnectionEditor(
profile: $editing,
@@ -640,6 +646,12 @@ struct MainView: View {
Button(kind == .view ? "Select First Rows" : "Show Definition") {
openSchemaObject(database: database, name: name, kind: kind)
}
Button("Edit Definition…") {
objectEditorTarget = WorkbenchObjectEditorTarget(
database: database, name: name,
kind: kind == .view ? .view : (kind == .procedure ? .procedure : .function)
)
}
}
}
if objects.isEmpty {
@@ -5186,3 +5198,171 @@ private struct WorkbenchSchemaEditorView: View {
}
}
}
// MARK: - View and Stored Routine Definition Editor
private struct WorkbenchObjectEditorTarget: Identifiable {
enum Kind: String { case view = "VIEW", procedure = "PROCEDURE", function = "FUNCTION" }
let database: String
let name: String
let kind: Kind
var id: String { database + "." + kind.rawValue + "." + name }
}
private struct WorkbenchObjectEditorView: View {
let profile: ConnectionProfile?
let target: WorkbenchObjectEditorTarget
@Environment(\.dismiss) private var dismiss
@State private var definition = ""
@State private var originalDefinition = ""
@State private var message: String?
@State private var isError = false
@State private var busy = false
@State private var confirmSave = false
private func quote(_ value: String) -> String {
"`" + value.replacingOccurrences(of: "`", with: "``") + "`"
}
private var qualified: String { quote(target.database) + "." + quote(target.name) }
private func query(_ sql: String) async throws -> QueryResult {
guard let profile else {
throw NSError(domain: "WorkbenchObjectEditor", code: 1,
userInfo: [NSLocalizedDescriptionKey: "Connect to MySQL first."])
}
return try await NativeMySQLRunner.run(
profile: profile, database: target.database,
password: PasswordStore.read(profile.id), sql: sql
)
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Edit " + target.kind.rawValue.capitalized, systemImage: "curlybraces")
.font(.title2.bold())
Text(qualified).font(.subheadline.monospaced()).foregroundStyle(.secondary)
Spacer()
Button("Close") { dismiss() }
}
if let message {
Text(message)
.foregroundStyle(isError ? .red : .green)
.textSelection(.enabled)
}
Text("Edit the complete CREATE statement. The original definition is kept in memory until this window closes.")
.font(.caption).foregroundStyle(.secondary)
TextEditor(text: $definition)
.font(.system(.body, design: .monospaced))
.frame(minHeight: 350)
.border(Color.secondary.opacity(0.3))
if target.kind != .view {
Text("MySQL does not support CREATE OR REPLACE for routines. Saving drops and recreates the routine. This is not atomic: grants, DEFINER and concurrent callers may be affected. Back up the routine first.")
.font(.caption).foregroundStyle(.orange)
}
HStack {
Button("Reload Original") { definition = originalDefinition }.disabled(busy)
Spacer()
ProgressView().controlSize(.small).opacity(busy ? 1 : 0)
Button("Save Definition…") { confirmSave = true }
.disabled(busy || definition.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || definition == originalDefinition)
.buttonStyle(.borderedProminent)
}
}
.padding(20)
.frame(minWidth: 850, minHeight: 560)
.task { await load() }
.confirmationDialog("Apply database definition?", isPresented: $confirmSave, titleVisibility: .visible) {
Button(target.kind == .view ? "Replace View" : "Drop and Recreate " + target.kind.rawValue.capitalized,
role: .destructive) { Task { await save() } }
Button("Cancel", role: .cancel) { }
} message: {
Text(target.kind == .view
? "This will replace the view definition on the MySQL server."
: "This drops the existing routine before recreating it. MySQL cannot roll back this operation.")
}
}
@MainActor
private func load() async {
busy = true
defer { busy = false }
do {
let result = try await query("SHOW CREATE " + target.kind.rawValue + " " + qualified)
guard let row = result.rows.first,
let index = result.columns.firstIndex(where: { $0.lowercased().hasPrefix("create ") }),
row.indices.contains(index) else {
throw NSError(domain: "WorkbenchObjectEditor", code: 2,
userInfo: [NSLocalizedDescriptionKey: "MySQL did not return the CREATE definition. Check SHOW VIEW or routine privileges."])
}
originalDefinition = row[index]
definition = originalDefinition
message = nil
} catch {
isError = true
message = error.localizedDescription
}
}
private func normalized(_ sql: String) -> String {
let trimmed = sql.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.hasSuffix(";") ? String(trimmed.dropLast()) : trimmed
}
@MainActor
private func save() async {
busy = true
defer { busy = false }
let sql = normalized(definition)
guard sql.range(of: #"^CREATE\s+"#, options: [.regularExpression, .caseInsensitive]) != nil else {
isError = true
message = "The definition must start with CREATE."
return
}
let expectedKind = target.kind.rawValue
guard sql.range(of: "\\b" + expectedKind + "\\b", options: [.regularExpression, .caseInsensitive]) != nil else {
isError = true
message = "Expected a CREATE " + expectedKind + " statement."
return
}
do {
if target.kind == .view {
// SHOW CREATE VIEW may include ALGORITHM / DEFINER / SQL SECURITY.
// Insert OR REPLACE immediately after CREATE, retaining those clauses.
let replacement = sql.replacingOccurrences(
of: #"^CREATE\s+(?:OR\s+REPLACE\s+)?"#,
with: "CREATE OR REPLACE ",
options: [.regularExpression, .caseInsensitive]
)
_ = try await query(replacement)
} else {
// MySQL has no CREATE OR REPLACE PROCEDURE/FUNCTION.
_ = try await query("DROP " + target.kind.rawValue + " " + qualified)
do {
_ = try await query(sql)
} catch {
// Best effort restoration of the original definition.
do {
_ = try await query(normalized(originalDefinition))
throw NSError(domain: "WorkbenchObjectEditor", code: 3,
userInfo: [NSLocalizedDescriptionKey: "Updated routine failed: \(error.localizedDescription). The original definition was restored; verify privileges and grants."])
} catch let restoreError {
if (restoreError as NSError).code == 3 && (restoreError as NSError).domain == "WorkbenchObjectEditor" {
throw restoreError
}
throw NSError(domain: "WorkbenchObjectEditor", code: 4,
userInfo: [NSLocalizedDescriptionKey: "Updated routine failed: \(error.localizedDescription). Restoration also failed: \(restoreError.localizedDescription). Restore from your backup immediately."])
}
}
}
originalDefinition = sql
definition = sql
isError = false
message = "Definition saved successfully."
} catch {
isError = true
message = error.localizedDescription
}
}
}
+11
View File
@@ -0,0 +1,11 @@
# Phase 56 — Edit schema objects
Baseline: Phase 55. Schema navigator context menus for Views, Stored Procedures and Functions now provide **Edit Definition…**.
- Loads original DDL with `SHOW CREATE VIEW`, `SHOW CREATE PROCEDURE` or `SHOW CREATE FUNCTION`.
- Displays an editable full-definition SQL text editor, a reload-original button, and explicit save confirmation.
- Views are updated with `CREATE OR REPLACE VIEW`, preserving ALGORITHM / DEFINER clauses from MySQL's output.
- MySQL routines require DROP then CREATE; save explicitly warns that this is not atomic, and attempts to restore the original definition if recreation fails. Restoration may also fail. Backup and check grants/DEFINER before editing.
- Save errors are displayed without closing the editor. Schema objects are refreshed when the editor closes.
Swift syntax parsed on Linux; native macOS compilation and interactive database testing were not available.