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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Structure tab context menu with Copy Name, Copy Definition (SQL), Duplicate, and Delete for columns, indexes, and foreign keys
- Foreign key preview: press Cmd+Enter on a FK cell to see the referenced row in a popover

## [0.27.2] - 2026-04-02
Expand Down
14 changes: 14 additions & 0 deletions Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,20 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
return def
}

// MARK: - Definition SQL (clipboard copy)

func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? {
buildColumnDefinitionSQL(column)
}

func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? {
buildIndexDefinitionSQL(index)
}

func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? {
buildForeignKeyDefinitionSQL(fk)
}

// MARK: - Column Reorder DDL

func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String? {
Expand Down
15 changes: 15 additions & 0 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,21 @@ final class PostgreSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
return def
}

// MARK: - Definition SQL (clipboard copy)

func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? {
pgColumnDefinition(column, inlinePK: false)
}

func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? {
let qualifiedTable = tableName.map { quoteIdentifier($0) } ?? "\"table\""
return pgIndexDefinition(index, qualifiedTable: qualifiedTable)
}

func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? {
pgForeignKeyDefinition(fk)
}

// MARK: - Helpers

private func stripLimitOffset(from query: String) -> String {
Expand Down
9 changes: 9 additions & 0 deletions Plugins/TableProPluginKit/PluginDatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public protocol PluginDatabaseDriver: AnyObject, Sendable {
func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String?
func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String?

// Definition SQL for clipboard copy (optional — return nil if not supported)
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String?
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String?
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String?

// Table operations (optional — return nil to use app-level fallback)
func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]?
func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String?
Expand Down Expand Up @@ -230,6 +235,10 @@ public extension PluginDatabaseDriver {
func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String? { nil }
func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? { nil }

func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil }
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { nil }
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil }

func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? { nil }
func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String? { nil }
func foreignKeyDisableStatements() -> [String]? { nil }
Expand Down
9 changes: 9 additions & 0 deletions TablePro/Core/Database/DatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ protocol DatabaseDriver: AnyObject {

func foreignKeyDisableStatements() -> [String]?
func foreignKeyEnableStatements() -> [String]?

// Definition SQL for clipboard copy
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String?
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String?
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String?
}

// MARK: - Schema Switching
Expand Down Expand Up @@ -193,6 +198,10 @@ extension DatabaseDriver {
func foreignKeyDisableStatements() -> [String]? { nil }
func foreignKeyEnableStatements() -> [String]? { nil }

func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil }
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { nil }
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil }

