-
Notifications
You must be signed in to change notification settings - Fork 256
Fix mapping issues around arrays and byte arrays #1196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
368e4b1
c7e4a75
406ca71
df106a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| #nullable enable | ||
|
|
||
| // ReSharper disable once CheckNamespace | ||
| namespace Npgsql.EntityFrameworkCore.PostgreSQL | ||
| { | ||
| internal static class TypeExtensions | ||
| { | ||
| internal static bool IsGenericList(this Type type) | ||
| => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); | ||
|
|
||
| internal static bool IsArrayOrGenericList(this Type type) | ||
| => type.IsArray || type.IsGenericList(); | ||
|
|
||
| internal static bool TryGetElementType(this Type type, out Type? elementType) | ||
| { | ||
| elementType = type.IsArray | ||
| ? type.GetElementType() | ||
| : type.IsGenericList() | ||
| ? type.GetGenericArguments()[0] | ||
| : null; | ||
| return elementType != null; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,41 +137,55 @@ protected virtual RelationalTypeMapping FindExistingMapping(in RelationalTypeMap | |
| : mapping; | ||
| } | ||
|
|
||
| RelationalTypeMapping FindArrayMapping(in RelationalTypeMappingInfo mappingInfo) | ||
| // TODO: This is duplicated from NpgsqlTypeMappingSource | ||
| protected virtual RelationalTypeMapping FindArrayMapping(in RelationalTypeMappingInfo mappingInfo) | ||
| { | ||
| // PostgreSQL array type names are the element plus [] | ||
| var clrType = mappingInfo.ClrType; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When could
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When reverse engineering an existing database - all you have is the store type from PostgreSQL, and you're creating the C# code model. |
||
| Type elementClrType = null; | ||
|
|
||
| if (clrType != null && !clrType.TryGetElementType(out elementClrType)) | ||
| return null; // Not an array/list | ||
|
|
||
| var storeType = mappingInfo.StoreTypeName; | ||
| var storeTypeNameBase = mappingInfo.StoreTypeNameBase; | ||
| if (storeType != null) | ||
| { | ||
| // PostgreSQL array type names are the element plus [] | ||
| if (!storeType.EndsWith("[]")) | ||
| return null; | ||
|
|
||
| // Note that we scaffold PostgreSQL arrays to C# arrays, not lists (which are also supported) | ||
|
|
||
| // TODO: In theory support the multiple mappings just like we do with scalars above | ||
| // (e.g. DateTimeOffset[] vs. DateTime[] | ||
| // TODO: In theory we should parse the element store type to get the base type. This is problematic in plugins | ||
| // as we have no access to the RelationalTypeMappingSource, where all this happens | ||
| var elementStoreType = storeType.Substring(0, storeType.Length - 2); | ||
| var elementMapping = FindExistingMapping(new RelationalTypeMappingInfo(elementStoreType, elementStoreType, | ||
| mappingInfo.IsUnicode, mappingInfo.Size, mappingInfo.Precision, mappingInfo.Scale)); | ||
| var elementStoreTypeNameBase = storeTypeNameBase.Substring(0, storeTypeNameBase.Length - 2); | ||
|
|
||
| RelationalTypeMapping elementMapping; | ||
|
|
||
| if (elementMapping != null) | ||
| if (elementClrType == null) | ||
| { | ||
| elementMapping = FindMapping(new RelationalTypeMappingInfo( | ||
| elementStoreType, elementStoreTypeNameBase, | ||
| mappingInfo.IsUnicode, mappingInfo.Size, mappingInfo.Precision, mappingInfo.Scale)); | ||
| } | ||
| else | ||
| { | ||
| var added = StoreTypeMappings.TryAdd(storeType, | ||
| new RelationalTypeMapping[] | ||
| { | ||
| new NpgsqlArrayArrayTypeMapping(storeType, elementMapping), | ||
| new NpgsqlArrayListTypeMapping(storeType, elementMapping) | ||
| }); | ||
| Debug.Assert(added); | ||
| var mapping = FindExistingMapping(mappingInfo); | ||
| Debug.Assert(mapping != null); | ||
| return mapping; | ||
| elementMapping = FindMapping(new RelationalTypeMappingInfo( | ||
| elementClrType, elementStoreType, elementStoreTypeNameBase, | ||
| mappingInfo.IsKeyOrIndex, mappingInfo.IsUnicode, mappingInfo.Size, mappingInfo.IsRowVersion, | ||
| mappingInfo.IsFixedLength, mappingInfo.Precision, mappingInfo.Scale)); | ||
|
|
||
| // If an element mapping was found only with the help of a value converter, return null and EF will | ||
| // construct the corresponding array mapping with a value converter. | ||
| if (elementMapping?.Converter != null) | ||
| return null; | ||
| } | ||
|
|
||
| // If no mapping was found for the element, there's no mapping for the array. | ||
| // Also, arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL | ||
| if (elementMapping == null || elementMapping is NpgsqlArrayTypeMapping) | ||
| return null; | ||
|
|
||
| return new NpgsqlArrayArrayTypeMapping(storeType, elementMapping); | ||
| } | ||
|
|
||
| var clrType = mappingInfo.ClrType; | ||
| if (clrType == null) | ||
| return null; | ||
|
|
||
|
|
@@ -180,32 +194,33 @@ RelationalTypeMapping FindArrayMapping(in RelationalTypeMappingInfo mappingInfo) | |
| var elementType = clrType.GetElementType(); | ||
| Debug.Assert(elementType != null, "Detected array type but element type is null"); | ||
|
|
||
| // If an element isn't supported, neither is its array | ||
| var elementMapping = FindExistingMapping(new RelationalTypeMappingInfo(elementType)); | ||
| if (elementMapping == null) | ||
| // If an element isn't supported, neither is its array. If the element is only supported via value | ||
| // conversion we also don't support it. | ||
| var elementMapping = FindMapping(new RelationalTypeMappingInfo(elementType)); | ||
| if (elementMapping == null || elementMapping.Converter != null) | ||
| return null; | ||
|
|
||
| // Arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL | ||
| if (elementMapping is NpgsqlArrayTypeMapping) | ||
| return null; | ||
|
|
||
| return ClrTypeMappings.GetOrAdd(clrType, new NpgsqlArrayArrayTypeMapping(elementMapping, clrType)); | ||
| return new NpgsqlArrayArrayTypeMapping(elementMapping, clrType); | ||
| } | ||
|
|
||
| if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(List<>)) | ||
| if (clrType.IsGenericList()) | ||
| { | ||
| var elementType = clrType.GetGenericArguments()[0]; | ||
|
|
||
| // If an element isn't supported, neither is its array | ||
| var elementMapping = FindExistingMapping(new RelationalTypeMappingInfo(elementType)); | ||
| var elementMapping = FindMapping(new RelationalTypeMappingInfo(elementType)); | ||
| if (elementMapping == null) | ||
| return null; | ||
|
|
||
| // Arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL | ||
| if (elementMapping is NpgsqlArrayTypeMapping) | ||
| return null; | ||
|
|
||
| return ClrTypeMappings.GetOrAdd(clrType, new NpgsqlArrayListTypeMapping(elementMapping, clrType)); | ||
| return new NpgsqlArrayListTypeMapping(elementMapping, clrType); | ||
| } | ||
|
|
||
| return null; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| using System; | ||
| using System.Collections; | ||
| using System.Collections.Generic; | ||
| using System.Data.Common; | ||
| using System.Text; | ||
| using Microsoft.EntityFrameworkCore.Storage; | ||
| using NpgsqlTypes; | ||
|
|
||
| #nullable enable | ||
|
|
||
| namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping | ||
| { | ||
|
|
@@ -19,9 +23,26 @@ public abstract class NpgsqlArrayTypeMapping : RelationalTypeMapping | |
| /// </summary> | ||
| public RelationalTypeMapping ElementMapping { get; } | ||
|
|
||
| /// <summary> | ||
| /// The database type used by Npgsql. | ||
| /// </summary> | ||
| public NpgsqlDbType? NpgsqlDbType { get; } | ||
|
|
||
| protected NpgsqlArrayTypeMapping(RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping) | ||
| : base(parameters) | ||
| => ElementMapping = elementMapping; | ||
| { | ||
| ElementMapping = elementMapping; | ||
|
|
||
| // If the element mapping has an NpgsqlDbType or DbType, set our own NpgsqlDbType as an array of that. | ||
| // Otherwise let the ADO.NET layer infer the PostgreSQL type. We can't always let it infer, otherwise | ||
| // when given a byte[] it will infer byte (but we want smallint[]) | ||
| NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Array | | ||
| (elementMapping is NpgsqlTypeMapping elementNpgsqlTypeMapping | ||
| ? elementNpgsqlTypeMapping.NpgsqlDbType | ||
| : elementMapping.DbType.HasValue | ||
| ? new NpgsqlParameter { DbType = elementMapping.DbType.Value }.NpgsqlDbType | ||
| : default(NpgsqlDbType?)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To large indentation, but it's a nit.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how Rider likes it... I've stopped fighting :)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I know, it's adjustable. For me it doesn't recommend such things, am using a cleaned up editor config from
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. If you really feel like it you can work on our .editorconfig :) |
||
| } | ||
|
|
||
| // The array-to-array mapping needs to know how to generate an SQL literal for a List<>, and | ||
| // the list-to-array mapping needs to know how to generate an SQL literal for an array. | ||
|
|
@@ -52,5 +73,17 @@ protected override string GenerateNonNullSqlLiteral(object value) | |
| sb.Append("[]"); | ||
| return sb.ToString(); | ||
| } | ||
|
|
||
| protected override void ConfigureParameter(DbParameter parameter) | ||
| { | ||
| var npgsqlParameter = parameter as NpgsqlParameter; | ||
| if (npgsqlParameter == null) | ||
| throw new ArgumentException($"Npgsql-specific type mapping {GetType()} being used with non-Npgsql parameter type {parameter.GetType().Name}"); | ||
|
|
||
| base.ConfigureParameter(parameter); | ||
|
|
||
| if (NpgsqlDbType.HasValue) | ||
| npgsqlParameter.NpgsqlDbType = NpgsqlDbType.Value; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to return an element type as is or
null.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure... This means that instead of:
we need to write:
which seems considerably more verbose/ugly. Another small advantage of TryGetElementType is that you can put an existing variable.
Both patterns seem to have advantages and disadvantages...