added typeaheead and introspetion

This commit is contained in:
Mark Gregory
2026-10-18 17:13:45 +01:00
parent 9609cb31c1
commit 7f6b63d05b
2 changed files with 118 additions and 0 deletions
+108
View File
@@ -6328,6 +6328,8 @@ private struct DBSynSchemaCompareView: View {
let sourceDatabase: String
@State private var targetDatabase = ""
@Environment(\.dismiss) private var dismiss
@State private var showMigrationPreview = false
@State private var selectedMigrationIDs: Set<String> = []
private var source: DatabaseModel.SchemaIntrospection? { model.introspection(for: sourceDatabase) }
private var target: DatabaseModel.SchemaIntrospection? { model.introspection(for: targetDatabase) }
@@ -6374,6 +6376,53 @@ private struct DBSynSchemaCompareView: View {
return items
}
// Phase 65: Generate only additive column DDL. Cached introspection does not
// contain complete CREATE TABLE or CREATE VIEW definitions, so those objects
// are deliberately excluded rather than producing unsafe guesses.
private struct Migration: Identifiable {
let id: String
let label: String
let sql: String
}
private func identifier(_ name: String) -> String {
"`" + name.replacingOccurrences(of: "`", with: "``") + "`"
}
private var migrations: [Migration] {
guard let source, let target, !targetDatabase.isEmpty else { return [] }
var result: [Migration] = []
let sharedTables = Set(source.tables).intersection(target.tables)
for table in sharedTables.sorted() {
let existing = Set((target.columnsByTable[table] ?? []).map(\.name))
for column in source.columnsByTable[table] ?? [] where !existing.contains(column.name) {
// Avoid inventing defaults or generating DDL that may fail on
// populated tables. Require manual review of every statement.
var definition = "\(identifier(column.name)) \(column.dataType)"
definition += column.nullable ? " NULL" : " NOT NULL"
if column.extra.uppercased().contains("AUTO_INCREMENT") {
// Auto-increment needs key/constraint context absent from the cache.
continue
}
let statement = "ALTER TABLE \(identifier(targetDatabase)).\(identifier(table)) ADD COLUMN \(definition);"
result.append(Migration(id: "\(table).\(column.name)", label: "\(table).\(column.name)", sql: statement))
}
}
return result
}
private var selectedMigrations: [Migration] {
migrations.filter { selectedMigrationIDs.contains($0.id) }
}
private var migrationSQL: String {
let header = "-- DBSyn Phase 65: review carefully before execution.\n" +
"-- Source: \(sourceDatabase) Target: \(targetDatabase)\n" +
"-- Additive columns only; no tables, views, keys or destructive changes.\n" +
"-- MySQL DDL implicitly commits; take a backup first.\n\n"
return header + selectedMigrations.map(\.sql).joined(separator: "\n")
}
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
@@ -6408,6 +6457,18 @@ private struct DBSynSchemaCompareView: View {
}
}
}
if source != nil && target != nil {
HStack {
Button("Review Additive Migration…") {
selectedMigrationIDs = Set(migrations.map(\.id))
showMigrationPreview = true
}
.disabled(migrations.isEmpty)
Text("\(migrations.count) eligible missing columns · tables and views require full DDL")
.font(.caption).foregroundStyle(.secondary)
Spacer()
}
}
Divider()
if targetDatabase.isEmpty {
ContentUnavailableView("Choose a target database", systemImage: "square.on.square", description: Text("Select another schema on this connection."))
@@ -6432,5 +6493,52 @@ private struct DBSynSchemaCompareView: View {
.onAppear {
if targetDatabase.isEmpty { targetDatabase = model.databases.first(where: { $0 != sourceDatabase }) ?? "" }
}
.sheet(isPresented: $showMigrationPreview) {
VStack(alignment: .leading, spacing: 12) {
Text("Review Additive Migration").font(.title2).bold()
Text("Select columns to add to \(targetDatabase). Nothing is executed automatically.")
.foregroundStyle(.secondary)
List(migrations) { migration in
Toggle(migration.label, isOn: Binding(
get: { selectedMigrationIDs.contains(migration.id) },
set: { enabled in
if enabled { selectedMigrationIDs.insert(migration.id) }
else { selectedMigrationIDs.remove(migration.id) }
}
))
}
.frame(minHeight: 130)
Text("SQL Preview").font(.headline)
ScrollView {
Text(migrationSQL)
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
}
.background(.quaternary.opacity(0.3))
.frame(minHeight: 150)
Text("Warning: NOT NULL columns without defaults may fail on populated tables. Verify types, indexes and constraints manually. MySQL DDL may implicitly commit.")
.font(.caption).foregroundStyle(.secondary)
HStack {
Spacer()
Button("Cancel") { showMigrationPreview = false }
Button("Copy SQL") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(migrationSQL, forType: .string)
}
.disabled(selectedMigrations.isEmpty)
Button("Open in SQL Editor") {
model.newQueryTab()
model.sql = migrationSQL
showMigrationPreview = false
dismiss()
}
.disabled(selectedMigrations.isEmpty)
}
}
.padding(20)
.frame(minWidth: 750, minHeight: 560)
}
}
}