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);
}