From c0ac9d57df23b676bebdbc717c860523f7fa1c2e Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Sun, 18 Oct 2026 17:26:31 +0100 Subject: [PATCH] added typeaheead and introspetion --- DBSyn/DatabaseModel.swift | 19 ++++++++ DBSyn/MainView.swift | 84 +++++++++++++++++++++++++++++--- docs/PHASE68-SQL-PRODUCTIVITY.md | 14 ++++++ 3 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 docs/PHASE68-SQL-PRODUCTIVITY.md diff --git a/DBSyn/DatabaseModel.swift b/DBSyn/DatabaseModel.swift index 0d979a3..102f89b 100644 --- a/DBSyn/DatabaseModel.swift +++ b/DBSyn/DatabaseModel.swift @@ -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() diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index 5477652..fc4470e 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -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) + } } } diff --git a/docs/PHASE68-SQL-PRODUCTIVITY.md b/docs/PHASE68-SQL-PRODUCTIVITY.md new file mode 100644 index 0000000..c68766b --- /dev/null +++ b/docs/PHASE68-SQL-PRODUCTIVITY.md @@ -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.