Skip to content

Commit f169b12

Browse files
committed
fix: resolve NotSupportedException for schema-qualified PostgreSQL enum types
When a PostgreSQL column references an enum type with schema qualification (e.g., `block_type public.post_block_type`), sqlc sets `column.Type.Schema` to `public`. However, `ConstructEnumsLookup` normalizes default schema enums under key `` (empty string), causing `GetEnumType` to fail the lookup and throw `NotSupportedException`. Fixed by normalizing the schema name in `GetEnumSchemaAndName` to empty string when it matches the driver's DefaultSchema. Changes: - Drivers/NpgsqlDriver.cs: normalize default schema in GetEnumSchemaAndName - unit-tests/CodegenTests/CodegenSchemaTests.cs: add failing-then-passing test - examples/config/postgresql/types/: add schema-qualified enum table + queries - examples/Npgsql*Example/: regenerated code with new model/queries - end2end/: add truncation for new table in e2e TearDown
1 parent 5f6252a commit f169b12

22 files changed

Lines changed: 808 additions & 13 deletions

File tree

Drivers/NpgsqlDriver.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ public override string AddParametersToCommand(Query query)
730730
}).JoinByNewLine();
731731
}
732732

733-
private static (string, string) GetEnumSchemaAndName(Column column)
733+
private (string, string) GetEnumSchemaAndName(Column column)
734734
{
735735
var schemaName = column.Type.Schema;
736736
var enumName = column.Type.Name;
@@ -740,6 +740,8 @@ private static (string, string) GetEnumSchemaAndName(Column column)
740740
schemaName = schemaAndEnum[0];
741741
enumName = schemaAndEnum[1];
742742
}
743+
if (schemaName == DefaultSchema)
744+
schemaName = string.Empty;
743745
return (schemaName, enumName);
744746
}
745747

end2end/EndToEndTests/NpgsqlDapperTester.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ public async Task EmptyTestsTable()
2121
await QuerySql.TruncatePostgresNetworkTypesAsync();
2222
await QuerySql.TruncatePostgresArrayTypesAsync();
2323
await QuerySql.TruncatePostgresSpecialTypesAsync();
24+
await QuerySql.TruncatePostgresQualifiedEnumTypesAsync();
2425
}
2526
}

end2end/EndToEndTests/NpgsqlTester.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ public async Task EmptyTestsTables()
2222
await QuerySql.TruncatePostgresArrayTypesAsync();
2323
await QuerySql.TruncatePostgresSpecialTypesAsync();
2424
await QuerySql.TruncatePostgresNotNullTypesAsync();
25+
await QuerySql.TruncatePostgresQualifiedEnumTypesAsync();
2526
}
2627
}

