Skip to content

Commit

Permalink
Type mappings
Browse files Browse the repository at this point in the history
Closes npgsql#1970
Closes npgsql#873
Closes npgsql#473
  • Loading branch information
roji committed Aug 29, 2021
1 parent fed659b commit 28a3236
Show file tree
Hide file tree
Showing 25 changed files with 1,255 additions and 193 deletions.
7 changes: 7 additions & 0 deletions EFCore.PG.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ Licensed under the Apache License, Version 2.0. See License.txt in the project r
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EXml_002ECodeStyle_002EFormatSettingsUpgrade_002EXmlMoveToCommonFormatterSettingsUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=annotatable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=composable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=daterange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=datetimeoffset/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=doesnt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=fallbacks/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=initializers/@EntryIndexedValue">True</s:Boolean>
Expand All @@ -228,11 +230,16 @@ Licensed under the Apache License, Version 2.0. See License.txt in the project r
<s:Boolean x:Key="/Default/UserDictionary/Words/=sproc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=sqlite/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subquery/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=timestamptz/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=timetz/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tsrange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tstzrange/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unignore/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=fixup/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=attacher/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Uniquify/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unlogged/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=utcnow/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xunit/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

<!-- From here it's EFCore.PG stuff -->
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.PG.NodaTime/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.FunctionalTests, PublicKey=" +
"0024000004800000940000000602000000240000525341310004000001000100" +
"2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" +
"8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" +
"7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" +
"29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")]
63 changes: 32 additions & 31 deletions src/EFCore.PG.NodaTime/Storage/Internal/NodaTimeMappings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,6 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal
{
#region timestamp

public class TimestampInstantMapping : NpgsqlTypeMapping
{
public TimestampInstantMapping() : base("timestamp", typeof(Instant), NpgsqlDbType.Timestamp) {}

protected TimestampInstantMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Timestamp) {}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new TimestampInstantMapping(parameters);

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimestampInstantMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter? converter)
=> new TimestampInstantMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMP '{InstantPattern.ExtendedIso.Format((Instant)value)}'";

public override Expression GenerateCodeLiteral(object value)
=> GenerateCodeLiteral((Instant)value);

internal static Expression GenerateCodeLiteral(Instant instant)
=> Expression.Call(FromUnixTimeTicks, Expression.Constant(instant.ToUnixTimeTicks()));

private static readonly MethodInfo FromUnixTimeTicks
= typeof(Instant).GetRuntimeMethod(nameof(Instant.FromUnixTimeTicks), new[] { typeof(long) })!;
}

public class TimestampLocalDateTimeMapping : NpgsqlTypeMapping
{
private static readonly ConstructorInfo ConstructorWithMinutes =
Expand Down Expand Up @@ -86,6 +57,36 @@ internal static Expression GenerateCodeLiteral(LocalDateTime dateTime)
}
}

// Used only with EnableLegacyTimestampBehavior
public class LegacyTimestampInstantMapping : NpgsqlTypeMapping
{
public LegacyTimestampInstantMapping() : base("timestamp", typeof(Instant), NpgsqlDbType.Timestamp) {}

protected LegacyTimestampInstantMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Timestamp) {}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new LegacyTimestampInstantMapping(parameters);

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new LegacyTimestampInstantMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter? converter)
=> new LegacyTimestampInstantMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMP '{InstantPattern.ExtendedIso.Format((Instant)value)}'";

public override Expression GenerateCodeLiteral(object value)
=> GenerateCodeLiteral((Instant)value);

internal static Expression GenerateCodeLiteral(Instant instant)
=> Expression.Call(FromUnixTimeTicks, Expression.Constant(instant.ToUnixTimeTicks()));

private static readonly MethodInfo FromUnixTimeTicks
= typeof(Instant).GetRuntimeMethod(nameof(Instant.FromUnixTimeTicks), new[] { typeof(long) })!;
}

#endregion timestamp

