From 1000f5f0ab6f90d8c821b574b06bac974b566e3a Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 16 May 2026 18:05:46 +0700 Subject: [PATCH] refactor(datagrid): restore reloadVersion external-only semantic and invalidate schema cache on refresh --- CHANGELOG.md | 1 + .../Core/Autocomplete/SQLSchemaProvider.swift | 10 +++++++ .../ChangeTracking/DataChangeManager.swift | 9 ------ .../StructureChangeManager.swift | 14 --------- .../Query/SchemaProviderRegistry.swift | 29 +++++++++++++++++-- 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b9aaa95..56a4e8a60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - AI Chat: Gemini provider now round-trips `thoughtSignature` on function calls, fixing the second-round 400 error after a tool runs. - AI Chat: GitHub Copilot tool registration was failing with "Expected string" schema validation errors. Optional fields now register with `type: "string"` instead of `type: ["string", "null"]` and are excluded from `required`, which Copilot's LSP validator accepts. - MySQL/MariaDB: `BIT(N)` columns now display as decimal numbers (`0`, `1`, `255`) instead of raw bytes that showed up as control characters like `^A` in the data grid. (#1272) +- Query editor autocomplete now reflects external column renames after Refresh. Previously the schema cache kept suggesting the old column name until the connection was reopened. - ClickHouse, BigQuery, CloudflareD1, LibSQL, Etcd, and DynamoDB: long-running queries no longer fail at 30 seconds when Settings > Query timeout is set higher. The HTTP transport now uses the configured query timeout plus a 30-second grace, so the server's `max_execution_time` (or equivalent) fires before the client gives up. Setting "No limit" raises the transport ceiling to 1 hour. (#1267) - AI Chat: DeepSeek V4 thinking content (`reasoning_content`) is now captured during streaming and passed back in subsequent turns, fixing 400 errors when using deepseek-v4-pro or deepseek-v4-flash. - MongoDB: the connection form now shows a Username field. It was hidden for databases where authentication is optional, so connections to auth-enabled servers saved with no credentials and every query failed with "requires authentication" even though the connection looked healthy. diff --git a/TablePro/Core/Autocomplete/SQLSchemaProvider.swift b/TablePro/Core/Autocomplete/SQLSchemaProvider.swift index 4eafa5788..9bb028bf6 100644 --- a/TablePro/Core/Autocomplete/SQLSchemaProvider.swift +++ b/TablePro/Core/Autocomplete/SQLSchemaProvider.swift @@ -155,6 +155,16 @@ actor SQLSchemaProvider { startEagerColumnLoad() } + func clearColumnCache() { + eagerColumnTask?.cancel() + eagerColumnTask = nil + columnCache.removeAll() + columnAccessOrder.removeAll() + if cachedDriver != nil { + startEagerColumnLoad() + } + } + // MARK: - Eager Column Loading private func startEagerColumnLoad() { diff --git a/TablePro/Core/ChangeTracking/DataChangeManager.swift b/TablePro/Core/ChangeTracking/DataChangeManager.swift index 29ba4b3ed..e97191310 100644 --- a/TablePro/Core/ChangeTracking/DataChangeManager.swift +++ b/TablePro/Core/ChangeTracking/DataChangeManager.swift @@ -128,7 +128,6 @@ final class DataChangeManager: ChangeManaging { ) guard recorded else { hasChanges = !pending.isEmpty - reloadVersion += 1 return } registerUndo(actionName: String(localized: "Edit Cell")) { target in @@ -138,7 +137,6 @@ final class DataChangeManager: ChangeManaging { )) } hasChanges = !pending.isEmpty - reloadVersion += 1 } func recordRowDeletion(rowIndex: Int, originalRow: [PluginCellValue]) { @@ -147,7 +145,6 @@ final class DataChangeManager: ChangeManaging { target.applyDataUndo(.rowDeletion(rowIndex: rowIndex, originalRow: originalRow)) } hasChanges = true - reloadVersion += 1 } func recordBatchRowDeletion(rows: [(rowIndex: Int, originalRow: [PluginCellValue])]) { @@ -165,7 +162,6 @@ final class DataChangeManager: ChangeManaging { target.applyDataUndo(.batchRowDeletion(rows: batchData)) } hasChanges = true - reloadVersion += 1 } func recordRowInsertion(rowIndex: Int, values: [PluginCellValue]) { @@ -174,7 +170,6 @@ final class DataChangeManager: ChangeManaging { target.applyDataUndo(.rowInsertion(rowIndex: rowIndex)) } hasChanges = true - reloadVersion += 1 } // MARK: - Undo Operations @@ -182,13 +177,11 @@ final class DataChangeManager: ChangeManaging { func undoRowDeletion(rowIndex: Int) { guard pending.undoRowDeletion(rowIndex: rowIndex) else { return } hasChanges = !pending.isEmpty - reloadVersion += 1 } func undoRowInsertion(rowIndex: Int) { guard pending.undoRowInsertion(rowIndex: rowIndex) else { return } hasChanges = !pending.isEmpty - reloadVersion += 1 } func undoBatchRowInsertion(rowIndices: [Int]) { @@ -199,7 +192,6 @@ final class DataChangeManager: ChangeManaging { target.applyDataUndo(.batchRowInsertion(rowIndices: validRows, rowValues: rowValues)) } hasChanges = !pending.isEmpty - reloadVersion += 1 } // MARK: - Core Undo Application @@ -227,7 +219,6 @@ final class DataChangeManager: ChangeManaging { } hasChanges = !pending.isEmpty - reloadVersion += 1 if let result = lastUndoResult { onUndoApplied?(result) diff --git a/TablePro/Core/SchemaTracking/StructureChangeManager.swift b/TablePro/Core/SchemaTracking/StructureChangeManager.swift index 87930966e..da9b9976a 100644 --- a/TablePro/Core/SchemaTracking/StructureChangeManager.swift +++ b/TablePro/Core/SchemaTracking/StructureChangeManager.swift @@ -137,7 +137,6 @@ final class StructureChangeManager: ChangeManaging { } undoManager.setActionName(String(localized: "Add Column")) validate() - reloadVersion += 1 } func addNewIndex() { @@ -151,7 +150,6 @@ final class StructureChangeManager: ChangeManaging { } undoManager.setActionName(String(localized: "Add Index")) validate() - reloadVersion += 1 } func addNewForeignKey() { @@ -165,7 +163,6 @@ final class StructureChangeManager: ChangeManaging { } undoManager.setActionName(String(localized: "Add Foreign Key")) validate() - reloadVersion += 1 } // MARK: - Paste Operations (public methods for adding copied items) @@ -179,7 +176,6 @@ final class StructureChangeManager: ChangeManaging { target.applySchemaUndo(.columnAdd(column: column)) } undoManager.setActionName(String(localized: "Add Column")) - reloadVersion += 1 } func addIndex(_ index: EditableIndexDefinition) { @@ -191,7 +187,6 @@ final class StructureChangeManager: ChangeManaging { target.applySchemaUndo(.indexAdd(index: index)) } undoManager.setActionName(String(localized: "Add Index")) - reloadVersion += 1 } func addForeignKey(_ foreignKey: EditableForeignKeyDefinition) { @@ -203,7 +198,6 @@ final class StructureChangeManager: ChangeManaging { target.applySchemaUndo(.foreignKeyAdd(fk: foreignKey)) } undoManager.setActionName(String(localized: "Add Foreign Key")) - reloadVersion += 1 } // MARK: - Column Operations @@ -240,7 +234,6 @@ final class StructureChangeManager: ChangeManaging { } validate() - reloadVersion += 1 } func deleteColumn(id: UUID) { @@ -266,7 +259,6 @@ final class StructureChangeManager: ChangeManaging { } validate() - reloadVersion += 1 } // MARK: - Index Operations @@ -303,7 +295,6 @@ final class StructureChangeManager: ChangeManaging { } validate() - reloadVersion += 1 } func deleteIndex(id: UUID) { @@ -329,7 +320,6 @@ final class StructureChangeManager: ChangeManaging { } validate() - reloadVersion += 1 } // MARK: - Foreign Key Operations @@ -366,7 +356,6 @@ final class StructureChangeManager: ChangeManaging { } validate() - reloadVersion += 1 } func deleteForeignKey(id: UUID) { @@ -392,7 +381,6 @@ final class StructureChangeManager: ChangeManaging { } validate() - reloadVersion += 1 } // MARK: - Row-Specific Undo Delete @@ -424,7 +412,6 @@ final class StructureChangeManager: ChangeManaging { pendingChanges.removeValue(forKey: key) untrackChangeKey(key) validate() - reloadVersion += 1 } // MARK: - Validation @@ -568,7 +555,6 @@ final class StructureChangeManager: ChangeManaging { } validate() - reloadVersion += 1 } private func applyColumnEditUndo(id: UUID, old: EditableColumnDefinition, new: EditableColumnDefinition) { diff --git a/TablePro/Core/Services/Query/SchemaProviderRegistry.swift b/TablePro/Core/Services/Query/SchemaProviderRegistry.swift index 6ae4a46a0..a44136b16 100644 --- a/TablePro/Core/Services/Query/SchemaProviderRegistry.swift +++ b/TablePro/Core/Services/Query/SchemaProviderRegistry.swift @@ -6,6 +6,7 @@ // Ref-counted with grace period removal to avoid redundant schema loads. // +import Combine import Foundation import os @@ -18,14 +19,38 @@ final class SchemaProviderRegistry { private var providers: [UUID: SQLSchemaProvider] = [:] private var refCounts: [UUID: Int] = [:] private var removalTasks: [UUID: Task] = [:] + private var cancellables: Set = [] #if DEBUG /// Test-only init for `@testable` tests in DEBUG builds; release builds must use `.shared`. - internal init() {} + internal init() { + subscribeToRefreshSignal() + } #else - private init() {} + private init() { + subscribeToRefreshSignal() + } #endif + private func subscribeToRefreshSignal() { + AppCommands.shared.refreshData + .sink { [weak self] connectionId in + self?.invalidateColumnCache(for: connectionId) + } + .store(in: &cancellables) + } + + func invalidateColumnCache(for connectionId: UUID?) { + if let id = connectionId { + guard let provider = providers[id] else { return } + Task { await provider.clearColumnCache() } + return + } + for provider in providers.values { + Task { await provider.clearColumnCache() } + } + } + func provider(for connectionId: UUID) -> SQLSchemaProvider? { providers[connectionId] }