-
Notifications
You must be signed in to change notification settings - Fork 256
Work in progress on composite support #708
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| 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; | ||
|
|
||
| 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) | ||
| { | ||
| 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) | ||
|
roji marked this conversation as resolved.
|
||
| 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.NullButNotEmpty(schema, nameof(schema)); | ||
| Check.NotEmpty(name, nameof(name)); | ||
|
roji marked this conversation as resolved.
|
||
|
|
||
| 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<PostgresComposite> 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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we create user-defined ranges first, so that they exist for use by composites? |
||
|
|
||
| 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(");"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a blocking issue for schema support here, or was it just simpler to skip it for now? |
||
| } | ||
|
|
||
| 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( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.