#region timestamptz
Expand All @@ -110,7 +111,7 @@ protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMPTZ '{InstantPattern.ExtendedIso.Format((Instant)value)}'";

public override Expression GenerateCodeLiteral(object value)
=> TimestampInstantMapping.GenerateCodeLiteral((Instant)value);
=> LegacyTimestampInstantMapping.GenerateCodeLiteral((Instant)value);
}

public class TimestampTzOffsetDateTimeMapping : NpgsqlTypeMapping
Expand Down Expand Up @@ -183,7 +184,7 @@ public override Expression GenerateCodeLiteral(object value)

return Expression.New(
Constructor,
TimestampInstantMapping.GenerateCodeLiteral(zonedDateTime.ToInstant()),
LegacyTimestampInstantMapping.GenerateCodeLiteral(zonedDateTime.ToInstant()),
Expression.Call(
Expression.MakeMemberAccess(
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,22 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal
{
public class NpgsqlNodaTimeTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin
{
#if DEBUG
internal static bool LegacyTimestampBehavior;
#else
static readonly bool LegacyTimestampBehavior;
#endif

static NpgsqlNodaTimeTypeMappingSourcePlugin()
=> LegacyTimestampBehavior = AppContext.TryGetSwitch("Npgsql.EnableLegacyTimestampBehavior", out var enabled) && enabled;

public virtual ConcurrentDictionary<string, RelationalTypeMapping[]> StoreTypeMappings { get; }
public virtual ConcurrentDictionary<Type, RelationalTypeMapping> ClrTypeMappings { get; }

#region TypeMapping

private readonly TimestampInstantMapping _timestampInstant = new();
private readonly TimestampLocalDateTimeMapping _timestampLocalDateTime = new();
private readonly LegacyTimestampInstantMapping _legacyTimestampInstant = new();

private readonly TimestampTzInstantMapping _timestamptzInstant = new();
private readonly TimestampTzZonedDateTimeMapping _timestamptzZonedDateTime = new();
Expand All @@ -45,22 +54,22 @@ public class NpgsqlNodaTimeTypeMappingSourcePlugin : IRelationalTypeMappingSourc
public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationHelper)
{
_timestampLocalDateTimeRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange<LocalDateTime>), _timestampLocalDateTime, sqlGenerationHelper);
_timestampInstantRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange<Instant>), _timestampInstant, sqlGenerationHelper);
_timestampInstantRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange<Instant>), _legacyTimestampInstant, sqlGenerationHelper);
_timestamptzInstantRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange<Instant>), _timestamptzInstant, sqlGenerationHelper);
_timestamptzZonedDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange<ZonedDateTime>), _timestamptzZonedDateTime, sqlGenerationHelper);
_timestamptzOffsetDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange<OffsetDateTime>), _timestamptzOffsetDateTime, sqlGenerationHelper);
_dateRange = new NpgsqlRangeTypeMapping("daterange", typeof(NpgsqlRange<LocalDate>), _date, sqlGenerationHelper);

var storeTypeMappings = new Dictionary<string, RelationalTypeMapping[]>(StringComparer.OrdinalIgnoreCase)
{
{ "timestamp", new RelationalTypeMapping[] { _timestampInstant, _timestampLocalDateTime } },
{ "timestamp without time zone", new RelationalTypeMapping[] { _timestampInstant, _timestampLocalDateTime } },
{ "timestamptz", new RelationalTypeMapping[] { _timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime } },
{
"timestamp without time zone", LegacyTimestampBehavior
? new RelationalTypeMapping[] { _legacyTimestampInstant, _timestampLocalDateTime }
: new RelationalTypeMapping[] { _timestampLocalDateTime }
},
{ "timestamp with time zone", new RelationalTypeMapping[] { _timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime } },
{ "date", new RelationalTypeMapping[] { _date } },
{ "time", new RelationalTypeMapping[] { _time } },
{ "time without time zone", new RelationalTypeMapping[] { _time } },
{ "timetz", new RelationalTypeMapping[] { _timetz } },
{ "time with time zone", new RelationalTypeMapping[] { _timetz } },
{ "interval", new RelationalTypeMapping[] { _periodInterval } },

Expand All @@ -69,9 +78,16 @@ public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationH
{ "daterange", new RelationalTypeMapping[] { _dateRange} }
};

