Skip to content

Commit 9b914cb

Browse files
authored
Merge pull request #444 from DataObjects-NET/master-included-columns-pgsql
Add support for included columns for indexes starting from PostgreSQL 12
2 parents 20f76c5 + 11196d5 commit 9b914cb

File tree

8 files changed

+198
-11
lines changed

8 files changed

+198
-11
lines changed

ChangeLog/7.2.0-RC-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[postgresql] Support for included columns for indexes starting from PostgreSQL 12

Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Extractor.cs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
// This code is distributed under MIT license terms.
33
// See the License.txt file in the project root for more information.
44

5+
using System;
6+
using System.Data.Common;
7+
using System.Linq;
8+
using System.Text.RegularExpressions;
59
using Xtensive.Sql.Model;
610

711
namespace Xtensive.Sql.Drivers.PostgreSql.v12_0
@@ -13,7 +17,10 @@ protected override void BuildPgCatalogSchema(Schema schema)
1317
{
1418
base.BuildPgCatalogSchema(schema);
1519
var defaultValuesTable = schema.Tables["pg_attrdef"];
16-
defaultValuesTable.CreateColumn("adbin", new SqlValueType(SqlType.Binary));
20+
_ = defaultValuesTable.CreateColumn("adbin", new SqlValueType(SqlType.Binary));
21+
22+
var indexesTable = schema.Tables["pg_index"];
23+
CreateInt2Column(indexesTable, "indnkeyatts");
1724
}
1825

1926
/// <inheritdoc/>
@@ -77,6 +84,126 @@ protected override ISqlCompileUnit BuildExtractTableAndDomainConstraintsQuery(Ex
7784
return select;
7885
}
7986

