Skip to content

Commit d765e69

Browse files
V13: Align database schemas of migrated and new database (#15934)
* Drop default constraint umbracoCacheInstruction table * Align umbracoContentVersion table * Update indexes on external login table * Align node table * Make relation type index unique * Remove user-group default constraint * Re-order methods * Make webhook url not nullable * Cleanup * Cleanup
1 parent 43920f9 commit d765e69

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,6 @@ protected virtual void DefinePlan()
105105
To<V_13_0_0.ChangeWebhookRequestObjectColumnToNvarcharMax>("{F74CDA0C-7AAA-48C8-94C6-C6EC3C06F599}");
106106
To<V_13_0_0.ChangeWebhookUrlColumnsToNvarcharMax>("{21C42760-5109-4C03-AB4F-7EA53577D1F5}");
107107
To<V_13_0_0.AddExceptionOccured>("{6158F3A3-4902-4201-835E-1ED7F810B2D8}");
108+
To<V_13_3_0.AlignUpgradedDatabase>("{985AF2BA-69D3-4DBA-95E0-AD3FA7459FA7}");
108109
}
109110
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
using NPoco;
2+
using Umbraco.Cms.Infrastructure.Persistence;
3+
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
4+
using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
5+
6+
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_3_0;
7+
8+
/// <summary>
9+
/// We see some differences between an updated database and a fresh one,
10+
/// the purpose of this migration is to align the two.
11+
/// </summary>
12+
public class AlignUpgradedDatabase : MigrationBase
13+
{
14+
public AlignUpgradedDatabase(IMigrationContext context)
15+
: base(context)
16+
{
17+
}
18+
19+
protected override void Migrate()
20+
{
21+
// We ignore SQLite since it's considered a development DB
22+
if (DatabaseType == DatabaseType.SQLite)
23+
{
24+
return;
25+
}
26+
27+
ColumnInfo[] columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray();
28+
Tuple<string, string, string, bool>[] indexes = SqlSyntax.GetDefinedIndexes(Database).ToArray();
29+
30+
DropCacheInstructionDefaultConstraint(columns);
31+
AlignContentVersionTable(columns);
32+
UpdateExternalLoginIndexes(indexes);
33+
AlignNodeTable(columns);
34+
MakeRelationTypeIndexUnique(indexes);
35+
RemoveUserGroupDefault(columns);
36+
MakeWebhookUrlNotNullable(columns);
37+
MakeWebhookLogUrlNotNullable(columns);
38+
}
39+
40+
private void MakeIndexUnique<TDto>(string tableName, string indexName, IEnumerable<Tuple<string, string, string, bool>> indexes)
41+
{
42+
// Let's only mess with the indexes if we have to.
43+
// Indexes are in format TableName, IndexName, ColumnName, IsUnique
44+
Tuple<string, string, string, bool>? loginProviderIndex = indexes.FirstOrDefault(x =>
45+
x.Item1 == tableName && x.Item2 == indexName);
46+
47+
// Item4 == IsUnique
48+
if (loginProviderIndex?.Item4 is false)
49+
{
50+
// The recommended way to change an index from non-unique to unique is to drop and recreate it.
51+
DeleteIndex<TDto>(indexName);
52+
CreateIndex<TDto>(indexName);
53+
}
54+
}
55+
56+
private void RemoveDefaultConstraint(string tableName, string columnName, IEnumerable<ColumnInfo> columns)
57+
{
58+
ColumnInfo? targetColumn = columns
59+
.FirstOrDefault(x => x.TableName == tableName && x.ColumnName == columnName);
60+
61+
if (targetColumn is null)
62+
{
63+
throw new InvalidOperationException($"Could not find {columnName} column on {tableName} table.");
64+
}
65+
66+
if (targetColumn.ColumnDefault is null)
67+
{
68+
return;
69+
}
70+
71+
Delete.DefaultConstraint()
72+
.OnTable(tableName)
73+
.OnColumn(columnName)
74+
.Do();
75+
}
76+
77+
private void RenameColumn(string tableName, string oldColumnName, string newColumnName, IEnumerable<ColumnInfo> columns)
78+
{
79+
ColumnInfo? targetColumn = columns
80+
.FirstOrDefault(x => x.TableName == tableName && x.ColumnName == oldColumnName);
81+
82+
if (targetColumn is null)
83+
{
84+
// The column was not found I.E. the column is correctly named
85+
return;
86+
}
87+
88+
Rename.Column(oldColumnName)
89+
.OnTable(tableName)
90+
.To(newColumnName)
91+
.Do();
92+
}
93+
94+
private void MakeNvarCharColumnNotNullable(string tableName, string columnName, IEnumerable<ColumnInfo> columns)
95+
{
96+
ColumnInfo? targetColumn = columns.FirstOrDefault(x => x.TableName == tableName && x.ColumnName == columnName);
97+
98+
if (targetColumn is null)
99+
{
100+
throw new InvalidOperationException($"Could not find {columnName} column in {tableName} table.");
101+
}
102+
103+
if (targetColumn.IsNullable is false)
104+
{
105+
return;
106+
}
107+
108+
Alter.Table(tableName)
109+
.AlterColumn(columnName)
110+
.AsCustom("nvarchar(max)")
111+
.NotNullable()
112+
.Do();
113+
}
114+
115+
private void DropCacheInstructionDefaultConstraint(IEnumerable<ColumnInfo> columns)
116+
=> RemoveDefaultConstraint("umbracoCacheInstruction", "jsonInstruction", columns);
117+
118+
private void AlignContentVersionTable(ColumnInfo[] columns)
119+
{
120+
// We need to do this to ensure we don't try to rename the constraint if it doesn't exist.
121+
const string tableName = "umbracoContentVersion";
122+
const string columnName = "VersionDate";
123+
ColumnInfo? versionDateColumn = columns
124+
.FirstOrDefault(x => x is { TableName: tableName, ColumnName: columnName });
125+
126+
if (versionDateColumn is null)
127+
{
128+
// The column was not found I.E. the column is correctly named
129+
return;
130+
}
131+
132+
RenameColumn(tableName, columnName, "versionDate", columns);
133+
134+
// Renames the default constraint for the column,
135+
// apparently the content version table used to be prefixed with cms and not umbraco
136+
// We don't have a fluid way to rename the default constraint so we have to use raw SQL
137+
// This should be okay though since we are only running this migration on SQL Server
138+
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
139+
"EXEC sp_rename N'DF_cmsContentVersion_VersionDate', N'DF_umbracoContentVersion_versionDate', N'OBJECT'");
140+
Database.Execute(renameConstraintQuery);
141+
}
142+
143+
private void UpdateExternalLoginIndexes(IEnumerable<Tuple<string, string, string, bool>> indexes)
144+
{
145+
const string userMemberOrKeyIndexName = "IX_umbracoExternalLogin_userOrMemberKey";
146+
147+
MakeIndexUnique<ExternalLoginDto>("umbracoExternalLogin", "IX_umbracoExternalLogin_LoginProvider", indexes);
148+
149+
if (IndexExists(userMemberOrKeyIndexName))
150+
{
151+
return;
152+
}
153+
154+
CreateIndex<ExternalLoginDto>(userMemberOrKeyIndexName);
155+
}
156+
157+
private void AlignNodeTable(ColumnInfo[] columns)
158+
{
159+
const string tableName = "umbracoNode";
160+
RenameColumn(tableName, "parentID", "parentId", columns);
161+
RenameColumn(tableName, "uniqueID", "uniqueId", columns);
162+
163+
const string extraIndexName = "IX_umbracoNode_ParentId";
164+
if (IndexExists(extraIndexName))
165+
{
166+
DeleteIndex<NodeDto>(extraIndexName);
167+
}
168+
}
169+
170+
private void MakeRelationTypeIndexUnique(Tuple<string, string, string, bool>[] indexes)
171+
=> MakeIndexUnique<RelationTypeDto>("umbracoRelationType", "IX_umbracoRelationType_alias", indexes);
172+
173+
private void RemoveUserGroupDefault(ColumnInfo[] columns)
174+
=> RemoveDefaultConstraint("umbracoUserGroup", "hasAccessToAllLanguages", columns);
175+
176+
private void MakeWebhookUrlNotNullable(ColumnInfo[] columns)
177+
=> MakeNvarCharColumnNotNullable("umbracoWebhook", "url", columns);
178+
179+
private void MakeWebhookLogUrlNotNullable(ColumnInfo[] columns)
180+
=> MakeNvarCharColumnNotNullable("umbracoWebhookLog", "url", columns);
181+
}

0 commit comments

Comments
 (0)