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)
}
}
}
+10
View File
@@ -0,0 +1,10 @@
# Phase 65 — Reviewed additive schema migration
From **Compare Schema Structures…**, select **Review Additive Migration…**.
Choose missing columns, review the SQL, copy it, or open it in a new SQL editor tab.
## Safety boundary
The generator only creates `ALTER TABLE ... ADD COLUMN` for columns present in the source but absent in an existing target table. It deliberately does **not** generate `DROP`, `MODIFY`, table creation, view creation, key/index changes, or automatically execute any DDL. Full table and view definitions and index dependencies are not available in the existing introspection snapshot. Auto-increment columns are skipped. No statement is applied without an explicit subsequent user action in the SQL editor.
Review constraints, defaults, data compatibility, and backups before execution. MySQL DDL generally causes implicit commits. Re-introspect both databases before preparing a migration.