Skip to content

Commit

Permalink
Support a few more type mappings in the compiled model (#2961)
Browse files Browse the repository at this point in the history
Part of #2949
  • Loading branch information
roji authored Nov 18, 2023
1 parent 92f881c commit fa5ba2e
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.EntityFrameworkCore.Design.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal;

Expand All @@ -28,6 +29,109 @@ public NpgsqlCSharpRuntimeAnnotationCodeGenerator(
{
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override bool Create(
CoreTypeMapping typeMapping,
CSharpRuntimeAnnotationCodeGeneratorParameters parameters,
ValueComparer? valueComparer = null,
ValueComparer? keyValueComparer = null,
ValueComparer? providerValueComparer = null)
{
var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer);

var mainBuilder = parameters.MainBuilder;

var npgsqlDbTypeBasedDefaultInstance = typeMapping switch
{
NpgsqlStringTypeMapping => NpgsqlStringTypeMapping.Default,
NpgsqlULongTypeMapping => NpgsqlULongTypeMapping.Default,
// NpgsqlMultirangeTypeMapping => NpgsqlMultirangeTypeMapping.Default,
_ => (INpgsqlTypeMapping?)null
};

if (npgsqlDbTypeBasedDefaultInstance is not null)
{
var npgsqlDbType = ((INpgsqlTypeMapping)typeMapping).NpgsqlDbType;

if (npgsqlDbType != npgsqlDbTypeBasedDefaultInstance.NpgsqlDbType)
{
mainBuilder.AppendLine(";");

mainBuilder.Append(
$"{parameters.TargetName}.TypeMapping = (({typeMapping.GetType().Name}){parameters.TargetName}.TypeMapping).Clone(npgsqlDbType: ");

mainBuilder
.Append(nameof(NpgsqlTypes))
.Append(".")
.Append(nameof(NpgsqlDbType))
.Append(".")
.Append(npgsqlDbType.ToString());

mainBuilder
.Append(")")
.DecrementIndent();
}

}

switch (typeMapping)
{
#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete
case NpgsqlEnumTypeMapping enumTypeMapping:
if (enumTypeMapping.NameTranslator != NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator)
{
throw new NotSupportedException(
"Mapped enums are only supported in the compiled model if they use the default name translator");
}
break;
#pragma warning restore CS0618

case NpgsqlRangeTypeMapping rangeTypeMapping:
{
var defaultInstance = NpgsqlRangeTypeMapping.Default;

var npgsqlDbTypeDifferent = rangeTypeMapping.NpgsqlDbType != defaultInstance.NpgsqlDbType;
var subtypeTypeMappingIsDifferent = rangeTypeMapping.SubtypeMapping != defaultInstance.SubtypeMapping;

if (npgsqlDbTypeDifferent || subtypeTypeMappingIsDifferent)
{
mainBuilder.AppendLine(";");

mainBuilder.AppendLine(
$"{parameters.TargetName}.TypeMapping = ((NpgsqlRangeTypeMapping){parameters.TargetName}.TypeMapping).Clone(")
.IncrementIndent();

mainBuilder
.Append("npgsqlDbType: ")
.Append(nameof(NpgsqlTypes))
.Append(".")
.Append(nameof(NpgsqlDbType))
.Append(".")
.Append(rangeTypeMapping.NpgsqlDbType.ToString())
.AppendLine(",");

mainBuilder.Append("subtypeTypeMapping: ");

Create(rangeTypeMapping.SubtypeMapping, parameters);

mainBuilder
.Append(")")
.DecrementIndent();
}

break;
}

}

return result;
}

/// <inheritdoc />
public override void Generate(IModel model, CSharpRuntimeAnnotationCodeGeneratorParameters parameters)
{
Expand Down
69 changes: 52 additions & 17 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
/// </summary>
public class NpgsqlEnumTypeMapping : RelationalTypeMapping
{
private readonly ISqlGenerationHelper _sqlGenerationHelper;
private readonly INpgsqlNameTranslator _nameTranslator;

/// <summary>
/// Translates the CLR member value to the PostgreSQL value label.
/// </summary>
Expand All @@ -25,14 +22,25 @@ public class NpgsqlEnumTypeMapping : RelationalTypeMapping
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public NpgsqlEnumTypeMapping(
string storeType,
string? storeTypeSchema,
Type enumType,
ISqlGenerationHelper sqlGenerationHelper,
INpgsqlNameTranslator? nameTranslator = null)
public static NpgsqlEnumTypeMapping Default { get; } = new();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual INpgsqlNameTranslator NameTranslator { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTranslator? nameTranslator = null)
: base(
sqlGenerationHelper.DelimitIdentifier(storeType, storeTypeSchema),
storeType,
enumType,
jsonValueReaderWriter: (JsonValueReaderWriter?)Activator.CreateInstance(
typeof(JsonPgEnumReaderWriter<>).MakeGenericType(enumType)))
Expand All @@ -46,8 +54,7 @@ public NpgsqlEnumTypeMapping(
nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator;
#pragma warning restore CS0618

_nameTranslator = nameTranslator;
_sqlGenerationHelper = sqlGenerationHelper;
NameTranslator = nameTranslator;
_members = CreateValueMapping(enumType, nameTranslator);
}

Expand All @@ -59,23 +66,32 @@ public NpgsqlEnumTypeMapping(
/// </summary>
protected NpgsqlEnumTypeMapping(
RelationalTypeMappingParameters parameters,
ISqlGenerationHelper sqlGenerationHelper,
INpgsqlNameTranslator nameTranslator)
: base(parameters)
{
_nameTranslator = nameTranslator;
_sqlGenerationHelper = sqlGenerationHelper;
NameTranslator = nameTranslator;
_members = CreateValueMapping(parameters.CoreParameters.ClrType, nameTranslator);
}

// This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled
// models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details.
private NpgsqlEnumTypeMapping()
: base("some_enum", typeof(int))
{
#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete
NameTranslator = NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator;
#pragma warning restore CS0618
_members = null!;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlEnumTypeMapping(parameters, _sqlGenerationHelper, _nameTranslator);
=> new NpgsqlEnumTypeMapping(parameters, NameTranslator);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -98,12 +114,31 @@ private static Dictionary<object, string> CreateValueMapping(Type enumType, INpg
x => x.GetValue(null)!,
x => x.GetCustomAttribute<PgNameAttribute>()?.PgName ?? nameTranslator.TranslateMemberName(x.Name));

private sealed class JsonPgEnumReaderWriter<T> : JsonValueReaderWriter<T>
// This is public for the compiled model
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public sealed class JsonPgEnumReaderWriter<T> : JsonValueReaderWriter<T>
where T : struct, Enum
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override T FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> Enum.Parse<T>(manager.CurrentReader.GetString()!);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override void ToJsonTyped(Utf8JsonWriter writer, T value)
=> writer.WriteStringValue(value.ToString());
}
Expand Down
27 changes: 27 additions & 0 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public class NpgsqlRangeTypeMapping : NpgsqlTypeMapping
private ConstructorInfo? _rangeConstructor2;
private ConstructorInfo? _rangeConstructor3;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static NpgsqlRangeTypeMapping Default { get; } = new();

// ReSharper disable once MemberCanBePrivate.Global
/// <summary>
/// The relational type mapping of the range's subtype.
Expand Down Expand Up @@ -94,6 +102,25 @@ protected NpgsqlRangeTypeMapping(
SubtypeMapping = subtypeMapping;
}

// This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled
// models. The constructor creates a completely blank type mapping, which will get cloned with all the correct details.
private NpgsqlRangeTypeMapping()
: this("int4range", typeof(NpgsqlRange<int>), NpgsqlDbType.IntegerRange, subtypeMapping: null!)
{
}

/// <summary>
/// This method exists only to support the compiled model.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
public virtual NpgsqlRangeTypeMapping Clone(NpgsqlDbType npgsqlDbType, RelationalTypeMapping subtypeTypeMapping)
=> new(Parameters, npgsqlDbType, subtypeTypeMapping);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlStringTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
/// </summary>
public class NpgsqlStringTypeMapping : StringTypeMapping, INpgsqlTypeMapping
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static new NpgsqlStringTypeMapping Default { get; } = new("text", NpgsqlDbType.Text);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down Expand Up @@ -51,6 +59,18 @@ protected NpgsqlStringTypeMapping(
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlStringTypeMapping(parameters, NpgsqlDbType);

/// <summary>
/// This method exists only to support the compiled model.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
public virtual NpgsqlStringTypeMapping Clone(NpgsqlDbType npgsqlDbType)
=> new(Parameters, npgsqlDbType);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
/// </summary>
public class NpgsqlULongTypeMapping : NpgsqlTypeMapping
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static NpgsqlULongTypeMapping Default { get; } = new("xid8", NpgsqlDbType.Xid8);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
4 changes: 3 additions & 1 deletion src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,9 @@ is PropertyInfo globalEnumTypeMappingsProperty
var name = components.Length > 1 ? string.Join(null, components.Skip(1)) : adoEnumMapping.PgTypeName;

var mapping = new NpgsqlEnumTypeMapping(
name, schema, adoEnumMapping.EnumClrType, sqlGenerationHelper, adoEnumMapping.NameTranslator);
sqlGenerationHelper.DelimitIdentifier(name, schema),
adoEnumMapping.EnumClrType,
adoEnumMapping.NameTranslator);
ClrTypeMappings[adoEnumMapping.EnumClrType] = mapping;
StoreTypeMappings[mapping.StoreType] = new RelationalTypeMapping[] { mapping };
}
Expand Down
14 changes: 2 additions & 12 deletions test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,25 +489,15 @@ public void ValueComparer_hstore_as_ImmutableDictionary()
[Fact]
public void GenerateSqlLiteral_returns_enum_literal()
{
var mapping = new NpgsqlEnumTypeMapping(
"dummy_enum",
null,
typeof(DummyEnum),
new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()),
new NpgsqlSnakeCaseNameTranslator());
var mapping = new NpgsqlEnumTypeMapping("dummy_enum", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator());

Assert.Equal("'sad'::dummy_enum", mapping.GenerateSqlLiteral(DummyEnum.Sad));
}

[Fact]
public void GenerateSqlLiteral_returns_enum_uppercase_literal()
{
var mapping = new NpgsqlEnumTypeMapping(
"DummyEnum",
null,
typeof(DummyEnum),
new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()),
new NpgsqlSnakeCaseNameTranslator());
var mapping = new NpgsqlEnumTypeMapping(@"""DummyEnum""", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator());

Assert.Equal(@"'sad'::""DummyEnum""", mapping.GenerateSqlLiteral(DummyEnum.Sad));
}
Expand Down

0 comments on commit fa5ba2e

Please sign in to comment.