Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Other-schema tables for Open Quickly and the sidebar filter read in one query on SQL Server.
- Other-schema tables for Open Quickly and the sidebar filter read in one query on DuckDB files.
- Tables and views from every schema in the MCP `search_schema` tool when no schema is named. (#3048)
- Sidebar filter on Oracle, Snowflake and BigQuery matching procedures, triggers and types only in schemas already read.

### Removed

Expand All @@ -103,6 +104,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Stale column and MongoDB field suggestions when a refresh ran while they were loading.
- Tables in an expanded Oracle or Snowflake schema missing from Open Quickly until the next refresh.
- Tables from the previous database listed under a schema after switching database on Snowflake or Trino.
- Hundreds of catalog queries from one keystroke in the sidebar filter on Oracle, Snowflake and BigQuery.
- Every schema a search had opened read again after each commit on Oracle, Snowflake and BigQuery.
- Schemas missing from Open Quickly on every reopen after one failed to load.
- Unexpanded schemas hidden by the sidebar filter in the Tree layout.
- Empty object sections opened as "No items" under every match while filtering the sidebar tree.
Expand Down
18 changes: 18 additions & 0 deletions TablePro/Core/Concurrency/CatalogFreshness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Foundation
struct CatalogFreshness<Key: Hashable> {
private var revisions: [Key: Int] = [:]
private var committed: [Key: Int] = [:]
private var started: [Key: Int] = [:]

func revision(for key: Key) -> Int {
revisions[key, default: 0]
Expand All @@ -24,6 +25,22 @@ struct CatalogFreshness<Key: Hashable> {
committed[key] == revision(for: key)
}

/// A read that finds nothing current asks for one fetch per revision. One that failed is not
/// asked for again until the next change, or every reader observing the failure would repeat it.
func needsFetch(_ key: Key) -> Bool {
!isCurrent(key) && started[key] != revision(for: key)
}

mutating func noteFetchStarted(_ revision: Int, for key: Key) {
started[key] = revision
}

/// A fetch cut short answered nothing, so the next read may ask again at the same revision.
mutating func noteFetchAbandoned(_ revision: Int, for key: Key) {
guard started[key] == revision else { return }
started.removeValue(forKey: key)
}

mutating func markChanged(_ key: Key) {
revisions[key, default: 0] &+= 1
}
Expand All @@ -39,5 +56,6 @@ struct CatalogFreshness<Key: Hashable> {
mutating func removeAll(where shouldRemove: (Key) -> Bool) {
revisions = revisions.filter { !shouldRemove($0.key) }
committed = committed.filter { !shouldRemove($0.key) }
started = started.filter { !shouldRemove($0.key) }
}
}
15 changes: 10 additions & 5 deletions TablePro/Core/Services/Query/CatalogEditAdoption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,24 @@ struct CatalogEditAdoption {
let loadedScope = schemaService.loadedScope(for: connectionId) else { return nil }
let browseDatabase = databaseManager.browseDatabaseName(for: session.connection)
guard loadedScope.database == browseDatabase else { return nil }
var schemas = Set(schemaService.schemas(for: connectionId).filter {
schemaService.hasLoadedContent(for: connectionId, schema: $0)
})
if let schema = loadedScope.schema {
var schemas = schemaService.schemasWithCurrentTables(for: connectionId)
if let schema = loadedScope.schema, holdsBrowsedSchemaInFlatList(session.connection.type) {
schemas.insert(schema)
}
return LoadedBrowseCatalog(
database: browseDatabase,
schemas: schemas,
tables: schemaService.allLoadedTables(for: connectionId)
tables: schemaService.currentTables(for: connectionId)
)
}

/// A schema-grouped engine's flat list is the browsed schema's, so that schema is answered for
/// even when it holds nothing. A hierarchical engine's flat list is empty, and its browsed schema
/// is answered for only by a per-schema list read since the last catalog change.
private func holdsBrowsedSchemaInFlatList(_ type: DatabaseType) -> Bool {
PluginManager.shared.databaseGroupingStrategy(for: type) != .hierarchicalSchema
}

/// Unstages queued operations whose object the freshly loaded catalog no longer has, judged by
/// the object each one names rather than by a bare table name.
func pruneStaleOperations(connectionId: UUID) {
Expand Down
21 changes: 20 additions & 1 deletion TablePro/Core/Services/Query/SchemaRefreshService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ final class SchemaRefreshService {
guard let scope = browseScope else {
throw DatabaseError.notConnected
}
let awaitedSchemas = schemasAwaitingJudgement(in: scope)
try await metadataDriverProvider.withMetadataDriver(
scope: scope,
workload: .bulk
Expand All @@ -309,7 +310,11 @@ final class SchemaRefreshService {
connection: connection,
scope: scope
)
await schemaService.refreshLoadedSchemaObjects(in: scope, driver: driver)
await schemaService.refreshLoadedSchemaObjects(
in: scope,
fetchingNow: awaitedSchemas,
driver: driver
)
}
} catch is CancellationError {
return
Expand All @@ -329,6 +334,20 @@ final class SchemaRefreshService {
}
await syncAutocompleteProvider(connectionId: connectionId)
}

/// The schemas judged against the refreshed catalog as soon as it settles: the browsed one, and
/// every one holding a queued truncate or drop, which a catalog change prunes when it finishes.
private func schemasAwaitingJudgement(in scope: DatabaseScope) -> Set<String> {
var schemas = Set([scope.schema].compactMap { $0 })
guard let session = databaseManager?.session(for: scope.connectionId) else { return schemas }
for ref in session.pendingTruncates.union(session.pendingDeletes) {
guard (ref.database ?? scope.database) == scope.database, let schema = ref.qualifyingSchema else {
continue
}
schemas.insert(schema)
}
return schemas
}
}

/// The browse scope's object list, routines, triggers, types and schema list, which is everything
Expand Down
Loading
Loading