added typeaheead and introspetion
This commit is contained in:
@@ -35,6 +35,7 @@ struct MainView: View {
|
||||
@State private var showingQueryPlan = false
|
||||
@State private var schemaChangesDatabase: String? = nil
|
||||
@State private var schemaCompareDatabase: String? = nil
|
||||
@State private var dataCompareDatabase: String? = nil
|
||||
@State private var showingQueryHistory = false
|
||||
@State private var showingSavedQueries = false
|
||||
@State private var showingSaveQuery = false
|
||||
@@ -98,6 +99,14 @@ struct MainView: View {
|
||||
DBSynSchemaChangesView(model: model, database: database)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { dataCompareDatabase != nil },
|
||||
set: { if !$0 { dataCompareDatabase = nil } }
|
||||
)) {
|
||||
if let database = dataCompareDatabase {
|
||||
DBSynDataCompareView(model: model, sourceDatabase: database)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { schemaCompareDatabase != nil },
|
||||
set: { if !$0 { schemaCompareDatabase = nil } }
|
||||
@@ -500,6 +509,7 @@ struct MainView: View {
|
||||
}
|
||||
Button("Review Schema Changes…") { schemaChangesDatabase = db }
|
||||
Button("Compare Schema Structures…") { schemaCompareDatabase = db }
|
||||
Button("Compare Table Data…") { dataCompareDatabase = db }
|
||||
Divider()
|
||||
Button("Set as Default Schema") { model.selectedDatabase = db }
|
||||
Button("Filter to This Schema") { schemaFilter = db }
|
||||
@@ -6542,3 +6552,114 @@ private struct DBSynSchemaCompareView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Phase 66: bounded, read-only table data comparison
|
||||
private struct DBSynDataCompareView: View {
|
||||
@ObservedObject var model: DatabaseModel
|
||||
let sourceDatabase: String
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var targetDatabase = ""
|
||||
@State private var table = ""
|
||||
@State private var isComparing = false
|
||||
@State private var report: DatabaseModel.DataComparisonReport?
|
||||
@State private var errorMessage: String?
|
||||
|
||||
private var commonTables: [String] {
|
||||
guard let source = model.introspection(for: sourceDatabase),
|
||||
let target = model.introspection(for: targetDatabase) else { return [] }
|
||||
return Array(Set(source.tables).intersection(target.tables)).sorted()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack {
|
||||
Text("Compare Table Data").font(.title2).bold()
|
||||
Spacer()
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
Text("Read-only comparison of up to 500 rows per table. No data is modified.")
|
||||
.foregroundStyle(.secondary)
|
||||
HStack {
|
||||
Text("Source: \(sourceDatabase)")
|
||||
Spacer()
|
||||
Picker("Target database", selection: $targetDatabase) {
|
||||
Text("Choose database").tag("")
|
||||
ForEach(model.databases.filter { $0 != sourceDatabase }, id: \.self) { name in
|
||||
Text(name).tag(name)
|
||||
}
|
||||
}.frame(width: 340)
|
||||
}
|
||||
HStack {
|
||||
Picker("Table", selection: $table) {
|
||||
Text("Choose table").tag("")
|
||||
ForEach(commonTables, id: \.self) { name in Text(name).tag(name) }
|
||||
}.frame(width: 420)
|
||||
Button("Refresh Metadata") {
|
||||
model.introspect(database: sourceDatabase)
|
||||
if !targetDatabase.isEmpty { model.introspect(database: targetDatabase) }
|
||||
}
|
||||
Spacer()
|
||||
Button("Compare") { compare() }
|
||||
.disabled(table.isEmpty || targetDatabase.isEmpty || isComparing)
|
||||
}
|
||||
if isComparing { ProgressView("Comparing table samples…") }
|
||||
if let errorMessage {
|
||||
Text(errorMessage).foregroundStyle(.red).textSelection(.enabled)
|
||||
}
|
||||
if let report {
|
||||
HStack {
|
||||
Text("Source: \(report.sourceRows) rows sampled")
|
||||
Text("Target: \(report.targetRows) rows sampled")
|
||||
Spacer()
|
||||
Button("Copy Report") {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(report.text, forType: .string)
|
||||
}
|
||||
}.font(.caption)
|
||||
if report.truncated {
|
||||
Label("At least one table exceeded 500 rows. Differences apply only to the retrieved samples; this is NOT a complete comparison.", systemImage: "exclamationmark.triangle")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
ScrollView {
|
||||
Text(report.text)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(12)
|
||||
}
|
||||
.background(Color(nsColor: .textBackgroundColor))
|
||||
} else if !isComparing && errorMessage == nil {
|
||||
ContentUnavailableView("Select two databases and a table", systemImage: "tablecells", description: Text("Introspect both databases to list their shared base tables."))
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(minWidth: 820, minHeight: 520)
|
||||
.onAppear {
|
||||
if targetDatabase.isEmpty {
|
||||
targetDatabase = model.databases.first(where: { $0 != sourceDatabase }) ?? ""
|
||||
}
|
||||
}
|
||||
.onChange(of: targetDatabase) { _, _ in table = ""; report = nil }
|
||||
.onChange(of: table) { _, _ in report = nil }
|
||||
}
|
||||
|
||||
private func compare() {
|
||||
isComparing = true
|
||||
errorMessage = nil
|
||||
report = nil
|
||||
let source = sourceDatabase
|
||||
let target = targetDatabase
|
||||
let selectedTable = table
|
||||
Task {
|
||||
do {
|
||||
let value = try await model.compareTableData(source: source, target: target, table: selectedTable)
|
||||
report = value
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
isComparing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user