// Set up aliases
storeTypeMappings["timestamp"] = storeTypeMappings["timestamp without time zone"];
storeTypeMappings["timestamptz"] = storeTypeMappings["timestamp with time zone"];
storeTypeMappings["time"] = storeTypeMappings["time without time zone"];
storeTypeMappings["timetz"] = storeTypeMappings["time with time zone"];

var clrTypeMappings = new Dictionary<Type, RelationalTypeMapping>
{
{ typeof(Instant), _timestampInstant },
{ typeof(Instant), LegacyTimestampBehavior ? _legacyTimestampInstant : _timestamptzInstant },

{ typeof(LocalDateTime), _timestampLocalDateTime },
{ typeof(ZonedDateTime), _timestamptzZonedDateTime },
{ typeof(OffsetDateTime), _timestamptzOffsetDateTime },
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore.PG/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@
"8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" +
"7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" +
"29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")]

[assembly: InternalsVisibleTo("Npgsql.EntityFrameworkCore.PostgreSQL.Tests, PublicKey=" +
"0024000004800000940000000602000000240000525341310004000001000100" +
"2b3c590b2a4e3d347e6878dc0ff4d21eb056a50420250c6617044330701d35c9" +
"8078a5df97a62d83c9a2db2d072523a8fc491398254c6b89329b8c1dcef43a1e" +
"7aa16153bcea2ae9a471145624826f60d7c8e71cd025b554a0177bd935a78096" +
"29f0a7afc778ebb4ad033e1bf512c1a9c6ceea26b077bc46cac93800435e77ee")]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
using NpgsqlTypes;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;

Expand All @@ -18,9 +20,17 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte
public class NpgsqlDateTimeMemberTranslator : IMemberTranslator
{
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
private readonly RelationalTypeMapping _timestampMapping;
private readonly RelationalTypeMapping _timestampTzMapping;

public NpgsqlDateTimeMemberTranslator(NpgsqlSqlExpressionFactory sqlExpressionFactory)
=> _sqlExpressionFactory = sqlExpressionFactory;
public NpgsqlDateTimeMemberTranslator(
IRelationalTypeMappingSource typeMappingSource,
NpgsqlSqlExpressionFactory sqlExpressionFactory)
{
_timestampMapping = typeMappingSource.FindMapping("timestamp without time zone")!;
_timestampTzMapping = typeMappingSource.FindMapping("timestamp with time zone")!;
_sqlExpressionFactory = sqlExpressionFactory;
}

/// <inheritdoc />
public virtual SqlExpression? Translate(
Expand All @@ -37,13 +47,17 @@ public NpgsqlDateTimeMemberTranslator(NpgsqlSqlExpressionFactory sqlExpressionFa

return member.Name switch
{
nameof(DateTime.Now) => Now(),
nameof(DateTime.UtcNow) =>
_sqlExpressionFactory.AtTimeZone(Now(), _sqlExpressionFactory.Constant("UTC"), returnType),
nameof(DateTime.Now) when NpgsqlTypeMappingSource.LegacyTimestampBehavior
=> UtcNow(),
nameof(DateTime.UtcNow) when NpgsqlTypeMappingSource.LegacyTimestampBehavior
=> _sqlExpressionFactory.AtTimeZone(UtcNow(), _sqlExpressionFactory.Constant("UTC"), returnType),

nameof(DateTime.Now) => LocalNow(),
nameof(DateTime.UtcNow) => UtcNow(),

nameof(DateTime.Today) => _sqlExpressionFactory.Function(
"date_trunc",
new SqlExpression[] { _sqlExpressionFactory.Constant("day"), Now() },
new SqlExpression[] { _sqlExpressionFactory.Constant("day"), LocalNow() },
nullable: true,
argumentsPropagateNullability: TrueArrays[2],
returnType),
Expand Down Expand Up @@ -81,13 +95,17 @@ public NpgsqlDateTimeMemberTranslator(NpgsqlSqlExpressionFactory sqlExpressionFa
_ => null
};

SqlFunctionExpression Now()
SqlExpression UtcNow()
=> _sqlExpressionFactory.Function(
"now",
Array.Empty<SqlExpression>(),
nullable: false,
argumentsPropagateNullability: TrueArrays[0],
returnType);
returnType,
_timestampTzMapping);

SqlExpression LocalNow()
=> _sqlExpressionFactory.Convert(UtcNow(), returnType, _timestampMapping);
}

private SqlExpression GetDatePartExpression(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
{
Expand Down Expand Up @@ -38,12 +39,19 @@ public class NpgsqlDateTimeMethodTranslator : IMethodCallTranslator
{ typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddMinutes), new[] { typeof(int) })!, "mins" },
};

private static readonly MethodInfo DateTimeToUniversalTimeMethod
= typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToUniversalTime), Array.Empty<Type>())!;
private static readonly MethodInfo DateTimeToLocalTimeMethod
= typeof(DateTime).GetRuntimeMethod(nameof(DateTime.ToLocalTime), Array.Empty<Type>())!;

