Skip to content
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

Update range mapping for schema-qualified and quoted ranges #626

Merged
merged 5 commits into from
Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ public class NpgsqlNodaTimeTypeMappingSourcePlugin : IRelationalTypeMappingSourc
/// <summary>
/// Constructs an instance of the <see cref="NpgsqlNodaTimeTypeMappingSourcePlugin"/> class.
/// </summary>
public NpgsqlNodaTimeTypeMappingSourcePlugin()
public NpgsqlNodaTimeTypeMappingSourcePlugin([NotNull] ISqlGenerationHelper sqlGenerationHelper)
{
_timestampLocalDateTimeRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange<LocalDateTime>), _timestampLocalDateTime);
_timestampInstantRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange<Instant>), _timestampInstant);
_timestamptzInstantRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange<Instant>), _timestamptzInstant);
_timestamptzZonedDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange<ZonedDateTime>), _timestamptzZonedDateTime);
_timestamptzOffsetDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange<OffsetDateTime>), _timestamptzOffsetDateTime);
_dateRange = new NpgsqlRangeTypeMapping("daterange", typeof(NpgsqlRange<LocalDate>), _date);
_timestampLocalDateTimeRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange<LocalDateTime>), _timestampLocalDateTime, sqlGenerationHelper);
_timestampInstantRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange<Instant>), _timestampInstant, 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)
{
Expand Down
29 changes: 26 additions & 3 deletions src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal
public class NpgsqlAnnotationCodeGenerator : AnnotationCodeGenerator
{
public NpgsqlAnnotationCodeGenerator([NotNull] AnnotationCodeGeneratorDependencies dependencies)
: base(dependencies)
{
}
: base(dependencies) {}

public override bool IsHandledByConvention(IModel model, IAnnotation annotation)
{
Expand Down Expand Up @@ -88,6 +86,31 @@ public override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnotati
enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels);
}

if (annotation.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix))
{
var rangeTypeDef = new PostgresRange(model, annotation.Name);

if (rangeTypeDef.CanonicalFunction == null &&
rangeTypeDef.SubtypeOpClass == null &&
rangeTypeDef.Collation == null &&
rangeTypeDef.SubtypeDiff == null)
{
return new MethodCallCodeFragment(nameof(NpgsqlModelBuilderExtensions.ForNpgsqlHasRange),
rangeTypeDef.Schema == "public" ? null : rangeTypeDef.Schema,
rangeTypeDef.Name,
rangeTypeDef.Subtype);
}

return new MethodCallCodeFragment(nameof(NpgsqlModelBuilderExtensions.ForNpgsqlHasRange),
austindrenski marked this conversation as resolved.
Show resolved Hide resolved
rangeTypeDef.Schema == "public" ? null : rangeTypeDef.Schema,
rangeTypeDef.Name,
rangeTypeDef.Subtype,
rangeTypeDef.CanonicalFunction,
rangeTypeDef.SubtypeOpClass,
rangeTypeDef.Collation,
rangeTypeDef.SubtypeDiff);
}

return null;
}

Expand Down
58 changes: 43 additions & 15 deletions src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,24 +94,28 @@ public override bool ApplyServices(IServiceCollection services)
/// Returns a copy of the current instance configured with the specified range mapping.
/// </summary>
[NotNull]
public virtual NpgsqlOptionsExtension WithUserRangeDefinition<TSubtype>(string rangeName, string subtypeName)
{
var clone = (NpgsqlOptionsExtension)Clone();

clone._userRangeDefinitions.Add(new UserRangeDefinition(rangeName, typeof(TSubtype), subtypeName));

return clone;
}
public virtual NpgsqlOptionsExtension WithUserRangeDefinition<TSubtype>(
[NotNull] string rangeName,
[CanBeNull] string schemaName = null,
[CanBeNull] string subtypeName = null)
=> WithUserRangeDefinition(rangeName, schemaName, typeof(TSubtype), subtypeName);

/// <summary>
/// Returns a copy of the current instance configured with the specified range mapping.
/// </summary>
[NotNull]
public virtual NpgsqlOptionsExtension WithUserRangeDefinition(string rangeName, Type subtypeClrType, string subtypeName)
public virtual NpgsqlOptionsExtension WithUserRangeDefinition(
[NotNull] string rangeName,
[CanBeNull] string schemaName,
[NotNull] Type subtypeClrType,
[CanBeNull] string subtypeName)
{
Check.NotEmpty(rangeName, nameof(rangeName));
Check.NotNull(subtypeClrType, nameof(subtypeClrType));

var clone = (NpgsqlOptionsExtension)Clone();

clone._userRangeDefinitions.Add(new UserRangeDefinition(rangeName, subtypeClrType, subtypeName));
clone._userRangeDefinitions.Add(new UserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName));

