Skip to content

Commit 1e64430

Browse files
authored
feat: add context menu for Structure tab (#561)
* feat: add context menu for Structure tab (columns, indexes, foreign keys) * fix: address code review issues for structure context menu * fix: allow any NSTableRowView subclass to provide context menu * feat: add context menu on empty space in Structure tab * fix: simplify empty space menu and remove last duplicated converters
1 parent cb649bd commit 1e64430

19 files changed

Lines changed: 396 additions & 80 deletions

CHANGELOG.md

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

1010
### Added
1111

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

1415
## [0.27.2] - 2026-04-02

Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,20 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
740740
return def
741741
}
742742

743+
// MARK: - Definition SQL (clipboard copy)
744+
745+
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? {
746+
buildColumnDefinitionSQL(column)
747+
}
748+
749+
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? {
750+
buildIndexDefinitionSQL(index)
751+
}
752+
753+
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? {
754+
buildForeignKeyDefinitionSQL(fk)
755+
}
756+
743757
// MARK: - Column Reorder DDL
744758

745759
func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String? {

Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,21 @@ final class PostgreSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
868868
return def
869869
}
870870

871+
// MARK: - Definition SQL (clipboard copy)
872+
873+
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? {
874+
pgColumnDefinition(column, inlinePK: false)
875+
}
876+
877+
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? {
878+
let qualifiedTable = tableName.map { quoteIdentifier($0) } ?? "\"table\""
879+
return pgIndexDefinition(index, qualifiedTable: qualifiedTable)
880+
}
881+
882+
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? {
883+
pgForeignKeyDefinition(fk)
884+
}
885+
871886
// MARK: - Helpers
872887

873888
private func stripLimitOffset(from query: String) -> String {

Plugins/TableProPluginKit/PluginDatabaseDriver.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ public protocol PluginDatabaseDriver: AnyObject, Sendable {
101101
func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String?
102102
func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String?
103103

104+
// Definition SQL for clipboard copy (optional — return nil if not supported)
105+
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String?
106+
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String?
107+
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String?
108+
104109
// Table operations (optional — return nil to use app-level fallback)
105110
func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]?
106111
func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String?
@@ -230,6 +235,10 @@ public extension PluginDatabaseDriver {
230235
func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String? { nil }
231236
func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? { nil }
232237

238+
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil }
239+
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { nil }
240+
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil }
241+
233242
func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? { nil }
234243
func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String? { nil }
235244
func foreignKeyDisableStatements() -> [String]? { nil }

TablePro/Core/Database/DatabaseDriver.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ protocol DatabaseDriver: AnyObject {
153153

154154
func foreignKeyDisableStatements() -> [String]?
155155
func foreignKeyEnableStatements() -> [String]?
156+
157+
// Definition SQL for clipboard copy
158+
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String?
159+
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String?
160+
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String?
156161
}
157162

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

