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 @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Last line of a helper process's output lost when it exits right after writing it.
- Composite, range and extension-typed PostgreSQL columns labelled `ENUM(…)` in the structure editor.
- Sidebar routines, triggers and types from the previous database after switching while it was still loading.
- Silent fallback order when foreign keys between the exported tables form a cycle. (#2517)
- Foreign keys declared twice in a SQL export on MySQL, SQL Server, DuckDB, Snowflake, CockroachDB and Redshift, and as an unsupported `ALTER TABLE` on SQLite, libSQL and Cloudflare D1. (#2517)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,13 @@ extension PostgreSQLPluginDriver {
var map: [String: [String]] = [:]
for row in result.rows {
guard let schemaName = row[safe: 0]?.asText,
let typeName = row[safe: 1]?.asText,
let label = row[safe: 2]?.asText else { continue }
let typeName = row[safe: 1]?.asText else { continue }
let key = PostgresColumnTypeResolver.qualifiedName(schema: schemaName, name: typeName)
map[key, default: []].append(label)
var labels = map[key] ?? []
if let label = row[safe: 2]?.asText {
labels.append(label)
}
map[key] = labels
}
return map
}
Expand Down
6 changes: 5 additions & 1 deletion Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,15 @@ enum PostgreSQLSchemaQueries {
WHERE t.typtype = 'e'
"""

/// Every enum appears, with a NULL label where it has none, so the column resolver can tell
/// an enum apart from a composite, a range or an extension's base type: all four reach it as
/// `USER-DEFINED`, and only an enum has a row here.
static let enumLabelQuery = """
SELECT n.nspname, t.typname, e.enumlabel
FROM pg_catalog.pg_type t
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
JOIN pg_catalog.pg_enum e ON e.enumtypid = t.oid
LEFT JOIN pg_catalog.pg_enum e ON e.enumtypid = t.oid
WHERE t.typtype = 'e'
ORDER BY n.nspname, t.typname, e.enumsortorder
"""

Expand Down
2 changes: 1 addition & 1 deletion Plugins/TableProPluginKit/PostgresColumnTypeResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public enum PostgresColumnTypeResolver {

if upper == "USER-DEFINED" {
guard let labels = enumLabelsByQualifiedName[key] else {
return Resolution(dataType: "ENUM(\(udtName))", allowedValues: nil)
return Resolution(dataType: udtName, allowedValues: nil)
}
return Resolution(dataType: "ENUM", allowedValues: labels)
}
Expand Down
37 changes: 32 additions & 5 deletions TableProTests/Plugins/PostgresColumnTypeResolverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,38 @@ struct PostgresColumnTypeResolverTests {
#expect(resolve("ARRAY", schema: "public", udt: "_pair").allowedValues == nil)
}

@Test("An enum with no catalog entry keeps the existing named fallback")
func fallsBackForUnknownEnum() {
let resolution = resolve("USER-DEFINED", schema: "app", udt: "nope")
#expect(resolution.dataType == "ENUM(nope)")
#expect(resolution.allowedValues == nil)
/// A composite, a range and an extension's base type all reach the resolver as
/// `USER-DEFINED`, exactly like an enum. Only an enum has a catalog entry, so a name without
/// one is a type of another kind and is shown as itself rather than as an enum it is not.
@Test("A user-defined type with no enum entry keeps its own name")
func keepsNameForNonEnumUserType() {
for udt in ["point3d", "floatrange", "hstore", "geometry"] {
let resolution = resolve("USER-DEFINED", schema: "app", udt: udt)
#expect(resolution.dataType == udt)
#expect(resolution.allowedValues == nil)
}
}

@Test("An enum declared with no labels is still an enum")
func resolvesEmptyEnum() {
let resolution = PostgresColumnTypeResolver.resolve(
rawDataType: "USER-DEFINED",
udtSchema: "app",
udtName: "empty",
enumLabelsByQualifiedName: ["app.empty": []],
arrayTypesByQualifiedName: [:]
)
#expect(resolution.dataType == "ENUM")
#expect(resolution.allowedValues?.isEmpty == true)
}

/// The catalog read has to list an enum with no labels, or the resolver could not tell it
/// from a composite.
@Test("The enum label query lists every enum, labels or not")
func enumLabelQueryListsLabellessEnums() {
let sql = PostgreSQLSchemaQueries.enumLabelQuery
#expect(sql.contains("LEFT JOIN pg_catalog.pg_enum"))
#expect(sql.contains("WHERE t.typtype = 'e'"))
}

@Test("Ordinary columns pass through unchanged")
Expand Down
Loading