return clone;
}
Expand Down Expand Up @@ -196,29 +200,53 @@ public virtual NpgsqlOptionsExtension WithRemoteCertificateValidationCallback([C

public class UserRangeDefinition
{
/// <summary>The name of the PostgreSQL range type to be mapped.</summary>
/// <summary>
/// The name of the PostgreSQL range type to be mapped.
/// </summary>
[NotNull]
public string RangeName { get; }

/// <summary>
/// The PostgreSQL schema in which the range is defined. If null, the default schema is used
/// (which is public unless changed on the model).
/// </summary>
[CanBeNull]
public string SchemaName { get; }

/// <summary>
/// The CLR type of the range's subtype (or element).
/// The actual mapped type will be an <see cref="NpgsqlRange{T}"/> over this type.
/// </summary>
[NotNull]
public Type SubtypeClrType { get; }

/// <summary>
/// Optionally, the name of the range's PostgreSQL subtype (or element).
/// This is usually not needed - the subtype will be inferred based on <see cref="SubtypeClrType"/>.
/// </summary>
[CanBeNull]
public string SubtypeName { get; }

public UserRangeDefinition(string rangeName, Type subtypeClrType, string subtypeName)
public UserRangeDefinition(
[NotNull] string rangeName,
[CanBeNull] string schemaName,
[NotNull] Type subtypeClrType,
[CanBeNull] string subtypeName)
{
RangeName = rangeName;
SubtypeClrType = subtypeClrType;
RangeName = Check.NotEmpty(rangeName, nameof(rangeName));
SchemaName = schemaName;
SubtypeClrType = Check.NotNull(subtypeClrType, nameof(subtypeClrType));
SubtypeName = subtypeName;
}

public void Deconstruct(out string rangeName, out Type subtypeClrType, out string subtypeName)
public void Deconstruct(
[NotNull] out string rangeName,
[CanBeNull] out string schemaName,
[NotNull] out Type subtypeClrType,
[CanBeNull] out string subtypeName)
{
rangeName = RangeName;
schemaName = SchemaName;
subtypeClrType = SubtypeClrType;
subtypeName = SubtypeName;
}
Expand Down
10 changes: 6 additions & 4 deletions src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public virtual void SetPostgresVersion([CanBeNull] Version postgresVersion)
/// The actual mapped type will be an <see cref="NpgsqlRange{T}"/> over this type.
/// </typeparam>
/// <param name="rangeName">The name of the PostgreSQL range type to be mapped.</param>
/// <param name="schemaName">The name of the PostgreSQL schema in which the range is defined.</param>
/// <param name="subtypeName">
/// Optionally, the name of the range's PostgreSQL subtype (or element).
/// This is usually not needed - the subtype will be inferred based on <typeparamref name="TSubtype"/>.
Expand All @@ -53,13 +54,14 @@ public virtual void SetPostgresVersion([CanBeNull] Version postgresVersion)
/// To map a range of PostgreSQL real, use the following:
/// <code>NpgsqlTypeMappingSource.MapRange{float}("floatrange");</code>
/// </example>
public virtual void MapRange<TSubtype>([NotNull] string rangeName, string subtypeName = null)
=> WithOption(e => e.WithUserRangeDefinition(rangeName, typeof(TSubtype), subtypeName));
public virtual void MapRange<TSubtype>([NotNull] string rangeName, string schemaName = null, string subtypeName = null)
=> MapRange(rangeName, typeof(TSubtype), schemaName, subtypeName);

/// <summary>
/// Maps a user-defined PostgreSQL range type for use.
/// </summary>
/// <param name="rangeName">The name of the PostgreSQL range type to be mapped.</param>
/// <param name="schemaName">The name of the PostgreSQL schema in which the range is defined.</param>
/// <param name="subtypeClrType">
/// The CLR type of the range's subtype (or element).
/// The actual mapped type will be an <see cref="NpgsqlRange{T}"/> over this type.
Expand All @@ -72,8 +74,8 @@ public virtual void MapRange<TSubtype>([NotNull] string rangeName, string subtyp
/// To map a range of PostgreSQL real, use the following:
/// <code>NpgsqlTypeMappingSource.MapRange("floatrange", typeof(float));</code>
/// </example>
public virtual void MapRange([NotNull] string rangeName, [NotNull] Type subtypeClrType, string subtypeName = null)
=> WithOption(e => e.WithUserRangeDefinition(rangeName, subtypeClrType, subtypeName));
public virtual void MapRange([NotNull] string rangeName, [NotNull] Type subtypeClrType, string schemaName = null, string subtypeName = null)
=> WithOption(e => e.WithUserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName));

/// <summary>
/// Appends NULLS FIRST to all ORDER BY clauses. This is important for the tests which were written
Expand Down
22 changes: 13 additions & 9 deletions src/EFCore.PG/Metadata/PostgresRange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static PostgresRange GetOrAddPostgresRange(
if (FindPostgresRange(annotatable, schema, name) is PostgresRange rangeType)
return rangeType;

rangeType = new PostgresRange(annotatable, BuildAnnotationName(schema, name));
rangeType = new PostgresRange(annotatable, BuildAnnotationName(annotatable, schema, name));
rangeType.SetData(subtype, canonicalFunction, subtypeOpClass, collation, subtypeDiff);
return rangeType;
}
Expand All @@ -47,27 +47,31 @@ public static PostgresRange FindPostgresRange(
Check.NotNull(annotatable, nameof(annotatable));
Check.NotEmpty(name, nameof(name));

var annotationName = BuildAnnotationName(schema, name);
var annotationName = BuildAnnotationName(annotatable, schema, name);

return annotatable[annotationName] == null ? null : new PostgresRange(annotatable, annotationName);
}

[NotNull]
static string BuildAnnotationName(string schema, string name)
=> NpgsqlAnnotationNames.RangePrefix + (schema == null ? name : schema + '.' + name);
static string BuildAnnotationName([NotNull] IAnnotatable annotatable, [CanBeNull] string schema, [NotNull] string name)
=> !string.IsNullOrEmpty(schema)
? $"{NpgsqlAnnotationNames.RangePrefix}{schema}.{name}"
: annotatable[RelationalAnnotationNames.DefaultSchema] is string defaultSchema && !string.IsNullOrEmpty(defaultSchema)
? $"{NpgsqlAnnotationNames.RangePrefix}{defaultSchema}.{name}"
: $"{NpgsqlAnnotationNames.RangePrefix}{name}";

[NotNull]
public static IEnumerable<PostgresRange> GetPostgresRanges([NotNull] IAnnotatable annotatable)
=> Check.NotNull(annotatable, nameof(annotatable))
.GetAnnotations()
.Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal))
.Select(a => new PostgresRange(annotatable, a.Name));
.GetAnnotations()
.Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal))
.Select(a => new PostgresRange(annotatable, a.Name));