201+
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil }
202+
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { nil }
203+
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil }
204+
196205
func testConnection() async throws -> Bool {
197206
try await connect()
198207
disconnect()

TablePro/Core/Plugins/PluginDriverAdapter.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,20 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable {
349349
pluginDriver.generateCreateTableSQL(definition: definition)
350350
}
351351

352+
// MARK: - Definition SQL (clipboard copy)
353+
354+
func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? {
355+
pluginDriver.generateColumnDefinitionSQL(column: column)
356+
}
357+
358+
func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? {
359+
pluginDriver.generateIndexDefinitionSQL(index: index, tableName: tableName)
360+
}
361+
362+
func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? {
363+
pluginDriver.generateForeignKeyDefinitionSQL(fk: fk)
364+
}
365+
352366
// MARK: - Table Operations
353367

354368
func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String] {

TablePro/Core/SchemaTracking/SchemaStatementGenerator.swift

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ struct SchemaStatementGenerator {
133133
// MARK: - Column Operations
134134

135135
private func generateAddColumn(_ column: EditableColumnDefinition) -> SchemaStatement? {
136-
guard let sql = pluginDriver.generateAddColumnSQL(table: tableName, column: toPluginColumnDefinition(column)) else {
136+
guard let sql = pluginDriver.generateAddColumnSQL(table: tableName, column: column.toPlugin()) else {
137137
return nil
138138
}
139139
return SchemaStatement(sql: sql, description: "Add column '\(column.name)'", isDestructive: false)
@@ -142,8 +142,8 @@ struct SchemaStatementGenerator {
142142
private func generateModifyColumn(old: EditableColumnDefinition, new: EditableColumnDefinition) -> SchemaStatement? {
143143
guard let sql = pluginDriver.generateModifyColumnSQL(
144144
table: tableName,
145-
oldColumn: toPluginColumnDefinition(old),
146-
newColumn: toPluginColumnDefinition(new)
145+
oldColumn: old.toPlugin(),
146+
newColumn: new.toPlugin()
147147
) else {
148148
return nil
149149
}
@@ -164,15 +164,15 @@ struct SchemaStatementGenerator {
164164
// MARK: - Index Operations
165165

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

173173
private func generateModifyIndex(old: EditableIndexDefinition, new: EditableIndexDefinition) -> SchemaStatement? {
174174
guard let dropSql = pluginDriver.generateDropIndexSQL(table: tableName, indexName: old.name),
175-
let addSql = pluginDriver.generateAddIndexSQL(table: tableName, index: toPluginIndexDefinition(new)) else {
175+
let addSql = pluginDriver.generateAddIndexSQL(table: tableName, index: new.toPlugin()) else {
176176
return nil
177177
}
178178
let sql = "\(dropSql);\n\(addSql);"
@@ -195,7 +195,7 @@ struct SchemaStatementGenerator {
195195
private func generateAddForeignKey(_ fk: EditableForeignKeyDefinition) -> SchemaStatement? {
196196
guard let sql = pluginDriver.generateAddForeignKeySQL(
197197
table: tableName,
198-
fk: toPluginForeignKeyDefinition(fk)
198+
fk: fk.toPlugin()
199199
) else {
200200
return nil
201201
}
@@ -204,7 +204,7 @@ struct SchemaStatementGenerator {
204204

205205
private func generateModifyForeignKey(old: EditableForeignKeyDefinition, new: EditableForeignKeyDefinition) -> SchemaStatement? {
206206
guard let dropSql = pluginDriver.generateDropForeignKeySQL(table: tableName, constraintName: old.name),
207-
let addSql = pluginDriver.generateAddForeignKeySQL(table: tableName, fk: toPluginForeignKeyDefinition(new)) else {
207+
let addSql = pluginDriver.generateAddForeignKeySQL(table: tableName, fk: new.toPlugin()) else {
208208
return nil
209209
}
210210
let sql = "\(dropSql);\n\(addSql);"
@@ -238,39 +238,4 @@ struct SchemaStatementGenerator {
238238
)
239239
}
240240

241-
// MARK: - Plugin Type Converters
242-
243-
private func toPluginColumnDefinition(_ col: EditableColumnDefinition) -> PluginColumnDefinition {
244-
PluginColumnDefinition(
245-
name: col.name,
246-
dataType: col.dataType,
247-
isNullable: col.isNullable,
248-
defaultValue: col.defaultValue,
249-
isPrimaryKey: col.isPrimaryKey,
250-
autoIncrement: col.autoIncrement,
251-
comment: col.comment,
252-
unsigned: col.unsigned,
253-
onUpdate: col.onUpdate
254-
)
255-
}
256-
257-
private func toPluginIndexDefinition(_ index: EditableIndexDefinition) -> PluginIndexDefinition {
258-
PluginIndexDefinition(
259-
name: index.name,
260-
columns: index.columns,
261-
isUnique: index.isUnique,
262-
indexType: index.type.rawValue
263-
)
264-
}
265-
266-
private func toPluginForeignKeyDefinition(_ fk: EditableForeignKeyDefinition) -> PluginForeignKeyDefinition {
267-
PluginForeignKeyDefinition(
268-
name: fk.name,
269-
columns: fk.columns,
270-
referencedTable: fk.referencedTable,
271-
referencedColumns: fk.referencedColumns,
272-
onDelete: fk.onDelete.rawValue,
273-
onUpdate: fk.onUpdate.rawValue
274-
)
275-
}
276241
}

TablePro/Models/Schema/ColumnDefinition.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import TableProPluginKit
910

1011
/// Column definition for schema modification (editable structure tab)
1112
struct EditableColumnDefinition: Hashable, Codable, Identifiable {
@@ -69,6 +70,14 @@ struct EditableColumnDefinition: Hashable, Codable, Identifiable {
6970
)
7071
}
7172

73+
func toPlugin() -> PluginColumnDefinition {
74+
PluginColumnDefinition(
75+
name: name, dataType: dataType, isNullable: isNullable, defaultValue: defaultValue,
76+
isPrimaryKey: isPrimaryKey, autoIncrement: autoIncrement, comment: comment,
77+
unsigned: unsigned, onUpdate: onUpdate
78+
)
79+
}
80+
7281
/// Convert back to ColumnInfo
7382
func toColumnInfo() -> ColumnInfo {
7483
ColumnInfo(

TablePro/Models/Schema/ForeignKeyDefinition.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import TableProPluginKit
910

1011
/// Foreign key definition for schema modification (editable structure tab)
1112
struct EditableForeignKeyDefinition: Hashable, Codable, Identifiable {
@@ -59,6 +60,13 @@ struct EditableForeignKeyDefinition: Hashable, Codable, Identifiable {
5960
)
6061
}
6162

63+
func toPlugin() -> PluginForeignKeyDefinition {
64+
PluginForeignKeyDefinition(
65+
name: name, columns: columns, referencedTable: referencedTable,
66+
referencedColumns: referencedColumns, onDelete: onDelete.rawValue, onUpdate: onUpdate.rawValue
67+
)
68+
}
69+
6270
/// Convert back to ForeignKeyInfo (single column only)
6371
func toForeignKeyInfo() -> ForeignKeyInfo? {
6472
guard let column = columns.first,

TablePro/Models/Schema/IndexDefinition.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import TableProPluginKit
910

1011
/// Index definition for schema modification (editable structure tab)
1112
struct EditableIndexDefinition: Hashable, Codable, Identifiable {
@@ -59,6 +60,10 @@ struct EditableIndexDefinition: Hashable, Codable, Identifiable {
5960
)
6061
}
6162

63+
func toPlugin() -> PluginIndexDefinition {
64+
PluginIndexDefinition(name: name, columns: columns, isUnique: isUnique, indexType: type.rawValue)
65+
}
66+
6267
/// Convert back to IndexInfo
6368
func toIndexInfo() -> IndexInfo {
6469
IndexInfo(

0 commit comments

Comments
 (0)