added typeaheead and introspetion

This commit is contained in:
Mark Gregory
2026-10-18 17:26:31 +01:00
parent d1cd3c2004
commit c0ac9d57df
3 changed files with 109 additions and 8 deletions
+19
View File
@@ -136,6 +136,25 @@ final class DatabaseModel: ObservableObject {
}
}
// Phase 68: updating a saved query keeps its stable identity and list position.
func updateSavedQuery(_ id: UUID, name: String, sql updatedSQL: String) {
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty,
!updatedSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
let index = savedQueries.firstIndex(where: { $0.id == id }) else { return }
savedQueries[index].name = trimmedName
savedQueries[index].sql = updatedSQL
persistSavedQueries()
}
func replaceSavedQueryWithEditor(_ id: UUID) {
guard let index = savedQueries.firstIndex(where: { $0.id == id }),
!sql.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
savedQueries[index].sql = sql
persistSavedQueries()
status = "Updated saved query: \(savedQueries[index].name)"
}
func deleteSavedQuery(_ id: UUID) {
savedQueries.removeAll { $0.id == id }
persistSavedQueries()
+76 -8
View File
@@ -1311,11 +1311,21 @@ private struct SQLHistorySheet: View {
@ObservedObject var model: DatabaseModel
@Environment(\.dismiss) private var dismiss
@State private var filter = ""
@State private var resultFilter = 0
@State private var selectedConnection = "All connections"
private var connections: [String] {
["All connections"] + Array(Set(model.queryHistory.map(\.connectionName))).sorted()
}
private var entries: [SQLHistoryEntry] {
model.queryHistory.filter {
filter.isEmpty || $0.sql.localizedCaseInsensitiveContains(filter) ||
$0.connectionName.localizedCaseInsensitiveContains(filter)
model.queryHistory.filter { entry in
(filter.isEmpty || entry.sql.localizedCaseInsensitiveContains(filter) ||
entry.connectionName.localizedCaseInsensitiveContains(filter) ||
entry.database.localizedCaseInsensitiveContains(filter) ||
entry.detail.localizedCaseInsensitiveContains(filter)) &&
(resultFilter == 0 || (resultFilter == 1 ? entry.succeeded : !entry.succeeded)) &&
(selectedConnection == "All connections" || entry.connectionName == selectedConnection)
}
}
@@ -1324,12 +1334,24 @@ private struct SQLHistorySheet: View {
HStack {
Text("Query History").font(.title2).bold()
Spacer()
Button("Clear History") { model.clearQueryHistory() }
Text("\(entries.count) of \(model.queryHistory.count)")
.font(.caption).foregroundStyle(.secondary)
Button("Clear History", role: .destructive) { model.clearQueryHistory() }
.disabled(model.queryHistory.isEmpty)
Button("Done") { dismiss() }
}
TextField("Filter SQL or connection", text: $filter)
.textFieldStyle(.roundedBorder)
HStack {
TextField("Filter SQL, database, or error", text: $filter)
.textFieldStyle(.roundedBorder)
Picker("Result", selection: $resultFilter) {
Text("All").tag(0)
Text("Succeeded").tag(1)
Text("Failed").tag(2)
}.frame(width: 155)
Picker("Connection", selection: $selectedConnection) {
ForEach(connections, id: \.self) { Text($0).tag($0) }
}.frame(width: 190)
}
List(entries) { entry in
VStack(alignment: .leading, spacing: 5) {
HStack {
@@ -1340,6 +1362,10 @@ private struct SQLHistorySheet: View {
Text(entry.connectionName).foregroundStyle(.secondary)
Text(entry.database).foregroundStyle(.secondary)
Spacer()
Button("Copy SQL") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(entry.sql, forType: .string)
}
Button("Open in New Tab") {
model.newQueryTab()
model.sql = entry.sql
@@ -1354,7 +1380,7 @@ private struct SQLHistorySheet: View {
}
}
.padding(20)
.frame(minWidth: 740, minHeight: 460)
.frame(minWidth: 800, minHeight: 480)
}
}
@@ -1362,12 +1388,17 @@ private struct SavedSQLQueriesSheet: View {
@ObservedObject var model: DatabaseModel
@Environment(\.dismiss) private var dismiss
@State private var filter = ""
@State private var editingID: UUID?
@State private var editedName = ""
@State private var editedSQL = ""
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Saved SQL Queries").font(.title2).bold()
Spacer()
Text("\(model.savedQueries.count) saved")
.font(.caption).foregroundStyle(.secondary)
Button("Done") { dismiss() }
}
TextField("Filter saved queries", text: $filter)
@@ -1383,6 +1414,15 @@ private struct SavedSQLQueriesSheet: View {
.lineLimit(2).foregroundStyle(.secondary)
}
Spacer()
Button("Copy SQL") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(entry.sql, forType: .string)
}
Button("Edit") {
editingID = entry.id
editedName = entry.name
editedSQL = entry.sql
}
Button("Open") {
model.openSavedQuery(entry)
dismiss()
@@ -1396,7 +1436,35 @@ private struct SavedSQLQueriesSheet: View {
}
}
.padding(20)
.frame(minWidth: 650, minHeight: 420)
.frame(minWidth: 750, minHeight: 450)
.sheet(isPresented: Binding(
get: { editingID != nil },
set: { if !$0 { editingID = nil } }
)) {
VStack(alignment: .leading, spacing: 12) {
Text("Edit Saved Query").font(.title2).bold()
TextField("Query name", text: $editedName)
TextEditor(text: $editedSQL)
.font(.system(.body, design: .monospaced))
.frame(minHeight: 220)
.border(Color.secondary.opacity(0.3))
HStack {
Spacer()
Button("Cancel") { editingID = nil }
Button("Save Changes") {
if let id = editingID {
model.updateSavedQuery(id, name: editedName, sql: editedSQL)
}
editingID = nil
}
.keyboardShortcut(.defaultAction)
.disabled(editedName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
editedSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
.padding(20)
.frame(minWidth: 620, minHeight: 350)
}
}
}
+14
View File
@@ -0,0 +1,14 @@
# Phase 68 — SQL Productivity
Based on Phase 67.
- Query History: search by SQL, connection, schema, or execution detail; filter by successful/failed execution and connection.
- Query History: copy SQL to clipboard, open a historical query in a new editor tab, and see matching result counts.
- Saved SQL Queries: edit the name and SQL of existing saved queries, with validation before saving.
- Saved SQL Queries: copy saved SQL without opening or executing it.
- Saved query edits preserve stable IDs and are persisted through the existing storage mechanism.
- No SQL is executed by browsing, editing, or copying history and saved queries.
## Verification
Swift source was checked structurally, but full Xcode compilation and macOS UI testing are required.