[NotNull]
public Annotatable Annotatable => (Annotatable)_annotatable;

[NotNull]
public string Schema => GetData().Schema;
[CanBeNull]
public string Schema => GetData().Schema ?? (string)_annotatable[RelationalAnnotationNames.DefaultSchema];

[NotNull]
public string Name => GetData().Name;
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ protected virtual void GenerateCreateRange(
// Schemas are normally created (or rather ensured) by the model differ, which scans all tables, sequences
// and other database objects. However, it isn't aware of ranges, so we always ensure schema on range creation.
if (rangeType.Schema != null)
Generate(new EnsureSchemaOperation { Name=rangeType.Schema }, model, builder);
Generate(new EnsureSchemaOperation { Name = rangeType.Schema }, model, builder);

builder
.Append("CREATE TYPE ")
Expand Down
34 changes: 28 additions & 6 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,43 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping
{
public class NpgsqlRangeTypeMapping : NpgsqlTypeMapping
{
[NotNull] readonly ISqlGenerationHelper _sqlGenerationHelper;

public RelationalTypeMapping SubtypeMapping { get; }

public NpgsqlRangeTypeMapping(
[NotNull] string storeType,
[NotNull] Type clrType,
[NotNull] RelationalTypeMapping subtypeMapping)
: base(storeType, clrType, GenerateNpgsqlDbType(subtypeMapping))
=> SubtypeMapping = subtypeMapping;
[NotNull] RelationalTypeMapping subtypeMapping,
[NotNull] ISqlGenerationHelper sqlGenerationHelper)
: this(storeType, null, clrType, subtypeMapping, sqlGenerationHelper) {}

protected NpgsqlRangeTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType)
: base(parameters, npgsqlDbType) { }
public NpgsqlRangeTypeMapping(
[NotNull] string storeType,
[CanBeNull] string storeTypeSchema,
austindrenski marked this conversation as resolved.
Show resolved Hide resolved
[NotNull] Type clrType,
[NotNull] RelationalTypeMapping subtypeMapping,
[NotNull] ISqlGenerationHelper sqlGenerationHelper)
: base(sqlGenerationHelper.DelimitIdentifier(storeType, storeTypeSchema), clrType, GenerateNpgsqlDbType(subtypeMapping))
{
SubtypeMapping = subtypeMapping;
_sqlGenerationHelper = sqlGenerationHelper;
}

protected NpgsqlRangeTypeMapping(
RelationalTypeMappingParameters parameters,
NpgsqlDbType npgsqlDbType,
[NotNull] RelationalTypeMapping subtypeMapping,
[NotNull] ISqlGenerationHelper sqlGenerationHelper)
: base(parameters, npgsqlDbType)
{
SubtypeMapping = subtypeMapping;
_sqlGenerationHelper = sqlGenerationHelper;
}

[NotNull]
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlRangeTypeMapping(parameters, NpgsqlDbType);
=> new NpgsqlRangeTypeMapping(parameters, NpgsqlDbType, SubtypeMapping, _sqlGenerationHelper);

protected override string GenerateNonNullSqlLiteral(object value)
{
Expand Down
Loading