private static readonly MethodInfo TimeOnlyIsBetweenMethod
= typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.IsBetween), new[] { typeof(TimeOnly), typeof(TimeOnly) })!;
private static readonly MethodInfo TimeOnlyAddTimeSpanMethod
= typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.Add), new[] { typeof(TimeSpan) })!;

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly RelationalTypeMapping _timestampMapping;
private readonly RelationalTypeMapping _timestampTzMapping;
private readonly RelationalTypeMapping _intervalMapping;
private readonly RelationalTypeMapping _textMapping;

Expand All @@ -52,6 +60,8 @@ public NpgsqlDateTimeMethodTranslator(
ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
_timestampMapping = typeMappingSource.FindMapping("timestamp without time zone")!;
_timestampTzMapping = typeMappingSource.FindMapping("timestamp with time zone")!;
_intervalMapping = typeMappingSource.FindMapping("interval")!;
_textMapping = typeMappingSource.FindMapping("text")!;
}
Expand All @@ -64,14 +74,27 @@ public NpgsqlDateTimeMethodTranslator(
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (instance is null)
{
return null;
}

if (TranslateDatePart(instance, method, arguments) is { } translated)
{
return translated;
}

if (method.DeclaringType == typeof(TimeOnly))
if (method.DeclaringType == typeof(DateTime))
{
if (method == DateTimeToUniversalTimeMethod)
{
return _sqlExpressionFactory.Convert(instance, method.ReturnType, _timestampTzMapping);
}
if (method == DateTimeToLocalTimeMethod)
{
return _sqlExpressionFactory.Convert(instance, method.ReturnType, _timestampMapping);
}
}
else if (method.DeclaringType == typeof(TimeOnly))
{
if (method == TimeOnlyIsBetweenMethod)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public NpgsqlMemberTranslatorProvider(
AddTranslators(
new IMemberTranslator[] {
new NpgsqlArrayTranslator(sqlExpressionFactory, JsonPocoTranslator, npgsqlOptions.UseRedshift),
new NpgsqlDateTimeMemberTranslator(sqlExpressionFactory),
new NpgsqlDateTimeMemberTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlJsonDomTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlLTreeTranslator(typeMappingSource, sqlExpressionFactory, model),
JsonPocoTranslator,
Expand Down
Loading

0 comments on commit 28a3236

Please sign in to comment.