Skip to content

Commit d50a2c4

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/external-access-persistence
# Conflicts: # CHANGELOG.md # TablePro/Core/Storage/StoredConnection.swift
2 parents 51baf68 + d97ce9d commit d50a2c4

140 files changed

Lines changed: 566 additions & 869 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Data grid now serves the row count from its existing cache instead of recomputing it on every layout pass, reducing CPU churn while scrolling large result sets.
13+
- Typing in the sidebar table search stays responsive on databases with thousands of tables; filtering runs after a short pause instead of on every keystroke.
14+
1015
### Fixed
1116

1217
- Oracle connections no longer crash the app when the server sends a backend message the driver cannot decode; the query fails with a clear error and the connection reconnects. (#483)
1318
- MongoDB TLS handshake failures now report the actual cause instead of always blaming a cipher or protocol mismatch. (#1418)
1419
- The External Clients access level no longer reverts to Read Only after saving and reopening a connection, so MCP clients keep the write access you granted. (#1730)
20+
- Typing fast with the autocomplete window open no longer stalls each keystroke; the live refilter is debounced, cancelable, and moved off the main thread.
1521

1622
## [0.52.0] - 2026-06-19
1723

Plugins/BigQueryDriverPlugin/BigQueryAuth.swift

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ internal final class ServiceAccountAuthProvider: @unchecked Sendable, BigQueryAu
8888

8989
self.clientEmail = email
9090
self.privateKeyPEM = key
91-
self.projectId = overrideProjectId?.isEmpty == false ? overrideProjectId! : saProjectId
91+
self.projectId = overrideProjectId.flatMap { $0.isEmpty ? nil : $0 } ?? saProjectId
9292

9393
if self.projectId.isEmpty {
9494
throw BigQueryError.authFailed("No project ID found in service account JSON or connection settings")
@@ -349,9 +349,7 @@ internal final class ADCAuthProvider: @unchecked Sendable, BigQueryAuthProvider
349349
}
350350

351351
let quotaProject = json["quota_project_id"] as? String ?? ""
352-
self.projectId = overrideProjectId?.isEmpty == false
353-
? overrideProjectId!
354-
: quotaProject
352+
self.projectId = overrideProjectId.flatMap { $0.isEmpty ? nil : $0 } ?? quotaProject
355353

356354
if self.projectId.isEmpty {
357355
throw BigQueryError.authFailed(
@@ -374,9 +372,7 @@ internal final class ADCAuthProvider: @unchecked Sendable, BigQueryAuthProvider
374372
}
375373

376374
// Resolve source credentials
377-
let resolvedProjectId = overrideProjectId?.isEmpty == false
378-
? overrideProjectId!
379-
: (json["quota_project_id"] as? String ?? "")
375+
let resolvedProjectId = overrideProjectId.flatMap { $0.isEmpty ? nil : $0 } ?? (json["quota_project_id"] as? String ?? "")
380376

381377
if resolvedProjectId.isEmpty {
382378
throw BigQueryError.authFailed(
@@ -548,7 +544,6 @@ private final class ImpersonatedServiceAccountDelegate: @unchecked Sendable, Big
548544
}
549545

550546
private func fetchImpersonatedToken() async throws -> String {
551-
// Get source token
552547
let sourceToken = try await sourceProvider.accessToken()
553548

554549
// Exchange for impersonated token
@@ -649,7 +644,6 @@ internal final class OAuthBrowserAuthProvider: @unchecked Sendable, BigQueryAuth
649644

650645
let redirectUri = "http://127.0.0.1:\(port)"
651646

652-
// Build authorization URL
653647
var components = URLComponents(string: Self.authEndpoint)
654648
components?.queryItems = [
655649
URLQueryItem(name: "client_id", value: clientId),
@@ -665,7 +659,6 @@ internal final class OAuthBrowserAuthProvider: @unchecked Sendable, BigQueryAuth
665659
throw BigQueryError.authFailed("Failed to build OAuth authorization URL")
666660
}
667661

668-
// Open browser
669662
Self.logger.info("Opening browser for OAuth authorization")
670663
await NSWorkspace.shared.open(authUrl)
671664

@@ -680,13 +673,10 @@ internal final class OAuthBrowserAuthProvider: @unchecked Sendable, BigQueryAuth
680673

681674
Self.logger.info("Received OAuth authorization code")
682675

683-
// Exchange auth code for tokens
684676
let tokens = try await exchangeAuthCode(code, redirectUri: redirectUri)
685677

686-
// Store refresh token
687678
lock.withLock { _refreshToken = tokens.refreshToken }
688679

689-
// Cache access token
690680
let newToken = CachedToken(
691681
token: tokens.accessToken,
692682
expiresAt: Date().addingTimeInterval(Double(tokens.expiresIn))

Plugins/BigQueryDriverPlugin/BigQueryConnection.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,6 @@ internal final class BigQueryConnection: @unchecked Sendable {
317317
_session = urlSession
318318
}
319319

320-
// Test connectivity
321320
do {
322321
_ = try await executeQuery("SELECT 1")
323322
} catch {
@@ -428,7 +427,6 @@ internal final class BigQueryConnection: @unchecked Sendable {
428427
_currentJobLocation = jobRef.location
429428
}
430429

431-
// Poll for completion if not done
432430
let finalJobResponse: BQJobResponse
433431
if let state = jobResponse.status?.state, state != "DONE" {
434432
finalJobResponse = try await pollJobCompletion(
@@ -441,7 +439,6 @@ internal final class BigQueryConnection: @unchecked Sendable {
441439
finalJobResponse = jobResponse
442440
}
443441

444-
// Extract DML affected rows from job statistics
445442
let dmlAffectedRows: Int
446443
if let numStr = finalJobResponse.statistics?.query?.numDmlAffectedRows {
447444
dmlAffectedRows = Int(numStr) ?? 0
@@ -453,7 +450,6 @@ internal final class BigQueryConnection: @unchecked Sendable {
453450
let totalBytesBilled = finalJobResponse.statistics?.query?.totalBytesBilled
454451
let cacheHit = finalJobResponse.statistics?.query?.cacheHit
455452

456-
// Fetch first page of results
457453
let firstPage = try await getQueryResults(
458454
jobId: jobId, location: jobRef.location, auth: auth, session: session
459455
)

Plugins/BigQueryDriverPlugin/BigQueryOAuthServer.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ internal final class BigQueryOAuthServer: @unchecked Sendable {
3535
func waitForAuthCode() async throws -> String {
3636
try await withCheckedThrowingContinuation { cont in
3737
lock.withLock { continuation = cont }
38-
// Start 2-minute timeout
3938
let task = Task {
4039
try? await Task.sleep(nanoseconds: 120_000_000_000)
4140
self.lock.withLock {

Plugins/BigQueryDriverPlugin/BigQueryPluginDriver.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,10 @@ internal final class BigQueryPluginDriver: PluginDatabaseDriver, @unchecked Send
208208
)
209209
}
210210

211-
// Tagged browsing queries
212211
if BigQueryQueryBuilder.isTaggedQuery(trimmed) {
213212
return try await executeTaggedQuery(trimmed, conn: conn, startTime: startTime)
214213
}
215214

216-
// Regular GoogleSQL
217215
let dataset = lock.withLock { _currentDataset }
218216
let result: BQExecuteResult
219217
do {
@@ -871,7 +869,6 @@ internal final class BigQueryPluginDriver: PluginDatabaseDriver, @unchecked Send
871869
let typeNames = BigQueryTypeMapper.columnTypeNames(from: schema)
872870
let rows = BigQueryTypeMapper.flattenRows(from: result.queryResponse, schema: schema)
873871

874-
// Update column cache
875872
lock.withLock { _columnCache["\(params.dataset).\(params.table)"] = colNames }
876873

877874
return PluginQueryResult(

Plugins/BigQueryDriverPlugin/BigQueryQueryBuilder.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ internal struct BigQueryQueryBuilder {
173173
var sql = "SELECT * FROM \(fqTable)"
174174
var whereClauses: [String] = []
175175

176-
// Filters
177176
if let filters = params.filters, !filters.isEmpty {
178177
let rawMode = params.logicMode ?? "AND"
179178
let logicMode = (rawMode.uppercased() == "OR") ? "OR" : "AND"
@@ -183,11 +182,8 @@ internal struct BigQueryQueryBuilder {
183182
}
184183
}
185184

186-
// Search
187185
if let searchText = params.searchText, !searchText.isEmpty {
188-
let searchCols = params.searchColumns?.isEmpty == false
189-
? params.searchColumns!
190-
: columns
186+
let searchCols = params.searchColumns.flatMap { $0.isEmpty ? nil : $0 } ?? columns
191187
let escapedSearch = searchText.replacingOccurrences(of: "'", with: "''")
192188
let searchClauses = searchCols.map { col in
193189
"CAST(\(quoteIdentifier(col)) AS STRING) LIKE '%\(escapedSearch)%'"
@@ -201,7 +197,6 @@ internal struct BigQueryQueryBuilder {
201197
sql += " WHERE " + whereClauses.joined(separator: " AND ")
202198
}
203199

204-
// Sort
205200
if let sortColumns = params.sortColumns, !sortColumns.isEmpty {
206201
let orderClauses = sortColumns.compactMap { sort -> String? in
207202
guard sort.columnIndex < columns.count else { return nil }
@@ -236,9 +231,7 @@ internal struct BigQueryQueryBuilder {
236231
}
237232

238233
if let searchText = params.searchText, !searchText.isEmpty {
239-
let searchCols = params.searchColumns?.isEmpty == false
240-
? params.searchColumns!
241-
: columns
234+
let searchCols = params.searchColumns.flatMap { $0.isEmpty ? nil : $0 } ?? columns
242235
let escapedSearch = searchText.replacingOccurrences(of: "'", with: "''")
243236
let searchClauses = searchCols.map { col in
244237
"CAST(\(quoteIdentifier(col)) AS STRING) LIKE '%\(escapedSearch)%'"

Plugins/CassandraDriverPlugin/CassandraConnection.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,6 @@ actor CassandraConnectionActor {
288288

289289
let startTime = Date()
290290

291-
// Prepare
292291
let prepareFuture = cass_session_prepare(session, cql)
293292
guard let prepareFuture else {
294293
throw CassandraPluginError.queryFailed("Failed to prepare statement")
@@ -307,7 +306,6 @@ actor CassandraConnectionActor {
307306
}
308307
defer { cass_prepared_free(prepared) }
309308

310-
// Bind parameters
311309
let statement = cass_prepared_bind(prepared)
312310
guard let statement else {
313311
throw CassandraPluginError.queryFailed("Failed to bind prepared statement")
@@ -331,7 +329,6 @@ actor CassandraConnectionActor {
331329
}
332330
}
333331

334-
// Execute
335332
let future = cass_session_execute(session, statement)
336333
guard let future else {
337334
throw CassandraPluginError.queryFailed("Failed to execute prepared statement")

Plugins/CassandraDriverPlugin/CassandraPlugin.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,6 @@ internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sen
304304
"""
305305
let result = try await execute(query: query)
306306

307-
// Parse and sort by kind order then position before mapping to PluginColumnInfo
308307
struct RawColumn {
309308
let name: String
310309
let dataType: String
@@ -390,7 +389,6 @@ internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sen
390389
let kind = row[safe: 1]?.asText ?? "COMPOSITES"
391390
let options = row[safe: 2]?.asText ?? ""
392391

393-
// Extract target column from options map
394392
var targetColumns: [String] = []
395393
if let targetRange = options.range(of: "target: ") {
396394
let target = String(options[targetRange.upperBound...])
@@ -419,7 +417,6 @@ internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sen
419417
func fetchTableDDL(table: String, schema: String?) async throws -> String {
420418
let ks = resolveKeyspace(schema)
421419

422-
// Build DDL from schema metadata
423420
let columns = try await fetchColumns(table: table, schema: ks)
424421

425422
let partitionKeys = columns.filter(\.isPrimaryKey)

Plugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ final class CloudflareD1PluginDriver: PluginDatabaseDriver, @unchecked Sendable
8080

8181
guard let match = databases.first(where: { $0.name == databaseName }) else {
8282
throw CloudflareD1Error(
83-
message: String(localized: "Database '\(databaseName)' not found in account")
83+
message: String(format: String(localized: "Database '%@' not found in account"), databaseName)
8484
)
8585
}
8686
databaseId = match.uuid
@@ -603,7 +603,7 @@ final class CloudflareD1PluginDriver: PluginDatabaseDriver, @unchecked Sendable
603603

604604
guard let resolvedUuid = uuid else {
605605
throw CloudflareD1Error(
606-
message: String(localized: "Database '\(database)' not found")
606+
message: String(format: String(localized: "Database '%@' not found"), database)
607607
)
608608
}
609609

Plugins/CloudflareD1DriverPlugin/D1HttpClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ final class D1HttpClient: @unchecked Sendable {
377377
Self.logger.warning("D1 rate limited. Retry-After: \(retryAfter ?? "not specified")")
378378
if let seconds = retryAfter {
379379
throw D1HttpError(
380-
message: String(localized: "Rate limited by Cloudflare. Retry after \(seconds) seconds.")
380+
message: String(format: String(localized: "Rate limited by Cloudflare. Retry after %@ seconds."), seconds)
381381
)
382382
} else {
383383
throw D1HttpError(

0 commit comments

Comments
 (0)