Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup Label="Package Versions">
<NpgsqlVersion>4.0.3</NpgsqlVersion>
<NpgsqlVersion>4.0.4-ci.1404</NpgsqlVersion>
<EFCoreVersion>2.2.0</EFCoreVersion>
<MicrosoftExtensionsVersion>$(EFCoreVersion)</MicrosoftExtensionsVersion>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
<packageSources>
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
<add key="DotnetCore" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
<add key="NpgsqlStable" value="https://www.myget.org/F/npgsql/api/v3/index.json" />
</packageSources>
</configuration>
11 changes: 11 additions & 0 deletions src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ public override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnotati
enumTypeDef.Schema, enumTypeDef.Name, enumTypeDef.Labels);
}

if (annotation.Name.StartsWith(NpgsqlAnnotationNames.CompositePrefix))
{
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;
}

Expand Down
95 changes: 91 additions & 4 deletions src/EFCore.PG/Extensions/NpgsqlModelBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,94 @@ public static ModelBuilder ForNpgsqlHasEnum<TEnum>(

#endregion

#region Composite

/// <summary>
/// Registers a user-defined composite type in the model.
/// </summary>
/// <param name="modelBuilder">The model builder in which to create the composite type.</param>
/// <param name="schema">The schema in which to create the composite type.</param>
/// <param name="name">The name of the composite type to create.</param>
/// <param name="fields">The composite list of fields, with their names and PostgreSQL types.</param>
/// <returns>
/// The updated <see cref="ModelBuilder"/>.
/// </returns>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/rowtypes.html
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/></exception>
[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.NullButNotEmpty(schema, nameof(schema));
Check.NotEmpty(name, nameof(name));
Check.NotNull(fields, nameof(fields));

Comment thread
roji marked this conversation as resolved.
modelBuilder.Model.Npgsql().GetOrAddPostgresComposite(schema, name, fields);
return modelBuilder;
}

/// <summary>
/// Registers a user-defined composite type in the model.
/// </summary>
/// <param name="modelBuilder">The model builder in which to create the composite type.</param>
/// <param name="name">The name of the composite type to create.</param>
/// <param name="fields">The composite list of fields, with their names and PostgreSQL types.</param>
/// <returns>
/// The updated <see cref="ModelBuilder"/>.
/// </returns>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/rowtypes.html
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/></exception>
[NotNull]
public static ModelBuilder ForNpgsqlHasComposite(
[NotNull] this ModelBuilder modelBuilder,
[NotNull] string name,
[NotNull] params (string Name, string StoreType)[] fields)
=> modelBuilder.ForNpgsqlHasComposite(null, name, fields);

/*
/// <summary>
/// Registers a user-defined composite type in the model.
/// </summary>
/// <param name="modelBuilder">The model builder in which to create the composite type.</param>
/// <param name="schema">The schema in which to create the composite type.</param>
/// <param name="name">The name of the composite type to create.</param>
/// <param name="nameTranslator">
/// The translator for name and label inference.
/// Defaults to <see cref="NpgsqlSnakeCaseNameTranslator"/>.</param>
/// <typeparam name="T"></typeparam>
/// <returns>
/// The updated <see cref="ModelBuilder"/>.
/// </returns>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/rowtypes.html
/// </remarks>
/// <exception cref="ArgumentNullException">builder</exception>
[NotNull]
public static ModelBuilder ForNpgsqlHasComposite<T>(
[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<T>(nameTranslator),
GetMemberPgNames<T>(nameTranslator));
}*/

#endregion

#region Templates

public static ModelBuilder HasDatabaseTemplate(
Expand Down Expand Up @@ -347,10 +435,9 @@ public static ModelBuilder ForNpgsqlUseTablespace(

// See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeMapping/TypeMapperBase.cs#L132-L138
[NotNull]
static string GetTypePgName<TEnum>([NotNull] INpgsqlNameTranslator nameTranslator) where TEnum : struct, Enum
=> typeof(TEnum).GetCustomAttribute<PgNameAttribute>()?.PgName ??
nameTranslator.TranslateTypeName(typeof(TEnum).Name);

static string GetTypePgName<T>([NotNull] INpgsqlNameTranslator nameTranslator)
=> typeof(T).GetCustomAttribute<PgNameAttribute>()?.PgName ??
nameTranslator.TranslateTypeName(typeof(T).Name);
// See: https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/TypeHandlers/EnumHandler.cs#L118-L129
[NotNull]
[ItemNotNull]
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
18 changes: 18 additions & 0 deletions src/EFCore.PG/Metadata/NpgsqlModelAnnotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ public virtual IReadOnlyList<PostgresEnum> 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<PostgresComposite> PostgresComposites
=> PostgresComposite.GetPostgresComposites(Model).ToList();

#endregion Composite types

#region Range types

public virtual PostgresRange GetOrAddPostgresRange(
Expand Down
129 changes: 129 additions & 0 deletions src/EFCore.PG/Metadata/PostgresComposite.cs
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)
Comment thread
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));
Comment thread
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
Expand Up @@ -57,6 +57,7 @@ public override IEnumerable<IAnnotation> 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));
}
}
69 changes: 69 additions & 0 deletions src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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))
Expand Down Expand Up @@ -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(");");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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(
Expand Down
Loading