new release script
This commit is contained in:
@@ -178,6 +178,78 @@ final class DatabaseModel: ObservableObject {
|
|||||||
@Published var schemaProcedures: [String: [String]] = [:]
|
@Published var schemaProcedures: [String: [String]] = [:]
|
||||||
@Published var schemaFunctions: [String: [String]] = [:]
|
@Published var schemaFunctions: [String: [String]] = [:]
|
||||||
@Published var schemaColumns: [String: [String]] = [:]
|
@Published var schemaColumns: [String: [String]] = [:]
|
||||||
|
// Introspection is a connection-scoped, RAM-only snapshot of table structures.
|
||||||
|
struct IntrospectedColumn: Identifiable {
|
||||||
|
var id: String { "\(table).\(name)" }
|
||||||
|
let table: String
|
||||||
|
let name: String
|
||||||
|
let dataType: String
|
||||||
|
let nullable: Bool
|
||||||
|
let key: String
|
||||||
|
let defaultValue: String?
|
||||||
|
let extra: String
|
||||||
|
}
|
||||||
|
struct SchemaIntrospection {
|
||||||
|
let connectionID: UUID
|
||||||
|
let database: String
|
||||||
|
let loadedAt: Date
|
||||||
|
let tables: [String]
|
||||||
|
let columnsByTable: [String: [IntrospectedColumn]]
|
||||||
|
}
|
||||||
|
@Published private(set) var introspections: [String: SchemaIntrospection] = [:]
|
||||||
|
@Published private(set) var introspectionLoading: Set<String> = []
|
||||||
|
private func introspectionKey(_ database: String, _ connectionID: UUID) -> String {
|
||||||
|
"\(connectionID.uuidString):\(database)"
|
||||||
|
}
|
||||||
|
func introspection(for database: String) -> SchemaIntrospection? {
|
||||||
|
guard let profile = selected else { return nil }
|
||||||
|
return introspections[introspectionKey(database, profile.id)]
|
||||||
|
}
|
||||||
|
func introspect(database: String) {
|
||||||
|
guard let profile = selected else { status = "Select a connection"; return }
|
||||||
|
let key = introspectionKey(database, profile.id)
|
||||||
|
guard !introspectionLoading.contains(key) else { return }
|
||||||
|
introspectionLoading.insert(key)
|
||||||
|
status = "Introspecting \(database)…"
|
||||||
|
let password = PasswordStore.read(profile.id)
|
||||||
|
let escaped = database.replacingOccurrences(of: "'", with: "''")
|
||||||
|
Task {
|
||||||
|
defer { introspectionLoading.remove(key) }
|
||||||
|
do {
|
||||||
|
let tableResult = try await NativeMySQLRunner.run(
|
||||||
|
profile: profile, database: database, password: password,
|
||||||
|
sql: "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '\(escaped)' AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME"
|
||||||
|
)
|
||||||
|
let columnResult = try await NativeMySQLRunner.run(
|
||||||
|
profile: profile, database: database, password: password,
|
||||||
|
sql: "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '\(escaped)' ORDER BY TABLE_NAME, ORDINAL_POSITION"
|
||||||
|
)
|
||||||
|
let tables = tableResult.rows.compactMap { $0.first }
|
||||||
|
var columns: [String: [IntrospectedColumn]] = [:]
|
||||||
|
for row in columnResult.rows where row.count >= 7 {
|
||||||
|
let column = IntrospectedColumn(
|
||||||
|
table: row[0], name: row[1], dataType: row[2],
|
||||||
|
nullable: row[3] == "YES", key: row[4],
|
||||||
|
defaultValue: row[5] == "NULL" ? nil : row[5], extra: row[6]
|
||||||
|
)
|
||||||
|
columns[row[0], default: []].append(column)
|
||||||
|
}
|
||||||
|
// Ignore results from a connection that has been removed while loading.
|
||||||
|
guard profiles.contains(where: { $0.id == profile.id }) else { return }
|
||||||
|
introspections[key] = SchemaIntrospection(
|
||||||
|
connectionID: profile.id, database: database,
|
||||||
|
loadedAt: Date(), tables: tables, columnsByTable: columns
|
||||||
|
)
|
||||||
|
schemaTables[database] = tables
|
||||||
|
schemaColumns[database] = Array(Set(columns.values.flatMap { $0 }.flatMap {
|
||||||
|
[$0.name, "\($0.table).\($0.name)"]
|
||||||
|
})).sorted()
|
||||||
|
status = "Introspection complete: \(tables.count) tables, \(columns.values.reduce(0) { $0 + $1.count }) columns in \(database) (RAM)"
|
||||||
|
} catch {
|
||||||
|
status = "Introspection failed for \(database): \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@Published var selectedDatabase = ""
|
@Published var selectedDatabase = ""
|
||||||
@Published var queryDurationText = ""
|
@Published var queryDurationText = ""
|
||||||
@Published var isRunning = false
|
@Published var isRunning = false
|
||||||
|
|||||||
+10
-1
@@ -472,6 +472,15 @@ struct MainView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
model.introspect(database: db)
|
||||||
|
} label: {
|
||||||
|
Label("Introspection — Load Table Structures into RAM", systemImage: "memorychip")
|
||||||
|
}
|
||||||
|
if let snapshot = model.introspection(for: db) {
|
||||||
|
Text("Cached: \(snapshot.tables.count) tables, \(snapshot.columnsByTable.values.reduce(0) { $0 + $1.count }) columns")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
Button("Set as Default Schema") { model.selectedDatabase = db }
|
Button("Set as Default Schema") { model.selectedDatabase = db }
|
||||||
Button("Filter to This Schema") { schemaFilter = db }
|
Button("Filter to This Schema") { schemaFilter = db }
|
||||||
Button("Clear Schema Filter") { schemaFilter = "" }
|
Button("Clear Schema Filter") { schemaFilter = "" }
|
||||||
@@ -963,7 +972,7 @@ struct MainView: View {
|
|||||||
.fill(WB.border)
|
.fill(WB.border)
|
||||||
.frame(width: 1)
|
.frame(width: 1)
|
||||||
|
|
||||||
SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, focusRequest: sqlFocusRequest, completions: Array(Set(model.schemaTables.values.flatMap { $0 } + model.schemaViews.values.flatMap { $0 } + (model.schemaColumns[model.selectedDatabase] ?? []))).sorted())
|
SQLTextView(text: $model.sql, selectedSQL: $selectedSQL, focusRequest: sqlFocusRequest, completions: Array(Set((model.schemaTables[model.selectedDatabase] ?? []) + (model.schemaViews[model.selectedDatabase] ?? []) + (model.schemaColumns[model.selectedDatabase] ?? []))).sorted())
|
||||||
}
|
}
|
||||||
.background(Color(nsColor: .textBackgroundColor))
|
.background(Color(nsColor: .textBackgroundColor))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user