From 7868fce652c7d034ace31e14281e03b51d59ec15 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 28 Apr 2026 01:16:57 +0700 Subject: [PATCH 1/3] test: repair test target compile errors after datagrid refactors --- .../AnyChangeManagerTests.swift | 22 +++++++++---------- .../Main/CommandActionsDispatchTests.swift | 3 +-- .../Views/Main/MainStatusBarLayoutTests.swift | 3 +-- .../Views/Main/SaveCompletionTests.swift | 21 ++++++++---------- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift b/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift index 51a6fae82..3a221296a 100644 --- a/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift +++ b/TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift @@ -18,7 +18,7 @@ struct AnyChangeManagerTests { func dataManagerHasChangesForwards() { let dataManager = DataChangeManager() dataManager.configureForTable(tableName: "users", columns: ["id", "name"], primaryKeyColumns: ["id"]) - let wrapper = AnyChangeManager(dataManager: dataManager) + let wrapper = AnyChangeManager(dataManager) #expect(wrapper.hasChanges == false) @@ -32,7 +32,7 @@ struct AnyChangeManagerTests { func dataManagerReloadVersionForwards() { let dataManager = DataChangeManager() dataManager.configureForTable(tableName: "users", columns: ["id", "name"], primaryKeyColumns: ["id"]) - let wrapper = AnyChangeManager(dataManager: dataManager) + let wrapper = AnyChangeManager(dataManager) let initialVersion = wrapper.reloadVersion dataManager.reloadVersion += 1 @@ -44,7 +44,7 @@ struct AnyChangeManagerTests { func isRowDeletedDelegatesCorrectly() { let dataManager = DataChangeManager() dataManager.configureForTable(tableName: "users", columns: ["id", "name"], primaryKeyColumns: ["id"]) - let wrapper = AnyChangeManager(dataManager: dataManager) + let wrapper = AnyChangeManager(dataManager) #expect(wrapper.isRowDeleted(0) == false) @@ -57,12 +57,12 @@ struct AnyChangeManagerTests { func recordCellChangeForwards() { let dataManager = DataChangeManager() dataManager.configureForTable(tableName: "users", columns: ["id", "name"], primaryKeyColumns: ["id"]) - let wrapper = AnyChangeManager(dataManager: dataManager) + let wrapper = AnyChangeManager(dataManager) wrapper.recordCellChange(rowIndex: 0, columnIndex: 1, columnName: "name", oldValue: "Alice", newValue: "Bob", originalRow: ["1", "Alice"]) #expect(dataManager.hasChanges == true) - #expect(!wrapper.changes.isEmpty) + #expect(!wrapper.rowChanges.isEmpty) } @Test("No retain cycle — wrapper can be deallocated") @@ -73,7 +73,7 @@ struct AnyChangeManagerTests { weak var weakWrapper: AnyChangeManager? do { - let wrapper = AnyChangeManager(dataManager: dataManager) + let wrapper = AnyChangeManager(dataManager) weakWrapper = wrapper #expect(weakWrapper != nil) } @@ -86,7 +86,7 @@ struct AnyChangeManagerTests { @Test("StructureChangeManager wrapper: isRowDeleted always returns false") func structureManagerIsRowDeletedAlwaysFalse() { let structureManager = StructureChangeManager() - let wrapper = AnyChangeManager(structureManager: structureManager) + let wrapper = AnyChangeManager(structureManager) #expect(wrapper.isRowDeleted(0) == false) #expect(wrapper.isRowDeleted(100) == false) @@ -95,7 +95,7 @@ struct AnyChangeManagerTests { @Test("StructureChangeManager wrapper: consumeChangedRowIndices returns empty set") func structureManagerConsumeChangedRowIndicesEmpty() { let structureManager = StructureChangeManager() - let wrapper = AnyChangeManager(structureManager: structureManager) + let wrapper = AnyChangeManager(structureManager) let indices = wrapper.consumeChangedRowIndices() #expect(indices.isEmpty) @@ -104,7 +104,7 @@ struct AnyChangeManagerTests { @Test("StructureChangeManager wrapper: hasChanges forwards correctly when false") func structureManagerHasChangesForwardsFalse() { let structureManager = StructureChangeManager() - let wrapper = AnyChangeManager(structureManager: structureManager) + let wrapper = AnyChangeManager(structureManager) #expect(wrapper.hasChanges == false) } @@ -112,7 +112,7 @@ struct AnyChangeManagerTests { @Test("StructureChangeManager wrapper: hasChanges forwards correctly when true") func structureManagerHasChangesForwardsTrue() { let structureManager = StructureChangeManager() - let wrapper = AnyChangeManager(structureManager: structureManager) + let wrapper = AnyChangeManager(structureManager) structureManager.addNewColumn() @@ -122,7 +122,7 @@ struct AnyChangeManagerTests { @Test("StructureChangeManager wrapper: reloadVersion forwards correctly") func structureManagerReloadVersionForwards() { let structureManager = StructureChangeManager() - let wrapper = AnyChangeManager(structureManager: structureManager) + let wrapper = AnyChangeManager(structureManager) let initialVersion = wrapper.reloadVersion structureManager.reloadVersion = 5 diff --git a/TableProTests/Views/Main/CommandActionsDispatchTests.swift b/TableProTests/Views/Main/CommandActionsDispatchTests.swift index 14332b4b6..41e048da8 100644 --- a/TableProTests/Views/Main/CommandActionsDispatchTests.swift +++ b/TableProTests/Views/Main/CommandActionsDispatchTests.swift @@ -20,7 +20,6 @@ struct CommandActionsDispatchTests { let state = SessionStateFactory.create(connection: connection, payload: nil) let coordinator = state.coordinator - var selectedRowIndices: Set = [] var selectedTables: Set = [] var pendingTruncates: Set = [] var pendingDeletes: Set = [] @@ -32,7 +31,7 @@ struct CommandActionsDispatchTests { coordinator: coordinator, filterStateManager: state.filterStateManager, connection: connection, - selectedRowIndices: Binding(get: { selectedRowIndices }, set: { selectedRowIndices = $0 }), + selectionState: coordinator.selectionState, selectedTables: Binding(get: { selectedTables }, set: { selectedTables = $0 }), pendingTruncates: Binding(get: { pendingTruncates }, set: { pendingTruncates = $0 }), pendingDeletes: Binding(get: { pendingDeletes }, set: { pendingDeletes = $0 }), diff --git a/TableProTests/Views/Main/MainStatusBarLayoutTests.swift b/TableProTests/Views/Main/MainStatusBarLayoutTests.swift index 34e32a837..b35b287e1 100644 --- a/TableProTests/Views/Main/MainStatusBarLayoutTests.swift +++ b/TableProTests/Views/Main/MainStatusBarLayoutTests.swift @@ -17,7 +17,7 @@ struct MainStatusBarLayoutTests { let filterManager = FilterStateManager() let colVisManager = ColumnVisibilityManager() let view = MainStatusBarView( - tab: nil, + snapshot: StatusBarSnapshot(tab: nil), filterStateManager: filterManager, columnVisibilityManager: colVisManager, allColumns: [], @@ -31,7 +31,6 @@ struct MainStatusBarLayoutTests { onOffsetChange: { _ in }, onPaginationGo: {} ) - // Smoke test: view constructs without error #expect(type(of: view.body) != Never.self) } } diff --git a/TableProTests/Views/Main/SaveCompletionTests.swift b/TableProTests/Views/Main/SaveCompletionTests.swift index 4bef58523..bfba6774c 100644 --- a/TableProTests/Views/Main/SaveCompletionTests.swift +++ b/TableProTests/Views/Main/SaveCompletionTests.swift @@ -261,20 +261,19 @@ struct SaveCompletionTests { tabManager.tabs[index].tableContext.tableName = "users" } - var selectedRows: Set = [] var editingCell: CellPosition? - coordinator.addNewRow(selectedRowIndices: &selectedRows, editingCell: &editingCell) - #expect(selectedRows.isEmpty) + coordinator.addNewRow(editingCell: &editingCell) + #expect(coordinator.selectionState.indices.isEmpty) #expect(editingCell == nil) - selectedRows = [0] - coordinator.deleteSelectedRows(indices: Set([0]), selectedRowIndices: &selectedRows) - #expect(selectedRows == [0]) + coordinator.selectionState.indices = [0] + coordinator.deleteSelectedRows(indices: Set([0])) + #expect(coordinator.selectionState.indices == [0]) - selectedRows = [] - coordinator.duplicateSelectedRow(index: 0, selectedRowIndices: &selectedRows, editingCell: &editingCell) - #expect(selectedRows.isEmpty) + coordinator.selectionState.indices = [] + coordinator.duplicateSelectedRow(index: 0, editingCell: &editingCell) + #expect(coordinator.selectionState.indices.isEmpty) #expect(editingCell == nil) } @@ -287,11 +286,9 @@ struct SaveCompletionTests { tabManager.tabs[index].tableContext.tableName = "users" } - var selectedRows: Set = [] var editingCell: CellPosition? - // Alert level doesn't block row staging — only gates at execution time - coordinator.addNewRow(selectedRowIndices: &selectedRows, editingCell: &editingCell) + coordinator.addNewRow(editingCell: &editingCell) #expect(tabManager.tabs.first?.execution.errorMessage == nil) } } From 27e63c47d39e4448b0dcdf92e9575258fb890750 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 28 Apr 2026 01:28:23 +0700 Subject: [PATCH 2/3] feat: derive SQL dialect from DatabaseType when none provided --- .../Core/ChangeTracking/SQLStatementGenerator.swift | 3 ++- TablePro/Core/Utilities/SQL/DialectQuoteHelper.swift | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/TablePro/Core/ChangeTracking/SQLStatementGenerator.swift b/TablePro/Core/ChangeTracking/SQLStatementGenerator.swift index ebcb83508..2d9d32d8e 100644 --- a/TablePro/Core/ChangeTracking/SQLStatementGenerator.swift +++ b/TablePro/Core/ChangeTracking/SQLStatementGenerator.swift @@ -41,7 +41,8 @@ struct SQLStatementGenerator { self.primaryKeyColumns = primaryKeyColumns self.databaseType = databaseType self.parameterStyle = parameterStyle ?? Self.defaultParameterStyle(for: databaseType) - self.quoteIdentifierFn = quoteIdentifier ?? quoteIdentifierFromDialect(dialect) + let resolvedDialect = resolveSQLDialect(for: databaseType, explicit: dialect) + self.quoteIdentifierFn = quoteIdentifier ?? quoteIdentifierFromDialect(resolvedDialect) } private static func defaultParameterStyle(for databaseType: DatabaseType) -> ParameterStyle { diff --git a/TablePro/Core/Utilities/SQL/DialectQuoteHelper.swift b/TablePro/Core/Utilities/SQL/DialectQuoteHelper.swift index 69eb3ccfb..cf0c947b7 100644 --- a/TablePro/Core/Utilities/SQL/DialectQuoteHelper.swift +++ b/TablePro/Core/Utilities/SQL/DialectQuoteHelper.swift @@ -24,3 +24,14 @@ func quoteIdentifierFromDialect(_ dialect: SQLDialectDescriptor?) -> (String) -> return "\(q)\(escaped)\(q)" } } + +/// Resolve a SQL dialect for a given database type, falling back to the +/// plugin metadata registry when no explicit dialect is supplied. +/// Returns nil for NoSQL databases (no SQL dialect registered). +func resolveSQLDialect( + for databaseType: DatabaseType, + explicit: SQLDialectDescriptor? = nil +) -> SQLDialectDescriptor? { + if let explicit { return explicit } + return PluginMetadataRegistry.shared.snapshot(forTypeId: databaseType.pluginTypeId)?.editor.sqlDialect +} From 57a397fb8ec54e2916317c43f6dbf72e81009ad6 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 28 Apr 2026 01:32:26 +0700 Subject: [PATCH 3/3] test: add FakePluginDriverAdapter for save completion tests --- .../TableOperationStatementProvider.swift | 19 ++++++++++++++ ...inContentCoordinator+TableOperations.swift | 9 ++++--- .../Views/Main/MainContentCoordinator.swift | 4 +++ .../Helpers/FakeTableOperationProvider.swift | 25 +++++++++++++++++++ .../Main/CommandActionsDispatchTests.swift | 4 +-- .../Views/Main/SaveCompletionTests.swift | 1 + 6 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 TablePro/Core/Plugins/TableOperationStatementProvider.swift create mode 100644 TableProTests/Helpers/FakeTableOperationProvider.swift diff --git a/TablePro/Core/Plugins/TableOperationStatementProvider.swift b/TablePro/Core/Plugins/TableOperationStatementProvider.swift new file mode 100644 index 000000000..b8c58e779 --- /dev/null +++ b/TablePro/Core/Plugins/TableOperationStatementProvider.swift @@ -0,0 +1,19 @@ +// +// TableOperationStatementProvider.swift +// TablePro +// + +import Foundation + +/// Source of dialect-specific table operation SQL (TRUNCATE, DROP, FK toggles). +/// Conformance is provided by PluginDriverAdapter for runtime use, and +/// can be substituted by tests to exercise table-operation paths without a +/// live driver session. +protocol TableOperationStatementProvider: AnyObject { + func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String] + func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String + func foreignKeyDisableStatements() -> [String]? + func foreignKeyEnableStatements() -> [String]? +} + +extension PluginDriverAdapter: TableOperationStatementProvider {} diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+TableOperations.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+TableOperations.swift index 6e13f63c9..c488df6c7 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+TableOperations.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+TableOperations.swift @@ -10,9 +10,12 @@ import Foundation extension MainContentCoordinator { // MARK: - Plugin Adapter Access - /// Returns the current connection's PluginDriverAdapter, if available. - private var currentPluginDriverAdapter: PluginDriverAdapter? { - DatabaseManager.shared.driver(for: connectionId) as? PluginDriverAdapter + /// Returns the current connection's TableOperationStatementProvider, if available. + /// Defaults to the live `PluginDriverAdapter` resolved via DatabaseManager; + /// `tableOperationOverride` lets tests substitute a fake without a live session. + private var currentPluginDriverAdapter: TableOperationStatementProvider? { + if let override = tableOperationOverride { return override } + return DatabaseManager.shared.driver(for: connectionId) as? PluginDriverAdapter } // MARK: - Table Operation SQL Generation diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index f1951067e..dafc26bb1 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -99,6 +99,10 @@ final class MainContentCoordinator { /// Stable identifier for this coordinator's window (set by MainContentView on appear) var windowId: UUID? + /// Test seam: when set, replaces the live PluginDriverAdapter for table operation SQL. + /// Production code never assigns this; tests inject a fake to exercise truncate/drop paths. + @ObservationIgnored var tableOperationOverride: TableOperationStatementProvider? + /// Direct reference to sidebar viewmodel — eliminates global notification broadcasts weak var sidebarViewModel: SidebarViewModel? diff --git a/TableProTests/Helpers/FakeTableOperationProvider.swift b/TableProTests/Helpers/FakeTableOperationProvider.swift new file mode 100644 index 000000000..4c74dd70d --- /dev/null +++ b/TableProTests/Helpers/FakeTableOperationProvider.swift @@ -0,0 +1,25 @@ +// +// FakeTableOperationProvider.swift +// TableProTests +// + +import Foundation +@testable import TablePro + +/// Minimal test double for `TableOperationStatementProvider`. +/// Returns ANSI-style SQL so tests can drive coordinator save paths without a live driver. +final class FakeTableOperationProvider: TableOperationStatementProvider { + func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String] { + let qualified = schema.map { "\($0).\(table)" } ?? table + return ["TRUNCATE TABLE \(qualified)\(cascade ? " CASCADE" : "")"] + } + + func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String { + let qualified = schema.map { "\($0).\(name)" } ?? name + return "DROP \(objectType) \(qualified)\(cascade ? " CASCADE" : "")" + } + + func foreignKeyDisableStatements() -> [String]? { nil } + + func foreignKeyEnableStatements() -> [String]? { nil } +} diff --git a/TableProTests/Views/Main/CommandActionsDispatchTests.swift b/TableProTests/Views/Main/CommandActionsDispatchTests.swift index 41e048da8..6d00199fa 100644 --- a/TableProTests/Views/Main/CommandActionsDispatchTests.swift +++ b/TableProTests/Views/Main/CommandActionsDispatchTests.swift @@ -8,8 +8,8 @@ import Foundation import SwiftUI -import Testing @testable import TablePro +import Testing @MainActor @Suite("CommandActions Dispatch") struct CommandActionsDispatchTests { @@ -24,7 +24,7 @@ struct CommandActionsDispatchTests { var pendingTruncates: Set = [] var pendingDeletes: Set = [] var tableOperationOptions: [String: TableOperationOptions] = [:] - var editingCell: CellPosition? = nil + var editingCell: CellPosition? let rightPanelState = RightPanelState() let actions = MainContentCommandActions( diff --git a/TableProTests/Views/Main/SaveCompletionTests.swift b/TableProTests/Views/Main/SaveCompletionTests.swift index bfba6774c..7b31cae34 100644 --- a/TableProTests/Views/Main/SaveCompletionTests.swift +++ b/TableProTests/Views/Main/SaveCompletionTests.swift @@ -22,6 +22,7 @@ struct SaveCompletionTests { var conn = TestFixtures.makeConnection(type: type) conn.safeModeLevel = safeModeLevel let state = SessionStateFactory.create(connection: conn, payload: nil) + state.coordinator.tableOperationOverride = FakeTableOperationProvider() return (state.coordinator, state.tabManager, state.changeManager) }