From 0e3334a86a07f26affd6362e2c46a73abf3e0461 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 29 Jun 2026 14:16:15 +0700 Subject: [PATCH 1/3] feat(plugins): show table and column comments in sidebar and grid --- CHANGELOG.md | 1 + .../MySQLDriverPlugin/MySQLPluginDriver.swift | 14 ++++-- .../PostgreSQLPluginDriver.swift | 4 +- .../PostgreSQLSchemaQueries.swift | 22 ++++++--- .../TableProPluginKit/PluginTableInfo.swift | 17 +++++++ .../QueryExecutionCoordinator+Helpers.swift | 7 ++- .../Core/Plugins/PluginDriverAdapter.swift | 3 +- .../Core/Services/Query/QueryExecutor.swift | 11 ++++- TablePro/Models/Query/QueryResult.swift | 4 +- TablePro/Models/Query/TableRows.swift | 12 ++++- .../Models/Settings/GeneralSettings.swift | 11 ++++- TablePro/TableProApp.swift | 9 ++++ .../Views/Results/DataGridColumnPool.swift | 8 ++- TablePro/Views/Results/DataGridView.swift | 4 ++ .../Views/RightSidebar/RightSidebarView.swift | 10 ++++ TablePro/Views/Sidebar/TableRowView.swift | 23 ++++++++- .../Query/ParseSchemaMetadataTests.swift | 49 +++++++++++++++++++ .../Models/Query/TableRowsTests.swift | 27 ++++++++++ TableProTests/Models/TableInfoTests.swift | 25 ++++++++++ .../Plugins/PluginTableInfoTests.swift | 39 +++++++++++++++ .../PostgreSQLFetchTablesCommentTests.swift | 29 +++++++++++ docs/databases/mysql.mdx | 6 ++- docs/databases/postgresql.mdx | 6 ++- 23 files changed, 317 insertions(+), 24 deletions(-) create mode 100644 TableProTests/Core/Services/Query/ParseSchemaMetadataTests.swift create mode 100644 TableProTests/Plugins/PluginTableInfoTests.swift create mode 100644 TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d871bd8e..6a539d31d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Per-tab database picker in the query editor toolbar. Each SQL tab can target its own database without clearing other tabs. - Single-clicking a table in the sidebar tree opens it in the current tab; double-clicking opens it in a new tab. +- Table and column comments from the database now show in the UI. The sidebar shows a table's comment in dimmed text after its name, the data grid column header tooltip includes the column comment, and the table inspector shows the table comment. Toggle from View > Show Object Comments. Available for MySQL and PostgreSQL. (#1771) ### Changed diff --git a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift index e92826be0..4fdee3ee6 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift @@ -188,13 +188,21 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { // MARK: - Schema Operations func fetchTables(schema: String?) async throws -> [PluginTableInfo] { - let result = try await execute(query: "SHOW FULL TABLES") + let query = """ + SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + """ + let result = try await execute(query: query) return result.rows.compactMap { row -> PluginTableInfo? in guard let name = row[safe: 0]?.asText else { return nil } let typeStr = (row[safe: 1]?.asText) ?? "BASE TABLE" - let type = typeStr.contains("VIEW") ? "VIEW" : "TABLE" - return PluginTableInfo(name: name, type: type) + let isView = typeStr.contains("VIEW") + let type = isView ? "VIEW" : "TABLE" + let rawComment = row[safe: 2]?.asText + let comment = (!isView && rawComment?.isEmpty == false) ? rawComment : nil + return PluginTableInfo(name: name, type: type, comment: comment) }.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift index ed4601d27..fedc493c9 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift @@ -182,7 +182,9 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable { case "VIEW": type = "VIEW" default: type = "TABLE" } - return PluginTableInfo(name: name, type: type) + let rawComment = row[safe: 2]?.asText + let comment = rawComment?.isEmpty == false ? rawComment : nil + return PluginTableInfo(name: name, type: type, comment: comment) } } diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift index 5e1be9490..5946707e8 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift @@ -79,18 +79,25 @@ enum PostgreSQLSchemaQueries { ) -> String { var unions: [String] = [ """ - SELECT table_name, table_type FROM information_schema.tables - WHERE table_schema = '\(schemaLiteral)' - AND table_type IN ('BASE TABLE', 'VIEW') + SELECT t.table_name, t.table_type, d.description AS table_comment + FROM information_schema.tables t + LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema + LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name AND c.relnamespace = n.oid + LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = 0 + WHERE t.table_schema = '\(schemaLiteral)' + AND t.table_type IN ('BASE TABLE', 'VIEW') """ ] if includeMaterializedViews { unions.append( """ - SELECT matviewname AS table_name, 'MATERIALIZED VIEW' AS table_type - FROM pg_matviews - WHERE schemaname = '\(schemaLiteral)' + SELECT m.matviewname AS table_name, 'MATERIALIZED VIEW' AS table_type, d.description AS table_comment + FROM pg_matviews m + LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = m.schemaname + LEFT JOIN pg_catalog.pg_class c ON c.relname = m.matviewname AND c.relnamespace = n.oid + LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = 0 + WHERE m.schemaname = '\(schemaLiteral)' """ ) } @@ -98,10 +105,11 @@ enum PostgreSQLSchemaQueries { if includeForeignTables { unions.append( """ - SELECT c.relname AS table_name, 'FOREIGN TABLE' AS table_type + SELECT c.relname AS table_name, 'FOREIGN TABLE' AS table_type, d.description AS table_comment FROM pg_foreign_table ft JOIN pg_class c ON c.oid = ft.ftrelid JOIN pg_namespace n ON n.oid = c.relnamespace + LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = 0 WHERE n.nspname = '\(schemaLiteral)' """ ) diff --git a/Plugins/TableProPluginKit/PluginTableInfo.swift b/Plugins/TableProPluginKit/PluginTableInfo.swift index fdc87b4cf..32c47d944 100644 --- a/Plugins/TableProPluginKit/PluginTableInfo.swift +++ b/Plugins/TableProPluginKit/PluginTableInfo.swift @@ -5,7 +5,23 @@ public struct PluginTableInfo: Codable, Sendable { public let type: String public let rowCount: Int? public let schema: String? + public let comment: String? + public init( + name: String, + type: String = "TABLE", + rowCount: Int? = nil, + schema: String? = nil, + comment: String? + ) { + self.name = name + self.type = type + self.rowCount = rowCount + self.schema = schema + self.comment = comment + } + + @_disfavoredOverload public init( name: String, type: String = "TABLE", @@ -16,5 +32,6 @@ public struct PluginTableInfo: Codable, Sendable { self.type = type self.rowCount = rowCount self.schema = schema + self.comment = nil } } diff --git a/TablePro/Core/Coordinators/QueryExecutionCoordinator+Helpers.swift b/TablePro/Core/Coordinators/QueryExecutionCoordinator+Helpers.swift index e8c8db7ed..44ffedb09 100644 --- a/TablePro/Core/Coordinators/QueryExecutionCoordinator+Helpers.swift +++ b/TablePro/Core/Coordinators/QueryExecutionCoordinator+Helpers.swift @@ -79,6 +79,7 @@ extension QueryExecutionCoordinator { var columnDefaults: [String: String?] = [:] var columnForeignKeys: [String: ForeignKeyInfo] = [:] var columnNullable: [String: Bool] = [:] + var columnComments: [String: String] = [:] for (index, colType) in columnTypes.enumerated() { if case .enumType(_, let values) = colType, let vals = values, index < columns.count { columnEnumValues[columns[index]] = vals @@ -91,6 +92,7 @@ extension QueryExecutionCoordinator { columnDefaults = metadata.columnDefaults columnForeignKeys = metadata.columnForeignKeys ?? [:] columnNullable = metadata.columnNullable + columnComments = metadata.columnComments foreignKeysFetched = metadata.columnForeignKeys != nil for (col, vals) in metadata.columnEnumValues { columnEnumValues[col] = vals @@ -100,6 +102,7 @@ extension QueryExecutionCoordinator { columnDefaults = existing.columnDefaults columnForeignKeys = existing.columnForeignKeys columnNullable = existing.columnNullable + columnComments = existing.columnComments foreignKeysFetched = existing.foreignKeysFetched for (col, vals) in existing.columnEnumValues where columnEnumValues[col] == nil { columnEnumValues[col] = vals @@ -114,6 +117,7 @@ extension QueryExecutionCoordinator { columnForeignKeys: columnForeignKeys, columnEnumValues: columnEnumValues, columnNullable: columnNullable, + columnComments: columnComments, foreignKeysFetched: foreignKeysFetched ) parent.setActiveTableRows(newTableRows, for: existingTabId) @@ -333,7 +337,8 @@ extension QueryExecutionCoordinator { rows.updateDisplayMetadata( columnDefaults: parsed.columnDefaults, columnForeignKeys: parsed.columnForeignKeys, - columnNullable: parsed.columnNullable + columnNullable: parsed.columnNullable, + columnComments: parsed.columnComments ) } diff --git a/TablePro/Core/Plugins/PluginDriverAdapter.swift b/TablePro/Core/Plugins/PluginDriverAdapter.swift index aab1e408d..a2e3e39f2 100644 --- a/TablePro/Core/Plugins/PluginDriverAdapter.swift +++ b/TablePro/Core/Plugins/PluginDriverAdapter.swift @@ -194,7 +194,8 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable { name: table.name, type: tableType, rowCount: table.rowCount, - schema: table.schema ?? schemaFallback + schema: table.schema ?? schemaFallback, + comment: table.comment ) } diff --git a/TablePro/Core/Services/Query/QueryExecutor.swift b/TablePro/Core/Services/Query/QueryExecutor.swift index c605b49f1..76377ccee 100644 --- a/TablePro/Core/Services/Query/QueryExecutor.swift +++ b/TablePro/Core/Services/Query/QueryExecutor.swift @@ -28,6 +28,7 @@ struct ParsedSchemaMetadata { let primaryKeyColumns: [String] let approximateRowCount: Int? let columnEnumValues: [String: [String]] + let columnComments: [String: String] } @MainActor @@ -169,10 +170,14 @@ final class QueryExecutor { fks = byColumn } var enumValues: [String: [String]] = [:] + var comments: [String: String] = [:] for col in schema.columns { if let values = col.allowedValues, !values.isEmpty { enumValues[col.name] = values } + if let comment = col.comment, !comment.isEmpty { + comments[col.name] = comment + } } return ParsedSchemaMetadata( columnDefaults: defaults, @@ -180,7 +185,8 @@ final class QueryExecutor { columnNullable: nullable, primaryKeyColumns: schema.columns.filter { $0.isPrimaryKey }.map(\.name), approximateRowCount: schema.approximateRowCount, - columnEnumValues: enumValues + columnEnumValues: enumValues, + columnComments: comments ) } @@ -200,7 +206,8 @@ final class QueryExecutor { columnNullable: nullable, primaryKeyColumns: primaryKeys, approximateRowCount: nil, - columnEnumValues: [:] + columnEnumValues: [:], + columnComments: [:] ) } diff --git a/TablePro/Models/Query/QueryResult.swift b/TablePro/Models/Query/QueryResult.swift index 57472d715..8665bceae 100644 --- a/TablePro/Models/Query/QueryResult.swift +++ b/TablePro/Models/Query/QueryResult.swift @@ -91,6 +91,7 @@ struct TableInfo: Identifiable, Hashable, Sendable { let type: TableType let rowCount: Int? let schema: String? + let comment: String? enum TableType: String, Sendable { case table = "TABLE" @@ -100,11 +101,12 @@ struct TableInfo: Identifiable, Hashable, Sendable { case systemTable = "SYSTEM TABLE" } - init(name: String, type: TableType, rowCount: Int?, schema: String? = nil) { + init(name: String, type: TableType, rowCount: Int?, schema: String? = nil, comment: String? = nil) { self.name = name self.type = type self.rowCount = rowCount self.schema = schema + self.comment = comment } static func == (lhs: TableInfo, rhs: TableInfo) -> Bool { diff --git a/TablePro/Models/Query/TableRows.swift b/TablePro/Models/Query/TableRows.swift index f6c329728..899f4780e 100644 --- a/TablePro/Models/Query/TableRows.swift +++ b/TablePro/Models/Query/TableRows.swift @@ -15,6 +15,7 @@ struct TableRows: Sendable { var columnForeignKeys: [String: ForeignKeyInfo] var columnEnumValues: [String: [String]] var columnNullable: [String: Bool] + var columnComments: [String: String] var foreignKeysFetched: Bool init( @@ -25,6 +26,7 @@ struct TableRows: Sendable { columnForeignKeys: [String: ForeignKeyInfo] = [:], columnEnumValues: [String: [String]] = [:], columnNullable: [String: Bool] = [:], + columnComments: [String: String] = [:], foreignKeysFetched: Bool = false ) { self.rows = rows @@ -35,6 +37,7 @@ struct TableRows: Sendable { self.columnForeignKeys = columnForeignKeys self.columnEnumValues = columnEnumValues self.columnNullable = columnNullable + self.columnComments = columnComments self.foreignKeysFetched = foreignKeysFetched } @@ -158,7 +161,8 @@ struct TableRows: Sendable { columnDefaults: [String: String?]? = nil, columnForeignKeys: [String: ForeignKeyInfo]? = nil, columnEnumValues: [String: [String]]? = nil, - columnNullable: [String: Bool]? = nil + columnNullable: [String: Bool]? = nil, + columnComments: [String: String]? = nil ) -> Delta { var didChange = false if let columnTypes, columnTypes != self.columnTypes { @@ -184,6 +188,10 @@ struct TableRows: Sendable { self.columnNullable = columnNullable didChange = true } + if let columnComments, columnComments != self.columnComments { + self.columnComments = columnComments + didChange = true + } return didChange ? .columnsReplaced : .none } @@ -195,6 +203,7 @@ struct TableRows: Sendable { columnForeignKeys: [String: ForeignKeyInfo] = [:], columnEnumValues: [String: [String]] = [:], columnNullable: [String: Bool] = [:], + columnComments: [String: String] = [:], foreignKeysFetched: Bool = false ) -> TableRows { var rows = ContiguousArray() @@ -211,6 +220,7 @@ struct TableRows: Sendable { columnForeignKeys: columnForeignKeys, columnEnumValues: columnEnumValues, columnNullable: columnNullable, + columnComments: columnComments, foreignKeysFetched: foreignKeysFetched ) } diff --git a/TablePro/Models/Settings/GeneralSettings.swift b/TablePro/Models/Settings/GeneralSettings.swift index babe1fd53..999f2b74d 100644 --- a/TablePro/Models/Settings/GeneralSettings.swift +++ b/TablePro/Models/Settings/GeneralSettings.swift @@ -63,12 +63,16 @@ struct GeneralSettings: Codable, Equatable { /// Whether to share anonymous usage analytics var shareAnalytics: Bool + /// Whether to show database object comments in the sidebar and data grid headers + var showObjectComments: Bool + static let `default` = GeneralSettings( startupBehavior: .reopenLast, language: .system, automaticallyCheckForUpdates: true, queryTimeoutSeconds: 60, - shareAnalytics: true + shareAnalytics: true, + showObjectComments: true ) init( @@ -76,13 +80,15 @@ struct GeneralSettings: Codable, Equatable { language: AppLanguage = .system, automaticallyCheckForUpdates: Bool = true, queryTimeoutSeconds: Int = 60, - shareAnalytics: Bool = true + shareAnalytics: Bool = true, + showObjectComments: Bool = true ) { self.startupBehavior = startupBehavior self.language = language self.automaticallyCheckForUpdates = automaticallyCheckForUpdates self.queryTimeoutSeconds = queryTimeoutSeconds self.shareAnalytics = shareAnalytics + self.showObjectComments = showObjectComments } init(from decoder: Decoder) throws { @@ -92,5 +98,6 @@ struct GeneralSettings: Codable, Equatable { automaticallyCheckForUpdates = try container.decodeIfPresent(Bool.self, forKey: .automaticallyCheckForUpdates) ?? true queryTimeoutSeconds = try container.decodeIfPresent(Int.self, forKey: .queryTimeoutSeconds) ?? 60 shareAnalytics = try container.decodeIfPresent(Bool.self, forKey: .shareAnalytics) ?? true + showObjectComments = try container.decodeIfPresent(Bool.self, forKey: .showObjectComments) ?? true } } diff --git a/TablePro/TableProApp.swift b/TablePro/TableProApp.swift index 35234b825..1ac21f65a 100644 --- a/TablePro/TableProApp.swift +++ b/TablePro/TableProApp.swift @@ -150,6 +150,13 @@ struct AppMenuCommands: Commands { ) } + private var showObjectCommentsBinding: Binding { + Binding( + get: { settingsManager.general.showObjectComments }, + set: { settingsManager.general.showObjectComments = $0 } + ) + } + private func shortcut(for action: ShortcutAction) -> KeyboardShortcut? { settingsManager.keyboard.keyboardShortcut(for: action) } @@ -603,6 +610,8 @@ struct AppMenuCommands: Commands { .pickerStyle(.inline) .disabled(!(actions?.canSwitchSidebarLayout ?? false)) + Toggle(String(localized: "Show Object Comments"), isOn: showObjectCommentsBinding) + Divider() Button("Toggle Filters") { diff --git a/TablePro/Views/Results/DataGridColumnPool.swift b/TablePro/Views/Results/DataGridColumnPool.swift index d6e81741e..870c2420a 100644 --- a/TablePro/Views/Results/DataGridColumnPool.swift +++ b/TablePro/Views/Results/DataGridColumnPool.swift @@ -28,6 +28,7 @@ final class DataGridColumnPool { tableView: NSTableView, schema: ColumnIdentitySchema, columnTypes: [ColumnType], + columnComments: [String: String], savedLayout: ColumnLayoutState?, isEditable: Bool, hiddenColumnNames: Set, @@ -52,6 +53,7 @@ final class DataGridColumnPool { column, name: columnName, columnType: slot < columnTypes.count ? columnTypes[slot] : nil, + comment: columnComments[columnName], width: resolvedWidth, isEditable: isEditable ) @@ -177,6 +179,7 @@ final class DataGridColumnPool { _ column: NSTableColumn, name: String, columnType: ColumnType?, + comment: String?, width: CGFloat, isEditable: Bool ) { @@ -187,12 +190,15 @@ final class DataGridColumnPool { column.headerCell = cell } - let tooltip: String + var tooltip: String if let typeName = columnType?.rawType ?? columnType?.displayName { tooltip = "\(name) (\(typeName))" } else { tooltip = name } + if let comment, !comment.isEmpty { + tooltip += "\n\(comment)" + } if column.headerToolTip != tooltip { column.headerToolTip = tooltip } diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index 5ac88e547..d926913f2 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -305,10 +305,14 @@ struct DataGridView: NSViewRepresentable { tableRows: TableRows, savedLayout: ColumnLayoutState? ) { + let columnComments = AppSettingsManager.shared.general.showObjectComments + ? tableRows.columnComments + : [:] coordinator.columnPool.reconcile( tableView: tableView, schema: coordinator.identitySchema, columnTypes: tableRows.columnTypes, + columnComments: columnComments, savedLayout: savedLayout, isEditable: isEditable, hiddenColumnNames: configuration.hiddenColumns, diff --git a/TablePro/Views/RightSidebar/RightSidebarView.swift b/TablePro/Views/RightSidebar/RightSidebarView.swift index 90495798b..06b6972b8 100644 --- a/TablePro/Views/RightSidebar/RightSidebarView.swift +++ b/TablePro/Views/RightSidebar/RightSidebarView.swift @@ -65,6 +65,16 @@ struct RightSidebarView: View { private func tableInfoContent(_ metadata: TableMetadata) -> some View { Form { + if let comment = metadata.comment, !comment.isEmpty { + Section { + Text(comment) + .fixedSize(horizontal: false, vertical: true) + .textSelection(.enabled) + } header: { + Text("COMMENT") + } + } + Section { LabeledContent( String(localized: "Data Size"), diff --git a/TablePro/Views/Sidebar/TableRowView.swift b/TablePro/Views/Sidebar/TableRowView.swift index d4f10de94..9e5a93fe0 100644 --- a/TablePro/Views/Sidebar/TableRowView.swift +++ b/TablePro/Views/Sidebar/TableRowView.swift @@ -49,6 +49,13 @@ struct TableRow: View { @State private var isHovered = false + private var visibleComment: String? { + guard AppSettingsManager.shared.general.showObjectComments, + let comment = table.comment, !comment.isEmpty + else { return nil } + return comment + } + @ViewBuilder private var pendingStateBadge: some View { if isPendingDelete { @@ -65,8 +72,19 @@ struct TableRow: View { var body: some View { HStack(spacing: 6) { Label { - Text(table.name) - .lineLimit(1) + HStack(spacing: 6) { + Text(table.name) + .lineLimit(1) + .layoutPriority(1) + if let visibleComment { + Text(visibleComment) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.tail) + .help(visibleComment) + } + } } icon: { Image(systemName: TableRowLogic.iconName(for: table.type)) .sidebarTint(Color.accentColor) @@ -106,6 +124,7 @@ struct TableRow: View { isFavorite: isFavorite ) ) + .accessibilityHint(visibleComment ?? "") .modifier(FavoriteAccessibilityAction(isFavorite: isFavorite, toggle: onToggleFavorite)) } } diff --git a/TableProTests/Core/Services/Query/ParseSchemaMetadataTests.swift b/TableProTests/Core/Services/Query/ParseSchemaMetadataTests.swift new file mode 100644 index 000000000..55d8673d8 --- /dev/null +++ b/TableProTests/Core/Services/Query/ParseSchemaMetadataTests.swift @@ -0,0 +1,49 @@ +import Foundation +import Testing +@testable import TablePro + +@Suite("QueryExecutor.parseSchemaMetadata - column comments") +struct ParseSchemaMetadataTests { + private func column(_ name: String, comment: String?) -> ColumnInfo { + ColumnInfo( + name: name, + dataType: "TEXT", + isNullable: true, + isPrimaryKey: false, + comment: comment + ) + } + + @Test("Populates columnComments from non-empty ColumnInfo comments") + func populatesComments() { + let schema = FetchedTableSchema( + columns: [column("id", comment: "Primary key"), column("name", comment: "Display name")], + foreignKeys: nil, + approximateRowCount: nil + ) + let parsed = QueryExecutor.parseSchemaMetadata(schema) + #expect(parsed.columnComments == ["id": "Primary key", "name": "Display name"]) + } + + @Test("Excludes nil and empty comments") + func excludesNilAndEmpty() { + let schema = FetchedTableSchema( + columns: [column("id", comment: nil), column("name", comment: ""), column("email", comment: "Contact")], + foreignKeys: nil, + approximateRowCount: nil + ) + let parsed = QueryExecutor.parseSchemaMetadata(schema) + #expect(parsed.columnComments == ["email": "Contact"]) + } + + @Test("Returns an empty dictionary when no column has a comment") + func emptyWhenNoComments() { + let schema = FetchedTableSchema( + columns: [column("id", comment: nil), column("name", comment: nil)], + foreignKeys: nil, + approximateRowCount: nil + ) + let parsed = QueryExecutor.parseSchemaMetadata(schema) + #expect(parsed.columnComments.isEmpty) + } +} diff --git a/TableProTests/Models/Query/TableRowsTests.swift b/TableProTests/Models/Query/TableRowsTests.swift index 51cf0c3e6..45a16850b 100644 --- a/TableProTests/Models/Query/TableRowsTests.swift +++ b/TableProTests/Models/Query/TableRowsTests.swift @@ -575,6 +575,33 @@ struct TableRowsMetadataTests { ) #expect(delta == .none) } + + @Test("updateDisplayMetadata stores columnComments and reports columnsReplaced") + func updateDisplayMetadataStoresComments() { + var table = Self.makeTable() + let delta = table.updateDisplayMetadata(columnComments: ["c1": "Primary key"]) + #expect(delta == .columnsReplaced) + #expect(table.columnComments == ["c1": "Primary key"]) + } + + @Test("updateDisplayMetadata returns Delta.none when columnComments are unchanged") + func updateDisplayMetadataUnchangedCommentsIsNoOp() { + var table = Self.makeTable() + _ = table.updateDisplayMetadata(columnComments: ["c1": "Primary key"]) + let delta = table.updateDisplayMetadata(columnComments: ["c1": "Primary key"]) + #expect(delta == .none) + } + + @Test("Factory preserves columnComments") + func factoryPreservesComments() { + let table = TableRows.from( + queryRows: [["a"]], + columns: ["c1"], + columnTypes: [.text(rawType: nil)], + columnComments: ["c1": "A note"] + ) + #expect(table.columnComments == ["c1": "A note"]) + } } @Suite("TableRows - metadata preservation regression") diff --git a/TableProTests/Models/TableInfoTests.swift b/TableProTests/Models/TableInfoTests.swift index 65843b951..e0670de29 100644 --- a/TableProTests/Models/TableInfoTests.swift +++ b/TableProTests/Models/TableInfoTests.swift @@ -151,6 +151,31 @@ struct TableInfoTests { #expect(selected.contains(lookup)) } + // MARK: - Comment does not affect identity + + @Test("Comment does not affect equality") + func testCommentDoesNotAffectEquality() { + let a = TableInfo(name: "users", type: .table, rowCount: nil, comment: "Account records") + let b = TableInfo(name: "users", type: .table, rowCount: nil, comment: nil) + #expect(a == b) + } + + @Test("Comment does not affect hash") + func testCommentDoesNotAffectHash() { + let a = TableInfo(name: "users", type: .table, rowCount: nil, comment: "Account records") + let b = TableInfo(name: "users", type: .table, rowCount: nil, comment: "Something else") + #expect(a.hashValue == b.hashValue) + } + + @Test("Set deduplication ignores comment") + func testSetDeduplicationIgnoresComment() { + let a = TableInfo(name: "orders", type: .table, rowCount: nil, comment: "First") + let b = TableInfo(name: "orders", type: .table, rowCount: nil, comment: "Second") + var set: Set = [a] + set.insert(b) + #expect(set.count == 1) + } + @Test("Subtracting sets works correctly") func testSetSubtraction() { let all: Set = [ diff --git a/TableProTests/Plugins/PluginTableInfoTests.swift b/TableProTests/Plugins/PluginTableInfoTests.swift new file mode 100644 index 000000000..91f353276 --- /dev/null +++ b/TableProTests/Plugins/PluginTableInfoTests.swift @@ -0,0 +1,39 @@ +import Foundation +import TableProPluginKit +import Testing + +@Suite("PluginTableInfo") +struct PluginTableInfoTests { + @Test("Init without comment leaves it nil") + func initWithoutComment() { + let info = PluginTableInfo(name: "users", type: "TABLE") + #expect(info.comment == nil) + } + + @Test("Init with comment stores it") + func initWithComment() { + let info = PluginTableInfo(name: "users", type: "TABLE", comment: "Account records") + #expect(info.comment == "Account records") + } + + @Test("comment round-trips through JSON encoding") + func commentRoundTrip() throws { + let original = PluginTableInfo(name: "orders", type: "TABLE", comment: "Customer orders") + let data = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(PluginTableInfo.self, from: data) + #expect(decoded.comment == "Customer orders") + } + + @Test("decoding a payload without comment keeps it nil for forward compatibility") + func legacyPayloadDecodesToNilComment() throws { + let legacyJson = """ + { + "name": "users", + "type": "TABLE" + } + """.data(using: .utf8)! + let decoded = try JSONDecoder().decode(PluginTableInfo.self, from: legacyJson) + #expect(decoded.comment == nil) + #expect(decoded.name == "users") + } +} diff --git a/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift b/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift new file mode 100644 index 000000000..d874e285f --- /dev/null +++ b/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift @@ -0,0 +1,29 @@ +import Foundation +import TableProPluginKit +import Testing + +@Suite("PostgreSQLSchemaQueries.fetchTables comments") +struct PostgreSQLFetchTablesCommentTests { + @Test("Base query selects the table comment from pg_description") + func baseQuerySelectsComment() { + let query = PostgreSQLSchemaQueries.fetchTables( + schemaLiteral: "public", + includeMaterializedViews: false, + includeForeignTables: false + ) + #expect(query.contains("table_comment")) + #expect(query.contains("pg_description")) + } + + @Test("Every union branch projects a comment column so columns stay aligned") + func allBranchesProjectComment() { + let query = PostgreSQLSchemaQueries.fetchTables( + schemaLiteral: "public", + includeMaterializedViews: true, + includeForeignTables: true + ) + let commentColumns = query.components(separatedBy: "AS table_comment").count - 1 + let branches = query.components(separatedBy: "UNION ALL").count + #expect(commentColumns == branches) + } +} diff --git a/docs/databases/mysql.mdx b/docs/databases/mysql.mdx index 446fcf7ba..7095f78b6 100644 --- a/docs/databases/mysql.mdx +++ b/docs/databases/mysql.mdx @@ -75,7 +75,11 @@ Same driver for both. MySQL 8.0 defaults to `caching_sha2_password` auth (vs Mar ## Features -Sidebar shows all accessible databases, tables, structure (columns, indexes, foreign keys), and DDL. Switch databases with **Cmd+K**. Full MySQL syntax support for queries: +Sidebar shows all accessible databases, tables, structure (columns, indexes, foreign keys), and DDL. Switch databases with **Cmd+K**. + +Table and column comments are shown in the UI: the sidebar shows a table's comment in dimmed text after its name (full text on hover), and the data grid column header tooltip includes the column comment. Turn this off from **View > Show Object Comments**. + +Full MySQL syntax support for queries: ```sql -- Select with JSON diff --git a/docs/databases/postgresql.mdx b/docs/databases/postgresql.mdx index 051f5d9ef..d9f0f0174 100644 --- a/docs/databases/postgresql.mdx +++ b/docs/databases/postgresql.mdx @@ -71,7 +71,11 @@ The database role must be set up for IAM auth (`GRANT rds_iam TO "user"`). Conne ## Features -Sidebar displays all accessible schemas and tables. Switch databases/schemas with **Cmd+K**. Table info shows structure (columns, indexes, constraints) and DDL. Full PostgreSQL syntax support: +Sidebar displays all accessible schemas and tables. Switch databases/schemas with **Cmd+K**. Table info shows structure (columns, indexes, constraints) and DDL. + +Table and column comments are shown in the UI: the sidebar shows a table's comment in dimmed text after its name (full text on hover), and the data grid column header tooltip includes the column comment. Turn this off from **View > Show Object Comments**. + +Full PostgreSQL syntax support: ```sql -- JSONB queries From be2b62497528d6410e16f8d52dadbdfb90ee0866 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 29 Jun 2026 14:33:47 +0700 Subject: [PATCH 2/3] refactor(plugins): address review of object-comment display --- .../MySQLDriverPlugin/MySQLPluginDriver.swift | 3 +-- .../PostgreSQLPluginDriver.swift | 3 +-- .../PostgreSQLSchemaQueries.swift | 16 ++++++---------- Plugins/TableProPluginKit/StringExtension.swift | 7 +++++++ TablePro/Core/Services/Query/QueryExecutor.swift | 2 +- .../Views/Results/DataGridUpdateSnapshot.swift | 1 + TablePro/Views/Results/DataGridView.swift | 3 ++- .../Views/RightSidebar/RightSidebarView.swift | 3 ++- TablePro/Views/Sidebar/TableRowView.swift | 1 - .../PostgreSQLFetchTablesCommentTests.swift | 15 +++++++++++++-- 10 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 Plugins/TableProPluginKit/StringExtension.swift diff --git a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift index 4fdee3ee6..fadb612d5 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift @@ -200,8 +200,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { let typeStr = (row[safe: 1]?.asText) ?? "BASE TABLE" let isView = typeStr.contains("VIEW") let type = isView ? "VIEW" : "TABLE" - let rawComment = row[safe: 2]?.asText - let comment = (!isView && rawComment?.isEmpty == false) ? rawComment : nil + let comment = isView ? nil : row[safe: 2]?.asText?.nilIfEmpty return PluginTableInfo(name: name, type: type, comment: comment) }.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift index fedc493c9..191c995ad 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift @@ -182,8 +182,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable { case "VIEW": type = "VIEW" default: type = "TABLE" } - let rawComment = row[safe: 2]?.asText - let comment = rawComment?.isEmpty == false ? rawComment : nil + let comment = row[safe: 2]?.asText?.nilIfEmpty return PluginTableInfo(name: name, type: type, comment: comment) } } diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift index 5946707e8..135e54431 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift @@ -79,11 +79,9 @@ enum PostgreSQLSchemaQueries { ) -> String { var unions: [String] = [ """ - SELECT t.table_name, t.table_type, d.description AS table_comment + SELECT t.table_name, t.table_type, + obj_description(to_regclass(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name)), 'pg_class') AS table_comment FROM information_schema.tables t - LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema - LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name AND c.relnamespace = n.oid - LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = 0 WHERE t.table_schema = '\(schemaLiteral)' AND t.table_type IN ('BASE TABLE', 'VIEW') """ @@ -92,11 +90,9 @@ enum PostgreSQLSchemaQueries { if includeMaterializedViews { unions.append( """ - SELECT m.matviewname AS table_name, 'MATERIALIZED VIEW' AS table_type, d.description AS table_comment + SELECT m.matviewname AS table_name, 'MATERIALIZED VIEW' AS table_type, + obj_description(to_regclass(quote_ident(m.schemaname) || '.' || quote_ident(m.matviewname)), 'pg_class') AS table_comment FROM pg_matviews m - LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = m.schemaname - LEFT JOIN pg_catalog.pg_class c ON c.relname = m.matviewname AND c.relnamespace = n.oid - LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = 0 WHERE m.schemaname = '\(schemaLiteral)' """ ) @@ -105,11 +101,11 @@ enum PostgreSQLSchemaQueries { if includeForeignTables { unions.append( """ - SELECT c.relname AS table_name, 'FOREIGN TABLE' AS table_type, d.description AS table_comment + SELECT c.relname AS table_name, 'FOREIGN TABLE' AS table_type, + obj_description(c.oid, 'pg_class') AS table_comment FROM pg_foreign_table ft JOIN pg_class c ON c.oid = ft.ftrelid JOIN pg_namespace n ON n.oid = c.relnamespace - LEFT JOIN pg_catalog.pg_description d ON d.objoid = c.oid AND d.objsubid = 0 WHERE n.nspname = '\(schemaLiteral)' """ ) diff --git a/Plugins/TableProPluginKit/StringExtension.swift b/Plugins/TableProPluginKit/StringExtension.swift new file mode 100644 index 000000000..bf43b5299 --- /dev/null +++ b/Plugins/TableProPluginKit/StringExtension.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension String { + var nilIfEmpty: String? { + isEmpty ? nil : self + } +} diff --git a/TablePro/Core/Services/Query/QueryExecutor.swift b/TablePro/Core/Services/Query/QueryExecutor.swift index 76377ccee..3e1a65e5a 100644 --- a/TablePro/Core/Services/Query/QueryExecutor.swift +++ b/TablePro/Core/Services/Query/QueryExecutor.swift @@ -175,7 +175,7 @@ final class QueryExecutor { if let values = col.allowedValues, !values.isEmpty { enumValues[col.name] = values } - if let comment = col.comment, !comment.isEmpty { + if let comment = col.comment?.nilIfEmpty { comments[col.name] = comment } } diff --git a/TablePro/Views/Results/DataGridUpdateSnapshot.swift b/TablePro/Views/Results/DataGridUpdateSnapshot.swift index 575835904..133ffd02d 100644 --- a/TablePro/Views/Results/DataGridUpdateSnapshot.swift +++ b/TablePro/Views/Results/DataGridUpdateSnapshot.swift @@ -20,4 +20,5 @@ struct DataGridUpdateSnapshot: Equatable { let rowHeight: CGFloat let alternatingRows: Bool let reloadVersion: Int + let showObjectComments: Bool } diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index d926913f2..e29267c74 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -166,7 +166,8 @@ struct DataGridView: NSViewRepresentable { hasMoveDelegate: delegate != nil, rowHeight: rowHeight, alternatingRows: alternatingRows, - reloadVersion: changeManager.reloadVersion + reloadVersion: changeManager.reloadVersion, + showObjectComments: AppSettingsManager.shared.general.showObjectComments ) if snapshot != coordinator.lastUpdateSnapshot { diff --git a/TablePro/Views/RightSidebar/RightSidebarView.swift b/TablePro/Views/RightSidebar/RightSidebarView.swift index 06b6972b8..8a1ef0633 100644 --- a/TablePro/Views/RightSidebar/RightSidebarView.swift +++ b/TablePro/Views/RightSidebar/RightSidebarView.swift @@ -65,7 +65,8 @@ struct RightSidebarView: View { private func tableInfoContent(_ metadata: TableMetadata) -> some View { Form { - if let comment = metadata.comment, !comment.isEmpty { + if AppSettingsManager.shared.general.showObjectComments, + let comment = metadata.comment, !comment.isEmpty { Section { Text(comment) .fixedSize(horizontal: false, vertical: true) diff --git a/TablePro/Views/Sidebar/TableRowView.swift b/TablePro/Views/Sidebar/TableRowView.swift index 9e5a93fe0..a5ad08a4e 100644 --- a/TablePro/Views/Sidebar/TableRowView.swift +++ b/TablePro/Views/Sidebar/TableRowView.swift @@ -124,7 +124,6 @@ struct TableRow: View { isFavorite: isFavorite ) ) - .accessibilityHint(visibleComment ?? "") .modifier(FavoriteAccessibilityAction(isFavorite: isFavorite, toggle: onToggleFavorite)) } } diff --git a/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift b/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift index d874e285f..ce32f81d4 100644 --- a/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift +++ b/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift @@ -4,7 +4,7 @@ import Testing @Suite("PostgreSQLSchemaQueries.fetchTables comments") struct PostgreSQLFetchTablesCommentTests { - @Test("Base query selects the table comment from pg_description") + @Test("Base query selects the table comment via obj_description") func baseQuerySelectsComment() { let query = PostgreSQLSchemaQueries.fetchTables( schemaLiteral: "public", @@ -12,7 +12,18 @@ struct PostgreSQLFetchTablesCommentTests { includeForeignTables: false ) #expect(query.contains("table_comment")) - #expect(query.contains("pg_description")) + #expect(query.contains("obj_description")) + } + + @Test("Base query does not reference pg_class/pg_namespace so the portability fallback stays minimal") + func baseQueryStaysPortable() { + let query = PostgreSQLSchemaQueries.fetchTables( + schemaLiteral: "public", + includeMaterializedViews: false, + includeForeignTables: false + ) + #expect(!query.contains("pg_catalog.pg_class")) + #expect(!query.contains("pg_catalog.pg_namespace")) } @Test("Every union branch projects a comment column so columns stay aligned") From 4770ce7af681d87bd315e12a1b3646d7f1dcb094 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 30 Jun 2026 04:38:39 +0700 Subject: [PATCH 3/3] fix(plugins): default columnComments param and degrade PostgreSQL comment fetch gracefully (#1782) --- .../PostgreSQLPluginDriver.swift | 25 ++++++++++--------- .../PostgreSQLSchemaQueries.swift | 17 ++++++++++--- .../Views/Results/DataGridColumnPool.swift | 2 +- .../PostgreSQLFetchTablesCommentTests.swift | 15 +++++++++++ 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift index 191c995ad..8381c07d1 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift @@ -16,6 +16,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable { private static let logger = Logger(subsystem: "com.TablePro.PostgreSQLDriver", category: "PostgreSQLPluginDriver") private static let undefinedTableSQLState = "42P01" + private static let undefinedFunctionSQLState = "42883" private var catalogPresence: PostgreSQLCatalogPresence? @@ -154,22 +155,22 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable { func fetchTables(schema: String?) async throws -> [PluginTableInfo] { let schemaLiteral = escapeLiteral(schema ?? core.currentSchema) - let query = PostgreSQLSchemaQueries.fetchTables( - schemaLiteral: schemaLiteral, - includeMaterializedViews: includesMaterializedViews(), - includeForeignTables: includesForeignTables() - ) + func query(includeOptionalCatalogs: Bool, includeComments: Bool) -> String { + PostgreSQLSchemaQueries.fetchTables( + schemaLiteral: schemaLiteral, + includeMaterializedViews: includeOptionalCatalogs && includesMaterializedViews(), + includeForeignTables: includeOptionalCatalogs && includesForeignTables(), + includeComments: includeComments + ) + } let result: PluginQueryResult do { - result = try await execute(query: query) + result = try await execute(query: query(includeOptionalCatalogs: true, includeComments: true)) } catch let error as LibPQPluginError where error.sqlState == Self.undefinedTableSQLState { - let baseQuery = PostgreSQLSchemaQueries.fetchTables( - schemaLiteral: schemaLiteral, - includeMaterializedViews: false, - includeForeignTables: false - ) - result = try await execute(query: baseQuery) + result = try await execute(query: query(includeOptionalCatalogs: false, includeComments: true)) + } catch let error as LibPQPluginError where error.sqlState == Self.undefinedFunctionSQLState { + result = try await execute(query: query(includeOptionalCatalogs: false, includeComments: false)) } return result.rows.compactMap { row -> PluginTableInfo? in diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift index 135e54431..90e189070 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift @@ -72,15 +72,24 @@ enum PostgreSQLSchemaQueries { /// `pg_foreign_table`, which some PostgreSQL-compatible engines do not /// implement; the caller passes `false` when those catalogs are absent so /// the whole query does not fail with `relation does not exist`. + /// + /// `includeComments` projects each table's comment via `obj_description` / + /// `to_regclass`. Engines that lack those functions fail the whole listing, + /// so the caller passes `false` to fall back to a comment-free listing. static func fetchTables( schemaLiteral: String, includeMaterializedViews: Bool, - includeForeignTables: Bool + includeForeignTables: Bool, + includeComments: Bool = true ) -> String { + func commentColumn(_ expression: String) -> String { + includeComments ? expression : "NULL::text" + } + var unions: [String] = [ """ SELECT t.table_name, t.table_type, - obj_description(to_regclass(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name)), 'pg_class') AS table_comment + \(commentColumn("obj_description(to_regclass(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name)), 'pg_class')")) AS table_comment FROM information_schema.tables t WHERE t.table_schema = '\(schemaLiteral)' AND t.table_type IN ('BASE TABLE', 'VIEW') @@ -91,7 +100,7 @@ enum PostgreSQLSchemaQueries { unions.append( """ SELECT m.matviewname AS table_name, 'MATERIALIZED VIEW' AS table_type, - obj_description(to_regclass(quote_ident(m.schemaname) || '.' || quote_ident(m.matviewname)), 'pg_class') AS table_comment + \(commentColumn("obj_description(to_regclass(quote_ident(m.schemaname) || '.' || quote_ident(m.matviewname)), 'pg_class')")) AS table_comment FROM pg_matviews m WHERE m.schemaname = '\(schemaLiteral)' """ @@ -102,7 +111,7 @@ enum PostgreSQLSchemaQueries { unions.append( """ SELECT c.relname AS table_name, 'FOREIGN TABLE' AS table_type, - obj_description(c.oid, 'pg_class') AS table_comment + \(commentColumn("obj_description(c.oid, 'pg_class')")) AS table_comment FROM pg_foreign_table ft JOIN pg_class c ON c.oid = ft.ftrelid JOIN pg_namespace n ON n.oid = c.relnamespace diff --git a/TablePro/Views/Results/DataGridColumnPool.swift b/TablePro/Views/Results/DataGridColumnPool.swift index 870c2420a..7432d6e4a 100644 --- a/TablePro/Views/Results/DataGridColumnPool.swift +++ b/TablePro/Views/Results/DataGridColumnPool.swift @@ -28,7 +28,7 @@ final class DataGridColumnPool { tableView: NSTableView, schema: ColumnIdentitySchema, columnTypes: [ColumnType], - columnComments: [String: String], + columnComments: [String: String] = [:], savedLayout: ColumnLayoutState?, isEditable: Bool, hiddenColumnNames: Set, diff --git a/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift b/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift index ce32f81d4..c6ae9687a 100644 --- a/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift +++ b/TableProTests/Plugins/PostgreSQLFetchTablesCommentTests.swift @@ -37,4 +37,19 @@ struct PostgreSQLFetchTablesCommentTests { let branches = query.components(separatedBy: "UNION ALL").count #expect(commentColumns == branches) } + + @Test("Comment-free fallback omits obj_description but keeps the aligned comment column") + func commentFreeFallbackOmitsObjDescription() { + let query = PostgreSQLSchemaQueries.fetchTables( + schemaLiteral: "public", + includeMaterializedViews: true, + includeForeignTables: true, + includeComments: false + ) + #expect(!query.contains("obj_description")) + #expect(!query.contains("to_regclass")) + let commentColumns = query.components(separatedBy: "AS table_comment").count - 1 + let branches = query.components(separatedBy: "UNION ALL").count + #expect(commentColumns == branches) + } }