diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index 03d7d436c..6efacd7a2 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -135,6 +135,8 @@ public override MethodCallCodeFragment GenerateFluentApi(IIndex index, IAnnotati { if (annotation.Name == NpgsqlAnnotationNames.IndexMethod) return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasMethod), annotation.Value); + if (annotation.Name == NpgsqlAnnotationNames.IndexOperators) + return new MethodCallCodeFragment(nameof(NpgsqlIndexBuilderExtensions.ForNpgsqlHasOperators), annotation.Value); return null; } diff --git a/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs index aea94006a..3286fcec3 100644 --- a/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlIndexBuilderExtensions.cs @@ -28,5 +28,24 @@ public static IndexBuilder ForNpgsqlHasMethod([NotNull] this IndexBuilder indexB return indexBuilder; } + + /// + /// The PostgreSQL index operators to be used. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + /// The builder for the index being configured. + /// The operators to use for each column. + /// A builder to further configure the index. + public static IndexBuilder ForNpgsqlHasOperators([NotNull] this IndexBuilder indexBuilder, [CanBeNull] params string[] operators) + { + Check.NotNull(indexBuilder, nameof(indexBuilder)); + Check.NullButNotEmpty(operators, nameof(operators)); + + indexBuilder.Metadata.Npgsql().Operators = operators; + + return indexBuilder; + } } } diff --git a/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs b/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs index d1919798e..2d8011af8 100644 --- a/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs +++ b/src/EFCore.PG/Metadata/INpgsqlIndexAnnotations.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Metadata; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Metadata; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata { @@ -11,5 +12,13 @@ public interface INpgsqlIndexAnnotations : IRelationalIndexAnnotations /// http://www.postgresql.org/docs/current/static/sql-createindex.html /// string Method { get; } + + /// + /// The PostgreSQL index operators to be used. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + IReadOnlyList Operators { get; } } } diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs index b360d0f02..ef2ad788f 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationNames.cs @@ -10,6 +10,7 @@ public static class NpgsqlAnnotationNames public const string HiLoSequenceName = Prefix + "HiLoSequenceName"; public const string HiLoSequenceSchema = Prefix + "HiLoSequenceSchema"; public const string IndexMethod = Prefix + "IndexMethod"; + public const string IndexOperators = Prefix + "IndexOperators"; public const string PostgresExtensionPrefix = Prefix + "PostgresExtension:"; public const string EnumPrefix = Prefix + "Enum:"; public const string RangePrefix = Prefix + "Range:"; diff --git a/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs b/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs index cab3e7973..30a52c53e 100644 --- a/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs +++ b/src/EFCore.PG/Metadata/NpgsqlIndexAnnotations.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System.Collections.Generic; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; @@ -28,8 +29,24 @@ public string Method set => SetMethod(value); } + /// + /// The PostgreSQL index operators to be used. + /// + /// + /// https://www.postgresql.org/docs/current/static/indexes-opclass.html + /// + public string[] Operators + { + get => (string[]) Annotations.Metadata[NpgsqlAnnotationNames.IndexOperators]; + set => SetOperators(value); + } + + IReadOnlyList INpgsqlIndexAnnotations.Operators => Operators; + protected virtual bool SetMethod(string value) => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexMethod, value); + protected virtual bool SetOperators(string[] value) + => Annotations.SetAnnotation(NpgsqlAnnotationNames.IndexOperators, value); } } diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs index 082a488af..7727f5d50 100644 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs @@ -47,6 +47,8 @@ public override IEnumerable For(IIndex index) { if (index.Npgsql().Method != null) yield return new Annotation(NpgsqlAnnotationNames.IndexMethod, index.Npgsql().Method); + if (index.Npgsql().Operators != null) + yield return new Annotation(NpgsqlAnnotationNames.IndexOperators, index.Npgsql().Operators); } public override IEnumerable For(IModel model) diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index 1f1bd68ea..60ece9273 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -536,6 +536,7 @@ protected override void Generate( Check.NotNull(builder, nameof(builder)); var method = (string)operation[NpgsqlAnnotationNames.IndexMethod]; + var operators = (string[])operation[NpgsqlAnnotationNames.IndexOperators]; builder.Append("CREATE "); @@ -559,7 +560,7 @@ protected override void Generate( builder .Append(" (") - .Append(ColumnList(operation.Columns)) + .Append(IndexColumnList(operation.Columns, operators)) .Append(")"); if (!string.IsNullOrEmpty(operation.Filter)) @@ -1133,6 +1134,47 @@ static string GetSchemaOrDefault([CanBeNull] string schema, [CanBeNull] IAnnotat bool VersionAtLeast(int major, int minor) => _postgresVersion is null || new Version(major, minor) <= _postgresVersion; + string IndexColumnList(string[] columns, string[] operators) + { + if (operators == null || operators.Length == 0) + return ColumnList(columns); + + return string.Join(", ", columns.Select((v, i) => + { + var identifier = Dependencies.SqlGenerationHelper.DelimitIdentifier(v); + + if (i >= operators.Length) + return identifier; + + var @operator = operators[i]; + + if (string.IsNullOrEmpty(@operator)) + return identifier; + + var delimitedOperator = TryParseSchema(@operator, out var name, out var schema) + ? Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema) + : Dependencies.SqlGenerationHelper.DelimitIdentifier(@operator); + + return string.Concat(identifier, " ", delimitedOperator); + })); + } + + static bool TryParseSchema(string identifier, out string name, out string schema) + { + var index = identifier.IndexOf('.'); + + if (index >= 0) + { + schema = identifier.Substring(0, index); + name = identifier.Substring(index + 1); + return true; + } + + schema = default; + name = default; + return false; + } + #endregion } } diff --git a/src/EFCore.PG/Utilities/Check.cs b/src/EFCore.PG/Utilities/Check.cs index 79f59f7ef..948bf20b4 100644 --- a/src/EFCore.PG/Utilities/Check.cs +++ b/src/EFCore.PG/Utilities/Check.cs @@ -92,6 +92,19 @@ public static string NullButNotEmpty([CanBeNull] string value, [InvokerParameter return value; } + public static IReadOnlyCollection NullButNotEmpty([CanBeNull] IReadOnlyCollection value, [InvokerParameterName] [NotNull] string parameterName) + { + if (!ReferenceEquals(value, null) + && (value.Count == 0)) + { + NotEmpty(parameterName, nameof(parameterName)); + + throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName)); + } + + return value; + } + public static IReadOnlyList HasNoNulls(IReadOnlyList value, [InvokerParameterName] [NotNull] string parameterName) where T : class { diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs index b6b35333a..0126f053e 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlMigrationSqlGeneratorTest.cs @@ -616,6 +616,40 @@ public void CreateIndexOperation_method() Sql); } + [Fact] + public void CreateIndexOperation_operations() + { + Generate(new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo", + Columns = new[] { "FirstName", "LastName" }, + [NpgsqlAnnotationNames.IndexOperators] = new[] { "text_pattern_ops" } + }); + + Assert.Equal( + "CREATE INDEX \"IX_People_Name\" ON dbo.\"People\" (\"FirstName\" text_pattern_ops, \"LastName\");" + EOL, + Sql); + } + + [Fact] + public void CreateIndexOperation_schema_qualified_operations() + { + Generate(new CreateIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo", + Columns = new[] { "FirstName" }, + [NpgsqlAnnotationNames.IndexOperators] = new[] { "myschema.TextOperation" } + }); + + Assert.Equal( + "CREATE INDEX \"IX_People_Name\" ON dbo.\"People\" (\"FirstName\" myschema.\"TextOperation\");" + EOL, + Sql); + } + [Fact] public void RenameIndexOperation() {