examples/NpgsqlDapperExample/Models.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ public class PostgresNotNullType
9696
{
9797
public required CEnum CEnumNotNull { get; init; }
9898
};
99+
public class PostgresQualifiedEnumType
100+
{
101+
public CEnum? CQualifiedEnum { get; init; }
102+
};
99103
public class ExtendedBio
100104
{
101105
public required string AuthorName { get; init; }

examples/NpgsqlDapperExample/QuerySql.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,4 +1887,74 @@ public async Task TruncatePostgresGeoTypesAsync()
18871887
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
18881888
await this.Transaction.Connection.ExecuteAsync(TruncatePostgresGeoTypesSql, transaction: this.Transaction);
18891889
}
1890+
1891+
private const string InsertPostgresQualifiedEnumTypesSql = @"INSERT INTO postgres_qualified_enum_types
1892+
(
1893+
c_qualified_enum
1894+
)
1895+
VALUES (
1896+
@c_qualified_enum::c_enum
1897+
)";
1898+
public class InsertPostgresQualifiedEnumTypesArgs
1899+
{
1900+
public CEnum? CQualifiedEnum { get; init; }
1901+
};
1902+
public async Task InsertPostgresQualifiedEnumTypesAsync(InsertPostgresQualifiedEnumTypesArgs args)
1903+
{
1904+
var queryParams = new Dictionary<string, object?>();
1905+
queryParams.Add("c_qualified_enum", args.CQualifiedEnum != null ? args.CQualifiedEnum.Value.Stringify() : null);
1906+
if (this.Transaction == null)
1907+
{
1908+
using (var connection = await GetDataSource().OpenConnectionAsync())
1909+
{
1910+
await connection.ExecuteAsync(InsertPostgresQualifiedEnumTypesSql, queryParams);
1911+
return;
1912+
}
1913+
}
1914+
1915+
if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != ConnectionState.Open)
1916+
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
1917+
await this.Transaction.Connection.ExecuteAsync(InsertPostgresQualifiedEnumTypesSql, queryParams, transaction: this.Transaction);
1918+
}
1919+
1920+
private const string GetPostgresQualifiedEnumTypesSql = @"SELECT
1921+
c_qualified_enum
1922+
FROM postgres_qualified_enum_types
1923+
LIMIT 1";
1924+
public class GetPostgresQualifiedEnumTypesRow
1925+
{
1926+
public CEnum? CQualifiedEnum { get; init; }
1927+
};
1928+
public async Task<GetPostgresQualifiedEnumTypesRow?> GetPostgresQualifiedEnumTypesAsync()
1929+
{
1930+
if (this.Transaction == null)
1931+
{
1932+
using (var connection = await GetDataSource().OpenConnectionAsync())
1933+
{
1934+
var result = await connection.QueryFirstOrDefaultAsync<GetPostgresQualifiedEnumTypesRow?>(GetPostgresQualifiedEnumTypesSql);
1935+
return result;
1936+
}
1937+
}
1938+
1939+
if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != ConnectionState.Open)
1940+
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
1941+
return await this.Transaction.Connection.QueryFirstOrDefaultAsync<GetPostgresQualifiedEnumTypesRow?>(GetPostgresQualifiedEnumTypesSql, transaction: this.Transaction);
1942+
}
1943+
1944+
private const string TruncatePostgresQualifiedEnumTypesSql = "TRUNCATE TABLE postgres_qualified_enum_types";
1945+
public async Task TruncatePostgresQualifiedEnumTypesAsync()
1946+
{
1947+
if (this.Transaction == null)
1948+
{
1949+
using (var connection = await GetDataSource().OpenConnectionAsync())
1950+
{
1951+
await connection.ExecuteAsync(TruncatePostgresQualifiedEnumTypesSql);
1952+
return;
1953+
}
1954+
}
1955+
1956+
if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != ConnectionState.Open)
1957+
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
1958+
await this.Transaction.Connection.ExecuteAsync(TruncatePostgresQualifiedEnumTypesSql, transaction: this.Transaction);
1959+
}
18901960
}