87+
/// <inheritdoc />
88+
protected override ISqlCompileUnit BuildExtractTableIndexesQuery(ExtractionContext context)
89+
{
90+
var tableMap = context.TableMap;
91+
var tableSpacesTable = PgTablespace;
92+
var relationsTable = PgClass;
93+
var indexTable = PgIndex;
94+
var dependencyTable = PgDepend;
95+
96+
//subselect that index was not created automatically
97+
var subSelect = SqlDml.Select(dependencyTable);
98+
subSelect.Where = dependencyTable["classid"] == PgClassOid &&
99+
dependencyTable["objid"] == indexTable["indexrelid"] &&
100+
dependencyTable["deptype"] == 'i';
101+
subSelect.Columns.Add(dependencyTable[0]);
102+
103+
//not automatically created indexes of our tables
104+
var select = SqlDml.Select(indexTable
105+
.InnerJoin(relationsTable, relationsTable["oid"] == indexTable["indexrelid"])
106+
.LeftOuterJoin(tableSpacesTable, tableSpacesTable["oid"] == relationsTable["reltablespace"]));
107+
select.Where = SqlDml.In(indexTable["indrelid"], CreateOidRow(tableMap.Keys)) && !SqlDml.Exists(subSelect);
108+
select.Columns.Add(indexTable["indrelid"]);
109+
select.Columns.Add(indexTable["indexrelid"]);
110+
select.Columns.Add(relationsTable["relname"]);
111+
select.Columns.Add(indexTable["indisunique"]);
112+
select.Columns.Add(indexTable["indisclustered"]);
113+
select.Columns.Add(indexTable["indkey"]);
114+
select.Columns.Add(tableSpacesTable["spcname"]);
115+
select.Columns.Add(indexTable["indnatts"]);
116+
select.Columns.Add(indexTable["indnkeyatts"]);
117+
select.Columns.Add(SqlDml.FunctionCall("pg_get_expr", indexTable["indexprs"], indexTable["indrelid"], true),
118+
"indexprstext");
119+
select.Columns.Add(SqlDml.FunctionCall("pg_get_expr", indexTable["indpred"], indexTable["indrelid"], true),
120+
"indpredtext");
121+
select.Columns.Add(SqlDml.FunctionCall("pg_get_indexdef", indexTable["indexrelid"]), "inddef");
122+
AddSpecialIndexQueryColumns(select, tableSpacesTable, relationsTable, indexTable, dependencyTable);
123+
return select;
124+
}
125+
126+
/// <inheritdoc />
127+
protected override int ReadTableIndexData(DbDataReader dataReader, ExtractionContext context)
128+
{
129+
var tableMap = context.TableMap;
130+
var tableColumns = context.TableColumnMap;
131+
132+
var maxColumnNumber = 0;
133+
var tableIdentifier = Convert.ToInt64(dataReader["indrelid"]);
134+
var indexIdentifier = Convert.ToInt64(dataReader["indexrelid"]);
135+
var indexName = dataReader["relname"].ToString();
136+
var isUnique = dataReader.GetBoolean(dataReader.GetOrdinal("indisunique"));
137+
var indexKey = (short[]) dataReader["indkey"];
138+
139+
var tablespaceName = (dataReader["spcname"] != DBNull.Value) ? dataReader["spcname"].ToString() : null;
140+
var filterExpression = (dataReader["indpredtext"] != DBNull.Value)
141+
? dataReader["indpredtext"].ToString()
142+
: string.Empty;
143+
144+
var table = tableMap[tableIdentifier];
145+
146+
var fullTextRegex =
147+
@"(?<=CREATE INDEX \S+ ON \S+ USING (?:gist|gin)(?:\s|\S)*)to_tsvector\('(\w+)'::regconfig, \(*(?:(?:\s|\)|\(|\|)*(?:\(""(\S+)""\)|'\s')::text)+\)";
148+
var indexScript = dataReader["inddef"].ToString();
149+
var matches = Regex.Matches(indexScript, fullTextRegex, RegexOptions.Compiled);
150+
if (matches.Count > 0) {
151+
// Fulltext index
152+
var fullTextIndex = table.CreateFullTextIndex(indexName);
153+
foreach (Match match in matches) {
154+
var columnConfigurationName = match.Groups[1].Value;
155+
foreach (Capture capture in match.Groups[2].Captures) {
156+
var columnName = capture.Value;
157+
var fullTextColumn = fullTextIndex.Columns[columnName]
158+
?? fullTextIndex.CreateIndexColumn(table.Columns.Single(column => column.Name == columnName));
159+
if (fullTextColumn.Languages[columnConfigurationName] == null)
160+
fullTextColumn.Languages.Add(new Language(columnConfigurationName));
161+
}
162+
}
163+
}
164+
else {
165+
//Regular index
166+
var index = table.CreateIndex(indexName);
167+
index.IsBitmap = false;
168+
index.IsUnique = isUnique;
169+
index.Filegroup = tablespaceName;
170+
if (!string.IsNullOrEmpty(filterExpression))
171+
index.Where = SqlDml.Native(filterExpression);
172+
173+
// Expression-based index
174+
var some = dataReader["indexprstext"];
175+
if (some != DBNull.Value) {
176+
context.ExpressionIndexMap[indexIdentifier] = new ExpressionIndexInfo(index, indexKey);
177+
maxColumnNumber = dataReader.GetInt16(dataReader.GetOrdinal("indnatts"));
178+
}
179+
else {
180+
var keyColumnNumber = dataReader.GetInt16(dataReader.GetOrdinal("indnkeyatts"));
181+
for (int j = 0; j < indexKey.Length; j++) {
182+
if (j < keyColumnNumber) {
183+
int colIndex = indexKey[j];
184+
if (colIndex > 0) {
185+
_ = index.CreateIndexColumn(tableColumns[tableIdentifier][colIndex], true);
186+
}
187+
else {
188+
//column index is 0
189+
//this means that this index column is an expression
190+
//which is not possible with SqlDom tables
191+
}
192+
}
193+
else {
194+
int colIndex = indexKey[j];
195+
index.NonkeyColumns.Add(tableColumns[tableIdentifier][colIndex]);
196+
}
197+
}
198+
}
199+
200+
ReadSpecialIndexProperties(dataReader, index);
201+
}
202+
203+
return maxColumnNumber;
204+
}
205+
206+
80207
// Constructors
81208

