added typeaheead and introspetion

This commit is contained in:
Mark Gregory
2026-10-18 17:11:33 +01:00
parent bcd33b3fa5
commit 9609cb31c1
5 changed files with 373 additions and 2 deletions
+61 -2
View File
@@ -212,6 +212,57 @@ final class DatabaseModel: ObservableObject {
return selectedDatabase.isEmpty ? (selected?.database ?? "") : selectedDatabase
}
// Phase 62: compare successive successful schema introspections.
struct SchemaChange: Identifiable {
let id = UUID()
let category: String
let object: String
let detail: String
}
struct SchemaChangeReport {
let database: String
let checkedAt: Date
let changes: [SchemaChange]
}
@Published private(set) var schemaChangeReports: [String: SchemaChangeReport] = [:]
func schemaChangeReport(for database: String) -> SchemaChangeReport? {
guard let profile = selected else { return nil }
return schemaChangeReports[introspectionKey(database, profile.id)]
}
private func compareSchemas(previous: SchemaIntrospection, current: SchemaIntrospection) -> [SchemaChange] {
var changes: [SchemaChange] = []
func compareObjects(_ kind: String, _ old: [String], _ new: [String]) {
let oldSet = Set(old), newSet = Set(new)
for name in newSet.subtracting(oldSet).sorted() {
changes.append(SchemaChange(category: "Added \(kind)", object: name, detail: "New \(kind.lowercased())"))
}
for name in oldSet.subtracting(newSet).sorted() {
changes.append(SchemaChange(category: "Removed \(kind)", object: name, detail: "No longer exists"))
}
}
compareObjects("Table", previous.tables, current.tables)
compareObjects("View", previous.views, current.views)
let allObjects = Set(previous.columnsByTable.keys).union(current.columnsByTable.keys)
for object in allObjects.sorted() {
let oldColumns = Dictionary(uniqueKeysWithValues: (previous.columnsByTable[object] ?? []).map { ($0.name, $0) })
let newColumns = Dictionary(uniqueKeysWithValues: (current.columnsByTable[object] ?? []).map { ($0.name, $0) })
for name in Set(newColumns.keys).subtracting(oldColumns.keys).sorted() {
changes.append(SchemaChange(category: "Added Column", object: "\(object).\(name)", detail: newColumns[name]?.dataType ?? ""))
}
for name in Set(oldColumns.keys).subtracting(newColumns.keys).sorted() {
changes.append(SchemaChange(category: "Removed Column", object: "\(object).\(name)", detail: oldColumns[name]?.dataType ?? ""))
}
for name in Set(oldColumns.keys).intersection(newColumns.keys).sorted() {
guard let before = oldColumns[name], let after = newColumns[name] else { continue }
if before.dataType != after.dataType || before.nullable != after.nullable || before.key != after.key || before.defaultValue != after.defaultValue || before.extra != after.extra {
changes.append(SchemaChange(category: "Changed Column", object: "\(object).\(name)", detail: "\(before.dataType) → \(after.dataType)"))
}
}
}
return changes
}
@Published private(set) var introspections: [String: SchemaIntrospection] = [:]
private let introspectionStorageKey = "DBSyn.introspection.snapshots.v2"
private func persistIntrospections() {
@@ -268,11 +319,19 @@ final class DatabaseModel: ObservableObject {
}
// Ignore results from a connection that has been removed while loading.
guard profiles.contains(where: { $0.id == profile.id }) else { return }
introspections[key] = SchemaIntrospection(
let previous = introspections[key]
let current = SchemaIntrospection(
connectionID: profile.id, database: database,
loadedAt: Date(), tables: tables, views: views, columnsByTable: columns
)
if let snapshot = introspections[key] { applyIntrospection(snapshot) }
if let previous {
schemaChangeReports[key] = SchemaChangeReport(
database: database, checkedAt: current.loadedAt,
changes: compareSchemas(previous: previous, current: current)
)
}
introspections[key] = current
applyIntrospection(current)
persistIntrospections()
status = "Introspection refreshed: \(tables.count) tables, \(views.count) views, \(columns.values.reduce(0) { $0 + $1.count }) columns in \(database)"
} catch {