examples/NpgsqlDapperExample/request.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,24 @@
686686
}
687687
}
688688
]
689+
},
690+
{
691+
"rel": {
692+
"name": "postgres_qualified_enum_types"
693+
},
694+
"columns": [
695+
{
696+
"name": "c_qualified_enum",
697+
"length": -1,
698+
"table": {
699+
"name": "postgres_qualified_enum_types"
700+
},
701+
"type": {
702+
"schema": "public",
703+
"name": "c_enum"
704+
}
705+
}
706+
]
689707
}
690708
],
691709
"enums": [
@@ -36229,6 +36247,53 @@
3622936247
"name": "TruncatePostgresGeoTypes",
3623036248
"cmd": ":exec",
3623136249
"filename": "query.sql"
36250+
},
36251+
{
36252+
"text": "INSERT INTO postgres_qualified_enum_types\n(\n c_qualified_enum\n)\nVALUES (\n $1::c_enum\n)",
36253+
"name": "InsertPostgresQualifiedEnumTypes",
36254+
"cmd": ":exec",
36255+
"parameters": [
36256+
{
36257+
"number": 1,
36258+
"column": {
36259+
"name": "c_qualified_enum",
36260+
"length": -1,
36261+
"type": {
36262+
"name": "c_enum"
36263+
}
36264+
}
36265+
}
36266+
],
36267+
"filename": "query.sql",
36268+
"insert_into_table": {
36269+
"name": "postgres_qualified_enum_types"
36270+
}
36271+
},
36272+
{
36273+
"text": "SELECT\n c_qualified_enum\nFROM postgres_qualified_enum_types\nLIMIT 1",
36274+
"name": "GetPostgresQualifiedEnumTypes",
36275+
"cmd": ":one",
36276+
"columns": [
36277+
{
36278+
"name": "c_qualified_enum",
36279+
"length": -1,
36280+
"table": {
36281+
"name": "postgres_qualified_enum_types"
36282+
},
36283+
"type": {
36284+
"schema": "public",
36285+
"name": "c_enum"
36286+
},
36287+
"originalName": "c_qualified_enum"
36288+
}
36289+
],
36290+
"filename": "query.sql"
36291+
},
36292+
{
36293+
"text": "TRUNCATE TABLE postgres_qualified_enum_types",
36294+
"name": "TruncatePostgresQualifiedEnumTypes",
36295+
"cmd": ":exec",
36296+
"filename": "query.sql"
3623236297
}
3623336298
],
3623436299
"sqlc_version": "v1.30.0",

