diff --git a/ChangeLog/7.2.0-RC-dev.txt b/ChangeLog/7.2.0-RC-dev.txt
index e69de29bb..eac5c3c13 100644
--- a/ChangeLog/7.2.0-RC-dev.txt
+++ b/ChangeLog/7.2.0-RC-dev.txt
@@ -0,0 +1 @@
+[postgresql] Support for included columns for indexes starting from PostgreSQL 12
\ No newline at end of file
diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Extractor.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Extractor.cs
index 71418c3b3..7e98eaddb 100644
--- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Extractor.cs
+++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Extractor.cs
@@ -2,6 +2,10 @@
// This code is distributed under MIT license terms.
// See the License.txt file in the project root for more information.
+using System;
+using System.Data.Common;
+using System.Linq;
+using System.Text.RegularExpressions;
using Xtensive.Sql.Model;
namespace Xtensive.Sql.Drivers.PostgreSql.v12_0
@@ -13,7 +17,10 @@ protected override void BuildPgCatalogSchema(Schema schema)
{
base.BuildPgCatalogSchema(schema);
var defaultValuesTable = schema.Tables["pg_attrdef"];
- defaultValuesTable.CreateColumn("adbin", new SqlValueType(SqlType.Binary));
+ _ = defaultValuesTable.CreateColumn("adbin", new SqlValueType(SqlType.Binary));
+
+ var indexesTable = schema.Tables["pg_index"];
+ CreateInt2Column(indexesTable, "indnkeyatts");
}
///
@@ -77,6 +84,126 @@ protected override ISqlCompileUnit BuildExtractTableAndDomainConstraintsQuery(Ex
return select;
}
+ ///
+ protected override ISqlCompileUnit BuildExtractTableIndexesQuery(ExtractionContext context)
+ {
+ var tableMap = context.TableMap;
+ var tableSpacesTable = PgTablespace;
+ var relationsTable = PgClass;
+ var indexTable = PgIndex;
+ var dependencyTable = PgDepend;
+
+ //subselect that index was not created automatically
+ var subSelect = SqlDml.Select(dependencyTable);
+ subSelect.Where = dependencyTable["classid"] == PgClassOid &&
+ dependencyTable["objid"] == indexTable["indexrelid"] &&
+ dependencyTable["deptype"] == 'i';
+ subSelect.Columns.Add(dependencyTable[0]);
+
+ //not automatically created indexes of our tables
+ var select = SqlDml.Select(indexTable
+ .InnerJoin(relationsTable, relationsTable["oid"] == indexTable["indexrelid"])
+ .LeftOuterJoin(tableSpacesTable, tableSpacesTable["oid"] == relationsTable["reltablespace"]));
+ select.Where = SqlDml.In(indexTable["indrelid"], CreateOidRow(tableMap.Keys)) && !SqlDml.Exists(subSelect);
+ select.Columns.Add(indexTable["indrelid"]);
+ select.Columns.Add(indexTable["indexrelid"]);
+ select.Columns.Add(relationsTable["relname"]);
+ select.Columns.Add(indexTable["indisunique"]);
+ select.Columns.Add(indexTable["indisclustered"]);
+ select.Columns.Add(indexTable["indkey"]);
+ select.Columns.Add(tableSpacesTable["spcname"]);
+ select.Columns.Add(indexTable["indnatts"]);
+ select.Columns.Add(indexTable["indnkeyatts"]);
+ select.Columns.Add(SqlDml.FunctionCall("pg_get_expr", indexTable["indexprs"], indexTable["indrelid"], true),
+ "indexprstext");
+ select.Columns.Add(SqlDml.FunctionCall("pg_get_expr", indexTable["indpred"], indexTable["indrelid"], true),
+ "indpredtext");
+ select.Columns.Add(SqlDml.FunctionCall("pg_get_indexdef", indexTable["indexrelid"]), "inddef");
+ AddSpecialIndexQueryColumns(select, tableSpacesTable, relationsTable, indexTable, dependencyTable);
+ return select;
+ }
+
+ ///
+ protected override int ReadTableIndexData(DbDataReader dataReader, ExtractionContext context)
+ {
+ var tableMap = context.TableMap;
+ var tableColumns = context.TableColumnMap;
+
+ var maxColumnNumber = 0;
+ var tableIdentifier = Convert.ToInt64(dataReader["indrelid"]);
+ var indexIdentifier = Convert.ToInt64(dataReader["indexrelid"]);
+ var indexName = dataReader["relname"].ToString();
+ var isUnique = dataReader.GetBoolean(dataReader.GetOrdinal("indisunique"));
+ var indexKey = (short[]) dataReader["indkey"];
+
+ var tablespaceName = (dataReader["spcname"] != DBNull.Value) ? dataReader["spcname"].ToString() : null;
+ var filterExpression = (dataReader["indpredtext"] != DBNull.Value)
+ ? dataReader["indpredtext"].ToString()
+ : string.Empty;
+
+ var table = tableMap[tableIdentifier];
+
+ var fullTextRegex =
+ @"(?<=CREATE INDEX \S+ ON \S+ USING (?:gist|gin)(?:\s|\S)*)to_tsvector\('(\w+)'::regconfig, \(*(?:(?:\s|\)|\(|\|)*(?:\(""(\S+)""\)|'\s')::text)+\)";
+ var indexScript = dataReader["inddef"].ToString();
+ var matches = Regex.Matches(indexScript, fullTextRegex, RegexOptions.Compiled);
+ if (matches.Count > 0) {
+ // Fulltext index
+ var fullTextIndex = table.CreateFullTextIndex(indexName);
+ foreach (Match match in matches) {
+ var columnConfigurationName = match.Groups[1].Value;
+ foreach (Capture capture in match.Groups[2].Captures) {
+ var columnName = capture.Value;
+ var fullTextColumn = fullTextIndex.Columns[columnName]
+ ?? fullTextIndex.CreateIndexColumn(table.Columns.Single(column => column.Name == columnName));
+ if (fullTextColumn.Languages[columnConfigurationName] == null)
+ fullTextColumn.Languages.Add(new Language(columnConfigurationName));
+ }
+ }
+ }
+ else {
+ //Regular index
+ var index = table.CreateIndex(indexName);
+ index.IsBitmap = false;
+ index.IsUnique = isUnique;
+ index.Filegroup = tablespaceName;
+ if (!string.IsNullOrEmpty(filterExpression))
+ index.Where = SqlDml.Native(filterExpression);
+
+ // Expression-based index
+ var some = dataReader["indexprstext"];
+ if (some != DBNull.Value) {
+ context.ExpressionIndexMap[indexIdentifier] = new ExpressionIndexInfo(index, indexKey);
+ maxColumnNumber = dataReader.GetInt16(dataReader.GetOrdinal("indnatts"));
+ }
+ else {
+ var keyColumnNumber = dataReader.GetInt16(dataReader.GetOrdinal("indnkeyatts"));
+ for (int j = 0; j < indexKey.Length; j++) {
+ if (j < keyColumnNumber) {
+ int colIndex = indexKey[j];
+ if (colIndex > 0) {
+ _ = index.CreateIndexColumn(tableColumns[tableIdentifier][colIndex], true);
+ }
+ else {
+ //column index is 0
+ //this means that this index column is an expression
+ //which is not possible with SqlDom tables
+ }
+ }
+ else {
+ int colIndex = indexKey[j];
+ index.NonkeyColumns.Add(tableColumns[tableIdentifier][colIndex]);
+ }
+ }
+ }
+
+ ReadSpecialIndexProperties(dataReader, index);
+ }
+
+ return maxColumnNumber;
+ }
+
+
// Constructors
public Extractor(SqlDriver driver)
diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/ServerInfoProvider.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/ServerInfoProvider.cs
index 8cbbf9139..781681939 100644
--- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/ServerInfoProvider.cs
+++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/ServerInfoProvider.cs
@@ -2,10 +2,14 @@
// This code is distributed under MIT license terms.
// See the License.txt file in the project root for more information.
+using Xtensive.Sql.Info;
+
namespace Xtensive.Sql.Drivers.PostgreSql.v12_0
{
internal class ServerInfoProvider : v10_0.ServerInfoProvider
{
+ protected override IndexFeatures GetIndexFeatures() => base.GetIndexFeatures() | IndexFeatures.NonKeyColumns;
+
// Constructors
public ServerInfoProvider(SqlDriver driver)
diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Translator.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Translator.cs
index e7e5312b1..821fa2ac3 100644
--- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Translator.cs
+++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Translator.cs
@@ -2,10 +2,35 @@
// This code is distributed under MIT license terms.
// See the License.txt file in the project root for more information.
+using Xtensive.Sql.Compiler;
+using Xtensive.Sql.Ddl;
+
namespace Xtensive.Sql.Drivers.PostgreSql.v12_0
{
internal class Translator : v10_0.Translator
{
+ public override void Translate(SqlCompilerContext context, SqlCreateIndex node, CreateIndexSection section)
+ {
+ var index = node.Index;
+ if (!index.IsFullText) {
+ var output = context.Output;
+ switch (section) {
+ case CreateIndexSection.NonkeyColumnsEnter:
+ _ = output.AppendOpeningPunctuation("INCLUDE (");
+ break;
+ case CreateIndexSection.NonkeyColumnsExit:
+ _ = output.AppendClosingPunctuation(")");
+ break;
+ default:
+ base.Translate(context, node, section);
+ break;
+ }
+ }
+ else {
+ base.Translate(context, node, section);
+ }
+ }
+
// Constructors
public Translator(SqlDriver driver)
diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs
index 76139d151..458738945 100644
--- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs
+++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs
@@ -294,22 +294,30 @@ public override void Translate(SqlCompilerContext context, SqlCreateIndex node,
if (index.IsSpatial) {
_ = output.Append(" USING GIST");
}
+ break;
+ case CreateIndexSection.ColumnsEnter:
_ = output.Append("(");
break;
- case CreateIndexSection.StorageOptions:
+ case CreateIndexSection.ColumnsExit:
_ = output.Append(")");
+ break;
+ case CreateIndexSection.NonkeyColumnsEnter:
+ break;
+ case CreateIndexSection.NonkeyColumnsExit:
+ break;
+ case CreateIndexSection.StorageOptions:
AppendIndexStorageParameters(output, index);
if (!string.IsNullOrEmpty(index.Filegroup)) {
_ = output.Append(" TABLESPACE ");
TranslateIdentifier(output, index.Filegroup);
}
- break;
- case CreateIndexSection.Exit:
break;
case CreateIndexSection.Where:
_ = output.Append(" WHERE ");
break;
+ case CreateIndexSection.Exit:
+ break;
default:
break;
;
diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_3/Translator.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_3/Translator.cs
index fb0fe8279..7107ca88f 100644
--- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_3/Translator.cs
+++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_3/Translator.cs
@@ -31,7 +31,10 @@ public override void Translate(SqlCompilerContext context, SqlCreateIndex node,
TranslateIdentifier(output, index.Name);
_ = output.Append(" ON ");
Translate(context, index.DataTable);
- _ = output.Append(" USING gin (");
+ _ = output.Append(" USING gin ");
+ break;
+ case CreateIndexSection.ColumnsEnter:
+ _ = output.Append("(");
break;
case CreateIndexSection.ColumnsExit:
// Add actual columns list
diff --git a/Orm/Xtensive.Orm.Tests.Sql/PostgreSql/ExtractorTest.cs b/Orm/Xtensive.Orm.Tests.Sql/PostgreSql/ExtractorTest.cs
index 45f49b005..9813ac74e 100644
--- a/Orm/Xtensive.Orm.Tests.Sql/PostgreSql/ExtractorTest.cs
+++ b/Orm/Xtensive.Orm.Tests.Sql/PostgreSql/ExtractorTest.cs
@@ -92,10 +92,23 @@ protected override string GetForeignKeyExtractionCleanUpScript() =>
protected override string GetIndexExtractionPrepareScript(string tableName)
{
- return
- $"CREATE TABLE \"{tableName}\" (\"column1\" int, \"column2\" int);" +
- $"\n CREATE INDEX \"{tableName}_index1_desc_asc\" on \"{tableName}\" (\"column1\" desc, \"column2\" asc);" +
- $"\n CREATE UNIQUE INDEX \"{tableName}_index1_u_asc_desc\" on \"{tableName}\" (\"column1\" asc, \"column2\" desc);";
+ // CREATE TABLE table1 (column1 int, column2 int);
+ // CREATE INDEX table1_index1_desc_asc on table1 (column1 desc, column2 asc);
+ // CREATE UNIQUE INDEX table1_index1_u_asc_desc on table1 (column1 asc, column2 desc);
+ // CREATE UNIQUE INDEX table1_index_with_included_columns on table1 (column1 asc) include (column2);
+ if (NonKeyColumnsSupported) {
+ return
+ $"CREATE TABLE \"{tableName}\" (\"column1\" int, \"column2\" int);" +
+ $"\n CREATE INDEX \"{tableName}_index1_desc_asc\" on \"{tableName}\" (\"column1\" desc, \"column2\" asc);" +
+ $"\n CREATE UNIQUE INDEX \"{tableName}_index1_u_asc_desc\" on \"{tableName}\" (\"column1\" asc, \"column2\" desc);" +
+ $"\n CREATE UNIQUE INDEX \"{tableName}_index_with_included_columns\" on \"{tableName}\" (\"column1\" asc) include (\"column2\");";
+ }
+ else {
+ return
+ $"CREATE TABLE \"{tableName}\" (\"column1\" int, \"column2\" int);" +
+ $"\n CREATE INDEX \"{tableName}_index1_desc_asc\" on \"{tableName}\" (\"column1\" desc, \"column2\" asc);" +
+ $"\n CREATE UNIQUE INDEX \"{tableName}_index1_u_asc_desc\" on \"{tableName}\" (\"column1\" asc, \"column2\" desc);";
+ }
}
protected override string GetIndexExtractionCleanUpScript(string tableName) => $"drop table \"{tableName}\";";
diff --git a/Orm/Xtensive.Orm.Tests/Storage/IgnoreRulesValidateTest.cs b/Orm/Xtensive.Orm.Tests/Storage/IgnoreRulesValidateTest.cs
index fea64c20e..576eb59ba 100644
--- a/Orm/Xtensive.Orm.Tests/Storage/IgnoreRulesValidateTest.cs
+++ b/Orm/Xtensive.Orm.Tests/Storage/IgnoreRulesValidateTest.cs
@@ -344,6 +344,7 @@ public class IgnoreRulesValidateTest
private readonly bool createConstraintsWithTable = StorageProviderInfo.Instance.Provider == StorageProvider.Sqlite;
private readonly bool noExceptionOnIndexKeyColumnDrop = StorageProviderInfo.Instance.Provider.In(StorageProvider.PostgreSql, StorageProvider.MySql);
+ private readonly bool noExceptionOnIndexIncludedColumnDrop = StorageProviderInfo.Instance.Provider.In(StorageProvider.PostgreSql);
private readonly SqlDriver sqlDriver = TestSqlDriver.Create(GetConnectionInfo());
private Key changedOrderKey;
@@ -646,8 +647,13 @@ public void DropIncludedColumnOfIgnoredIndexTest()
var ignoreRuleCollection = new IgnoreRuleCollection();
_ = ignoreRuleCollection.IgnoreIndex("IX_Ignored_Index").WhenTable("MyEntity2");
- _ = Assert.Throws(
- () => BuildDomain(DomainUpgradeMode.Perform, ignoreRuleCollection, model5Types).Dispose());
+ if (noExceptionOnIndexIncludedColumnDrop) {
+ BuildDomain(DomainUpgradeMode.Perform, ignoreRuleCollection, model6Types).Dispose();
+ }
+ else {
+ _ = Assert.Throws(
+ () => BuildDomain(DomainUpgradeMode.Perform, ignoreRuleCollection, model5Types).Dispose());
+ }
}
[Test]