func testConnection() async throws -> Bool {
try await connect()
disconnect()
Expand Down
14 changes: 14 additions & 0 deletions TablePro/Core/Plugins/PluginDriverAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,20 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable {
pluginDriver.generateCreateTableSQL(definition: definition)
}

// MARK: - Definition SQL (clipboard copy)

func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? {
pluginDriver.generateColumnDefinitionSQL(column: column)
}

func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? {
pluginDriver.generateIndexDefinitionSQL(index: index, tableName: tableName)
}

func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? {
pluginDriver.generateForeignKeyDefinitionSQL(fk: fk)
}

// MARK: - Table Operations

func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String] {
Expand Down
49 changes: 7 additions & 42 deletions TablePro/Core/SchemaTracking/SchemaStatementGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ struct SchemaStatementGenerator {
// MARK: - Column Operations

private func generateAddColumn(_ column: EditableColumnDefinition) -> SchemaStatement? {
guard let sql = pluginDriver.generateAddColumnSQL(table: tableName, column: toPluginColumnDefinition(column)) else {
guard let sql = pluginDriver.generateAddColumnSQL(table: tableName, column: column.toPlugin()) else {
return nil
}
return SchemaStatement(sql: sql, description: "Add column '\(column.name)'", isDestructive: false)
Expand All @@ -142,8 +142,8 @@ struct SchemaStatementGenerator {
private func generateModifyColumn(old: EditableColumnDefinition, new: EditableColumnDefinition) -> SchemaStatement? {
guard let sql = pluginDriver.generateModifyColumnSQL(
table: tableName,
oldColumn: toPluginColumnDefinition(old),
newColumn: toPluginColumnDefinition(new)
oldColumn: old.toPlugin(),
newColumn: new.toPlugin()
) else {
return nil
}
Expand All @@ -164,15 +164,15 @@ struct SchemaStatementGenerator {
// MARK: - Index Operations

private func generateAddIndex(_ index: EditableIndexDefinition) -> SchemaStatement? {
guard let sql = pluginDriver.generateAddIndexSQL(table: tableName, index: toPluginIndexDefinition(index)) else {
guard let sql = pluginDriver.generateAddIndexSQL(table: tableName, index: index.toPlugin()) else {
return nil
}
return SchemaStatement(sql: sql, description: "Add index '\(index.name)'", isDestructive: false)
}

private func generateModifyIndex(old: EditableIndexDefinition, new: EditableIndexDefinition) -> SchemaStatement? {
guard let dropSql = pluginDriver.generateDropIndexSQL(table: tableName, indexName: old.name),
let addSql = pluginDriver.generateAddIndexSQL(table: tableName, index: toPluginIndexDefinition(new)) else {
let addSql = pluginDriver.generateAddIndexSQL(table: tableName, index: new.toPlugin()) else {
return nil
}
let sql = "\(dropSql);\n\(addSql);"
Expand All @@ -195,7 +195,7 @@ struct SchemaStatementGenerator {
private func generateAddForeignKey(_ fk: EditableForeignKeyDefinition) -> SchemaStatement? {
guard let sql = pluginDriver.generateAddForeignKeySQL(
table: tableName,
fk: toPluginForeignKeyDefinition(fk)
fk: fk.toPlugin()
) else {
return nil
}
Expand All @@ -204,7 +204,7 @@ struct SchemaStatementGenerator {

private func generateModifyForeignKey(old: EditableForeignKeyDefinition, new: EditableForeignKeyDefinition) -> SchemaStatement? {
guard let dropSql = pluginDriver.generateDropForeignKeySQL(table: tableName, constraintName: old.name),
let addSql = pluginDriver.generateAddForeignKeySQL(table: tableName, fk: toPluginForeignKeyDefinition(new)) else {
let addSql = pluginDriver.generateAddForeignKeySQL(table: tableName, fk: new.toPlugin()) else {
return nil
}
let sql = "\(dropSql);\n\(addSql);"
Expand Down Expand Up @@ -238,39 +238,4 @@ struct SchemaStatementGenerator {
)
}

// MARK: - Plugin Type Converters

private func toPluginColumnDefinition(_ col: EditableColumnDefinition) -> PluginColumnDefinition {
PluginColumnDefinition(
name: col.name,
dataType: col.dataType,
isNullable: col.isNullable,
defaultValue: col.defaultValue,
isPrimaryKey: col.isPrimaryKey,
autoIncrement: col.autoIncrement,
comment: col.comment,
unsigned: col.unsigned,
onUpdate: col.onUpdate
)
}

private func toPluginIndexDefinition(_ index: EditableIndexDefinition) -> PluginIndexDefinition {
PluginIndexDefinition(
name: index.name,
columns: index.columns,
isUnique: index.isUnique,
indexType: index.type.rawValue
)
}

private func toPluginForeignKeyDefinition(_ fk: EditableForeignKeyDefinition) -> PluginForeignKeyDefinition {
PluginForeignKeyDefinition(
name: fk.name,
columns: fk.columns,
referencedTable: fk.referencedTable,
referencedColumns: fk.referencedColumns,
onDelete: fk.onDelete.rawValue,
onUpdate: fk.onUpdate.rawValue
)
}
}
9 changes: 9 additions & 0 deletions TablePro/Models/Schema/ColumnDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import TableProPluginKit

/// Column definition for schema modification (editable structure tab)
struct EditableColumnDefinition: Hashable, Codable, Identifiable {
Expand Down Expand Up @@ -69,6 +70,14 @@ struct EditableColumnDefinition: Hashable, Codable, Identifiable {
)
}

func toPlugin() -> PluginColumnDefinition {
PluginColumnDefinition(
name: name, dataType: dataType, isNullable: isNullable, defaultValue: defaultValue,
isPrimaryKey: isPrimaryKey, autoIncrement: autoIncrement, comment: comment,
unsigned: unsigned, onUpdate: onUpdate
)
}

/// Convert back to ColumnInfo
func toColumnInfo() -> ColumnInfo {
ColumnInfo(
Expand Down
8 changes: 8 additions & 0 deletions TablePro/Models/Schema/ForeignKeyDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import TableProPluginKit

/// Foreign key definition for schema modification (editable structure tab)
struct EditableForeignKeyDefinition: Hashable, Codable, Identifiable {
Expand Down Expand Up @@ -59,6 +60,13 @@ struct EditableForeignKeyDefinition: Hashable, Codable, Identifiable {
)
}

func toPlugin() -> PluginForeignKeyDefinition {
PluginForeignKeyDefinition(
name: name, columns: columns, referencedTable: referencedTable,
referencedColumns: referencedColumns, onDelete: onDelete.rawValue, onUpdate: onUpdate.rawValue
)
}

/// Convert back to ForeignKeyInfo (single column only)
func toForeignKeyInfo() -> ForeignKeyInfo? {
guard let column = columns.first,
Expand Down
5 changes: 5 additions & 0 deletions TablePro/Models/Schema/IndexDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import TableProPluginKit

/// Index definition for schema modification (editable structure tab)
struct EditableIndexDefinition: Hashable, Codable, Identifiable {
Expand Down Expand Up @@ -59,6 +60,10 @@ struct EditableIndexDefinition: Hashable, Codable, Identifiable {
)
}

func toPlugin() -> PluginIndexDefinition {
PluginIndexDefinition(name: name, columns: columns, isUnique: isUnique, indexType: type.rawValue)
}

/// Convert back to IndexInfo
func toIndexInfo() -> IndexInfo {
IndexInfo(
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -7809,6 +7809,9 @@
}
}
}
},
"Copy Definition" : {

},
"Copy error message" : {

Expand Down
4 changes: 4 additions & 0 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
var onFilterColumn: ((String) -> Void)?
var onHideColumn: ((String) -> Void)?
var onMoveRow: ((Int, Int) -> Void)?
var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)?
var emptySpaceMenu: (() -> NSMenu?)?
var onNavigateFK: ((String, ForeignKeyInfo) -> Void)?
var getVisualState: ((Int) -> RowVisualState)?
var dropdownColumns: Set<Int>?
Expand Down Expand Up @@ -243,6 +245,8 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
onFilterColumn = nil
onHideColumn = nil
onNavigateFK = nil
rowViewProvider = nil
emptySpaceMenu = nil
getVisualState = nil
}

Expand Down
8 changes: 8 additions & 0 deletions TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ struct DataGridView: NSViewRepresentable {
var hiddenColumns: Set<String> = []
var onHideColumn: ((String) -> Void)?
var onMoveRow: ((Int, Int) -> Void)?
var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)?
var emptySpaceMenu: (() -> NSMenu?)?

@Binding var selectedRowIndices: Set<Int>
@Binding var sortState: SortState
Expand Down Expand Up @@ -177,6 +179,8 @@ struct DataGridView: NSViewRepresentable {
scrollView.documentView = tableView
context.coordinator.tableView = tableView
context.coordinator.onMoveRow = onMoveRow
context.coordinator.rowViewProvider = rowViewProvider
context.coordinator.emptySpaceMenu = emptySpaceMenu
context.coordinator.rebuildColumnMetadataCache()
if let connectionId {
context.coordinator.observeTeardown(connectionId: connectionId)
Expand Down Expand Up @@ -240,6 +244,8 @@ struct DataGridView: NSViewRepresentable {
coordinator.onDeleteRows = onDeleteRows
coordinator.getVisualState = getVisualState
coordinator.onNavigateFK = onNavigateFK
coordinator.rowViewProvider = rowViewProvider
coordinator.emptySpaceMenu = emptySpaceMenu
return
}
let previousIdentity = coordinator.lastIdentity
Expand Down Expand Up @@ -307,6 +313,8 @@ struct DataGridView: NSViewRepresentable {
coordinator.onMoveRow = onMoveRow
coordinator.getVisualState = getVisualState
coordinator.onNavigateFK = onNavigateFK
coordinator.rowViewProvider = rowViewProvider
coordinator.emptySpaceMenu = emptySpaceMenu
coordinator.dropdownColumns = dropdownColumns
coordinator.typePickerColumns = typePickerColumns
coordinator.connectionId = connectionId
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Results/Extensions/DataGridView+Columns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ extension TableViewCoordinator {
}

func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
if let provider = rowViewProvider {
return provider(tableView, row, self)
}
let rowView = (tableView.makeView(withIdentifier: Self.rowViewIdentifier, owner: nil) as? TableRowViewWithMenu)
?? TableRowViewWithMenu()
rowView.identifier = Self.rowViewIdentifier
Expand Down
7 changes: 6 additions & 1 deletion TablePro/Views/Results/KeyHandlingTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,18 @@ final class KeyHandlingTableView: NSTableView {
let clickedRow = row(at: point)

if clickedRow >= 0,
let rowView = rowView(atRow: clickedRow, makeIfNecessary: false) as? TableRowViewWithMenu {
let rowView = rowView(atRow: clickedRow, makeIfNecessary: false) {
if !selectedRowIndexes.contains(clickedRow) {
selectRowIndexes(IndexSet(integer: clickedRow), byExtendingSelection: false)
}
return rowView.menu(for: event)
}

// Empty space: ask coordinator for a fallback menu (e.g., Structure tab "Add" actions)
if let menu = coordinator?.emptySpaceMenu?() {
return menu
}

return super.menu(for: event)
}
}
Loading
Loading