Skip to content
Closed
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: 2 additions & 1 deletion TablePro/Core/ChangeTracking/SQLStatementGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions TablePro/Core/Plugins/TableOperationStatementProvider.swift
Original file line number Diff line number Diff line change
@@ -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 {}
11 changes: 11 additions & 0 deletions TablePro/Core/Utilities/SQL/DialectQuoteHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Views/Main/MainContentCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
22 changes: 11 additions & 11 deletions TableProTests/Core/ChangeTracking/AnyChangeManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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")
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -104,15 +104,15 @@ 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)
}

@Test("StructureChangeManager wrapper: hasChanges forwards correctly when true")
func structureManagerHasChangesForwardsTrue() {
let structureManager = StructureChangeManager()
let wrapper = AnyChangeManager(structureManager: structureManager)
let wrapper = AnyChangeManager(structureManager)

structureManager.addNewColumn()

Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions TableProTests/Helpers/FakeTableOperationProvider.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
7 changes: 3 additions & 4 deletions TableProTests/Views/Main/CommandActionsDispatchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import Foundation
import SwiftUI
import Testing
@testable import TablePro
import Testing

@MainActor @Suite("CommandActions Dispatch")
struct CommandActionsDispatchTests {
Expand All @@ -20,19 +20,18 @@ struct CommandActionsDispatchTests {
let state = SessionStateFactory.create(connection: connection, payload: nil)
let coordinator = state.coordinator

var selectedRowIndices: Set<Int> = []
var selectedTables: Set<TableInfo> = []
var pendingTruncates: Set<String> = []
var pendingDeletes: Set<String> = []
var tableOperationOptions: [String: TableOperationOptions] = [:]
var editingCell: CellPosition? = nil
var editingCell: CellPosition?
let rightPanelState = RightPanelState()

let actions = MainContentCommandActions(
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 }),
Expand Down
3 changes: 1 addition & 2 deletions TableProTests/Views/Main/MainStatusBarLayoutTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand All @@ -31,7 +31,6 @@ struct MainStatusBarLayoutTests {
onOffsetChange: { _ in },
onPaginationGo: {}
)
// Smoke test: view constructs without error
#expect(type(of: view.body) != Never.self)
}
}
22 changes: 10 additions & 12 deletions TableProTests/Views/Main/SaveCompletionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -261,20 +262,19 @@ struct SaveCompletionTests {
tabManager.tabs[index].tableContext.tableName = "users"
}

var selectedRows: Set<Int> = []
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)
}

Expand All @@ -287,11 +287,9 @@ struct SaveCompletionTests {
tabManager.tabs[index].tableContext.tableName = "users"
}

var selectedRows: Set<Int> = []
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)
}
}
Loading