82209
public Extractor(SqlDriver driver)

Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/ServerInfoProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
// This code is distributed under MIT license terms.
33
// See the License.txt file in the project root for more information.
44

5+
using Xtensive.Sql.Info;
6+
57
namespace Xtensive.Sql.Drivers.PostgreSql.v12_0
68
{
79
internal class ServerInfoProvider : v10_0.ServerInfoProvider
810
{
11+
protected override IndexFeatures GetIndexFeatures() => base.GetIndexFeatures() | IndexFeatures.NonKeyColumns;
12+
913
// Constructors
1014

1115
public ServerInfoProvider(SqlDriver driver)

Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v12_0/Translator.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,35 @@
22
// This code is distributed under MIT license terms.
33
// See the License.txt file in the project root for more information.
44

5+
using Xtensive.Sql.Compiler;
6+
using Xtensive.Sql.Ddl;
7+
58
namespace Xtensive.Sql.Drivers.PostgreSql.v12_0
69
{
710
internal class Translator : v10_0.Translator
811
{
12+
public override void Translate(SqlCompilerContext context, SqlCreateIndex node, CreateIndexSection section)
13+
{
14+
var index = node.Index;
15+
if (!index.IsFullText) {
16+
var output = context.Output;
17+
switch (section) {
18+
case CreateIndexSection.NonkeyColumnsEnter:
19+
_ = output.AppendOpeningPunctuation("INCLUDE (");
20+
break;
21+
case CreateIndexSection.NonkeyColumnsExit:
22+
_ = output.AppendClosingPunctuation(")");
23+
break;
24+
default:
25+
base.Translate(context, node, section);
26+
break;
27+
}
28+
}
29+
else {
30+
base.Translate(context, node, section);
31+
}
32+
}
33+
934
// Constructors
1035

1136
public Translator(SqlDriver driver)

Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_0/Translator.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,22 +294,30 @@ public override void Translate(SqlCompilerContext context, SqlCreateIndex node,
294294
if (index.IsSpatial) {
295295
_ = output.Append(" USING GIST");
296296
}
297+
break;
298+
case CreateIndexSection.ColumnsEnter:
297299
_ = output.Append("(");
298300
break;
299-
case CreateIndexSection.StorageOptions:
301+
case CreateIndexSection.ColumnsExit:
300302
_ = output.Append(")");
303+
break;
304+
case CreateIndexSection.NonkeyColumnsEnter:
305+
break;
306+
case CreateIndexSection.NonkeyColumnsExit:
307+
break;
308+
case CreateIndexSection.StorageOptions:
301309
AppendIndexStorageParameters(output, index);
302310
if (!string.IsNullOrEmpty(index.Filegroup)) {
303311
_ = output.Append(" TABLESPACE ");
304312
TranslateIdentifier(output, index.Filegroup);
305313
}
306314

307-
break;
308-
case CreateIndexSection.Exit:
309315
break;
310316
case CreateIndexSection.Where:
311317
_ = output.Append(" WHERE ");
312318
break;
319+
case CreateIndexSection.Exit:
320+
break;
313321
default:
314322
break;
315323
;

Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/v8_3/Translator.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ public override void Translate(SqlCompilerContext context, SqlCreateIndex node,
3131
TranslateIdentifier(output, index.Name);
3232
_ = output.Append(" ON ");
3333
Translate(context, index.DataTable);
34-
_ = output.Append(" USING gin (");
34+
_ = output.Append(" USING gin ");
35+
break;
36+
case CreateIndexSection.ColumnsEnter:
37+
_ = output.Append("(");
3538
break;
3639
case CreateIndexSection.ColumnsExit:
3740
// Add actual columns list

Orm/Xtensive.Orm.Tests.Sql/PostgreSql/ExtractorTest.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,23 @@ protected override string GetForeignKeyExtractionCleanUpScript() =>
9292

9393
protected override string GetIndexExtractionPrepareScript(string tableName)
9494
{
95-
return
96-
$"CREATE TABLE \"{tableName}\" (\"column1\" int, \"column2\" int);" +
97-
$"\n CREATE INDEX \"{tableName}_index1_desc_asc\" on \"{tableName}\" (\"column1\" desc, \"column2\" asc);" +
98-
$"\n CREATE UNIQUE INDEX \"{tableName}_index1_u_asc_desc\" on \"{tableName}\" (\"column1\" asc, \"column2\" desc);";
95+
// CREATE TABLE table1 (column1 int, column2 int);
96+
// CREATE INDEX table1_index1_desc_asc on table1 (column1 desc, column2 asc);
97+
// CREATE UNIQUE INDEX table1_index1_u_asc_desc on table1 (column1 asc, column2 desc);
98+
// CREATE UNIQUE INDEX table1_index_with_included_columns on table1 (column1 asc) include (column2);
99+
if (NonKeyColumnsSupported) {
100+
return
101+
$"CREATE TABLE \"{tableName}\" (\"column1\" int, \"column2\" int);" +
102+
$"\n CREATE INDEX \"{tableName}_index1_desc_asc\" on \"{tableName}\" (\"column1\" desc, \"column2\" asc);" +
103+
$"\n CREATE UNIQUE INDEX \"{tableName}_index1_u_asc_desc\" on \"{tableName}\" (\"column1\" asc, \"column2\" desc);" +
104+
$"\n CREATE UNIQUE INDEX \"{tableName}_index_with_included_columns\" on \"{tableName}\" (\"column1\" asc) include (\"column2\");";
105+
}
106+
else {
107+
return
108+
$"CREATE TABLE \"{tableName}\" (\"column1\" int, \"column2\" int);" +
109+
$"\n CREATE INDEX \"{tableName}_index1_desc_asc\" on \"{tableName}\" (\"column1\" desc, \"column2\" asc);" +
110+
$"\n CREATE UNIQUE INDEX \"{tableName}_index1_u_asc_desc\" on \"{tableName}\" (\"column1\" asc, \"column2\" desc);";
111+
}
99112
}
100113
protected override string GetIndexExtractionCleanUpScript(string tableName) => $"drop table \"{tableName}\";";
101114

Orm/Xtensive.Orm.Tests/Storage/IgnoreRulesValidateTest.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ public class IgnoreRulesValidateTest
344344

345345
private readonly bool createConstraintsWithTable = StorageProviderInfo.Instance.Provider == StorageProvider.Sqlite;
346346
private readonly bool noExceptionOnIndexKeyColumnDrop = StorageProviderInfo.Instance.Provider.In(StorageProvider.PostgreSql, StorageProvider.MySql);
347+
private readonly bool noExceptionOnIndexIncludedColumnDrop = StorageProviderInfo.Instance.Provider.In(StorageProvider.PostgreSql);
347348
private readonly SqlDriver sqlDriver = TestSqlDriver.Create(GetConnectionInfo());
348349

349350
private Key changedOrderKey;
@@ -646,8 +647,13 @@ public void DropIncludedColumnOfIgnoredIndexTest()
646647
var ignoreRuleCollection = new IgnoreRuleCollection();
647648
_ = ignoreRuleCollection.IgnoreIndex("IX_Ignored_Index").WhenTable("MyEntity2");
648649

649-
_ = Assert.Throws<StorageException>(
650-
() => BuildDomain(DomainUpgradeMode.Perform, ignoreRuleCollection, model5Types).Dispose());
650+
if (noExceptionOnIndexIncludedColumnDrop) {
651+
BuildDomain(DomainUpgradeMode.Perform, ignoreRuleCollection, model6Types).Dispose();
652+
}
653+
else {
654+
_ = Assert.Throws<StorageException>(
655+
() => BuildDomain(DomainUpgradeMode.Perform, ignoreRuleCollection, model5Types).Dispose());
656+
}
651657
}
652658

653659
[Test]

0 commit comments

Comments
 (0)