new tests

This commit is contained in:
Mark Gregory
2026-10-19 10:22:14 +01:00
parent ab8783a1f7
commit 6251f2490b
2 changed files with 42 additions and 12 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ struct DBSynApp: App {
}
}
.environmentObject(model)
.sheet(isPresented: $showingMigrationGenerator) { LaravelMigrationView(model: model) }
.sheet(isPresented: $showingMigrationGenerator) { LaravelMigrationView(model: model, onClose: { showingMigrationGenerator = false }) }
.frame(minWidth: 800, minHeight: 520)
}
.defaultSize(width: 1200, height: 800)
+41 -11
View File
@@ -19,10 +19,15 @@ enum LaravelMigrationGenerator {
}
guard depth == 0 else { throw NSError(domain: "DBSyn.Migration", code: 2, userInfo: [NSLocalizedDescriptionKey: "Unbalanced CREATE TABLE parentheses."]) }
let body = String(ddl[start..<end]); var lines: [String] = []
for definition in splitColumns(body) {
let definitions = splitColumns(body)
let primaryKeys = definitions.filter { $0.trimmingCharacters(in: .whitespacesAndNewlines).uppercased().hasPrefix("PRIMARY KEY") }
.flatMap { backtickNames($0) }
let autoPrimary = definitions.contains { $0.uppercased().contains("AUTO_INCREMENT") } && primaryKeys.count == 1
for definition in definitions {
let item = definition.trimmingCharacters(in: .whitespacesAndNewlines)
let upper = item.uppercased()
if upper.hasPrefix("PRIMARY KEY") {
if autoPrimary { continue }
let columns = backtickNames(item)
if columns.count == 1 {
lines.append(" $table->primary('\(columns[0])');")
@@ -71,6 +76,10 @@ enum LaravelMigrationGenerator {
var args = "'\(col)'"
if method == "string" || method == "char", let range = type.range(of: #"\(\s*\d+\s*\)"#, options: .regularExpression) { args += ", " + type[range].filter(\.isNumber) }
if method == "decimal", let range = type.range(of: #"\(\s*\d+\s*,\s*\d+\s*\)"#, options: .regularExpression) { args += ", " + type[range].dropFirst().dropLast() }
if upper.contains("AUTO_INCREMENT") && primaryKeys == [col] && method == "bigInteger" {
lines.append(" $table->id('\(col)');")
continue
}
var expr = " $table->\(method)(\(args))"
if upper.contains("UNSIGNED") { expr += "->unsigned()" }
if !upper.contains("NOT NULL") { expr += "->nullable()" }
@@ -126,6 +135,9 @@ struct LaravelMigrationView: View {
@State private var tableName = ""
@State private var chosenDatabase = ""
@State private var isLoading = false
@State private var tableSearch = ""
@State private var statusText = ""
var onClose: () -> Void = {}
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
@@ -135,6 +147,8 @@ struct LaravelMigrationView: View {
Text("Convert a MySQL table or CREATE TABLE SQL into a Laravel migration.").foregroundStyle(.secondary)
}
Spacer()
Button(action: onClose) { Image(systemName: "xmark.circle.fill").font(.title2).foregroundStyle(.secondary) }
.buttonStyle(.plain).help("Close Migration Generator")
}
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 12) {
@@ -158,13 +172,16 @@ struct LaravelMigrationView: View {
TextField("Enter a table name", text: $tableName)
.frame(maxWidth: 330)
if !availableTables.isEmpty {
Picker("Choose table", selection: $tableName) {
Text("Choose table…").tag("")
ForEach(availableTables, id: \.self) { name in
Text(name).tag(name)
Menu {
TextField("Search tables", text: $tableSearch)
Divider()
ForEach(filteredTables.prefix(100), id: \.self) { name in
Button(name) { tableName = name }
}
if filteredTables.isEmpty { Text("No matching tables") }
} label: {
Label("Browse Tables (\(availableTables.count))", systemImage: "magnifyingglass")
}
.labelsHidden()
.frame(maxWidth: 230)
}
Button(isLoading ? "Loading…" : "Load Table") { loadTable() }
@@ -188,10 +205,12 @@ struct LaravelMigrationView: View {
}
}
if !errorText.isEmpty { Text(errorText).foregroundStyle(.red).font(.callout) }
if !statusText.isEmpty { Text(statusText).foregroundStyle(.green).font(.callout) }
HStack {
Text("Review indexes, foreign keys, defaults and unsupported MySQL types before running a migration.").font(.caption).foregroundStyle(.secondary)
Spacer()
Button("Generate") { generate() }.buttonStyle(.borderedProminent)
Button("Generate / Refresh") { generate() }.buttonStyle(.borderedProminent)
.disabled(sourceSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
Button("Copy PHP") { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(generated, forType: .string) }.disabled(generated.isEmpty)
Button("Save Migration…") { save() }.disabled(generated.isEmpty)
}
@@ -203,7 +222,8 @@ struct LaravelMigrationView: View {
chosenDatabase = model.completionDatabase
}
}
.onChange(of: chosenDatabase) { _, _ in tableName = "" }
.onChange(of: chosenDatabase) { _, _ in tableName = ""; tableSearch = "" }
.onChange(of: sourceSQL) { _, _ in statusText = "Source changed — select Generate / Refresh to update PHP." }
}
private var databaseOptions: [String] {
Array(Set(model.databases + model.schemaTables.keys + [model.completionDatabase].filter { !$0.isEmpty })).sorted()
@@ -211,9 +231,19 @@ struct LaravelMigrationView: View {
private var availableTables: [String] {
model.schemaTables[chosenDatabase] ?? []
}
private var filteredTables: [String] {
let query = tableSearch.trimmingCharacters(in: .whitespacesAndNewlines)
return query.isEmpty ? availableTables : availableTables.filter { $0.localizedCaseInsensitiveContains(query) }
}
private func generate() {
do { generated = try LaravelMigrationGenerator.generate(sourceSQL); errorText = "" }
catch { errorText = error.localizedDescription }
do {
generated = try LaravelMigrationGenerator.generate(sourceSQL)
errorText = ""
statusText = "Migration generated — review the PHP, then Copy or Save."
} catch {
errorText = error.localizedDescription
statusText = ""
}
}
private func openFile() {
let panel = NSOpenPanel(); panel.allowedContentTypes = [UTType(filenameExtension: "sql") ?? .plainText, .plainText]; panel.allowsMultipleSelection = false
@@ -248,7 +278,7 @@ struct LaravelMigrationView: View {
let name = tableName.isEmpty ? "table" : tableName.lowercased()
panel.nameFieldStringValue = "\(prefix)_\(name)_table.php"
if panel.runModal() == .OK, let url = panel.url {
do { try generated.write(to: url, atomically: true, encoding: .utf8); errorText = "" }
do { try generated.write(to: url, atomically: true, encoding: .utf8); errorText = ""; statusText = "Saved: \(url.lastPathComponent)" }
catch { errorText = error.localizedDescription }
}
}