added typeaheead and introspetion
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user