33// TablePro
44//
55
6+ import Combine
67import Foundation
78import os
89
@@ -14,14 +15,26 @@ final class SchemaService {
1415 private( set) var states : [ UUID : SchemaState ] = [ : ]
1516 private( set) var procedures : [ UUID : [ RoutineInfo ] ] = [ : ]
1617 private( set) var functions : [ UUID : [ RoutineInfo ] ] = [ : ]
18+ private( set) var schemasInOrder : [ UUID : [ String ] ] = [ : ]
1719
1820 @ObservationIgnored private var lastLoadDates : [ UUID : Date ] = [ : ]
1921 @ObservationIgnored private let loadDedup = OnceTask < UUID , [ TableInfo ] > ( )
2022 @ObservationIgnored private let procedureDedup = OnceTask < UUID , [ RoutineInfo ] > ( )
2123 @ObservationIgnored private let functionDedup = OnceTask < UUID , [ RoutineInfo ] > ( )
24+ @ObservationIgnored private let schemasDedup = OnceTask < UUID , [ String ] > ( )
25+ @ObservationIgnored private var settingsCancellable : AnyCancellable ?
26+ @ObservationIgnored private var lastDisplaySchemas : Bool = false
2227 @ObservationIgnored private static let logger = Logger ( subsystem: " com.TablePro " , category: " SchemaService " )
2328
24- init ( ) { }
29+ init ( ) {
30+ lastDisplaySchemas = AppSettingsManager . shared. sidebar. displaySchemas
31+ settingsCancellable = AppEvents . shared. sidebarSettingsChanged
32+ . sink { [ weak self] in
33+ Task { @MainActor [ weak self] in
34+ self ? . handleSidebarSettingsChange ( )
35+ }
36+ }
37+ }
2538
2639 func state( for connectionId: UUID ) -> SchemaState {
2740 states [ connectionId] ?? . idle
@@ -46,6 +59,10 @@ final class SchemaService {
4659 procedures ( for: connectionId) + functions( for: connectionId)
4760 }
4861
62+ func schemas( for connectionId: UUID ) -> [ String ] {
63+ schemasInOrder [ connectionId] ?? [ ]
64+ }
65+
4966 func load( connectionId: UUID , driver: DatabaseDriver , connection: DatabaseConnection ) async {
5067 switch state ( for: connectionId) {
5168 case . loaded:
@@ -74,9 +91,13 @@ final class SchemaService {
7491 }
7592
7693 func reloadProcedures( connectionId: UUID , driver: DatabaseDriver ) async {
94+ let visibleSchemas = visibleSchemasForGroupedReload ( connectionId: connectionId, driver: driver)
7795 do {
7896 let routines = try await procedureDedup. execute ( key: connectionId) {
79- try await driver. fetchProcedures ( schema: nil )
97+ if let schemas = visibleSchemas {
98+ return try await Self . fetchRoutinesAcrossSchemas ( driver: driver, schemas: schemas, kind: . procedure)
99+ }
100+ return try await driver. fetchProcedures ( schema: nil )
80101 }
81102 procedures [ connectionId] = routines
82103 } catch is CancellationError {
@@ -89,9 +110,13 @@ final class SchemaService {
89110 }
90111
91112 func reloadFunctions( connectionId: UUID , driver: DatabaseDriver ) async {
113+ let visibleSchemas = visibleSchemasForGroupedReload ( connectionId: connectionId, driver: driver)
92114 do {
93115 let routines = try await functionDedup. execute ( key: connectionId) {
94- try await driver. fetchFunctions ( schema: nil )
116+ if let schemas = visibleSchemas {
117+ return try await Self . fetchRoutinesAcrossSchemas ( driver: driver, schemas: schemas, kind: . function)
118+ }
119+ return try await driver. fetchFunctions ( schema: nil )
95120 }
96121 functions [ connectionId] = routines
97122 } catch is CancellationError {
@@ -107,9 +132,11 @@ final class SchemaService {
107132 await loadDedup. cancel ( key: connectionId)
108133 await procedureDedup. cancel ( key: connectionId)
109134 await functionDedup. cancel ( key: connectionId)
135+ await schemasDedup. cancel ( key: connectionId)
110136 states. removeValue ( forKey: connectionId)
111137 procedures. removeValue ( forKey: connectionId)
112138 functions. removeValue ( forKey: connectionId)
139+ schemasInOrder. removeValue ( forKey: connectionId)
113140 lastLoadDates. removeValue ( forKey: connectionId)
114141 }
115142
@@ -120,6 +147,23 @@ final class SchemaService {
120147 ) async {
121148 states [ connectionId] = . loading
122149
150+ let wantsGrouping = AppSettingsManager . shared. sidebar. displaySchemas
151+ && PluginManager . shared. supportsSchemaSwitching ( for: connection. type)
152+
153+ if wantsGrouping {
154+ await runSchemaGroupedLoad ( connectionId: connectionId, driver: driver, connection: connection)
155+ } else {
156+ await runFlatLoad ( connectionId: connectionId, driver: driver, connection: connection)
157+ }
158+ }
159+
160+ private func runFlatLoad(
161+ connectionId: UUID ,
162+ driver: DatabaseDriver ,
163+ connection: DatabaseConnection
164+ ) async {
165+ schemasInOrder. removeValue ( forKey: connectionId)
166+
123167 async let tablesTask : [ TableInfo ] = loadDedup. execute ( key: connectionId) {
124168 try await driver. fetchTables ( )
125169 }
@@ -155,6 +199,104 @@ final class SchemaService {
155199 }
156200 }
157201
202+ private func runSchemaGroupedLoad(
203+ connectionId: UUID ,
204+ driver: DatabaseDriver ,
205+ connection: DatabaseConnection
206+ ) async {
207+ let dbType = connection. type
208+ let allSchemas : [ String ]
209+ do {
210+ allSchemas = try await schemasDedup. execute ( key: connectionId) {
211+ try await driver. fetchSchemas ( )
212+ }
213+ } catch is CancellationError {
214+ return
215+ } catch {
216+ Self . logger. warning (
217+ " [schema] fetchSchemas failed connId= \( connectionId, privacy: . public) error= \( error. localizedDescription, privacy: . public) ; falling back to flat load "
218+ )
219+ await runFlatLoad ( connectionId: connectionId, driver: driver, connection: connection)
220+ return
221+ }
222+
223+ let systemSchemas = Set ( PluginManager . shared. systemSchemaNames ( for: dbType) )
224+ let visibleSchemas = allSchemas. filter { !systemSchemas. contains ( $0) }
225+ schemasInOrder [ connectionId] = visibleSchemas
226+
227+ async let tablesTask : [ TableInfo ] = loadDedup. execute ( key: connectionId) {
228+ try await Self . fetchTablesAcrossSchemas ( driver: driver, schemas: visibleSchemas)
229+ }
230+ async let proceduresTask : [ RoutineInfo ] = Self . fetchRoutinesSafely (
231+ connectionId: connectionId,
232+ kind: . procedure,
233+ dedup: procedureDedup,
234+ fetch: { try await Self . fetchRoutinesAcrossSchemas ( driver: driver, schemas: visibleSchemas, kind: . procedure) }
235+ )
236+ async let functionsTask : [ RoutineInfo ] = Self . fetchRoutinesSafely (
237+ connectionId: connectionId,
238+ kind: . function,
239+ dedup: functionDedup,
240+ fetch: { try await Self . fetchRoutinesAcrossSchemas ( driver: driver, schemas: visibleSchemas, kind: . function) }
241+ )
242+
243+ let loadedProcedures = await proceduresTask
244+ let loadedFunctions = await functionsTask
245+
246+ do {
247+ let tables = try await tablesTask
248+ states [ connectionId] = . loaded( tables)
249+ procedures [ connectionId] = loadedProcedures
250+ functions [ connectionId] = loadedFunctions
251+ lastLoadDates [ connectionId] = Date ( )
252+ } catch is CancellationError {
253+ return
254+ } catch {
255+ Self . logger. warning (
256+ " [schema] grouped load failed connId= \( connectionId, privacy: . public) error= \( error. localizedDescription, privacy: . public) "
257+ )
258+ states [ connectionId] = . failed( error. localizedDescription)
259+ }
260+ }
261+
262+ private func visibleSchemasForGroupedReload( connectionId: UUID , driver: DatabaseDriver ) -> [ String ] ? {
263+ guard AppSettingsManager . shared. sidebar. displaySchemas else { return nil }
264+ let schemas = schemasInOrder [ connectionId] ?? [ ]
265+ guard !schemas. isEmpty else { return nil }
266+ return schemas
267+ }
268+
269+ private static func fetchTablesAcrossSchemas(
270+ driver: DatabaseDriver ,
271+ schemas: [ String ]
272+ ) async throws -> [ TableInfo ] {
273+ var aggregated : [ TableInfo ] = [ ]
274+ for schema in schemas {
275+ try Task . checkCancellation ( )
276+ let tables = try await driver. fetchTables ( schema: schema)
277+ aggregated. append ( contentsOf: tables)
278+ }
279+ return aggregated
280+ }
281+
282+ private static func fetchRoutinesAcrossSchemas(
283+ driver: DatabaseDriver ,
284+ schemas: [ String ] ,
285+ kind: RoutineInfo . Kind
286+ ) async throws -> [ RoutineInfo ] {
287+ var aggregated : [ RoutineInfo ] = [ ]
288+ for schema in schemas {
289+ try Task . checkCancellation ( )
290+ let routines : [ RoutineInfo ]
291+ switch kind {
292+ case . procedure: routines = try await driver. fetchProcedures ( schema: schema)
293+ case . function: routines = try await driver. fetchFunctions ( schema: schema)
294+ }
295+ aggregated. append ( contentsOf: routines)
296+ }
297+ return aggregated
298+ }
299+
158300 private static func fetchRoutinesSafely(
159301 connectionId: UUID ,
160302 kind: RoutineInfo . Kind ,
@@ -172,4 +314,21 @@ final class SchemaService {
172314 return [ ]
173315 }
174316 }
317+
318+ private func handleSidebarSettingsChange( ) {
319+ let now = AppSettingsManager . shared. sidebar. displaySchemas
320+ guard now != lastDisplaySchemas else { return }
321+ lastDisplaySchemas = now
322+
323+ let sessions = DatabaseManager . shared. activeSessions
324+ for (connectionId, session) in sessions {
325+ guard let driver = session. driver else { continue }
326+ let connection = session. connection
327+ Task { [ weak self] in
328+ guard let self else { return }
329+ await self . invalidate ( connectionId: connectionId)
330+ await self . reload ( connectionId: connectionId, driver: driver, connection: connection)
331+ }
332+ }
333+ }
175334}
0 commit comments