From ecd656183108c0ffc2daa95ad0304649efa37bec Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Sun, 18 Oct 2026 16:09:06 +0100 Subject: [PATCH] new release script --- DBSyn/DatabaseModel.swift | 72 +++++++++++++++++++++++++++++++++++++++ DBSyn/MainView.swift | 11 +++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/DBSyn/DatabaseModel.swift b/DBSyn/DatabaseModel.swift index e3735b8..7425594 100644 --- a/DBSyn/DatabaseModel.swift +++ b/DBSyn/DatabaseModel.swift @@ -178,6 +178,78 @@ final class DatabaseModel: ObservableObject { @Published var schemaProcedures: [String: [String]] = [:] @Published var schemaFunctions: [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 = [] + 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 queryDurationText = "" @Published var isRunning = false diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index dbfb0fe..d9ebf6e 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -472,6 +472,15 @@ struct MainView: View { } } .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("Filter to This Schema") { schemaFilter = db } Button("Clear Schema Filter") { schemaFilter = "" } @@ -963,7 +972,7 @@ struct MainView: View { .fill(WB.border) .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)) }