diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index 5ece6b2852..1b360ee834 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -163,8 +163,12 @@ public override MethodCallCodeFragment GenerateFluentApi(IIndex index, IAnnotati return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasMethod), annotation.Value); if (annotation.Name == NpgsqlAnnotationNames.IndexOperators) return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasOperators), annotation.Value); - if (annotation.Name == NpgsqlAnnotationNames.IndexSortOptions) - return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasSortOptions), annotation.Value); + if (annotation.Name == NpgsqlAnnotationNames.IndexCollation) + return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasCollation), annotation.Value); + if (annotation.Name == NpgsqlAnnotationNames.IndexDescendingOrder) + return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasDescendingOrder), annotation.Value); + if (annotation.Name == NpgsqlAnnotationNames.IndexNullsFirst) + return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasNullsFirst), annotation.Value); if (annotation.Name == NpgsqlAnnotationNames.IndexInclude) return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlInclude), annotation.Value); diff --git a/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs index ddbf3d7c67..9627b057d9 100644 --- a/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Linq.Expressions; using JetBrains.Annotations; @@ -57,22 +57,62 @@ public static IndexBuilder ForNpgsqlHasOperators( } /// - /// The PostgreSQL index sort options to be used. + /// The PostgreSQL index collation to be used. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-collations.html + /// + /// The builder for the index being configured. + /// The sort options to use for each column. + /// A builder to further configure the index. + public static IndexBuilder ForNpgsqlHasCollation( + [NotNull] this IndexBuilder indexBuilder, + [CanBeNull, ItemNotNull] params string[] values) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(values, nameof(values)); + + indexBuilder.Metadata.Npgsql().Collation = values; + + return indexBuilder; + } + + /// + /// The PostgreSQL index order to be used. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-ordering.html + /// + /// The builder for the index being configured. + /// The sort order to use for each column. + /// A builder to further configure the index. + public static IndexBuilder ForNpgsqlHasDescendingOrder( + [NotNull] this IndexBuilder indexBuilder, + [CanBeNull, ItemNotNull] params bool?[] values) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + + indexBuilder.Metadata.Npgsql().DescendingOrder = values; + + return indexBuilder; + } + + /// + /// The PostgreSQL index NULL ordering to be used. /// /// /// https://www.postgresql.org/docs/current/static/indexes-ordering.html /// /// The builder for the index being configured. - /// The sort options to use for each column. + /// The sort order to use for each column. /// A builder to further configure the index. - public static IndexBuilder ForNpgsqlHasSortOptions( + public static IndexBuilder ForNpgsqlHasNullsFirst( [NotNull] this IndexBuilder indexBuilder, - [CanBeNull, ItemNotNull] params string[] sortOptions) + [CanBeNull, ItemNotNull] params bool?[] values) { Check.NotNull(indexBuilder, nameof(indexBuilder)); - Check.NullButNotEmpty(sortOptions, nameof(sortOptions)); - indexBuilder.Metadata.Npgsql().SortOptions = sortOptions; + indexBuilder.Metadata.Npgsql().NullsFirst = values; return indexBuilder; } diff --git a/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs b/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs index 5bffca5741..7d681593ac 100644 --- a/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs +++ b/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs @@ -6,7 +6,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata public interface INpgsqlIndexAnnotations : IRelationalIndexAnnotations { /// - /// The PostgreSQL index method to be used. Null selects the default (currently btree). + /// The method to be used, or null if it hasn't been specified. /// /// /// http://www.postgresql.org/docs/current/static/sql-createindex.html @@ -14,7 +14,7 @@ public interface INpgsqlIndexAnnotations : IRelationalIndexAnnotations string Method { get; } /// - /// The PostgreSQL index operators to be used, or null if they have not been specified. + /// The column operators to be used, or null if they have not been specified. /// /// /// https://www.postgresql.org/docs/current/static/indexes-opclass.html @@ -22,15 +22,31 @@ public interface INpgsqlIndexAnnotations : IRelationalIndexAnnotations IReadOnlyList Operators { get; } /// - /// The PostgreSQL index sort options to be used, or null if they have not been specified. + /// The column collations to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-collations.html + /// + IReadOnlyList Collation { get; } + + /// + /// The column sort orders to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-ordering.html + /// + IReadOnlyList DescendingOrder { get; } + + /// + /// The column NULL sort orders to be used, or null if they have not been specified. /// /// /// https://www.postgresql.org/docs/current/static/indexes-ordering.html /// - IReadOnlyList SortOptions { get; } + IReadOnlyList NullsFirst { get; } /// - /// The PostgreSQL included property names, or null if they have not been specified. + /// The included property names, or null if they have not been specified. /// /// /// https://www.postgresql.org/docs/current/static/sql-createindex.html diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs index cc6138b4aa..6e16a006c7 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs @@ -11,7 +11,9 @@ public static class NpgsqlAnnotationNames public const string HiLoSequenceSchema = Prefix + "HiLoSequenceSchema"; public const string IndexMethod = Prefix + "IndexMethod"; public const string IndexOperators = Prefix + "IndexOperators"; - public const string IndexSortOptions = Prefix + "IndexSortOptions"; + public const string IndexCollation = Prefix + "IndexCollation"; + public const string IndexDescendingOrder = Prefix + "IndexDescendingOrder"; + public const string IndexNullsFirst = Prefix + "IndexNullsFirst"; public const string IndexInclude = Prefix + "IndexInclude"; public const string PostgresExtensionPrefix = Prefix + "PostgresExtension:"; public const string EnumPrefix = Prefix + "Enum:"; diff --git a/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs b/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs index 17978e7724..0a2e99898c 100644 --- a/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs +++ b/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs @@ -18,7 +18,7 @@ protected NpgsqlIndexAnnotations([NotNull] RelationalAnnotations annotations) } /// - /// The PostgreSQL index method to be used. Null selects the default (currently btree). + /// The method to be used, or null if it hasn't been specified. /// /// /// http://www.postgresql.org/docs/current/static/sql-createindex.html @@ -30,7 +30,7 @@ public string Method } /// - /// The PostgreSQL index operators to be used, or null if they have not been specified. + /// The column operators to be used, or null if they have not been specified. /// /// /// https://www.postgresql.org/docs/current/static/indexes-opclass.html @@ -44,21 +44,49 @@ public string[] Operators IReadOnlyList INpgsqlIndexAnnotations.Operators => Operators; /// - /// The PostgreSQL index sort options to be used, or null if they have not been specified. + /// The column collations to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-collations.html + /// + public string[] Collation + { + get => (string[])Annotations.Metadata[NpgsqlAnnotationNames.IndexCollation]; + set => SetCollation(value); + } + + IReadOnlyList INpgsqlIndexAnnotations.Collation => Collation; + + /// + /// The column sort orders to be used, or null if they have not been specified. /// /// /// https://www.postgresql.org/docs/current/static/indexes-ordering.html /// - public string[] SortOptions + public bool?[] DescendingOrder { - get => (string[])Annotations.Metadata[NpgsqlAnnotationNames.IndexSortOptions]; - set => SetSortOptions(value); + get => (bool?[])Annotations.Metadata[NpgsqlAnnotationNames.IndexDescendingOrder]; + set => SetDescendingOrder(value); } - IReadOnlyList INpgsqlIndexAnnotations.SortOptions => SortOptions; + IReadOnlyList INpgsqlIndexAnnotations.DescendingOrder => DescendingOrder; /// - /// The PostgreSQL included property names, or null if they have not been specified. + /// The column NULL sort orders to be used, or null if they have not been specified. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-ordering.html + /// + public bool?[] NullsFirst + { + get => (bool?[])Annotations.Metadata[NpgsqlAnnotationNames.IndexNullsFirst]; + set => SetNullsFirst(value); + } + + IReadOnlyList INpgsqlIndexAnnotations.NullsFirst => NullsFirst; + + /// + /// The included property names, or null if they have not been specified. /// /// /// https://www.postgresql.org/docs/current/static/sql-createindex.html @@ -77,8 +105,14 @@ protected virtual bool SetMethod(string value) protected virtual bool SetOperators(string[] value) => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexOperators, value); - protected virtual bool SetSortOptions(string[] value) - => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexSortOptions, value); + protected virtual bool SetCollation(string[] value) + => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexCollation, value); + + protected virtual bool SetDescendingOrder(bool?[] value) + => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexDescendingOrder, value); + + protected virtual bool SetNullsFirst(bool?[] value) + => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexNullsFirst, value); protected virtual bool SetIncludeProperties(string[] value) => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexInclude, value); diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs index 3a51adb651..96bc98d997 100644 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs @@ -45,8 +45,12 @@ public override IEnumerable For(IIndex index) yield return new Annotation(NpgsqlAnnotationNames.IndexMethod, index.Npgsql().Method); if (index.Npgsql().Operators != null) yield return new Annotation(NpgsqlAnnotationNames.IndexOperators, index.Npgsql().Operators); - if (index.Npgsql().SortOptions != null) - yield return new Annotation(NpgsqlAnnotationNames.IndexSortOptions, index.Npgsql().SortOptions); + if (index.Npgsql().Collation != null) + yield return new Annotation(NpgsqlAnnotationNames.IndexCollation, index.Npgsql().Collation); + if (index.Npgsql().DescendingOrder != null) + yield return new Annotation(NpgsqlAnnotationNames.IndexDescendingOrder, index.Npgsql().DescendingOrder); + if (index.Npgsql().NullsFirst != null) + yield return new Annotation(NpgsqlAnnotationNames.IndexNullsFirst, index.Npgsql().NullsFirst); if (index.Npgsql().IncludeProperties != null) yield return new Annotation(NpgsqlAnnotationNames.IndexInclude, index.Npgsql().IncludeProperties); } diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index be7c1c3a90..6c62bb38ae 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -600,12 +600,11 @@ protected override void Generate( .Append(method); } - var operators = operation[NpgsqlAnnotationNames.IndexOperators] as string[]; - var sortOptions = operation[NpgsqlAnnotationNames.IndexSortOptions] as string[]; + var indexColumns = GetIndexColumns(operation); builder .Append(" (") - .Append(IndexColumnList(operation.Columns, operators, sortOptions)) + .Append(IndexColumnList(indexColumns)) .Append(")"); IndexOptions(operation, model, builder); @@ -1202,40 +1201,42 @@ static string GenerateStorageParameterValue(object value) bool VersionAtLeast(int major, int minor) => _postgresVersion is null || new Version(major, minor) <= _postgresVersion; - string IndexColumnList(string[] columns, string[] operators, string[] sortOptions) + string IndexColumnList(IndexColumn[] columns) { var isFirst = true; var builder = new StringBuilder(); - for (int i = 0; i < columns.Length; i++) + for (var i = 0; i < columns.Length; i++) { if (!isFirst) builder.Append(", "); - builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columns[i])); + var column = columns[i]; + + builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(column.Name)); - if (operators != null && i < operators.Length) + if (!string.IsNullOrEmpty(column.Operator)) { - var @operator = operators[i]; + var delimitedOperator = TryParseSchema(column.Operator, out var name, out var schema) + ? Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema) + : Dependencies.SqlGenerationHelper.DelimitIdentifier(column.Operator); - if (!string.IsNullOrEmpty(@operator)) - { - var delimitedOperator = TryParseSchema(@operator, out var name, out var schema) - ? Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema) - : Dependencies.SqlGenerationHelper.DelimitIdentifier(@operator); + builder.Append(" ").Append(delimitedOperator); + } - builder.Append(" ").Append(delimitedOperator); - } + if (!string.IsNullOrEmpty(column.Collation)) + { + builder.Append(" COLLATE ").Append(column.Collation); } - if (sortOptions != null && i < sortOptions.Length) + if (column.DescendingOrder.HasValue) { - var sortOption = sortOptions[i]; + builder.Append(" ").Append(column.DescendingOrder.Value ? "DESC" : "ASC"); + } - if (!string.IsNullOrEmpty(sortOption)) - { - builder.Append(" ").Append(sortOption); - } + if (column.NullsFirst.HasValue) + { + builder.Append(" NULLS ").Append(column.NullsFirst.Value ? "FIRST" : "LAST"); } isFirst = false; @@ -1260,6 +1261,51 @@ static bool TryParseSchema(string identifier, out string name, out string schema return false; } + static IndexColumn[] GetIndexColumns(CreateIndexOperation operation) + { + var operators = operation[NpgsqlAnnotationNames.IndexOperators] as string[]; + var collations = operation[NpgsqlAnnotationNames.IndexCollation] as string[]; + var descendingOrders = operation[NpgsqlAnnotationNames.IndexDescendingOrder] as bool?[]; + var nullsFirsts = operation[NpgsqlAnnotationNames.IndexNullsFirst] as bool?[]; + + var columns = new IndexColumn[operation.Columns.Length]; + + for (var i = 0; i < columns.Length; i++) + { + var name = operation.Columns[i]; + var @operator = i < operators?.Length ? operators[i] : null; + var collation = i < collations?.Length ? collations[i] : null; + var descendingOrder = i < descendingOrders?.Length ? descendingOrders[i] : null; + var nullsFirst = i < nullsFirsts?.Length ? nullsFirsts[i] : null; + + columns[i] = new IndexColumn(name, @operator, collation, descendingOrder, nullsFirst); + } + + return columns; + } + + struct IndexColumn + { + public IndexColumn(string name, string @operator, string collation, bool? descendingOrder, bool? nullsFirst) + { + Name = name; + Operator = @operator; + Collation = collation; + DescendingOrder = descendingOrder; + NullsFirst = nullsFirst; + } + + public string Name { get; } + + public string Operator { get; } + + public string Collation { get; } + + public bool? DescendingOrder { get; } + + public bool? NullsFirst { get; } + } + #endregion } } diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs index 747816d4f3..437b016ae9 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; @@ -692,7 +692,7 @@ public void CreateIndexOperation_schema_qualified_operations() } [Fact] - public void CreateIndexOperation_sort_options() + public void CreateIndexOperation_collation() { Generate(new CreateIndexOperation { @@ -700,16 +700,16 @@ public void CreateIndexOperation_sort_options() Table = "People", Schema = "dbo", Columns = new[] { "FirstName", "LastName" }, - [NpgsqlAnnotationNames.IndexSortOptions] = new[] { "ASC NULLS FIRST", "DESC" } + [NpgsqlAnnotationNames.IndexCollation] = new[] { null, "de_DE" } }); Assert.Equal( - "CREATE INDEX \"IX_People_Name\" ON dbo.\"People\" (\"FirstName\" ASC NULLS FIRST, \"LastName\" DESC);" + EOL, + "CREATE INDEX \"IX_People_Name\" ON dbo.\"People\" (\"FirstName\", \"LastName\" COLLATE de_DE);" + EOL, Sql); } [Fact] - public void CreateIndexOperation_operations_and_sort_options() + public void CreateIndexOperation_sort_order() { Generate(new CreateIndexOperation { @@ -717,12 +717,28 @@ public void CreateIndexOperation_operations_and_sort_options() Table = "People", Schema = "dbo", Columns = new[] { "FirstName", "LastName" }, - [NpgsqlAnnotationNames.IndexOperators] = new[] { "text_pattern_ops" }, - [NpgsqlAnnotationNames.IndexSortOptions] = new[] { "ASC NULLS FIRST", "DESC" } + [NpgsqlAnnotationNames.IndexDescendingOrder] = new bool?[] { true, false } }); Assert.Equal( - "CREATE INDEX \"IX_People_Name\" ON dbo.\"People\" (\"FirstName\" text_pattern_ops ASC NULLS FIRST, \"LastName\" DESC);" + EOL, + "CREATE INDEX \"IX_People_Name\" ON dbo.\"People\" (\"FirstName\" DESC, \"LastName\" ASC);" + EOL, + Sql); + } + + [Fact] + public void CreateIndexOperation_nulls_first() + { + Generate(new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo", + Columns = new[] { "FirstName", "LastName" }, + [NpgsqlAnnotationNames.IndexNullsFirst] = new bool?[] { true, false } + }); + + Assert.Equal( + "CREATE INDEX \"IX_People_Name\" ON dbo.\"People\" (\"FirstName\" NULLS FIRST, \"LastName\" NULLS LAST);" + EOL, Sql); }