From 47d976e8938d2e440b1f44c186365a3599d1435d Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 16 Nov 2018 09:19:02 +0200 Subject: [PATCH 1/2] Work in progress on composite support --- Directory.Build.props | 2 +- NuGet.config | 1 + .../Internal/NpgsqlAnnotationCodeGenerator.cs | 11 ++ .../NpgsqlModelBuilderExtensions.cs | 94 +++++++++++++- .../Internal/NpgsqlAnnotationNames.cs | 1 + .../Metadata/NpgsqlModelAnnotations.cs | 18 +++ src/EFCore.PG/Metadata/PostgresComposite.cs | 122 ++++++++++++++++++ .../NpgsqlMigrationsAnnotationProvider.cs | 1 + .../NpgsqlMigrationsSqlGenerator.cs | 69 ++++++++++ .../NpgsqlSqlTranslatingExpressionVisitor.cs | 48 ++++++- .../Internal/CompositeMemberExpression.cs | 83 ++++++++++++ .../Sql/Internal/NpgsqlQuerySqlGenerator.cs | 15 +++ .../Mapping/NpgsqlCompositeTypeMapping.cs | 62 +++++++++ .../Storage/Internal/NpgsqlDatabaseCreator.cs | 4 +- .../Internal/NpgsqlTypeMappingSource.cs | 117 ++++++++++++++--- .../NpgsqlMigrationSqlGeneratorTest.cs | 38 ++++++ 16 files changed, 658 insertions(+), 28 deletions(-) create mode 100644 src/EFCore.PG/Metadata/PostgresComposite.cs create mode 100644 src/EFCore.PG/Query/Expressions/Internal/CompositeMemberExpression.cs create mode 100644 src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCompositeTypeMapping.cs diff --git a/Directory.Build.props b/Directory.Build.props index 8fbc8875e..ad147c23f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@  - 4.0.3 + 4.0.4-ci.1404 2.2.0 $(EFCoreVersion) diff --git a/NuGet.config b/NuGet.config index afb10e3c9..22e7b1d32 100644 --- a/NuGet.config +++ b/NuGet.config @@ -6,5 +6,6 @@ + diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index e34256d3b..c5d6f5789 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -88,6 +88,17 @@ public override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnotati enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels); } + if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix)) + { + var compositeTypeDef = new PostgresComposite(model, annotation.Name); + + return compositeTypeDef.Schema == "public" + ? new MethodCallCodeFragment(nameof(NpgsqlModelBuilderExtensions.ForNpgsqlHasComposite), + compositeTypeDef.Name, compositeTypeDef.Fields) + : new MethodCallCodeFragment(nameof(NpgsqlModelBuilderExtensions.ForNpgsqlHasComposite), + compositeTypeDef.Schema, compositeTypeDef.Name, compositeTypeDef.Fields); + } + return null; } diff --git a/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs index 00794a0aa..8f77c79b8 100644 --- a/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs @@ -247,6 +247,93 @@ public static ModelBuilder ForNpgsqlHasEnum( #endregion + #region Composite + + /// + /// Registers a user-defined composite type in the model. + /// + /// The model builder in which to create the composite type. + /// The schema in which to create the composite type. + /// The name of the composite type to create. + /// The composite list of fields, with their names and PostgreSQL types. + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/rowtypes.html + /// + /// builder + [NotNull] + public static ModelBuilder ForNpgsqlHasComposite( + [NotNull] this ModelBuilder modelBuilder, + [CanBeNull] string schema, + [NotNull] string name, + [NotNull] params (string Name, string StoreType)[] fields) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotEmpty(name, nameof(name)); + Check.NotNull(fields, nameof(fields)); + + modelBuilder.Model.Npgsql().GetOrAddPostgresComposite(schema, name, fields); + return modelBuilder; + } + + /// + /// Registers a user-defined composite type in the model. + /// + /// The model builder in which to create the composite type. + /// The name of the composite type to create. + /// The composite list of fields, with their names and PostgreSQL types. + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/rowtypes.html + /// + /// builder + [NotNull] + public static ModelBuilder ForNpgsqlHasComposite( + [NotNull] this ModelBuilder modelBuilder, + [NotNull] string name, + [NotNull] params (string Name, string StoreType)[] fields) + => modelBuilder.ForNpgsqlHasComposite(null, name, fields); + + /* + /// + /// Registers a user-defined composite type in the model. + /// + /// The model builder in which to create the composite type. + /// The schema in which to create the composite type. + /// The name of the composite type to create. + /// + /// The translator for name and label inference. + /// Defaults to . + /// + /// + /// The updated . + /// + /// + /// See: https://www.postgresql.org/docs/current/rowtypes.html + /// + /// builder + [NotNull] + public static ModelBuilder ForNpgsqlHasComposite( + [NotNull] this ModelBuilder modelBuilder, + [CanBeNull] string schema = null, + [CanBeNull] string name = null, + [CanBeNull] INpgsqlNameTranslator nameTranslator = null) + { + if (nameTranslator == null) + nameTranslator = NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; + + return modelBuilder.ForNpgsqlHasComposite( + schema, + name ?? GetTypePgName(nameTranslator), + GetMemberPgNames(nameTranslator)); + }*/ + + #endregion + #region Templates public static ModelBuilder HasDatabaseTemplate( @@ -347,10 +434,9 @@ public static ModelBuilder ForNpgsqlUseTablespace( // See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeMapping/TypeMapperBase.cs#L132-L138 [NotNull] - static string GetTypePgName([NotNull] INpgsqlNameTranslator nameTranslator) where TEnum : struct, Enum - => typeof(TEnum).GetCustomAttribute()?.PgName ?? - nameTranslator.TranslateTypeName(typeof(TEnum).Name); - + static string GetTypePgName([NotNull] INpgsqlNameTranslator nameTranslator) + => typeof(T).GetCustomAttribute()?.PgName ?? + nameTranslator.TranslateTypeName(typeof(T).Name); // See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeHandlers/EnumHandler.cs#L118-L129 [NotNull] [ItemNotNull] diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs index 7586db565..c215ba97b 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs @@ -14,6 +14,7 @@ public static class NpgsqlAnnotationNames public const string IndexInclude = Prefix + "IndexInclude"; public const string PostgresExtensionPrefix = Prefix + "PostgresExtension:"; public const string EnumPrefix = Prefix + "Enum:"; + public const string CompositePrefix = Prefix + "Composite:"; public const string RangePrefix = Prefix + "Range:"; public const string DatabaseTemplate = Prefix + "DatabaseTemplate"; public const string Tablespace = Prefix + "Tablespace"; diff --git a/src/EFCore.PG/Metadata/NpgsqlModelAnnotations.cs b/src/EFCore.PG/Metadata/NpgsqlModelAnnotations.cs index d6ff7a89b..208b83e64 100644 --- a/src/EFCore.PG/Metadata/NpgsqlModelAnnotations.cs +++ b/src/EFCore.PG/Metadata/NpgsqlModelAnnotations.cs @@ -90,6 +90,24 @@ public virtual IReadOnlyList PostgresEnums #endregion Enum types + #region Composite types + + public virtual PostgresComposite GetOrAddPostgresComposite( + [CanBeNull] string schema, + [NotNull] string name, + [NotNull] params (string Name, string StoreType)[] fields) + => PostgresComposite.GetOrAddPostgresComposite((IMutableModel)Model, schema, name, fields); + + public virtual PostgresComposite GetOrAddPostgresComposite( + [NotNull] string name, + [NotNull] params (string Name, string StoreType)[] fields) + => GetOrAddPostgresComposite(null, name, fields); + + public virtual IReadOnlyList PostgresComposites + => PostgresComposite.GetPostgresComposites(Model).ToList(); + + #endregion Composite types + #region Range types public virtual PostgresRange GetOrAddPostgresRange( diff --git a/src/EFCore.PG/Metadata/PostgresComposite.cs b/src/EFCore.PG/Metadata/PostgresComposite.cs new file mode 100644 index 000000000..1490fc666 --- /dev/null +++ b/src/EFCore.PG/Metadata/PostgresComposite.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata +{ + public class PostgresComposite + { + readonly IAnnotatable _annotatable; + readonly string _annotationName; + + internal PostgresComposite(IAnnotatable annotatable, string annotationName) + { + _annotatable = annotatable; + _annotationName = annotationName; + } + + public static PostgresComposite GetOrAddPostgresComposite( + [NotNull] IMutableAnnotatable annotatable, + [CanBeNull] string schema, + [NotNull] string name, + [NotNull] params (string Name, string StoreType)[] fields) + { + if (FindPostgresComposite(annotatable, schema, name) is PostgresComposite CompositeType) + return CompositeType; + + // Adding a new composite definition. + // Each composite annotation has an ordering number in the annotation value: composite CREATE TYPE + // migrations need to be generated in a specific order, since they may depend on each other (composite + // types nested within other composite types). + // Find the next free ordering number + var ordering = GetPostgresComposites(annotatable).Any() + ? GetPostgresComposites(annotatable).Select(a => a.Ordering).Max() + 1 + : 0; + + CompositeType = new PostgresComposite(annotatable, BuildAnnotationName(schema, name)); + CompositeType.SetData(ordering, fields); + return CompositeType; + } + + public static PostgresComposite GetOrAddPostgresComposite( + [NotNull] IMutableAnnotatable annotatable, + [NotNull] string name, + [NotNull] params (string Name, string StoreType)[] fields) + => GetOrAddPostgresComposite(annotatable, null, name, fields); + + public static PostgresComposite FindPostgresComposite( + [NotNull] IAnnotatable annotatable, + [CanBeNull] string schema, + [NotNull] string name) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NotEmpty(name, nameof(name)); + + var annotationName = BuildAnnotationName(schema, name); + + return annotatable[annotationName] == null ? null : new PostgresComposite(annotatable, annotationName); + } + + static string BuildAnnotationName(string schema, string name) + => NpgsqlAnnotationNames.CompositePrefix + (schema == null ? name : schema + '.' + name); + + public static IEnumerable GetPostgresComposites([NotNull] IAnnotatable annotatable) + { + Check.NotNull(annotatable, nameof(annotatable)); + + return annotatable.GetAnnotations() + .Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.CompositePrefix, StringComparison.Ordinal)) + .Select(a => new PostgresComposite(annotatable, a.Name)); + } + + public Annotatable Annotatable => (Annotatable)_annotatable; + + public string Schema => GetData().Schema; + + public string Name => GetData().Name; + + public int Ordering => GetData().Ordering; + + public (string Name, string StoreType)[] Fields => GetData().Fields; + + (string Schema, string Name, int Ordering, (string Name, string StoreType)[] Fields) GetData() + { + return !(Annotatable[_annotationName] is string annotationValue) + ? (null, null, 0, null) + : Deserialize(_annotationName, annotationValue); + } + + void SetData(int ordering, (string Name, string StoreType)[] fields) + => Annotatable[_annotationName] = ordering + ";" + string.Join(";", fields.Select(f => $"{f.Name},{f.StoreType}")); + + static (string schema, string name, int ordering, (string Name, string StoreType)[]) Deserialize( + [NotNull] string annotationName, + [NotNull] string annotationValue) + { + Check.NotEmpty(annotationValue, nameof(annotationValue)); + + if (!int.TryParse(annotationValue.Split(';')[0], out var ordering)) + throw new ArgumentException("Cannot parse composite ordering from annotation: " + annotationName); + + var fields = annotationValue.Split(';') + .Skip(1) + .Select(s => (s.Split(',')[0], s.Split(',')[1])).ToArray(); + + var schemaAndName = annotationName.Substring(NpgsqlAnnotationNames.CompositePrefix.Length).Split('.'); + switch (schemaAndName.Length) + { + case 1: + return (null, schemaAndName[0], ordering, fields); + case 2: + return (schemaAndName[0], schemaAndName[1], ordering, fields); + default: + throw new ArgumentException("Cannot parse composite name from annotation: " + annotationName); + } + } + } +} diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs index 3998f0613..851ec5698 100644 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs @@ -57,6 +57,7 @@ public override IEnumerable For(IModel model) => model.GetAnnotations().Where(a => a.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) || a.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) || + a.Name.StartsWith(NpgsqlAnnotationNames.CompositePrefix, StringComparison.Ordinal) || a.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal)); } } diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index efef990d2..2139dedee 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -659,6 +659,7 @@ protected override void Generate( Check.NotNull(builder, nameof(builder)); GenerateEnumStatements(operation, model, builder); + GenerateCompositeStatements(operation, model, builder); GenerateRangeStatements(operation, model, builder); foreach (var extension in PostgresExtension.GetPostgresExtensions(operation)) @@ -766,6 +767,74 @@ protected virtual void GenerateDropEnum( #endregion Enum management + #region Composite management + + protected virtual void GenerateCompositeStatements( + [NotNull] AlterDatabaseOperation operation, + [NotNull] IModel model, + [NotNull] MigrationCommandListBuilder builder) + { + foreach (var compositeTypeToCreate in PostgresComposite.GetPostgresComposites(operation) + .Where(nc => PostgresComposite.GetPostgresComposites(operation.OldDatabase).All(oc => oc.Name != nc.Name)) + .OrderBy(nc => nc.Ordering)) + { + GenerateCreateComposite(compositeTypeToCreate, model, builder); + } + + foreach (var compositeTypeToDrop in PostgresComposite.GetPostgresComposites(operation.OldDatabase) + .Where(oc => PostgresComposite.GetPostgresComposites(operation).All(nc => nc.Name != oc.Name)) + .OrderByDescending(nc => nc.Ordering)) + { + GenerateDropComposite(compositeTypeToDrop, operation.OldDatabase, builder); + } + + // TODO: Composite field alteration is actually supported by PostgreSQL + // TODO: Also composite rename support + if (PostgresComposite.GetPostgresComposites(operation).FirstOrDefault(nc => + PostgresComposite.GetPostgresComposites(operation.OldDatabase).Any(oc => oc.Name == nc.Name) + ) is PostgresComposite compositeTypeToAlter) + { + throw new NotSupportedException($"Altering composite type ${compositeTypeToAlter} isn't supported (for now)."); + } + } + + protected virtual void GenerateCreateComposite( + [NotNull] PostgresComposite compositeType, + [NotNull] IModel model, + [NotNull] MigrationCommandListBuilder builder) + { + var schema = GetSchemaOrDefault(compositeType.Schema, model); + + // 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 enums, so we always ensure schema on enum creation. + if (schema != null) + Generate(new EnsureSchemaOperation { Name = schema }, model, builder); + + builder + .Append("CREATE TYPE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(compositeType.Name, schema)) + .Append(" AS (") + // TODO: Schema support for the field type + .Append(string.Join(", ", compositeType.Fields.Select( + f => $"{Dependencies.SqlGenerationHelper.DelimitIdentifier(f.Name)} {Dependencies.SqlGenerationHelper.DelimitIdentifier(f.StoreType)}"))) + .AppendLine(");"); + } + + protected virtual void GenerateDropComposite( + [NotNull] PostgresComposite compositeType, + [CanBeNull] IAnnotatable oldDatabase, + [NotNull] MigrationCommandListBuilder builder) + { + var schema = GetSchemaOrDefault(compositeType.Schema, oldDatabase); + + builder + .Append("DROP TYPE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(compositeType.Name, schema)) + .AppendLine(";"); + } + + #endregion Composite management + #region Range management protected virtual void GenerateRangeStatements( diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index fe8d4530b..8c9dca03c 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -1,13 +1,23 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Extensions.Internal; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Expressions; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; using Remotion.Linq.Clauses.ResultOperators; @@ -54,6 +64,11 @@ public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVis /// [NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor; + /// + /// The type mapping source + /// + [NotNull] readonly NpgsqlTypeMappingSource _typeMappingSource; + /// public NpgsqlSqlTranslatingExpressionVisitor( [NotNull] SqlTranslatingExpressionVisitorDependencies dependencies, @@ -62,7 +77,10 @@ public NpgsqlSqlTranslatingExpressionVisitor( [CanBeNull] Expression topLevelPredicate = null, bool inProjection = false) : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) - => _queryModelVisitor = queryModelVisitor; + { + _typeMappingSource = (NpgsqlTypeMappingSource)dependencies.TypeMappingSource; + _queryModelVisitor = queryModelVisitor; + } /// protected override Expression VisitSubQuery(SubQueryExpression expression) @@ -200,5 +218,31 @@ whereClause.Predicate is MethodCallExpression methocCall return null; } } + + protected override Expression VisitMember(MemberExpression memberExpression) + { + Check.NotNull(memberExpression, nameof(memberExpression)); + + var expr = base.VisitMember(memberExpression); + if (expr != null) + return expr; + + // ReSharper disable HeuristicUnreachableCode + var properties = MemberAccessBindingExpressionVisitor.GetPropertyPath( + memberExpression.Expression, _queryModelVisitor.QueryCompilationContext, out _); + if (properties.Count == 0) + return null; + var lastPropertyType = properties[properties.Count - 1].ClrType; + if (_typeMappingSource.FindMapping(lastPropertyType) is NpgsqlCompositeTypeMapping compositeMapping) + { + return new CompositeMemberExpression( + Visit(memberExpression.Expression), + compositeMapping.NameTranslator.TranslateMemberName(memberExpression.Member.Name), + memberExpression.Type); + } + // ReSharper restore HeuristicUnreachableCode + + return null; + } } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/CompositeMemberExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/CompositeMemberExpression.cs new file mode 100644 index 000000000..607ac58fb --- /dev/null +++ b/src/EFCore.PG/Query/Expressions/Internal/CompositeMemberExpression.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +// ReSharper disable ArgumentsStyleLiteral +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal +{ + /// + /// Represents a member access on a mapped PostgreSQL composite type. + /// + [DebuggerDisplay("{" + nameof(ToString) + "()}")] + public class CompositeMemberExpression : Expression, IEquatable + { + /// + /// Gets the containing object of the composite member. + /// + [NotNull] + public virtual Expression Expression { get; } + + /// + /// Gets the composite member to be accessed. + /// + [NotNull] + public virtual string Member { get; } + + /// + public override ExpressionType NodeType => ExpressionType.Extension; + + /// + public override Type Type { get; } + + /// + /// Initializes a new instance of the class. + /// + public CompositeMemberExpression( + [NotNull] Expression expression, + [NotNull] string member, + [NotNull] Type returnType) + { + Expression = expression; + Member = member; + Type = returnType; + } + + /// + protected override Expression Accept(ExpressionVisitor visitor) + => visitor is NpgsqlQuerySqlGenerator npgsqlGenerator + ? npgsqlGenerator.VisitCompositeMember(this) + : base.Accept(visitor); + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var e = visitor.Visit(Expression) ?? Expression; + return e != Expression ? new CompositeMemberExpression(e, Member, Type) : this; + } + + /// + public override bool Equals(object obj) => obj is CompositeMemberExpression e && Equals(e); + + /// + public bool Equals(CompositeMemberExpression other) + => other != null && Member == other.Member && Expression.Equals(other.Expression); + + /// + public override int GetHashCode() + { + unchecked + { + return (Expression.GetHashCode() * 397) ^ Member.GetHashCode(); + } + } + + /// + public override string ToString() => Expression + "." + Member; + } +} diff --git a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs index 1da319ef9..91cc0c443 100644 --- a/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs @@ -3,10 +3,13 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Expressions; +using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; using Microsoft.EntityFrameworkCore.Query.Sql; using Microsoft.EntityFrameworkCore.Storage; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; using Remotion.Linq.Clauses; @@ -479,6 +482,18 @@ public virtual Expression VisitPgFunction([NotNull] PgFunctionExpression express return expression; } + public virtual Expression VisitCompositeMember([NotNull] CompositeMemberExpression e) + { + // Surround the expression with parentheses to prevent PostgreSQL from interpreting it as a table. + // See https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-ACCESSING + Sql.Append('('); + Visit(e.Expression); + Sql + .Append(").") + .Append(SqlGenerator.DelimitIdentifier(e.Member)); + return e; + } + #endregion } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCompositeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCompositeTypeMapping.cs new file mode 100644 index 000000000..0790979af --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlCompositeTypeMapping.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping +{ + public class NpgsqlCompositeTypeMapping : RelationalTypeMapping + { + [NotNull] static readonly NpgsqlSqlGenerationHelper SqlGenerationHelper = + new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()); + + [CanBeNull] readonly string _storeTypeSchema; + + [NotNull] public INpgsqlNameTranslator NameTranslator { get; } + + [NotNull] + public override string StoreType => SqlGenerationHelper.DelimitIdentifier(base.StoreType, _storeTypeSchema); + + public NpgsqlCompositeTypeMapping( + [NotNull] string storeType, + [CanBeNull] string storeTypeSchema, + [NotNull] Type clrType, + [CanBeNull] INpgsqlNameTranslator nameTranslator = null) + : base(storeType, clrType) + { + if (nameTranslator == null) + nameTranslator = NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; + + NameTranslator = nameTranslator; + _storeTypeSchema = storeTypeSchema; + //_members = CreateValueMapping(enumType, nameTranslator); + } + + protected NpgsqlCompositeTypeMapping( + RelationalTypeMappingParameters parameters, + [CanBeNull] string storeTypeSchema, + [NotNull] INpgsqlNameTranslator nameTranslator) + : base(parameters) + { + NameTranslator = nameTranslator; + _storeTypeSchema = storeTypeSchema; + //_members = CreateValueMapping(parameters.CoreParameters.ClrType, nameTranslator); + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new NpgsqlCompositeTypeMapping(parameters, _storeTypeSchema, NameTranslator); + + // TODO: Requires PostgreSQL composite fields definition from the ADO layer, which we don't have. + // This is currently problematic since the EF Core mapping is constructed based on the ADO global mapping, + // but composite fields are only loaded once a specific connection is made to a database. + // protected override string GenerateNonNullSqlLiteral(object value) => $"'{_members[value]}'::{StoreType}"; + + // TODO: Look for a constructor on ClrType that has parameters matching in name and type all the + // type's members? Or should we make use of the PostgreSQL composite fields definition from + // the ADO layer, once we get it here somehow (necessary anyway for SQL generation)? + // public override Expression GenerateCodeLiteral(object value) {} + } +} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs b/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs index a412afd83..b02317dde 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlDatabaseCreator.cs @@ -292,11 +292,13 @@ public override void CreateTables() var operations = Dependencies.ModelDiffer.GetDifferences(null, Dependencies.Model); var commands = Dependencies.MigrationsSqlGenerator.Generate(operations, Dependencies.Model); - // If a PostgreSQL extension, enum or range was added, we want Npgsql to reload all types at the ADO.NET level. + // If a PostgreSQL extension, enum, composite or range was added, + // we want Npgsql to reload all types at the ADO.NET level. var reloadTypes = operations.Any(o => o is AlterDatabaseOperation && ( PostgresExtension.GetPostgresExtensions(o).Any() || PostgresEnum.GetPostgresEnums(o).Any() || + PostgresComposite.GetPostgresComposites(o).Any() || PostgresRange.GetPostgresRanges(o).Any() ) ); diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 7275a697a..b4d942db2 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -15,6 +15,8 @@ using Npgsql.TypeHandlers; using NpgsqlTypes; +using AdoTypeMapping = Npgsql.TypeMapping.NpgsqlTypeMapping; + namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal { public class NpgsqlTypeMappingSource : RelationalTypeMappingSource @@ -238,57 +240,72 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); - LoadUserDefinedTypeMappings(); + //LoadUserDefinedTypeMappings(); _userRangeDefinitions = npgsqlOptions?.UserRangeDefinitions ?? new UserRangeDefinition[0]; } + /* /// - /// To be used in case user-defined mappings are added late, after this TypeMappingSource has already been initialized. - /// This is basically only for test usage. + /// Loads global enum and composite mappings from the ADO level, and creates mappings for them at the EF Core level. /// + /// + /// This method gets called from tests, allowing enum/composite tests to be added after the TypeMappingSource + /// is initialized. + /// public void LoadUserDefinedTypeMappings() - { - SetupEnumMappings(); - } - - /// - /// Gets all global enum mappings from the ADO.NET layer and creates mappings for them - /// - void SetupEnumMappings() { foreach (var adoMapping in NpgsqlConnection.GlobalTypeMapper.Mappings.Where(m => m.TypeHandlerFactory is IEnumTypeHandlerFactory)) { + var enumHandlerFactory = adoMapping.TypeHandlerFactory as IEnumTypeHandlerFactory; + + // TODO: Expose MappedCompositeTypeHandlerFactory in Npgsql (but consider an interface instead) + if (enumHandlerFactory == null && + adoMapping.TypeHandlerFactory.GetType().Name != "MappedCompositeTypeHandlerFactory") + { + return; + } + var storeType = adoMapping.PgTypeName; var clrType = adoMapping.ClrTypes.SingleOrDefault(); if (clrType == null) { - // TODO: Log skipping the enum + // TODO: Log skipping the enum/composite continue; } - var nameTranslator = ((IEnumTypeHandlerFactory)adoMapping.TypeHandlerFactory).NameTranslator; - // TODO: update with schema per https://github.com/npgsql/npgsql/issues/2121 var components = storeType.Split('.'); var schema = components.Length > 1 ? components.First() : null; var name = components.Length > 1 ? string.Join(null, components.Skip(1)) : storeType; - var mapping = new NpgsqlEnumTypeMapping(name, schema, clrType, nameTranslator); + RelationalTypeMapping mapping; + if (enumHandlerFactory != null) + { + var nameTranslator = ((IEnumTypeHandlerFactory)adoMapping.TypeHandlerFactory).NameTranslator; + mapping = new NpgsqlEnumTypeMapping(name, schema, clrType, nameTranslator); + } + else + { + // TODO: NameTranslator + mapping = new NpgsqlEnumTypeMapping(name, schema, clrType); + } + ClrTypeMappings[clrType] = mapping; - StoreTypeMappings[mapping.StoreType] = new RelationalTypeMapping[] { mapping }; + StoreTypeMappings[mapping.StoreType] = new[] { mapping }; } - } + }*/ protected override RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) => // First, try any plugins, allowing them to override built-in mappings (e.g. NodaTime) base.FindMapping(mappingInfo) ?? // Then, any mappings that have already been set up FindExistingMapping(mappingInfo) ?? - // Try any array mappings which have not yet been set up + // Then try to see if any array, user-defined range, enum or composite mapping match. FindArrayMapping(mappingInfo) ?? - // Try any user-defined range mappings which have not yet been set up - FindUserRangeMapping(mappingInfo); + FindUserRangeMapping(mappingInfo) ?? + FindEnumMapping(mappingInfo) ?? + FindCompositeMapping(mappingInfo); protected virtual RelationalTypeMapping FindExistingMapping(in RelationalTypeMappingInfo mappingInfo) { @@ -491,5 +508,65 @@ protected virtual RelationalTypeMapping FindUserRangeMapping(in RelationalTypeMa return rangeMapping; } + + protected virtual RelationalTypeMapping FindEnumMapping(in RelationalTypeMappingInfo mappingInfo) + { + if (mappingInfo.ClrType?.IsEnum != true) + return null; // ClrType was given and is not an enum + + return null; // NotImplemented + } + + protected virtual RelationalTypeMapping FindCompositeMapping(in RelationalTypeMappingInfo mappingInfo) + { + var compositeMappings = NpgsqlConnection.GlobalTypeMapper.Mappings + .Where(m => m.TypeHandlerFactory is IMappedCompositeTypeHandlerFactory); + + AdoTypeMapping adoMapping = null; + var storeType = mappingInfo.StoreTypeName; + var clrType = mappingInfo.ClrType; + + if (storeType != null) + { + adoMapping = compositeMappings.SingleOrDefault(cm => cm.PgTypeName == storeType); + + if (adoMapping == null) + return null; + + if (clrType == null) + { + // No ClrType was provided in the incoming MappingInfo (we're scaffolding). Pick the first + // (and only) ClrType specified in the ADO mapping + clrType = adoMapping.ClrTypes[0]; + } + if (clrType != null && adoMapping.ClrTypes.Contains(clrType)) + { + // If the incoming MappingInfo also contains a ClrType (in addition to the StoreType), make sure + // it's included in the ADO mapping + return null; + } + } + else if (clrType != null) + adoMapping = compositeMappings.SingleOrDefault(cm => cm.ClrTypes.Contains(clrType)); + + if (adoMapping == null) + return null; + + // TODO: we need to also get the fields... + + // Finally, construct a composite mapping and add it to our lookup dictionaries - next time it will be found as + // an existing mapping + + Debug.Assert(clrType != null); + var components = adoMapping.PgTypeName.Split('.'); + var schema = components.Length > 1 ? components.First() : null; + var name = components.Length > 1 ? string.Join(null, components.Skip(1)) : adoMapping.PgTypeName; + var translator = ((IMappedCompositeTypeHandlerFactory)adoMapping.TypeHandlerFactory).NameTranslator; + var compositeMapping = new NpgsqlCompositeTypeMapping(name, schema, clrType, translator); + StoreTypeMappings[adoMapping.PgTypeName] = new RelationalTypeMapping[] { compositeMapping }; + ClrTypeMappings[clrType] = compositeMapping; + + return compositeMapping; + } } } diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs index 3809caead..68e06e438 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs @@ -753,6 +753,44 @@ public void DropPostgresEnum() #endregion Enums + #region Composites + + [Fact] + public void CreatePostgresComposite() + { + var op = new AlterDatabaseOperation(); + PostgresComposite.GetOrAddPostgresComposite(op, "public", "my_composite", new[] { ("f1", "int"), ("f2", "text") }); + Generate(op); + + Assert.Equal(@"CREATE TYPE public.my_composite AS (f1 int, f2 text);" + EOL, Sql); + } + + [Fact] + public void CreatePostgresCompositeWithSchema() + { + var op = new AlterDatabaseOperation(); + PostgresComposite.GetOrAddPostgresComposite(op, "some_schema", "my_composite", new[] { ("f1", "int"), ("f2", "text") }); + Generate(op); + + Assert.Equal( + @"CREATE SCHEMA IF NOT EXISTS some_schema;" + EOL + + @"GO" + EOL + EOL + + @"CREATE TYPE some_schema.my_composite AS (f1 int, f2 text);" + EOL, + Sql); + } + + [Fact] + public void DropPostgresComposite() + { + var op = new AlterDatabaseOperation(); + PostgresComposite.GetOrAddPostgresComposite(op.OldDatabase, "public", "my_composite", new[] { ("f1", "int"), ("f2", "text") }); + Generate(op); + + Assert.Equal(@"DROP TYPE public.my_composite;" + EOL, Sql); + } + + #endregion Composites + #region PostgreSQL Storage Parameters [Fact] From ec1e3db23e707a829e8db363acacc198d36a4c56 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 18 Nov 2018 13:36:50 +0200 Subject: [PATCH 2/2] Address partial review by @austindrenski --- .../Design/Internal/NpgsqlAnnotationCodeGenerator.cs | 2 +- src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs | 5 +++-- src/EFCore.PG/Metadata/PostgresComposite.cs | 7 +++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index c5d6f5789..16e2d00f9 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -88,7 +88,7 @@ public override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnotati enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels); } - if (annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix)) + if (annotation.Name.StartsWith(NpgsqlAnnotationNames.CompositePrefix)) { var compositeTypeDef = new PostgresComposite(model, annotation.Name); diff --git a/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs index 8f77c79b8..ea90d663e 100644 --- a/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs @@ -262,7 +262,7 @@ public static ModelBuilder ForNpgsqlHasEnum( /// /// See: https://www.postgresql.org/docs/current/rowtypes.html /// - /// builder + /// [NotNull] public static ModelBuilder ForNpgsqlHasComposite( [NotNull] this ModelBuilder modelBuilder, @@ -271,6 +271,7 @@ public static ModelBuilder ForNpgsqlHasComposite( [NotNull] params (string Name, string StoreType)[] fields) { Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NullButNotEmpty(schema, nameof(schema)); Check.NotEmpty(name, nameof(name)); Check.NotNull(fields, nameof(fields)); @@ -290,7 +291,7 @@ [NotNull] params (string Name, string StoreType)[] fields) /// /// See: https://www.postgresql.org/docs/current/rowtypes.html /// - /// builder + /// [NotNull] public static ModelBuilder ForNpgsqlHasComposite( [NotNull] this ModelBuilder modelBuilder, diff --git a/src/EFCore.PG/Metadata/PostgresComposite.cs b/src/EFCore.PG/Metadata/PostgresComposite.cs index 1490fc666..bad1323e8 100644 --- a/src/EFCore.PG/Metadata/PostgresComposite.cs +++ b/src/EFCore.PG/Metadata/PostgresComposite.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; @@ -26,6 +27,11 @@ public static PostgresComposite GetOrAddPostgresComposite( [NotNull] string name, [NotNull] params (string Name, string StoreType)[] fields) { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotNull(name, nameof(name)); + Check.NullButNotEmpty(fields, nameof(fields)); + if (FindPostgresComposite(annotatable, schema, name) is PostgresComposite CompositeType) return CompositeType; @@ -55,6 +61,7 @@ public static PostgresComposite FindPostgresComposite( [NotNull] string name) { Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); Check.NotEmpty(name, nameof(name)); var annotationName = BuildAnnotationName(schema, name);