From 7e3ed52c89e09923bd6e6cd69c8ec49384f8f060 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 17 Nov 2018 16:54:20 +0200 Subject: [PATCH 1/2] Scaffold index operators See #662, #704 --- .../Internal/NpgsqlDatabaseModelFactory.cs | 17 ++++++++++++++ .../NpgsqlDatabaseModelFactoryTest.cs | 23 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs index e3e51e89b..7dcc4d090 100644 --- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs +++ b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs @@ -412,6 +412,14 @@ static void GetIndexes( [NotNull] List constraintIndexes, [NotNull] IDiagnosticsLogger logger) { + // Load the pg_opclass table (https://www.postgresql.org/docs/current/catalog-pg-opclass.html), + // which is referenced by the indices we'll load below + var opClasses = new Dictionary(); + using (var command = new NpgsqlCommand("SELECT oid, opcname, opcdefault FROM pg_opclass", connection)) + using (var reader = command.ExecuteReader()) + foreach (var opClass in reader.Cast()) + opClasses[opClass.GetValueOrDefault("oid")] = (opClass.GetValueOrDefault("opcname"), opClass.GetValueOrDefault("opcdefault")); + var commandText = $@" SELECT idxcls.oid AS idx_oid, @@ -421,6 +429,7 @@ static void GetIndexes( indisunique, indkey, amname, + indclass, CASE WHEN indexprs IS NULL THEN NULL ELSE pg_get_expr(indexprs, cls.oid) @@ -508,6 +517,14 @@ NOT indisprimary AND if (record.GetValueOrDefault("amname") is string indexMethod && indexMethod != "btree") index[NpgsqlAnnotationNames.IndexMethod] = indexMethod; + // Handle index operator classes, which we pre-loaded + var opClassNames = record + .GetValueOrDefault("indclass") + .Select(oid => opClasses.TryGetValue(oid, out var opc) && !opc.IsDefault ? opc.Name : null) + .ToArray(); + if (opClassNames.Any(op => op != null)) + index[NpgsqlAnnotationNames.IndexOperators] = opClassNames; + table.Indexes.Add(index); IndexEnd: ; diff --git a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs index 29cc68f90..0608925f2 100644 --- a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs @@ -1532,6 +1532,29 @@ public void Index_method() @"DROP TABLE ""IndexMethod"""); } + [Fact] + public void Index_operators() + { + Test( + @" +CREATE TABLE ""IndexOperators"" (a text, b text); +CREATE INDEX ix_with ON ""IndexOperators"" (a, b varchar_pattern_ops); +CREATE INDEX ix_without ON ""IndexOperators"" (a, b);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var table = dbModel.Tables.Single(); + + var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); + Assert.Equal(new[] { null, "varchar_pattern_ops" }, indexWith.FindAnnotation(NpgsqlAnnotationNames.IndexOperators).Value); + + var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); + Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexOperators)); + }, + @"DROP TABLE ""IndexOperators"""); + } + [Fact] public void Comments() { From 79f86da7f1ed7f65927c45abcd37d4c7093381b4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 17 Nov 2018 17:27:23 +0200 Subject: [PATCH 2/2] Scaffold included (covered) index columns See #697, #704 --- .../Internal/NpgsqlDatabaseModelFactory.cs | 31 ++++++++++++++++--- .../NpgsqlDatabaseModelFactoryTest.cs | 29 +++++++++++++++++ .../TestUtilities/NpgsqlTestStore.cs | 20 ++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs index 7dcc4d090..1a209ba3d 100644 --- a/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs +++ b/src/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs @@ -427,6 +427,7 @@ static void GetIndexes( cls.relname AS cls_relname, idxcls.relname AS idx_relname, indisunique, + {(connection.PostgreSqlVersion >= new Version(11, 0) ? "indnkeyatts" : "indnatts AS indnkeyatts")}, indkey, amname, indclass, @@ -478,7 +479,10 @@ NOT indisprimary AND IsUnique = record.GetValueOrDefault("indisunique") }; + var numKeyColumns = record.GetValueOrDefault("indnkeyatts"); var columnIndices = record.GetValueOrDefault("indkey"); + var tableColumns = (List)table.Columns; + if (columnIndices.Any(i => i == 0)) { // Expression index, not supported @@ -493,12 +497,11 @@ NOT indisprimary AND */ } - var columns = (List)table.Columns; - foreach (var i in columnIndices) + // Key columns come before non-key (included) columns, process them first + foreach (var i in columnIndices.Take(numKeyColumns)) { - if (columns[i - 1] is DatabaseColumn indexColumn) - index.Columns.Add(indexColumn); - + if (tableColumns[i - 1] is DatabaseColumn indexKeyColumn) + index.Columns.Add(indexKeyColumn); else { logger.UnsupportedColumnIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); @@ -506,6 +509,24 @@ NOT indisprimary AND } } + // Now go over non-key (included columns) if any are present + if (columnIndices.Length > numKeyColumns) + { + var nonKeyColumns = new List(); + foreach (var i in columnIndices.Skip(numKeyColumns)) + { + if (tableColumns[i - 1] is DatabaseColumn indexKeyColumn) + nonKeyColumns.Add(indexKeyColumn.Name); + else + { + logger.UnsupportedColumnIndexSkippedWarning(index.Name, DisplayName(tableSchema, tableName)); + goto IndexEnd; + } + } + + index[NpgsqlAnnotationNames.IndexInclude] = nonKeyColumns.ToArray(); + } + if (record.GetValueOrDefault("pred") is string predicate) index.Filter = predicate; diff --git a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs index 0608925f2..2c9c2e9c3 100644 --- a/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Scaffolding/NpgsqlDatabaseModelFactoryTest.cs @@ -1555,6 +1555,35 @@ public void Index_operators() @"DROP TABLE ""IndexOperators"""); } + [Fact] + public void Index_covering() + { + if (Fixture.TestStore.GetPostgresVersion() < new Version(11, 0)) + return; + + Test( + @" +CREATE TABLE ""IndexCovering"" (a text, b text, c text); +CREATE INDEX ix_with ON ""IndexCovering"" (a) INCLUDE (b, c); +CREATE INDEX ix_without ON ""IndexCovering"" (a, b, c);", + Enumerable.Empty(), + Enumerable.Empty(), + dbModel => + { + var table = dbModel.Tables.Single(); + + var indexWith = table.Indexes.Single(i => i.Name == "ix_with"); + Assert.Equal("a", indexWith.Columns.Single().Name); + Assert.Equal(new[] { "b", "c" }, indexWith.FindAnnotation(NpgsqlAnnotationNames.IndexInclude).Value); + + var indexWithout = table.Indexes.Single(i => i.Name == "ix_without"); + Assert.Equal(new[] { "a", "b", "c" }, indexWithout.Columns.Select(i => i.Name).ToArray()); + Assert.Null(indexWithout.FindAnnotation(NpgsqlAnnotationNames.IndexInclude)); + + }, + @"DROP TABLE ""IndexCovering"""); + } + [Fact] public void Comments() { diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs index 3c28c9b5a..9b6cf6db7 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlTestStore.cs @@ -416,6 +416,26 @@ private static DbCommand CreateCommand( return command; } + public Version GetPostgresVersion() + { + var opened = false; + if (Connection.State == ConnectionState.Closed) + { + Connection.Open(); + opened = true; + } + + try + { + return ((NpgsqlConnection)Connection).PostgreSqlVersion; + } + finally + { + if (opened) + Connection.Close(); + } + } + public override void Dispose() { base.Dispose();