Skip to content

Commit d1a7a46

Browse files
committed
Column schema fixes for domains
* The base PostgresType was returned instead of the domain's PostgresType. * typmod (e.g. column length) wasn't properly provided for domain types (e.g. domain type based on varchar(2)). Fixes #1553, fixes #1569
1 parent d11f693 commit d1a7a46

File tree

4 files changed

+69
-30
lines changed

4 files changed

+69
-30
lines changed

src/Npgsql/PostgresTypes/PostgresDomainType.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,39 +21,45 @@
2121
// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
2222
#endregion
2323

24+
using JetBrains.Annotations;
25+
2426
namespace Npgsql.PostgresTypes
2527
{
2628
/// <summary>
2729
/// Represents a PostgreSQL domain type.
2830
/// </summary>
2931
/// <remarks>
3032
/// See https://www.postgresql.org/docs/current/static/sql-createdomain.html.
31-
///
33+
///
3234
/// When PostgreSQL returns a RowDescription for a domain type, the type OID is the base type's
3335
/// (so fetching a domain type over text returns a RowDescription for text).
3436
/// However, when a composite type is returned, the type OID there is that of the domain,
3537
/// so we provide "clean" support for domain types.
3638
/// </remarks>
3739
public class PostgresDomainType : PostgresType
3840
{
39-
readonly PostgresType _basePostgresType;
41+
/// <summary>
42+
/// The PostgreSQL data type of the base type, i.e. the type this domain is based on.
43+
/// </summary>
44+
[PublicAPI]
45+
public PostgresType BaseType { get; }
4046

4147
/// <summary>
4248
/// Constructs a representation of a PostgreSQL domain data type.
4349
/// </summary>
44-
protected internal PostgresDomainType(string ns, string name, uint oid, PostgresType basePostgresType)
50+
protected internal PostgresDomainType(string ns, string name, uint oid, PostgresType baseType)
4551
: base(ns, name, oid)
4652
{
47-
_basePostgresType = basePostgresType;
53+
BaseType = baseType;
4854
}
4955

5056
internal override TypeHandler Activate(TypeHandlerRegistry registry)
5157
{
5258
TypeHandler baseTypeHandler;
53-
if (!registry.TryGetByOID(_basePostgresType.OID, out baseTypeHandler))
59+
if (!registry.TryGetByOID(BaseType.OID, out baseTypeHandler))
5460
{
5561
// Base type hasn't been set up yet, do it now
56-
baseTypeHandler = _basePostgresType.Activate(registry);
62+
baseTypeHandler = BaseType.Activate(registry);
5763
}
5864

5965
// Make the domain type OID point to the base type's type handler, the wire encoding

src/Npgsql/Schema/DbColumnSchemaGenerator.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Diagnostics;
88
using System.Linq;
99
using Npgsql.BackendMessages;
10+
using Npgsql.PostgresTypes;
1011
using Npgsql.TypeHandlers;
1112

1213
namespace Npgsql.Schema
@@ -26,7 +27,8 @@ internal DbColumnSchemaGenerator(NpgsqlConnection connection, RowDescriptionMess
2627

2728
const string GetColumnsQuery = @"
2829
SELECT
29-
typ.oid AS typoid, nspname, relname, attname, typname, attrelid, attnum, atttypmod, attnotnull,
30+
typ.oid AS typoid, nspname, relname, attname, typ.typname, attrelid, attnum, attnotnull,
31+
CASE WHEN typ.typtype = 'd' THEN typ.typtypmod ELSE atttypmod END AS typmod,
3032
CASE WHEN atthasdef THEN (SELECT pg_get_expr(adbin, cls.oid) FROM pg_attrdef WHERE adrelid = cls.oid AND adnum = attr.attnum) ELSE NULL END AS default,
3133
CASE WHEN col.is_updatable = 'YES' THEN true ELSE false END AS is_updatable,
3234
EXISTS (
@@ -94,13 +96,10 @@ internal ReadOnlyCollection<NpgsqlDbColumn> GetColumnSchema()
9496
{
9597
for (; reader.Read(); populatedColumns++)
9698
{
97-
var column = LoadColumnDefinition(reader);
99+
var column = LoadColumnDefinition(reader, _connection.Connector.TypeHandlerRegistry);
98100

99101
var ordinal = fields.FindIndex(f => f.TableOID == column.TableOID && f.ColumnAttributeNumber - 1 == column.ColumnAttributeNumber);
100102
Debug.Assert(ordinal >= 0);
101-
var field = fields[ordinal];
102-
103-
column.PostgresType = field.PostgresType;
104103

105104
// The column's ordinal is with respect to the resultset, not its table
106105
column.ColumnOrdinal = ordinal;
@@ -136,7 +135,7 @@ internal ReadOnlyCollection<NpgsqlDbColumn> GetColumnSchema()
136135
return result.AsReadOnly();
137136
}
138137

139-
NpgsqlDbColumn LoadColumnDefinition(NpgsqlDataReader reader)
138+
NpgsqlDbColumn LoadColumnDefinition(NpgsqlDataReader reader, TypeHandlerRegistry registry)
140139
{
141140
var columnName = reader.GetString(reader.GetOrdinal("attname"));
142141
var column = new NpgsqlDbColumn
@@ -159,12 +158,14 @@ NpgsqlDbColumn LoadColumnDefinition(NpgsqlDataReader reader)
159158
TypeOID = reader.GetFieldValue<uint>(reader.GetOrdinal("typoid"))
160159
};
161160

161+
column.PostgresType = registry.PostgresTypes.ByOID[column.TypeOID];
162+
162163
var defaultValueOrdinal = reader.GetOrdinal("default");
163164
column.DefaultValue = reader.IsDBNull(defaultValueOrdinal) ? null : reader.GetString(defaultValueOrdinal);
164165

165166
column.IsAutoIncrement = column.DefaultValue != null && column.DefaultValue.StartsWith("nextval(");
166167

167-
ColumnPostConfig(column, reader.GetInt32(reader.GetOrdinal("atttypmod")));
168+
ColumnPostConfig(column, reader.GetInt32(reader.GetOrdinal("typmod")));
168169

169170
return column;
170171
}
@@ -211,7 +212,11 @@ void ColumnPostConfig(NpgsqlDbColumn column, int typeModifier)
211212
if (typeModifier == -1)
212213
return;
213214

214-
switch (column.DataTypeName)
215+
// If the column's type is a domain, use its base data type to interpret the typmod
216+
var dataTypeName = column.PostgresType is PostgresDomainType
217+
? ((PostgresDomainType)column.PostgresType).BaseType.Name
218+
: column.DataTypeName;
219+
switch (dataTypeName)
215220
{
216221
case "bpchar":
217222
case "char":

src/Npgsql/TypeHandlerRegistry.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class TypeHandlerRegistry
6161
[CanBeNull]
6262
internal Dictionary<Type, TypeHandler> ArrayHandlerByType { get; set; }
6363

64-
AvailablePostgresTypes _postgresTypes;
64+
internal AvailablePostgresTypes PostgresTypes { get; private set; }
6565

6666
/// <summary>
6767
/// A counter that is updated when this registry activates its global mappings.
@@ -117,14 +117,14 @@ internal static async Task Setup(NpgsqlConnector connector, NpgsqlTimeout timeou
117117
if (!BackendTypeCache.TryGetValue(connector.ConnectionString, out var types))
118118
types = BackendTypeCache[connector.ConnectionString] = await LoadBackendTypes(connector, timeout, async);
119119

120-
connector.TypeHandlerRegistry._postgresTypes = types;
120+
connector.TypeHandlerRegistry.PostgresTypes = types;
121121
connector.TypeHandlerRegistry.ActivateGlobalMappings();
122122
}
123123

124124
TypeHandlerRegistry(NpgsqlConnector connector)
125125
{
126126
Connector = connector;
127-
_postgresTypes = EmptyPostgresTypes;
127+
PostgresTypes = EmptyPostgresTypes;
128128
UnrecognizedTypeHandler = new UnknownTypeHandler(this);
129129
ByOID = new Dictionary<uint, TypeHandler>();
130130
ByDbType = new Dictionary<DbType, TypeHandler>();
@@ -393,8 +393,8 @@ PostgresCompositeType GetCompositeType(string pgName)
393393
{
394394
// First check if the composite type definition has already been loaded from the database
395395
if (pgName.IndexOf('.') == -1
396-
? _postgresTypes.ByName.TryGetValue(pgName, out var postgresType)
397-
: _postgresTypes.ByFullName.TryGetValue(pgName, out postgresType))
396+
? PostgresTypes.ByName.TryGetValue(pgName, out var postgresType)
397+
: PostgresTypes.ByFullName.TryGetValue(pgName, out postgresType))
398398
{
399399
var asComposite = postgresType as PostgresCompositeType;
400400
if (asComposite == null)
@@ -449,7 +449,7 @@ PostgresCompositeType GetCompositeType(string pgName)
449449
fields.Add(new RawCompositeField { PgName = reader.GetString(0), TypeOID = reader.GetFieldValue<uint>(1) });
450450

451451
var compositeType = new PostgresCompositeType(ns, name, oid, fields);
452-
compositeType.AddTo(_postgresTypes);
452+
compositeType.AddTo(PostgresTypes);
453453

454454
reader.NextResult(); // Load the array type
455455

@@ -459,7 +459,7 @@ PostgresCompositeType GetCompositeType(string pgName)
459459
var arrayName = reader.GetString(1);
460460
var arrayOID = reader.GetFieldValue<uint>(2);
461461

462-
new PostgresArrayType(arrayNs, arrayName, arrayOID, compositeType).AddTo(_postgresTypes);
462+
new PostgresArrayType(arrayNs, arrayName, arrayOID, compositeType).AddTo(PostgresTypes);
463463
} else
464464
Log.Warn($"Could not find array type corresponding to composite {pgName}");
465465

@@ -487,7 +487,7 @@ internal bool TryGetByOID(uint oid, out TypeHandler handler)
487487
{
488488
if (ByOID.TryGetValue(oid, out handler))
489489
return true;
490-
if (!_postgresTypes.ByOID.TryGetValue(oid, out var postgresType))
490+
if (!PostgresTypes.ByOID.TryGetValue(oid, out var postgresType))
491491
return false;
492492

493493
handler = postgresType.Activate(this);
@@ -525,7 +525,7 @@ internal bool TryGetByOID(uint oid, out TypeHandler handler)
525525
throw new InvalidCastException($"When specifying NpgsqlDbType.{nameof(NpgsqlDbType.Enum)}, {nameof(NpgsqlParameter.SpecificType)} must be specified as well");
526526

527527
// Base, range or array of base/range
528-
if (_postgresTypes.ByNpgsqlDbType.TryGetValue(npgsqlDbType, out var postgresType))
528+
if (PostgresTypes.ByNpgsqlDbType.TryGetValue(npgsqlDbType, out var postgresType))
529529
return postgresType.Activate(this);
530530

531531
// We don't have a backend type for this NpgsqlDbType. This could be because it's not yet supported by
@@ -543,7 +543,7 @@ internal TypeHandler this[DbType dbType]
543543
{
544544
if (ByDbType.TryGetValue(dbType, out var handler))
545545
return handler;
546-
if (_postgresTypes.ByDbType.TryGetValue(dbType, out var postgresType))
546+
if (PostgresTypes.ByDbType.TryGetValue(dbType, out var postgresType))
547547
return postgresType.Activate(this);
548548
throw new NotSupportedException("This DbType is not supported in Npgsql: " + dbType);
549549
}
@@ -582,7 +582,7 @@ internal TypeHandler this[Type type]
582582
return handler;
583583

584584
// Try to find the backend type by a simple lookup on the given CLR type, this will handle base types.
585-
if (_postgresTypes.ByClrType.TryGetValue(type, out var postgresType))
585+
if (PostgresTypes.ByClrType.TryGetValue(type, out var postgresType))
586586
return postgresType.Activate(this);
587587

588588
// Try to see if it is an array type
@@ -605,13 +605,13 @@ internal TypeHandler this[Type type]
605605
// Special check for byte[] - bytea not array of int2
606606
if (type == typeof(byte[]))
607607
{
608-
if (!_postgresTypes.ByClrType.TryGetValue(typeof(byte[]), out var byteaPostgresType))
608+
if (!PostgresTypes.ByClrType.TryGetValue(typeof(byte[]), out var byteaPostgresType))
609609
throw new NpgsqlException("The PostgreSQL 'bytea' type is missing");
610610
return byteaPostgresType.Activate(this);
611611
}
612612

613613
// Get the elements backend type and activate its array backend type
614-
if (!_postgresTypes.ByClrType.TryGetValue(arrayElementType, out var elementPostgresType))
614+
if (!PostgresTypes.ByClrType.TryGetValue(arrayElementType, out var elementPostgresType))
615615
{
616616
if (arrayElementType.GetTypeInfo().IsEnum)
617617
throw new NotSupportedException($"The CLR enum type {arrayElementType.Name} must be mapped with Npgsql before usage, please refer to the documentation.");
@@ -628,7 +628,7 @@ internal TypeHandler this[Type type]
628628
// Range type which hasn't yet been set up
629629
if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(NpgsqlRange<>))
630630
{
631-
if (!_postgresTypes.ByClrType.TryGetValue(type.GetGenericArguments()[0], out var subtypePostgresType) ||
631+
if (!PostgresTypes.ByClrType.TryGetValue(type.GetGenericArguments()[0], out var subtypePostgresType) ||
632632
subtypePostgresType.Range == null)
633633
{
634634
throw new NpgsqlException($"The .NET range type {type.Name} isn't supported in your PostgreSQL, use CREATE TYPE AS RANGE");
@@ -854,15 +854,15 @@ PostgresType GetBackendTypeByName(string pgName)
854854
if (i == -1)
855855
{
856856
// No dot, this is a partial type name
857-
if (!_postgresTypes.ByName.TryGetValue(pgName, out postgresType))
857+
if (!PostgresTypes.ByName.TryGetValue(pgName, out postgresType))
858858
throw new NpgsqlException($"A PostgreSQL type with the name {pgName} was not found in the database");
859859
if (postgresType == null)
860860
throw new NpgsqlException($"More than one PostgreSQL type was found with the name {pgName}, please specify a full name including schema");
861861
return postgresType;
862862
}
863863

864864
// Full type name with namespace
865-
if (!_postgresTypes.ByFullName.TryGetValue(pgName, out postgresType))
865+
if (!PostgresTypes.ByFullName.TryGetValue(pgName, out postgresType))
866866
throw new Exception($"A PostgreSQL type with the name {pgName} was not found in the database");
867867
return postgresType;
868868
}

test/Npgsql.Tests/ReaderNewSchemaTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using System.Data;
2727
using System.Linq;
2828
using System.Threading.Tasks;
29+
using Npgsql.PostgresTypes;
2930
using NUnit.Framework;
3031

3132
namespace Npgsql.Tests
@@ -547,6 +548,33 @@ public void SameColumnName()
547548
}
548549
}
549550

551+
[Test, IssueLink("https://github.com/npgsql/npgsql/issues/1553")]
552+
public void DomainTypes()
553+
{
554+
using (var conn = OpenConnection())
555+
{
556+
conn.ExecuteNonQuery("DROP DOMAIN IF EXISTS mydomain; CREATE DOMAIN mydomain AS varchar(2)");
557+
try
558+
{
559+
conn.ReloadTypes();
560+
conn.ExecuteNonQuery("CREATE TEMP TABLE data (domain mydomain)");
561+
using (var cmd = new NpgsqlCommand("SELECT domain FROM data", conn))
562+
using (var reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo))
563+
{
564+
var domainSchema = reader.GetColumnSchema().Single(c => c.ColumnName == "domain");
565+
Assert.That(domainSchema.ColumnSize, Is.EqualTo(2));
566+
var pgType = domainSchema.PostgresType;
567+
Assert.That(pgType, Is.InstanceOf<PostgresDomainType>());
568+
Assert.That(((PostgresDomainType)pgType).BaseType.Name, Is.EqualTo("varchar"));
569+
}
570+
}
571+
finally
572+
{
573+
conn.ExecuteNonQuery("DROP TABLE data; DROP DOMAIN mydomain");
574+
}
575+
}
576+
}
577+
550578
#region Not supported
551579

552580
[Test]

0 commit comments

Comments
 (0)