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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Refresh (Cmd+R) now acts only on the focused window's connection, instead of also reloading views and clearing autocomplete caches for every other open connection.
- Holding Cmd+R no longer queues a backlog of refreshes that kept running after the key was released; refresh fires once per key press, and rapid presses collapse into a single reload.
- Switching PostgreSQL schemas now sets the search path to just the selected schema instead of also keeping "public" on it. Unqualified references to objects in "public", such as extension functions, need a "public." prefix while another schema is selected. (#1662)
- The inspector panel can now be resized freely by dragging its divider instead of being capped at a fixed width.

### Fixed

- PostgreSQL and Redshift autocomplete now completes tables and columns from schemas other than the one selected in the sidebar, so a schema-qualified query like `SELECT * FROM s2.orders` suggests `s2`'s columns. (#1668)
- Favorite keywords now work again: favorites scoped to a deleted connection were silently kept in storage but hidden everywhere, so typing their keyword did nothing. Deleting a connection now also deletes its saved queries, their folders, and per-table filters, the delete confirmation says so, and favorites already orphaned by an earlier delete are cleaned up at launch.
- Keyword autocomplete and SQL keyword suggestions now work in editors without a database connection, and favorites appear in the completion popup immediately instead of after a short delay.
- Typing a favorite's keyword in the Quick Switcher now finds the saved query instead of ranking it below name matches.
Expand Down
39 changes: 39 additions & 0 deletions Plugins/PostgreSQLDriverPlugin/ColumnQueryShape.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// ColumnQueryShape.swift
// PostgreSQLDriverPlugin
//
// Shared single-table vs all-tables fragments for the PostgreSQL-family column
// introspection queries. Extracted so the PostgreSQL and Redshift builders
// describe only what differs (joins, projections) instead of repeating the
// shape logic. Compiled into the test target via TableProTests/PluginTestSources.
//

import Foundation

enum ColumnQueryShape {
struct Fragments {
let selectPrefix: String
let pkSelect: String
let pkTableFilter: String
let pkJoin: String
let mainTableFilter: String
let orderBy: String
}

/// Fragments for a column query scoped to one schema. Passing `tableLiteral`
/// restricts the query to a single table; passing `nil` returns every table's
/// columns, prefixes each row with `table_name`, and orders by table.
static func fragments(tableLiteral: String?) -> Fragments {
let includesTableName = tableLiteral == nil
return Fragments(
selectPrefix: includesTableName ? "c.table_name,\n" : "",
pkSelect: includesTableName ? "kcu.table_name, kcu.column_name" : "kcu.column_name",
pkTableFilter: tableLiteral.map { "\n AND tc.table_name = '\($0)'" } ?? "",
pkJoin: includesTableName
? "c.table_name = pk.table_name AND c.column_name = pk.column_name"
: "c.column_name = pk.column_name",
mainTableFilter: tableLiteral.map { " AND c.table_name = '\($0)'" } ?? "",
orderBy: includesTableName ? "c.table_name, c.ordinal_position" : "c.ordinal_position"
)
}
}
4 changes: 0 additions & 4 deletions Plugins/PostgreSQLDriverPlugin/LibPQDriverCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,4 @@ extension LibPQBackedDriver {
func escapeLiteral(_ str: String) -> String {
escapeStringLiteral(str)
}

var escapedSchema: String {
escapeLiteral(core.currentSchema)
}
}
117 changes: 31 additions & 86 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver+Columns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,102 +8,34 @@ import TableProPluginKit

extension PostgreSQLPluginDriver {
func fetchColumns(table: String, schema: String?) async throws -> [PluginColumnInfo] {
let safeSchema = escapeStringLiteral(currentSchema ?? "public")
let safeSchema = escapeStringLiteral(schema ?? core.currentSchema)
let safeTable = escapeStringLiteral(table)
let enumMap = try await fetchEnumLabelMap(schema: safeSchema)
let caps = versionedCapabilities
let identityProjection = caps.hasIdentityColumns ? "a.attidentity" : "NULL::text"
let generatedProjection = caps.hasGeneratedColumns ? "a.attgenerated" : "NULL::text"
let attributeJoin = (caps.hasIdentityColumns || caps.hasGeneratedColumns) ? """
LEFT JOIN pg_catalog.pg_attribute a
ON a.attrelid = st.relid
AND a.attname = c.column_name
AND NOT a.attisdropped
""" : ""
let query = """
SELECT
c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
c.collation_name,
pgd.description,
c.udt_name,
CASE WHEN pk.column_name IS NOT NULL THEN 'YES' ELSE 'NO' END AS is_pk,
\(identityProjection),
\(generatedProjection)
FROM information_schema.columns c
LEFT JOIN pg_catalog.pg_statio_all_tables st
ON st.schemaname = c.table_schema
AND st.relname = c.table_name
LEFT JOIN pg_catalog.pg_description pgd
ON pgd.objoid = st.relid
AND pgd.objsubid = c.ordinal_position
\(attributeJoin)
LEFT JOIN (
SELECT DISTINCT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = '\(safeSchema)'
AND tc.table_name = '\(safeTable)'
) pk ON c.column_name = pk.column_name
WHERE c.table_schema = '\(safeSchema)' AND c.table_name = '\(safeTable)'
ORDER BY c.ordinal_position
"""
let projections = columnProjections()
let query = PostgreSQLSchemaQueries.columnsQuery(
schemaLiteral: safeSchema,
tableLiteral: safeTable,
identityProjection: projections.identity,
generatedProjection: projections.generated,
attributeJoin: projections.attributeJoin
)
let result = try await execute(query: query)
return result.rows.compactMap { row in
mapPgColumnRow(row, tableNameOffset: 0, enumLabelsByType: enumMap)
}
}

func fetchAllColumns(schema: String?) async throws -> [String: [PluginColumnInfo]] {
let safeSchema = escapeStringLiteral(currentSchema ?? "public")
let safeSchema = escapeStringLiteral(schema ?? core.currentSchema)
let enumMap = try await fetchEnumLabelMap(schema: safeSchema)
let caps = versionedCapabilities
let identityProjection = caps.hasIdentityColumns ? "a.attidentity" : "NULL::text"
let generatedProjection = caps.hasGeneratedColumns ? "a.attgenerated" : "NULL::text"
let attributeJoin = (caps.hasIdentityColumns || caps.hasGeneratedColumns) ? """
LEFT JOIN pg_catalog.pg_attribute a
ON a.attrelid = st.relid
AND a.attname = c.column_name
AND NOT a.attisdropped
""" : ""
let query = """
SELECT
c.table_name,
c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
c.collation_name,
pgd.description,
c.udt_name,
CASE WHEN pk.column_name IS NOT NULL THEN 'YES' ELSE 'NO' END AS is_pk,
\(identityProjection),
\(generatedProjection)
FROM information_schema.columns c
LEFT JOIN pg_catalog.pg_statio_all_tables st
ON st.schemaname = c.table_schema
AND st.relname = c.table_name
LEFT JOIN pg_catalog.pg_description pgd
ON pgd.objoid = st.relid
AND pgd.objsubid = c.ordinal_position
\(attributeJoin)
LEFT JOIN (
SELECT DISTINCT kcu.table_name, kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = '\(safeSchema)'
) pk ON c.table_name = pk.table_name AND c.column_name = pk.column_name
WHERE c.table_schema = '\(safeSchema)'
ORDER BY c.table_name, c.ordinal_position
"""
let projections = columnProjections()
let query = PostgreSQLSchemaQueries.columnsQuery(
schemaLiteral: safeSchema,
tableLiteral: nil,
identityProjection: projections.identity,
generatedProjection: projections.generated,
attributeJoin: projections.attributeJoin
)
let result = try await execute(query: query)
var allColumns: [String: [PluginColumnInfo]] = [:]
for row in result.rows {
Expand All @@ -115,6 +47,19 @@ extension PostgreSQLPluginDriver {
return allColumns
}

private func columnProjections() -> (identity: String, generated: String, attributeJoin: String) {
let caps = versionedCapabilities
let identity = caps.hasIdentityColumns ? "a.attidentity" : "NULL::text"
let generated = caps.hasGeneratedColumns ? "a.attgenerated" : "NULL::text"
let attributeJoin = (caps.hasIdentityColumns || caps.hasGeneratedColumns) ? """
LEFT JOIN pg_catalog.pg_attribute a
ON a.attrelid = st.relid
AND a.attname = c.column_name
AND NOT a.attisdropped
""" : ""
return (identity, generated, attributeJoin)
}

fileprivate func fetchEnumLabelMap(schema: String) async throws -> [String: [String]] {
let query = """
SELECT t.typname, e.enumlabel
Expand Down
34 changes: 21 additions & 13 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
}

func fetchForeignKeys(table: String, schema: String?) async throws -> [PluginForeignKeyInfo] {
let schemaLiteral = escapeLiteral(schema ?? core.currentSchema)
let query = """
SELECT
con.conname,
Expand Down Expand Up @@ -261,7 +262,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
ON ref_col.attrelid = con.confrelid AND ref_col.attnum = cols.ref_attnum
WHERE con.contype = 'f'
AND src_cl.relname = '\(escapeLiteral(table))'
AND src_ns.nspname = '\(escapedSchema)'
AND src_ns.nspname = '\(schemaLiteral)'
ORDER BY con.conname, cols.ord
"""
let result = try await execute(query: query)
Expand All @@ -282,11 +283,12 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
onUpdate: row[6].asText ?? "NO ACTION"
)
}
Self.logger.info("[fk] postgres fetchForeignKeys schema=\(self.core.currentSchema, privacy: .public) table=\(table, privacy: .public) rows=\(result.rows.count) parsed=\(foreignKeys.count)")
Self.logger.info("[fk] postgres fetchForeignKeys schema=\(schema ?? self.core.currentSchema, privacy: .public) table=\(table, privacy: .public) rows=\(result.rows.count) parsed=\(foreignKeys.count)")
return foreignKeys
}

func fetchAllForeignKeys(schema: String?) async throws -> [String: [PluginForeignKeyInfo]] {
let schemaLiteral = escapeLiteral(schema ?? core.currentSchema)
let query = """
SELECT
src_cl.relname AS table_name,
Expand Down Expand Up @@ -321,7 +323,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
JOIN pg_catalog.pg_attribute ref_col
ON ref_col.attrelid = con.confrelid AND ref_col.attnum = cols.ref_attnum
WHERE con.contype = 'f'
AND src_ns.nspname = '\(escapedSchema)'
AND src_ns.nspname = '\(schemaLiteral)'
ORDER BY src_cl.relname, con.conname, cols.ord
"""
let result = try await execute(query: query)
Expand Down Expand Up @@ -364,6 +366,8 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {

func fetchTableDDL(table: String, schema: String?) async throws -> String {
let safeTable = escapeLiteral(table)
let resolvedSchema = schema ?? core.currentSchema
let schemaLiteral = escapeLiteral(resolvedSchema)
let quotedTable = quoteIdentifier(table)
let caps = versionedCapabilities

Expand Down Expand Up @@ -410,7 +414,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
WHERE c.relname = '\(safeTable)'
AND n.nspname = '\(escapedSchema)'
AND n.nspname = '\(schemaLiteral)'
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY a.attnum
Expand All @@ -423,7 +427,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
JOIN pg_class c ON c.oid = con.conrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = '\(safeTable)'
AND n.nspname = '\(escapedSchema)'
AND n.nspname = '\(schemaLiteral)'
AND con.contype IN ('p', 'u', 'c')
ORDER BY
CASE con.contype WHEN 'p' THEN 0 WHEN 'u' THEN 1 WHEN 'c' THEN 2 END
Expand All @@ -433,13 +437,13 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
SELECT indexdef
FROM pg_indexes
WHERE tablename = '\(safeTable)'
AND schemaname = '\(escapedSchema)'
AND schemaname = '\(schemaLiteral)'
AND indexname NOT IN (
SELECT conname FROM pg_constraint
JOIN pg_class ON pg_class.oid = conrelid
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
WHERE pg_class.relname = '\(safeTable)'
AND pg_namespace.nspname = '\(escapedSchema)'
AND pg_namespace.nspname = '\(schemaLiteral)'
)
ORDER BY indexname
"""
Expand All @@ -459,7 +463,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
var parts = columnDefs
parts.append(contentsOf: constraints)

let quotedSchema = quoteIdentifier(core.currentSchema)
let quotedSchema = quoteIdentifier(resolvedSchema)
let ddl = "CREATE TABLE \(quotedSchema).\(quotedTable) (\n " +
parts.joined(separator: ",\n ") +
"\n);"
Expand All @@ -470,11 +474,12 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
}

func fetchViewDefinition(view: String, schema: String?) async throws -> String {
let schemaLiteral = escapeLiteral(schema ?? core.currentSchema)
let query = """
SELECT 'CREATE OR REPLACE VIEW ' || quote_ident(schemaname) || '.' || quote_ident(viewname) || ' AS ' || E'\\n' || definition AS ddl
FROM pg_views
WHERE viewname = '\(escapeLiteral(view))'
AND schemaname = '\(escapedSchema)'
AND schemaname = '\(schemaLiteral)'
"""
let result = try await execute(query: query)
guard let firstRow = result.rows.first, let ddl = firstRow[0].asText else {
Expand All @@ -484,6 +489,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
}

func fetchTableMetadata(table: String, schema: String?) async throws -> PluginTableMetadata {
let schemaLiteral = escapeLiteral(schema ?? core.currentSchema)
let query = """
SELECT
pg_total_relation_size(c.oid) AS total_size,
Expand All @@ -494,7 +500,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = '\(escapeLiteral(table))'
AND n.nspname = '\(escapedSchema)'
AND n.nspname = '\(schemaLiteral)'
"""
let result = try await execute(query: query)
guard let row = result.rows.first else {
Expand Down Expand Up @@ -574,6 +580,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {

func fetchDependentTypes(table: String, schema: String?) async throws -> [(name: String, labels: [String])] {
let safeTable = escapeLiteral(table)
let schemaLiteral = escapeLiteral(schema ?? core.currentSchema)
let query = """
SELECT DISTINCT t.typname,
array_agg(e.enumlabel ORDER BY e.enumsortorder)
Expand All @@ -583,7 +590,7 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
JOIN pg_type t ON t.oid = a.atttypid
JOIN pg_enum e ON e.enumtypid = t.oid
WHERE c.relname = '\(safeTable)'
AND n.nspname = '\(escapedSchema)'
AND n.nspname = '\(schemaLiteral)'
AND a.attnum > 0
AND NOT a.attisdropped
GROUP BY t.typname
Expand All @@ -602,6 +609,8 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
func fetchDependentSequences(table: String, schema: String?) async throws -> [(name: String, ddl: String)] {
guard includesSequencesCatalog() else { return [] }
let safeTable = escapeLiteral(table)
let schemaName = schema ?? core.currentSchema
let schemaLiteral = escapeLiteral(schemaName)
let query = """
SELECT s.sequencename,
s.start_value,
Expand All @@ -616,11 +625,10 @@ final class PostgreSQLPluginDriver: LibPQBackedDriver, @unchecked Sendable {
JOIN pg_sequences s ON s.schemaname = n.nspname
AND pg_get_expr(ad.adbin, ad.adrelid) LIKE '%' || quote_ident(s.sequencename) || '%'
WHERE c.relname = '\(safeTable)'
AND n.nspname = '\(escapedSchema)'
AND n.nspname = '\(schemaLiteral)'
AND pg_get_expr(ad.adbin, ad.adrelid) LIKE '%nextval%'
"""
let result = try await execute(query: query)
let schemaName = schema ?? core.currentSchema
return result.rows.compactMap { row -> (name: String, ddl: String)? in
guard let seqName = row[0].asText else { return nil }
let startVal = row[1].asText ?? "1"
Expand Down
Loading
Loading