added typeaheead and introspetion
This commit is contained in:
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user