Skip to content

Commit 1912816

Browse files
Add SyncIndexes schema upgrade mode (#181)
* Add new SyncIndexes SchemaUpgradeMode Add extension methods on IActionSequnece to check whether it contains expected actions Add condition in upgrade process for syncing indexes Add tests Bump version * Add nullable annotation in MyEntity.cs * Move ActionSequenceExtensionsTests to Xtensive.Orm.Tests * Fix comments * Rename SyncIndexes to SyncIndexesSafely Fix comments --------- Co-authored-by: Sergei Pavlov <[email protected]>
1 parent 2b27576 commit 1912816

File tree

8 files changed

+518
-0
lines changed

8 files changed

+518
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Reflection;
7+
using NUnit.Framework;
8+
using Xtensive.Modelling.Actions;
9+
using Xtensive.Modelling.Actions.Extensions;
10+
using Xtensive.Modelling.Comparison;
11+
using Xtensive.Modelling.Comparison.Hints;
12+
using Xtensive.Orm.Upgrade.Model;
13+
using Xtensive.Sql;
14+
using Comparer = Xtensive.Modelling.Comparison.Comparer;
15+
16+
namespace Xtensive.Extensions
17+
{
18+
[TestFixture]
19+
public class ActionSequenceExtensionsTests
20+
{
21+
[Test]
22+
[TestCaseSource(nameof(AddSecondaryIndexTestCases))]
23+
public void ContainsActionsOfType_WhenCreatedIndex_ReturnsExpectedResult(IEnumerable<Type> types, bool expectedResult)
24+
{
25+
// Arrange
26+
var originalStorage = CreateStorage();
27+
var originalTable = CreateTable(originalStorage);
28+
var originalColumn = AddColumn<string>(originalTable);
29+
30+
var actionSequence = GetActionSequence(originalStorage, (storage, _) => {
31+
var table = storage.Tables[originalTable.Name];
32+
var column = table.Columns[originalColumn.Name];
33+
AddSecondaryIndex(column);
34+
});
35+
36+
// Act
37+
var result =
38+
actionSequence.ContainsActionsOfType<SecondaryIndexInfo>(types);
39+
40+
// Assert
41+
Assert.AreEqual(result, expectedResult);
42+
}
43+
44+
[Test]
45+
[TestCaseSource(nameof(RemoveSecondaryIndexTestCases))]
46+
public void ContainsActionsOfType_WhenRemovedIndex_ReturnsExpectedResult(IEnumerable<Type> types, bool expectedResult)
47+
{
48+
// Arrange
49+
var originalStorage = CreateStorage();
50+
var originalTable = CreateTable(originalStorage);
51+
var originalColumn = AddColumn<string>(originalTable);
52+
var originalIndex = AddSecondaryIndex(originalColumn);
53+
54+
var actionSequence = GetActionSequence(originalStorage, (storage, _) => {
55+
var table = storage.Tables[originalTable.Name];
56+
var index = table.SecondaryIndexes[originalIndex.Name];
57+
table.SecondaryIndexes.Remove(index);
58+
});
59+
60+
// Act
61+
var result =
62+
actionSequence.ContainsActionsOfType<SecondaryIndexInfo>(types);
63+
64+
// Assert
65+
Assert.AreEqual(result, expectedResult);
66+
}
67+
68+
[Test]
69+
[TestCaseSource(nameof(AllActionsTestCaseData))]
70+
public void ContainsActionsOfType_WhenChangedOtherNodeThenExpected_ReturnsFalse(Type actionType)
71+
{
72+
// Arrange
73+
var originalStorage = CreateStorage();
74+
var originalTable = CreateTable(originalStorage);
75+
76+
var actionSequence = GetActionSequence(originalStorage, (storage, _) => {
77+
var table = storage.Tables[originalTable.Name];
78+
AddColumn<long>(table);
79+
});
80+
81+
// Act
82+
var result =
83+
actionSequence.ContainsActionsOfType<SecondaryIndexInfo>(new[] { actionType });
84+
85+
// Assert
86+
Assert.IsFalse(result);
87+
}
88+
89+
[Test]
90+
[TestCaseSource(nameof(DisallowedStorageChangesTestCaseData))]
91+
public void ContainsActionsOfType_WhenDisallowedStorageChangesLogged_ReturnsFalse(Action<StorageModel> action)
92+
{
93+
// Arrange
94+
var originalStorage = CreateStorage();
95+
CreateTable(originalStorage);
96+
97+
var actionSequence = GetActionSequence(originalStorage, (storage, _) => {
98+
action(storage);
99+
});
100+
101+
// Act
102+
var result =
103+
actionSequence.ContainsActionsOfType<SecondaryIndexInfo>(new[] {
104+
typeof(CreateNodeAction), typeof(RemoveNodeAction)
105+
});
106+
107+
// Assert
108+
Assert.IsFalse(result);
109+
}
110+
111+
[Test]
112+
[TestCaseSource(nameof(DisallowedTableChangesTestCaseData))]
113+
public void ContainsActionsOfType_WhenDisallowedTableActionsLogged_ReturnsFalse(Action<TableInfo> action)
114+
{
115+
// Arrange
116+
var originalStorage = CreateStorage();
117+
var originalTable = CreateTable(originalStorage);
118+
AddColumn<long>(originalTable);
119+
AddColumn<string>(originalTable);
120+
121+
var actionSequence = GetActionSequence(originalStorage, (storage, _) => {
122+
var table = storage.Tables[originalTable.Name];
123+
action(table);
124+
});
125+
126+
// Act
127+
var result =
128+
actionSequence.ContainsActionsOfType<SecondaryIndexInfo>(new[] {
129+
typeof(CreateNodeAction), typeof(RemoveNodeAction)
130+
});
131+
132+
// Assert
133+
Assert.IsFalse(result);
134+
}
135+
136+
private static StorageModel CreateStorage() => new("storage");
137+
138+
private static TableInfo CreateTable(StorageModel storageInfo)
139+
{
140+
var table = new TableInfo(storageInfo, Guid.NewGuid().ToString());
141+
var id = new StorageColumnInfo(table, "Id", new StorageTypeInfo(typeof (int), new SqlValueType(SqlType.Int32)));
142+
var pk = new PrimaryIndexInfo(table, "PK_A");
143+
_ = new KeyColumnRef(pk, id);
144+
pk.PopulateValueColumns();
145+
146+
return table;
147+
}
148+
149+
private static StorageColumnInfo AddColumn<TType>(TableInfo table, string? name = null)
150+
{
151+
var column = new StorageColumnInfo(table, name ?? Guid.NewGuid().ToString(),
152+
new StorageTypeInfo(typeof(TType), new SqlValueType(typeof(TType).Name), false));
153+
154+
var pk = table.PrimaryIndex;
155+
pk.ValueColumns.Clear();
156+
pk.PopulateValueColumns();
157+
158+
return column;
159+
}
160+
161+
private static SecondaryIndexInfo AddSecondaryIndex(params StorageColumnInfo[] columns)
162+
{
163+
var table = columns[0].Parent;
164+
var index = new SecondaryIndexInfo(table, $"index{Guid.NewGuid()}");
165+
166+
foreach (var column in columns) {
167+
_ = new KeyColumnRef(index, column);
168+
}
169+
index.PopulatePrimaryKeyColumns();
170+
171+
return index;
172+
}
173+
174+
private static ActionSequence GetActionSequence(StorageModel origin, Action<StorageModel, HintSet> mutator)
175+
{
176+
var clonedStorage = Clone(origin);
177+
var hints = new HintSet(origin, clonedStorage);
178+
mutator.Invoke(clonedStorage, hints);
179+
origin.Validate();
180+
clonedStorage.Validate();
181+
182+
var comparer = new Comparer();
183+
var diff = comparer.Compare(origin, clonedStorage, hints);
184+
return new ActionSequence() {
185+
new Upgrader().GetUpgradeSequence(diff, hints, comparer)
186+
};
187+
}
188+
189+
private static StorageModel Clone(StorageModel storage) => (StorageModel) storage.Clone(null, storage.Name);
190+
191+
private static readonly IEnumerable<Type> AllNodeActionsBesidesGroup = Assembly.GetAssembly(typeof(NodeAction))!
192+
.DefinedTypes
193+
.Where(type => type.IsSubclassOf(typeof(NodeAction))).Except(new[] { typeof(GroupingNodeAction) });
194+
195+
private static readonly IEnumerable<Type> AllActionBesidesCreateAndRemove =
196+
AllNodeActionsBesidesGroup.Except(new[] { typeof(CreateNodeAction), typeof(RemoveNodeAction) });
197+
198+
public static IEnumerable AddSecondaryIndexTestCases
199+
{
200+
get {
201+
yield return new TestCaseData(new object?[] { new [] { typeof(CreateNodeAction) }, true });
202+
yield return new TestCaseData(new object?[] { new [] { typeof(RemoveNodeAction) }, false });
203+
yield return new TestCaseData(new object?[] { new [] { typeof(CreateNodeAction), typeof(RemoveNodeAction) }, true });
204+
yield return new TestCaseData(new object?[] { AllActionBesidesCreateAndRemove, false });
205+
}
206+
}
207+
208+
public static IEnumerable RemoveSecondaryIndexTestCases
209+
{
210+
get {
211+
yield return new TestCaseData(new object?[] { new [] { typeof(CreateNodeAction) }, false });
212+
yield return new TestCaseData(new object?[] { new [] { typeof(RemoveNodeAction) }, true });
213+
yield return new TestCaseData(new object?[] { new [] { typeof(CreateNodeAction), typeof(RemoveNodeAction) }, true });
214+
yield return new TestCaseData(new object?[] { AllActionBesidesCreateAndRemove, false });
215+
}
216+
}
217+
218+
219+
public static IEnumerable AllActionsTestCaseData => AllNodeActionsBesidesGroup.Select(actionType => new TestCaseData(actionType));
220+
221+
public static IEnumerable DisallowedStorageChangesTestCaseData
222+
{
223+
get {
224+
yield return new TestCaseData(new object?[] { new Action<StorageModel>(storageInfo => CreateTable(storageInfo)) });
225+
yield return new TestCaseData(new object?[] { new Action<StorageModel>(storageInfo => storageInfo.Tables.First().Remove()) });
226+
}
227+
}
228+
229+
public static IEnumerable DisallowedTableChangesTestCaseData
230+
{
231+
get {
232+
yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => table.PrimaryIndex.Remove()) });
233+
// TODO: possibly not supported
234+
// yield return new TestCaseData(new object?[] {
235+
// new Action<TableInfo>(table => {
236+
// table.PrimaryIndex.KeyColumns.First().Remove();
237+
// new KeyColumnRef(table.PrimaryIndex, table.Columns.Last());
238+
// table.PrimaryIndex.ValueColumns.Clear();
239+
// table.PrimaryIndex.PopulateValueColumns();
240+
// })
241+
// });
242+
yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => table.PrimaryIndex.Name = $"OtherName{Guid.NewGuid() }") });
243+
yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => AddColumn<string>(table)) });
244+
yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => {
245+
table.Columns.Last().Remove();
246+
table.PrimaryIndex.ValueColumns.Clear();
247+
table.PrimaryIndex.PopulateValueColumns();
248+
}) });
249+
yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => table.Columns.Last().Name = $"OtherName{Guid.NewGuid()}") });
250+
// TODO: DefaultValue is marked as IgnoreInComparison
251+
//yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => table.Columns.Last(c => c.Type.Type == typeof(string)).DefaultValue = $"") });
252+
yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => table.Columns.Last().Type = new StorageTypeInfo(typeof(byte), new SqlValueType(SqlType.Int8))) });
253+
// TODO: seems like index change not handled in upgrade
254+
// yield return new TestCaseData(new object?[] { new Action<TableInfo>(table => {
255+
// table.Columns.Last().Index -= 1;
256+
// table.PrimaryIndex.ValueColumns.Clear();
257+
// table.PrimaryIndex.PopulateValueColumns();
258+
// }) });
259+
}
260+
}
261+
}
262+
}
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#nullable enable
2+
using System;
3+
using System.Linq.Expressions;
4+
5+
namespace Xtensive.Orm.Tests.Upgrade.SyncIndexes
6+
{
7+
8+
namespace V1
9+
{
10+
[HierarchyRoot]
11+
public class MyEntity : Entity
12+
{
13+
[Key, Field]
14+
public long Id { get; set; }
15+
16+
[Field]
17+
public string? Name { get; set; }
18+
19+
[Field]
20+
public long Value { get; set; }
21+
22+
[Field]
23+
public long Count { get; set; }
24+
}
25+
}
26+
27+
namespace V2
28+
{
29+
[Index(nameof(Value), Filter = nameof(CountIs), Name = "IX_Value")]
30+
[Index(nameof(Count))]
31+
[HierarchyRoot]
32+
public class MyEntity : Entity
33+
{
34+
private static Expression<Func<MyEntity, bool>> CountIs() => e => e.Count == 1;
35+
36+
[Key, Field]
37+
public long Id { get; set; }
38+
39+
[Field]
40+
public string? Name { get; set; }
41+
42+
[Field]
43+
public long Value { get; set; }
44+
45+
[Field]
46+
public long Count { get; set; }
47+
}
48+
}
49+
50+
namespace V3
51+
{
52+
[Index(nameof(Value), Filter = nameof(CountIs), Name = "IX_Value")]
53+
[HierarchyRoot]
54+
public class MyEntity : Entity
55+
{
56+
private static Expression<Func<MyEntity, bool>> CountIs() => e => e.Count1 == 1;
57+
58+
[Key, Field]
59+
public long Id { get; set; }
60+
61+
[Field]
62+
public string? Name { get; set; }
63+
64+
[Field]
65+
public long Value { get; set; }
66+
67+
[Field]
68+
public long Count1 { get; set; }
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)