added typeaheead and introspetion
This commit is contained in:
@@ -233,6 +233,60 @@ final class DatabaseModel: ObservableObject {
|
||||
return DataComparisonReport(sourceRows: a.count, targetRows: b.count, truncated: truncated, text: lines.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
// Phase 67: safe, explicit same-server INSERT preview for missing primary keys.
|
||||
// No SQL is executed by this method; the user reviews the statement first.
|
||||
struct DataSyncPreview {
|
||||
let sql: String
|
||||
let keyColumns: [String]
|
||||
let explanation: String
|
||||
}
|
||||
|
||||
func previewMissingRowsSync(source: String, target: String, table: String) throws -> DataSyncPreview {
|
||||
guard selected != nil else {
|
||||
throw NSError(domain: "DBSyn.DataSync", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connect to MySQL first."])
|
||||
}
|
||||
guard source != target, !source.isEmpty, !target.isEmpty else {
|
||||
throw NSError(domain: "DBSyn.DataSync", code: 2, userInfo: [NSLocalizedDescriptionKey: "Choose different source and target databases."])
|
||||
}
|
||||
guard let left = introspection(for: source), let right = introspection(for: target),
|
||||
left.tables.contains(table), right.tables.contains(table) else {
|
||||
throw NSError(domain: "DBSyn.DataSync", code: 3, userInfo: [NSLocalizedDescriptionKey: "Introspect both databases; the table must exist in both."])
|
||||
}
|
||||
let leftColumns = left.columnsByTable[table] ?? []
|
||||
let rightColumns = right.columnsByTable[table] ?? []
|
||||
guard !leftColumns.isEmpty,
|
||||
leftColumns.map(\.name) == rightColumns.map(\.name),
|
||||
zip(leftColumns, rightColumns).allSatisfy({ $0.dataType == $1.dataType && $0.nullable == $1.nullable }) else {
|
||||
throw NSError(domain: "DBSyn.DataSync", code: 4, userInfo: [NSLocalizedDescriptionKey: "Column names, order, types and nullability must match before generating SQL."])
|
||||
}
|
||||
let keys = leftColumns.filter { $0.key == "PRI" }.map(\.name)
|
||||
let rightKeys = rightColumns.filter { $0.key == "PRI" }.map(\.name)
|
||||
guard !keys.isEmpty, keys == rightKeys else {
|
||||
throw NSError(domain: "DBSyn.DataSync", code: 5, userInfo: [NSLocalizedDescriptionKey: "A matching primary key is required. No statement was generated."])
|
||||
}
|
||||
func q(_ value: String) -> String { "`" + value.replacingOccurrences(of: "`", with: "``") + "`" }
|
||||
let sourceTable = "\(q(source)).\(q(table))"
|
||||
let targetTable = "\(q(target)).\(q(table))"
|
||||
let columns = leftColumns.map { q($0.name) }.joined(separator: ", ")
|
||||
let sourceValues = leftColumns.map { "s.\(q($0.name))" }.joined(separator: ", ")
|
||||
let predicates = keys.map { "t.\(q($0)) = s.\(q($0))" }.joined(separator: " AND ")
|
||||
let sql = """
|
||||
-- DBSyn Phase 67: REVIEW BEFORE EXECUTING.
|
||||
-- Inserts only rows whose primary key is missing in the target.
|
||||
-- Existing target rows are NEVER updated or deleted.
|
||||
-- Take a backup and verify privileges, triggers and foreign keys first.
|
||||
INSERT INTO \(targetTable) (\(columns))
|
||||
SELECT \(sourceValues)
|
||||
FROM \(sourceTable) AS s
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM \(targetTable) AS t
|
||||
WHERE \(predicates)
|
||||
);
|
||||
"""
|
||||
return DataSyncPreview(sql: sql, keyColumns: keys,
|
||||
explanation: "Preview only. Uses matching primary key (\(keys.joined(separator: ", "))). Inserts missing keys; does not update changed rows. Review generated columns, triggers, constraints and backups before execution.")
|
||||
}
|
||||
|
||||
@Published var selectedID: UUID?
|
||||
@Published var queryTabs: [WorkbenchQueryTab] = [
|
||||
WorkbenchQueryTab(
|
||||
|
||||
Reference in New Issue
Block a user