examples/NpgsqlDapperExample/request.message

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
2
44
postgresql-examples/config/postgresql/authors/schema.sql+examples/config/postgresql/types/schema.sql",examples/config/postgresql/authors/query.sql"*examples/config/postgresql/types/query.sqlb�
55
examples/NpgsqlDapperExamplecsharp�{"debugRequest":true,"generateCsproj":true,"namespaceName":"NpgsqlDapperExampleGen","overrides":[{"column":"GetPostgresFunctions:max_integer","csharp_type":{"notNull":false,"type":"int"}},{"column":"GetPostgresFunctions:max_varchar","csharp_type":{"notNull":false,"type":"string"}},{"column":"GetPostgresFunctions:max_timestamp","csharp_type":{"notNull":true,"type":"DateTime"}},{"column":"GetPostgresSpecialTypesCnt:c_json","csharp_type":{"notNull":false,"type":"JsonElement"}},{"column":"GetPostgresSpecialTypesCnt:c_jsonb","csharp_type":{"notNull":false,"type":"JsonElement"}},{"column":"*:c_json_string_override","csharp_type":{"notNull":false,"type":"string"}},{"column":"*:c_xml_string_override","csharp_type":{"notNull":false,"type":"string"}},{"column":"*:c_macaddr8","csharp_type":{"notNull":false,"type":"string"}},{"column":"*:c_timestamp_noda_instant_override","csharp_type":{"notNull":false,"type":"Instant"}}],"targetFramework":"net8.0","useDapper":true}*
6-
./dist/LocalRunner�� public"�public�
6+
./dist/LocalRunner�� public"�public�
77
authors)
88
id0���������R authorsb  bigserial&
99
name0���������R authorsbtext#
@@ -98,7 +98,9 @@ c_jsonpath0
9898
c_xml0���������Rpostgres_special_typesbxmlC
9999
c_xml_string_override0���������Rpostgres_special_typesbxml`
100100
postgres_not_null_typesC
101-
c_enum_not_null0���������Rpostgres_not_null_typesbc_enum"
101+
c_enum_not_null0���������Rpostgres_not_null_typesbc_enums
102+
postgres_qualified_enum_typesP
103+
c_qualified_enum0���������Rpostgres_qualified_enum_typesbpublicc_enum"
102104
c_enumsmallmediumbig" pg_temp"�
103105
pg_catalog�
104106
&
@@ -10917,4 +10919,18 @@ hSELECT c_point, c_line, c_lseg, c_box, c_path, c_polygon, c_circle FROM postgre
1091710919
c_path0���������Rpostgres_geometric_typesbpathzc_path"H
1091810920
c_polygon0���������Rpostgres_geometric_typesb polygonz c_polygon"E
1091910921
c_circle0���������Rpostgres_geometric_typesbcirclezc_circle: query.sqlU
10920-
'TRUNCATE TABLE postgres_geometric_typesTruncatePostgresGeoTypes:exec: query.sql"v1.30.0*�{"overrideDriverVersion":"","generateCsproj":true,"targetFramework":"net8.0","namespaceName":"NpgsqlDapperExampleGen","useDapper":true,"overrideDapperVersion":"","overrides":[{"column":"GetPostgresFunctions:max_integer","csharp_type":{"type":"int","notNull":false}},{"column":"GetPostgresFunctions:max_varchar","csharp_type":{"type":"string","notNull":false}},{"column":"GetPostgresFunctions:max_timestamp","csharp_type":{"type":"DateTime","notNull":true}},{"column":"GetPostgresSpecialTypesCnt:c_json","csharp_type":{"type":"JsonElement","notNull":false}},{"column":"GetPostgresSpecialTypesCnt:c_jsonb","csharp_type":{"type":"JsonElement","notNull":false}},{"column":"*:c_json_string_override","csharp_type":{"type":"string","notNull":false}},{"column":"*:c_xml_string_override","csharp_type":{"type":"string","notNull":false}},{"column":"*:c_macaddr8","csharp_type":{"type":"string","notNull":false}},{"column":"*:c_timestamp_noda_instant_override","csharp_type":{"type":"Instant","notNull":false}}],"debugRequest":false,"useCentralPackageManagement":false,"withAsyncSuffix":true}
10922+
'TRUNCATE TABLE postgres_geometric_typesTruncatePostgresGeoTypes:exec: query.sql�
10923+
\INSERT INTO postgres_qualified_enum_types
10924+
(
10925+
c_qualified_enum
10926+
)
10927+
VALUES (
10928+
$1::c_enum
10929+
) InsertPostgresQualifiedEnumTypes:exec*+'
10930+
c_qualified_enum0���������bc_enum: query.sqlBpostgres_qualified_enum_types�
10931+
FSELECT
10932+
c_qualified_enum
10933+
FROM postgres_qualified_enum_types
10934+
LIMIT 1GetPostgresQualifiedEnumTypes:one"b
10935+
c_qualified_enum0���������Rpostgres_qualified_enum_typesbpublicc_enumzc_qualified_enum: query.sqld
10936+
,TRUNCATE TABLE postgres_qualified_enum_types"TruncatePostgresQualifiedEnumTypes:exec: query.sql"v1.30.0*�{"overrideDriverVersion":"","generateCsproj":true,"targetFramework":"net8.0","namespaceName":"NpgsqlDapperExampleGen","useDapper":true,"overrideDapperVersion":"","overrides":[{"column":"GetPostgresFunctions:max_integer","csharp_type":{"type":"int","notNull":false}},{"column":"GetPostgresFunctions:max_varchar","csharp_type":{"type":"string","notNull":false}},{"column":"GetPostgresFunctions:max_timestamp","csharp_type":{"type":"DateTime","notNull":true}},{"column":"GetPostgresSpecialTypesCnt:c_json","csharp_type":{"type":"JsonElement","notNull":false}},{"column":"GetPostgresSpecialTypesCnt:c_jsonb","csharp_type":{"type":"JsonElement","notNull":false}},{"column":"*:c_json_string_override","csharp_type":{"type":"string","notNull":false}},{"column":"*:c_xml_string_override","csharp_type":{"type":"string","notNull":false}},{"column":"*:c_macaddr8","csharp_type":{"type":"string","notNull":false}},{"column":"*:c_timestamp_noda_instant_override","csharp_type":{"type":"Instant","notNull":false}}],"debugRequest":false,"useCentralPackageManagement":false,"withAsyncSuffix":true}

examples/NpgsqlDapperLegacyExample/Models.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ public class PostgresNotNullType
9797
{
9898
public CEnum CEnumNotNull { get; set; }
9999
};
100+
public class PostgresQualifiedEnumType
101+
{
102+
public CEnum? CQualifiedEnum { get; set; }
103+
};
100104
public class ExtendedBio
101105
{
102106
public string AuthorName { get; set; }

examples/NpgsqlDapperLegacyExample/QuerySql.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,5 +1888,75 @@ public async Task TruncatePostgresGeoTypesAsync()
18881888
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
18891889
await this.Transaction.Connection.ExecuteAsync(TruncatePostgresGeoTypesSql, transaction: this.Transaction);
18901890
}
1891+
1892+
private const string InsertPostgresQualifiedEnumTypesSql = @"INSERT INTO postgres_qualified_enum_types
1893+
(
1894+
c_qualified_enum
1895+
)
1896+
VALUES (
1897+
@c_qualified_enum::c_enum
1898+
)";
1899+
public class InsertPostgresQualifiedEnumTypesArgs
1900+
{
1901+
public CEnum? CQualifiedEnum { get; set; }
1902+
};
1903+
public async Task InsertPostgresQualifiedEnumTypesAsync(InsertPostgresQualifiedEnumTypesArgs args)
1904+
{
1905+
var queryParams = new Dictionary<string, object>();
1906+
queryParams.Add("c_qualified_enum", args.CQualifiedEnum != null ? args.CQualifiedEnum.Value.Stringify() : null);
1907+
if (this.Transaction == null)
1908+
{
1909+
using (var connection = await GetDataSource().OpenConnectionAsync())
1910+
{
1911+
await connection.ExecuteAsync(InsertPostgresQualifiedEnumTypesSql, queryParams);
1912+
return;
1913+
}
1914+
}
1915+
1916+
if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != ConnectionState.Open)
1917+
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
1918+
await this.Transaction.Connection.ExecuteAsync(InsertPostgresQualifiedEnumTypesSql, queryParams, transaction: this.Transaction);
1919+
}
1920+
1921+
private const string GetPostgresQualifiedEnumTypesSql = @"SELECT
1922+
c_qualified_enum
1923+
FROM postgres_qualified_enum_types
1924+
LIMIT 1";
1925+
public class GetPostgresQualifiedEnumTypesRow
1926+
{
1927+
public CEnum? CQualifiedEnum { get; set; }
1928+
};
1929+
public async Task<GetPostgresQualifiedEnumTypesRow> GetPostgresQualifiedEnumTypesAsync()
1930+
{
1931+
if (this.Transaction == null)
1932+
{
1933+
using (var connection = await GetDataSource().OpenConnectionAsync())
1934+
{
1935+
var result = await connection.QueryFirstOrDefaultAsync<GetPostgresQualifiedEnumTypesRow>(GetPostgresQualifiedEnumTypesSql);
1936+
return result;
1937+
}
1938+
}
1939+
1940+
if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != ConnectionState.Open)
1941+
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
1942+
return await this.Transaction.Connection.QueryFirstOrDefaultAsync<GetPostgresQualifiedEnumTypesRow>(GetPostgresQualifiedEnumTypesSql, transaction: this.Transaction);
1943+
}
1944+
1945+
private const string TruncatePostgresQualifiedEnumTypesSql = "TRUNCATE TABLE postgres_qualified_enum_types";
1946+
public async Task TruncatePostgresQualifiedEnumTypesAsync()
1947+
{
1948+
if (this.Transaction == null)
1949+
{
1950+
using (var connection = await GetDataSource().OpenConnectionAsync())
1951+
{
1952+
await connection.ExecuteAsync(TruncatePostgresQualifiedEnumTypesSql);
1953+
return;
1954+
}
1955+
}
1956+
1957+
if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != ConnectionState.Open)
1958+
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
1959+
await this.Transaction.Connection.ExecuteAsync(TruncatePostgresQualifiedEnumTypesSql, transaction: this.Transaction);
1960+
}
18911961
}
18921962
}

0 commit comments

Comments
 (0)