using System.Collections.Concurrent;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
// ReSharper disable once CheckNamespace
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
///
/// 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.
///
public class NpgsqlNodaTimeTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin
{
#if DEBUG
internal static bool LegacyTimestampBehavior;
internal static bool DisableDateTimeInfinityConversions;
#else
internal static readonly bool LegacyTimestampBehavior;
internal static readonly bool DisableDateTimeInfinityConversions;
#endif
static NpgsqlNodaTimeTypeMappingSourcePlugin()
{
LegacyTimestampBehavior = AppContext.TryGetSwitch("Npgsql.EnableLegacyTimestampBehavior", out var enabled) && enabled;
DisableDateTimeInfinityConversions = AppContext.TryGetSwitch("Npgsql.DisableDateTimeInfinityConversions", out enabled) && enabled;
}
///
/// 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.
///
public virtual ConcurrentDictionary StoreTypeMappings { get; }
///
/// 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.
///
public virtual ConcurrentDictionary ClrTypeMappings { get; }
#region TypeMapping
private readonly TimestampLocalDateTimeMapping _timestampLocalDateTime = new();
private readonly LegacyTimestampInstantMapping _legacyTimestampInstant = new();
private readonly TimestampTzInstantMapping _timestamptzInstant = new();
private readonly TimestampTzZonedDateTimeMapping _timestamptzZonedDateTime = new();
private readonly TimestampTzOffsetDateTimeMapping _timestamptzOffsetDateTime = new();
private readonly DateMapping _date = new();
private readonly TimeMapping _time = new();
private readonly TimeTzMapping _timetz = new();
private readonly PeriodIntervalMapping _periodInterval = new();
private readonly DurationIntervalMapping _durationInterval = new();
// PostgreSQL has no native type for representing time zones - it just uses the IANA ID as text.
private readonly DateTimeZoneMapping _timeZone = new("text");
// Built-in ranges
private readonly NpgsqlRangeTypeMapping _timestampLocalDateTimeRange;
private readonly NpgsqlRangeTypeMapping _legacyTimestampInstantRange;
private readonly NpgsqlRangeTypeMapping _timestamptzInstantRange;
private readonly NpgsqlRangeTypeMapping _timestamptzZonedDateTimeRange;
private readonly NpgsqlRangeTypeMapping _timestamptzOffsetDateTimeRange;
private readonly NpgsqlRangeTypeMapping _dateRange;
private readonly DateIntervalRangeMapping _dateIntervalRange = new();
private readonly IntervalRangeMapping _intervalRange = new();
// Built-in multiranges
private readonly NpgsqlMultirangeTypeMapping _timestampLocalDateTimeMultirangeArray;
private readonly NpgsqlMultirangeTypeMapping _legacyTimestampInstantMultirangeArray;
private readonly NpgsqlMultirangeTypeMapping _timestamptzInstantMultirangeArray;
private readonly NpgsqlMultirangeTypeMapping _timestamptzZonedDateTimeMultirangeArray;
private readonly NpgsqlMultirangeTypeMapping _timestamptzOffsetDateTimeMultirangeArray;
private readonly NpgsqlMultirangeTypeMapping _dateRangeMultirangeArray;
private readonly DateIntervalMultirangeMapping _dateIntervalMultirangeArray;
private readonly IntervalMultirangeMapping _intervalMultirangeArray;
private readonly NpgsqlMultirangeTypeMapping _timestampLocalDateTimeMultirangeList;
private readonly NpgsqlMultirangeTypeMapping _legacyTimestampInstantMultirangeList;
private readonly NpgsqlMultirangeTypeMapping _timestamptzInstantMultirangeList;
private readonly NpgsqlMultirangeTypeMapping _timestamptzZonedDateTimeMultirangeList;
private readonly NpgsqlMultirangeTypeMapping _timestamptzOffsetDateTimeMultirangeList;
private readonly NpgsqlMultirangeTypeMapping _dateRangeMultirangeList;
private readonly DateIntervalMultirangeMapping _dateIntervalMultirangeList;
private readonly IntervalMultirangeMapping _intervalMultirangeList;
#endregion
///
/// Constructs an instance of the class.
///
public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationHelper)
{
_timestampLocalDateTimeRange
= new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _timestampLocalDateTime, sqlGenerationHelper);
_legacyTimestampInstantRange
= new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _legacyTimestampInstant, sqlGenerationHelper);
_timestamptzInstantRange
= new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzInstant, sqlGenerationHelper);
_timestamptzZonedDateTimeRange
= new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzZonedDateTime, sqlGenerationHelper);
_timestamptzOffsetDateTimeRange
= new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzOffsetDateTime, sqlGenerationHelper);
_dateRange
= new NpgsqlRangeTypeMapping("daterange", typeof(NpgsqlRange), _date, sqlGenerationHelper);
_timestampLocalDateTimeMultirangeArray = new NpgsqlMultirangeTypeMapping(
"tsmultirange", typeof(NpgsqlRange[]), _timestampLocalDateTimeRange, sqlGenerationHelper);
_legacyTimestampInstantMultirangeArray = new NpgsqlMultirangeTypeMapping(
"tsmultirange", typeof(NpgsqlRange[]), _legacyTimestampInstantRange, sqlGenerationHelper);
_timestamptzInstantMultirangeArray = new NpgsqlMultirangeTypeMapping(
"tstzmultirange", typeof(NpgsqlRange[]), _timestamptzInstantRange, sqlGenerationHelper);
_timestamptzZonedDateTimeMultirangeArray = new NpgsqlMultirangeTypeMapping(
"tstzmultirange", typeof(NpgsqlRange[]), _timestamptzZonedDateTimeRange, sqlGenerationHelper);
_timestamptzOffsetDateTimeMultirangeArray = new NpgsqlMultirangeTypeMapping(
"tstzmultirange", typeof(NpgsqlRange[]), _timestamptzOffsetDateTimeRange, sqlGenerationHelper);
_dateRangeMultirangeArray = new NpgsqlMultirangeTypeMapping(
"datemultirange", typeof(NpgsqlRange[]), _dateRange, sqlGenerationHelper);
_dateIntervalMultirangeArray = new DateIntervalMultirangeMapping(typeof(DateInterval[]), _dateIntervalRange);
_intervalMultirangeArray = new IntervalMultirangeMapping(typeof(Interval[]), _intervalRange);
_timestampLocalDateTimeMultirangeList = new NpgsqlMultirangeTypeMapping(
"tsmultirange", typeof(List>), _timestampLocalDateTimeRange, sqlGenerationHelper);
_legacyTimestampInstantMultirangeList = new NpgsqlMultirangeTypeMapping(
"tsmultirange", typeof(List>), _legacyTimestampInstantRange, sqlGenerationHelper);
_timestamptzInstantMultirangeList = new NpgsqlMultirangeTypeMapping(
"tstzmultirange", typeof(List>), _timestamptzInstantRange, sqlGenerationHelper);
_timestamptzZonedDateTimeMultirangeList = new NpgsqlMultirangeTypeMapping(
"tstzmultirange", typeof(List>), _timestamptzZonedDateTimeRange, sqlGenerationHelper);
_timestamptzOffsetDateTimeMultirangeList = new NpgsqlMultirangeTypeMapping(
"tstzmultirange", typeof(List>), _timestamptzOffsetDateTimeRange, sqlGenerationHelper);
_dateRangeMultirangeList = new NpgsqlMultirangeTypeMapping(
"datemultirange", typeof(List>), _dateRange, sqlGenerationHelper);
_dateIntervalMultirangeList = new DateIntervalMultirangeMapping(typeof(List), _dateIntervalRange);
_intervalMultirangeList = new IntervalMultirangeMapping(typeof(List), _intervalRange);
var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{
// We currently allow _legacyTimestampInstant even in non-legacy mode, since when upgrading to 6.0 with existing
// migrations, model snapshots still contain old mappings (Instant mapped to timestamp), and EF Core's model differ
// expects type mappings to be found for these. See https://github.com/dotnet/efcore/issues/26168.
"timestamp without time zone", LegacyTimestampBehavior
? new RelationalTypeMapping[] { _legacyTimestampInstant, _timestampLocalDateTime }
: new RelationalTypeMapping[] { _timestampLocalDateTime, _legacyTimestampInstant }
},
{ "timestamp with time zone", new RelationalTypeMapping[] { _timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime } },
{ "date", new RelationalTypeMapping[] { _date } },
{ "time without time zone", new RelationalTypeMapping[] { _time } },
{ "time with time zone", new RelationalTypeMapping[] { _timetz } },
{ "interval", new RelationalTypeMapping[] { _periodInterval, _durationInterval } },
{ "tsrange", LegacyTimestampBehavior
? new RelationalTypeMapping[] { _legacyTimestampInstantRange, _timestampLocalDateTimeRange }
: new RelationalTypeMapping[] { _timestampLocalDateTimeRange, _legacyTimestampInstantRange }
},
{ "tstzrange", new RelationalTypeMapping[] { _intervalRange, _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange } },
{ "daterange", new RelationalTypeMapping[] { _dateIntervalRange, _dateRange } },
{ "tsmultirange", LegacyTimestampBehavior
? new RelationalTypeMapping[] { _legacyTimestampInstantMultirangeArray, _legacyTimestampInstantMultirangeList, _timestampLocalDateTimeMultirangeArray, _timestampLocalDateTimeMultirangeList }
: new RelationalTypeMapping[] { _timestampLocalDateTimeMultirangeArray, _timestampLocalDateTimeMultirangeList, _legacyTimestampInstantMultirangeArray, _legacyTimestampInstantMultirangeList }
},
{
"tstzmultirange", new RelationalTypeMapping[]
{
_intervalMultirangeArray, _intervalMultirangeList,
_timestamptzInstantMultirangeArray, _timestamptzInstantMultirangeList,
_timestamptzZonedDateTimeMultirangeArray, _timestamptzZonedDateTimeMultirangeList,
_timestamptzOffsetDateTimeMultirangeArray, _timestamptzOffsetDateTimeMultirangeList
}
},
{
"datemultirange", new RelationalTypeMapping[]
{
_dateIntervalMultirangeArray, _dateIntervalMultirangeList,
_dateRangeMultirangeArray, _dateRangeMultirangeList
}
}
};
// 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
{
{ typeof(Instant), LegacyTimestampBehavior ? _legacyTimestampInstant : _timestamptzInstant },
{ typeof(LocalDateTime), _timestampLocalDateTime },
{ typeof(ZonedDateTime), _timestamptzZonedDateTime },
{ typeof(OffsetDateTime), _timestamptzOffsetDateTime },
{ typeof(LocalDate), _date },
{ typeof(LocalTime), _time },
{ typeof(OffsetTime), _timetz },
{ typeof(Period), _periodInterval },
{ typeof(Duration), _durationInterval },
// See DateTimeZone below
{ typeof(NpgsqlRange), LegacyTimestampBehavior ? _legacyTimestampInstantRange : _timestamptzInstantRange },
{ typeof(NpgsqlRange), _timestampLocalDateTimeRange },
{ typeof(NpgsqlRange), _timestamptzZonedDateTimeRange },
{ typeof(NpgsqlRange), _timestamptzOffsetDateTimeRange },
{ typeof(NpgsqlRange), _dateRange },
{ typeof(DateInterval), _dateIntervalRange },
{ typeof(Interval), _intervalRange },
{ typeof(NpgsqlRange[]), LegacyTimestampBehavior ? _legacyTimestampInstantMultirangeArray : _timestamptzInstantMultirangeArray },
{ typeof(NpgsqlRange[]), _timestampLocalDateTimeMultirangeArray },
{ typeof(NpgsqlRange[]), _timestamptzZonedDateTimeMultirangeArray },
{ typeof(NpgsqlRange[]), _timestamptzOffsetDateTimeMultirangeArray },
{ typeof(NpgsqlRange[]), _dateRangeMultirangeArray },
{ typeof(DateInterval[]), _dateIntervalMultirangeArray },
{ typeof(Interval[]), _intervalMultirangeArray },
{ typeof(List>), LegacyTimestampBehavior ? _legacyTimestampInstantMultirangeList : _timestamptzInstantMultirangeList },
{ typeof(List>), _timestampLocalDateTimeMultirangeList },
{ typeof(List>), _timestamptzZonedDateTimeMultirangeList },
{ typeof(List>), _timestamptzOffsetDateTimeMultirangeList },
{ typeof(List>), _dateRangeMultirangeList },
{ typeof(List), _dateIntervalMultirangeList },
{ typeof(List), _intervalMultirangeList }
};
StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase);
ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings);
}
///
/// 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.
///
public virtual RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
=> FindBaseMapping(mappingInfo)?.Clone(mappingInfo)
?? FindArrayMapping(mappingInfo)?.Clone(mappingInfo);
///
/// 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.
///
protected virtual RelationalTypeMapping? FindBaseMapping(in RelationalTypeMappingInfo mappingInfo)
{
var clrType = mappingInfo.ClrType;
var storeTypeName = mappingInfo.StoreTypeName;
var storeTypeNameBase = mappingInfo.StoreTypeNameBase;
if (storeTypeName is not null)
{
if (StoreTypeMappings.TryGetValue(storeTypeName, out var mappings))
{
if (clrType is null)
{
return mappings[0];
}
foreach (var m in mappings)
{
if (m.ClrType == clrType)
{
return m;
}
}
return null;
}
if (StoreTypeMappings.TryGetValue(storeTypeNameBase!, out mappings))
{
if (clrType is null)
{
return mappings[0].Clone(in mappingInfo);
}
foreach (var m in mappings)
{
if (m.ClrType == clrType)
{
return m.Clone(in mappingInfo);
}
}
return null;
}
}
if (clrType is not null)
{
if (ClrTypeMappings.TryGetValue(clrType, out var mapping))
{
return mapping;
}
if (clrType.IsAssignableTo(typeof(DateTimeZone)))
{
return _timeZone;
}
}
return null;
}
///
/// 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.
///
// TODO: This is duplicated from NpgsqlTypeMappingSource
protected virtual RelationalTypeMapping? FindArrayMapping(in RelationalTypeMappingInfo mappingInfo)
{
var clrType = mappingInfo.ClrType;
Type? elementClrType = null;
if (clrType is not null && !clrType.TryGetElementType(out elementClrType))
{
return null; // Not an array/list
}
var storeType = mappingInfo.StoreTypeName;
var storeTypeNameBase = mappingInfo.StoreTypeNameBase;
if (storeType is not null)
{
// PostgreSQL array type names are the element plus []
if (!storeType.EndsWith("[]", StringComparison.Ordinal))
{
return null;
}
var elementStoreType = storeType.Substring(0, storeType.Length - 2);
var elementStoreTypeNameBase = storeTypeNameBase!.Substring(0, storeTypeNameBase.Length - 2);
RelationalTypeMapping? elementMapping;
if (elementClrType is null)
{
elementMapping = FindMapping(new RelationalTypeMappingInfo(
elementStoreType, elementStoreTypeNameBase,
mappingInfo.IsUnicode, mappingInfo.Size, mappingInfo.Precision, mappingInfo.Scale));
}
else
{
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 is not 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 is null or NpgsqlArrayTypeMapping)
{
return null;
}
return new NpgsqlArrayTypeMapping(storeType, clrType ?? elementMapping.ClrType.MakeArrayType(), elementMapping);
}
// TODO: Clean this up, should not be needed
if (clrType is null)
{
return null;
}
if (clrType.IsArray)
{
var elementType = clrType.GetElementType();
Debug.Assert(elementType is not null, "Detected array type but element type is 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 is null || elementMapping.Converter is not null)
{
return null;
}
// Arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL
if (elementMapping is NpgsqlArrayTypeMapping)
{
return null;
}
return new NpgsqlArrayTypeMapping(clrType, elementMapping);
}
if (clrType.IsGenericList())
{
var elementType = clrType.GetGenericArguments()[0];
// If an element isn't supported, neither is its array
var elementMapping = FindMapping(new RelationalTypeMappingInfo(elementType));
if (elementMapping is null)
{
return null;
}
// Arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL
if (elementMapping is NpgsqlArrayTypeMapping)
{
return null;
}
return new NpgsqlArrayTypeMapping(clrType, elementMapping);
}
return null;
}
}