added typeaheead and introspetion
This commit is contained in:
@@ -162,6 +162,77 @@ final class DatabaseModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 66: compare bounded table samples without modifying either database.
|
||||
struct DataComparisonReport {
|
||||
let sourceRows: Int
|
||||
let targetRows: Int
|
||||
let truncated: Bool
|
||||
let text: String
|
||||
}
|
||||
|
||||
func compareTableData(source: String, target: String, table: String) async throws -> DataComparisonReport {
|
||||
guard let profile = selected else {
|
||||
throw NSError(domain: "DBSyn.DataCompare", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connect to MySQL first."])
|
||||
}
|
||||
guard let sourceMetadata = introspection(for: source),
|
||||
let targetMetadata = introspection(for: target),
|
||||
sourceMetadata.tables.contains(table), targetMetadata.tables.contains(table) else {
|
||||
throw NSError(domain: "DBSyn.DataCompare", code: 2, userInfo: [NSLocalizedDescriptionKey: "Introspect both databases and select a shared base table."])
|
||||
}
|
||||
let sourceColumns = (sourceMetadata.columnsByTable[table] ?? []).map(\.name)
|
||||
let targetColumns = (targetMetadata.columnsByTable[table] ?? []).map(\.name)
|
||||
guard !sourceColumns.isEmpty, sourceColumns == targetColumns else {
|
||||
throw NSError(domain: "DBSyn.DataCompare", code: 3, userInfo: [NSLocalizedDescriptionKey: "The table columns must match in name and order before comparing data."])
|
||||
}
|
||||
func quote(_ identifier: String) -> String {
|
||||
"`" + identifier.replacingOccurrences(of: "`", with: "``") + "`"
|
||||
}
|
||||
let password = PasswordStore.read(profile.id)
|
||||
let sql = "SELECT * FROM \(quote(table)) LIMIT 501"
|
||||
let left = try await NativeMySQLRunner.run(profile: profile, database: source, password: password, sql: sql)
|
||||
let right = try await NativeMySQLRunner.run(profile: profile, database: target, password: password, sql: sql)
|
||||
guard left.columns == right.columns else {
|
||||
throw NSError(domain: "DBSyn.DataCompare", code: 4, userInfo: [NSLocalizedDescriptionKey: "Returned column layouts differ; comparison aborted."])
|
||||
}
|
||||
let limit = 500
|
||||
let truncated = left.rows.count > limit || right.rows.count > limit
|
||||
let a = Array(left.rows.prefix(limit))
|
||||
let b = Array(right.rows.prefix(limit))
|
||||
// JSON encoding gives unambiguous row keys, including embedded delimiters.
|
||||
func counts(_ rows: [[String]]) -> [String: Int] {
|
||||
var output: [String: Int] = [:]
|
||||
for row in rows {
|
||||
guard let bytes = try? JSONEncoder().encode(row),
|
||||
let key = String(data: bytes, encoding: .utf8) else { continue }
|
||||
output[key, default: 0] += 1
|
||||
}
|
||||
return output
|
||||
}
|
||||
let leftCounts = counts(a)
|
||||
let rightCounts = counts(b)
|
||||
let keys = Set(leftCounts.keys).union(rightCounts.keys).sorted()
|
||||
var lines = ["DBSyn Phase 66 — Read-only Table Data Comparison",
|
||||
"Source: \(source).\(table) — \(a.count) sampled rows",
|
||||
"Target: \(target).\(table) — \(b.count) sampled rows",
|
||||
"Columns: \(left.columns.joined(separator: ", "))", ""]
|
||||
if truncated {
|
||||
lines.append("WARNING: Truncated to 500 rows per side; without ORDER BY, MySQL does not guarantee which rows are returned. These results are not a complete or deterministic comparison.")
|
||||
lines.append("")
|
||||
}
|
||||
var differences = 0
|
||||
for key in keys {
|
||||
let leftCount = leftCounts[key, default: 0]
|
||||
let rightCount = rightCounts[key, default: 0]
|
||||
if leftCount != rightCount {
|
||||
differences += 1
|
||||
lines.append("Source count \(leftCount), target count \(rightCount): \(key)")
|
||||
}
|
||||
}
|
||||
if differences == 0 { lines.append("No differences detected in retrieved rows.") }
|
||||
else { lines.insert("Distinct differing row values: \(differences)", at: 4) }
|
||||
return DataComparisonReport(sourceRows: a.count, targetRows: b.count, truncated: truncated, text: lines.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
@Published var selectedID: UUID?
|
||||
@Published var queryTabs: [WorkbenchQueryTab] = [
|
||||
WorkbenchQueryTab(
|
||||
|
||||
Reference in New Issue
Block a user