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-16 22:46:01 +01:00
parent 5db95d7212
commit 8db8b30ed8
28 changed files with 287 additions and 280 deletions
+81 -3
View File
@@ -750,7 +750,20 @@ struct MainView: View {
}
private var sqlEditor: some View {
HStack(spacing: 0) {
VStack(spacing: 0) {
if model.status == "Error" && !model.result.message.isEmpty {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
Text(model.result.message)
.font(.system(size: 11, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(8)
.background(Color.red.opacity(0.09))
}
HStack(spacing: 0) {
if UserDefaults.standard.object(
forKey: "preferences.showLineNumbers"
) as? Bool ?? true {
@@ -767,7 +780,8 @@ struct MainView: View {
SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, completions: Array(Set(model.schemaTables.values.flatMap { $0 } + model.schemaViews.values.flatMap { $0 } + (model.schemaColumns[model.selectedDatabase] ?? []))).sorted())
}
.background(Color(nsColor: .textBackgroundColor))
.background(Color(nsColor: .textBackgroundColor))
}
}
private var lineNumbers: some View {
@@ -1349,6 +1363,50 @@ private struct WorkbenchExplainView: View {
return value.trimmingCharacters(in: .whitespacesAndNewlines)
}
// Advisory analysis of the tabular EXPLAIN result. These are optimizer
// estimates, not measured execution statistics or automatic index DDL.
private var planInsights: [String] {
guard !useJSON, !plan.columns.isEmpty else { return [] }
let headings = plan.columns.map { $0.lowercased() }
func field(_ row: [String], _ name: String) -> String {
guard let index = headings.firstIndex(of: name), index < row.count else { return "" }
return row[index]
}
var messages: [String] = []
for row in plan.rows {
let table = field(row, "table")
let label = table.isEmpty ? "A query step" : "Table \(table)"
let access = field(row, "type").uppercased()
let key = field(row, "key")
let estimatedRows = field(row, "rows")
let extra = field(row, "extra").lowercased()
if access == "ALL" {
messages.append("\(label): full table scan (type=ALL). Check selectivity, predicates and available indexes before considering a new index.")
} else if access == "INDEX" {
messages.append("\(label): full index scan (type=index). This is not necessarily a problem for small tables or covering indexes.")
}
if key.isEmpty || key.uppercased() == "NULL" {
messages.append("\(label): no index selected for this step. An index is not always beneficial.")
}
if extra.contains("using temporary") {
messages.append("\(label): temporary table may be used for grouping, sorting or other operations.")
}
if extra.contains("using filesort") {
messages.append("\(label): additional sort operation indicated (Using filesort).")
}
if !estimatedRows.isEmpty, estimatedRows.uppercased() != "NULL" {
messages.append("\(label): optimizer estimates \(estimatedRows) rows examined for this step.")
}
}
return messages
}
private var copyablePlan: String {
if useJSON { return plan.rows.first?.first ?? "" }
return ([plan.columns.joined(separator: "\t")] + plan.rows.map { $0.joined(separator: "\t") })
.joined(separator: "\n")
}
private var eligible: Bool {
// Deliberately restrict this inspector to SELECT queries. EXPLAIN for
// UPDATE/DELETE is supported by MySQL but not offered here to keep
@@ -1385,6 +1443,11 @@ private struct WorkbenchExplainView: View {
.frame(width: 240)
Button("Run EXPLAIN") { Task { await loadPlan() } }
.disabled(isLoading || !eligible || model.selected == nil)
Button("Copy Plan") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(copyablePlan, forType: .string)
}
.disabled(plan.columns.isEmpty)
if isLoading { ProgressView().controlSize(.small) }
if let duration {
Text(String(format: "%.3f s", duration))
@@ -1413,7 +1476,22 @@ private struct WorkbenchExplainView: View {
} else {
ContentUnavailableView("No plan loaded", systemImage: "chart.bar.doc.horizontal")
}
Text("EXPLAIN shows estimates, not actual query execution times. The plan depends on the server's current statistics and configuration.")
if !planInsights.isEmpty {
DisclosureGroup("Plan observations (\(planInsights.count))") {
ScrollView {
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(planInsights.enumerated()), id: \.offset) { _, observation in
Label(observation, systemImage: "info.circle")
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.font(.callout)
.padding(.vertical, 6)
}
.frame(maxHeight: 160)
}
}
Text("EXPLAIN shows estimates, not actual query execution times. Observations are heuristics, not index recommendations. Confirm with representative data before changing production indexes.")
.font(.caption)
.foregroundStyle(.secondary)
}