new tests
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ struct DBSynApp: App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(model)
|
.environmentObject(model)
|
||||||
.sheet(isPresented: $showingMigrationGenerator) { LaravelMigrationView(model: model) }
|
.sheet(isPresented: $showingMigrationGenerator) { LaravelMigrationView(model: model, onClose: { showingMigrationGenerator = false }) }
|
||||||
.frame(minWidth: 800, minHeight: 520)
|
.frame(minWidth: 800, minHeight: 520)
|
||||||
}
|
}
|
||||||
.defaultSize(width: 1200, height: 800)
|
.defaultSize(width: 1200, height: 800)
|
||||||
|
|||||||
@@ -19,10 +19,15 @@ enum LaravelMigrationGenerator {
|
|||||||
}
|
}
|
||||||
guard depth == 0 else { throw NSError(domain: "DBSyn.Migration", code: 2, userInfo: [NSLocalizedDescriptionKey: "Unbalanced CREATE TABLE parentheses."]) }
|
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] = []
|
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 item = definition.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let upper = item.uppercased()
|
let upper = item.uppercased()
|
||||||
if upper.hasPrefix("PRIMARY KEY") {
|
if upper.hasPrefix("PRIMARY KEY") {
|
||||||
|
if autoPrimary { continue }
|
||||||
let columns = backtickNames(item)
|
let columns = backtickNames(item)
|
||||||
if columns.count == 1 {
|
if columns.count == 1 {
|
||||||
lines.append(" $table->primary('\(columns[0])');")
|
lines.append(" $table->primary('\(columns[0])');")
|
||||||
@@ -71,6 +76,10 @@ enum LaravelMigrationGenerator {
|
|||||||
var args = "'\(col)'"
|
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 == "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 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))"
|
var expr = " $table->\(method)(\(args))"
|
||||||
if upper.contains("UNSIGNED") { expr += "->unsigned()" }
|
if upper.contains("UNSIGNED") { expr += "->unsigned()" }
|
||||||
if !upper.contains("NOT NULL") { expr += "->nullable()" }
|
if !upper.contains("NOT NULL") { expr += "->nullable()" }
|
||||||
@@ -126,6 +135,9 @@ struct LaravelMigrationView: View {
|
|||||||
@State private var tableName = ""
|
@State private var tableName = ""
|
||||||
@State private var chosenDatabase = ""
|
@State private var chosenDatabase = ""
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
|
@State private var tableSearch = ""
|
||||||
|
@State private var statusText = ""
|
||||||
|
var onClose: () -> Void = {}
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -135,6 +147,8 @@ struct LaravelMigrationView: View {
|
|||||||
Text("Convert a MySQL table or CREATE TABLE SQL into a Laravel migration.").foregroundStyle(.secondary)
|
Text("Convert a MySQL table or CREATE TABLE SQL into a Laravel migration.").foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
Button(action: onClose) { Image(systemName: "xmark.circle.fill").font(.title2).foregroundStyle(.secondary) }
|
||||||
|
.buttonStyle(.plain).help("Close Migration Generator")
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -158,13 +172,16 @@ struct LaravelMigrationView: View {
|
|||||||
TextField("Enter a table name", text: $tableName)
|
TextField("Enter a table name", text: $tableName)
|
||||||
.frame(maxWidth: 330)
|
.frame(maxWidth: 330)
|
||||||
if !availableTables.isEmpty {
|
if !availableTables.isEmpty {
|
||||||
Picker("Choose table", selection: $tableName) {
|
Menu {
|
||||||
Text("Choose table…").tag("")
|
TextField("Search tables", text: $tableSearch)
|
||||||
ForEach(availableTables, id: \.self) { name in
|
Divider()
|
||||||
Text(name).tag(name)
|
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)
|
.frame(maxWidth: 230)
|
||||||
}
|
}
|
||||||
Button(isLoading ? "Loading…" : "Load Table") { loadTable() }
|
Button(isLoading ? "Loading…" : "Load Table") { loadTable() }
|
||||||
@@ -188,10 +205,12 @@ struct LaravelMigrationView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !errorText.isEmpty { Text(errorText).foregroundStyle(.red).font(.callout) }
|
if !errorText.isEmpty { Text(errorText).foregroundStyle(.red).font(.callout) }
|
||||||
|
if !statusText.isEmpty { Text(statusText).foregroundStyle(.green).font(.callout) }
|
||||||
HStack {
|
HStack {
|
||||||
Text("Review indexes, foreign keys, defaults and unsupported MySQL types before running a migration.").font(.caption).foregroundStyle(.secondary)
|
Text("Review indexes, foreign keys, defaults and unsupported MySQL types before running a migration.").font(.caption).foregroundStyle(.secondary)
|
||||||
Spacer()
|
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("Copy PHP") { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(generated, forType: .string) }.disabled(generated.isEmpty)
|
||||||
Button("Save Migration…") { save() }.disabled(generated.isEmpty)
|
Button("Save Migration…") { save() }.disabled(generated.isEmpty)
|
||||||
}
|
}
|
||||||
@@ -203,7 +222,8 @@ struct LaravelMigrationView: View {
|
|||||||
chosenDatabase = model.completionDatabase
|
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] {
|
private var databaseOptions: [String] {
|
||||||
Array(Set(model.databases + model.schemaTables.keys + [model.completionDatabase].filter { !$0.isEmpty })).sorted()
|
Array(Set(model.databases + model.schemaTables.keys + [model.completionDatabase].filter { !$0.isEmpty })).sorted()
|
||||||
@@ -211,9 +231,19 @@ struct LaravelMigrationView: View {
|
|||||||
private var availableTables: [String] {
|
private var availableTables: [String] {
|
||||||
model.schemaTables[chosenDatabase] ?? []
|
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() {
|
private func generate() {
|
||||||
do { generated = try LaravelMigrationGenerator.generate(sourceSQL); errorText = "" }
|
do {
|
||||||
catch { errorText = error.localizedDescription }
|
generated = try LaravelMigrationGenerator.generate(sourceSQL)
|
||||||
|
errorText = ""
|
||||||
|
statusText = "Migration generated — review the PHP, then Copy or Save."
|
||||||
|
} catch {
|
||||||
|
errorText = error.localizedDescription
|
||||||
|
statusText = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private func openFile() {
|
private func openFile() {
|
||||||
let panel = NSOpenPanel(); panel.allowedContentTypes = [UTType(filenameExtension: "sql") ?? .plainText, .plainText]; panel.allowsMultipleSelection = false
|
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()
|
let name = tableName.isEmpty ? "table" : tableName.lowercased()
|
||||||
panel.nameFieldStringValue = "\(prefix)_\(name)_table.php"
|
panel.nameFieldStringValue = "\(prefix)_\(name)_table.php"
|
||||||
if panel.runModal() == .OK, let url = panel.url {
|
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 }
|
catch { errorText = error.localizedDescription }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user