Skip to content

Commit 58b3371

Browse files
magnusbakken-zetadisplaymsallin
authored andcommitted
#163 Add support for defining a default collation
1 parent 732c7a0 commit 58b3371

12 files changed

+226
-44
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The following features are supported:
2727
- Index (Decorate columns with the `Index` attribute. Indices are automatically created for foreign keys by default. To prevent this you can remove the convention `ForeignKeyIndexConvention`)
2828
- Unique constraint (Decorate columns with the `UniqueAttribute`, which is part of this library)
2929
- Collate constraint (Decorate columns with the `CollateAttribute`, which is part of this library. Use `CollationFunction.Custom` to specify a own collation function.)
30+
- Default collation (pass an instance of Collation as constructor parameter for an initializer to specify a default collation).
3031
- SQL default value (Decorate columns with the `SqlDefaultValueAttribute`, which is part of this library)
3132

3233
## Install
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System.Data.Common;
2+
using System.Data.Entity;
3+
using System.Data.Entity.Infrastructure;
4+
using System.Data.SQLite;
5+
using System.Linq;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
using SQLite.CodeFirst.Console;
8+
using SQLite.CodeFirst.Console.Entity;
9+
10+
namespace SQLite.CodeFirst.Test.IntegrationTests
11+
{
12+
[TestClass]
13+
public class SqlGenerationDefaultCollationTest
14+
{
15+
private const string ReferenceSql =
16+
@"
17+
CREATE TABLE ""MyTable"" ([Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, [Name] nvarchar NOT NULL COLLATE custom_collate, FOREIGN KEY ([Id]) REFERENCES ""Coaches""([Id]));
18+
CREATE TABLE ""Coaches"" ([Id] INTEGER PRIMARY KEY, [FirstName] nvarchar (50) COLLATE NOCASE, [LastName] nvarchar (50) COLLATE custom_collate, [Street] nvarchar (100) COLLATE custom_collate, [City] nvarchar NOT NULL COLLATE custom_collate, [CreatedUtc] datetime NOT NULL DEFAULT (DATETIME('now')));
19+
CREATE TABLE ""TeamPlayer"" ([Id] INTEGER PRIMARY KEY, [Number] int NOT NULL, [TeamId] int NOT NULL, [FirstName] nvarchar (50) COLLATE NOCASE, [LastName] nvarchar (50) COLLATE custom_collate, [Street] nvarchar (100) COLLATE custom_collate, [City] nvarchar NOT NULL COLLATE custom_collate, [CreatedUtc] datetime NOT NULL DEFAULT (DATETIME('now')), [Mentor_Id] int, FOREIGN KEY ([Mentor_Id]) REFERENCES ""TeamPlayer""([Id]), FOREIGN KEY ([TeamId]) REFERENCES ""MyTable""([Id]) ON DELETE CASCADE);
20+
CREATE TABLE ""Stadions"" ([Name] nvarchar (128) NOT NULL COLLATE custom_collate, [Street] nvarchar (128) NOT NULL COLLATE custom_collate, [City] nvarchar (128) NOT NULL COLLATE custom_collate, [Order] int NOT NULL, [Team_Id] int NOT NULL, PRIMARY KEY([Name], [Street], [City]), FOREIGN KEY ([Team_Id]) REFERENCES ""MyTable""([Id]) ON DELETE CASCADE);
21+
CREATE TABLE ""Foos"" ([FooId] INTEGER PRIMARY KEY, [Name] nvarchar COLLATE custom_collate, [FooSelf1Id] int, [FooSelf2Id] int, [FooSelf3Id] int, FOREIGN KEY ([FooSelf1Id]) REFERENCES ""Foos""([FooId]), FOREIGN KEY ([FooSelf2Id]) REFERENCES ""Foos""([FooId]), FOREIGN KEY ([FooSelf3Id]) REFERENCES ""Foos""([FooId]));
22+
CREATE TABLE ""FooSelves"" ([FooSelfId] INTEGER PRIMARY KEY, [FooId] int NOT NULL, [Number] int NOT NULL, FOREIGN KEY ([FooId]) REFERENCES ""Foos""([FooId]) ON DELETE CASCADE);
23+
CREATE TABLE ""FooSteps"" ([FooStepId] INTEGER PRIMARY KEY, [FooId] int NOT NULL, [Number] int NOT NULL, FOREIGN KEY ([FooId]) REFERENCES ""Foos""([FooId]) ON DELETE CASCADE);
24+
CREATE TABLE ""FooCompositeKeys"" ([Id] int NOT NULL, [Version] nvarchar (20) NOT NULL COLLATE custom_collate, [Name] nvarchar (255) COLLATE custom_collate, PRIMARY KEY([Id], [Version]));
25+
CREATE TABLE ""FooRelationshipAs"" ([Id] INTEGER PRIMARY KEY, [Name] nvarchar (255) COLLATE custom_collate);
26+
CREATE TABLE ""FooRelationshipBs"" ([Id] INTEGER PRIMARY KEY, [Name] nvarchar (255) COLLATE custom_collate);
27+
CREATE TABLE ""FooRelationshipAFooCompositeKeys"" ([FooRelationshipA_Id] int NOT NULL, [FooCompositeKey_Id] int NOT NULL, [FooCompositeKey_Version] nvarchar (20) NOT NULL COLLATE custom_collate, PRIMARY KEY([FooRelationshipA_Id], [FooCompositeKey_Id], [FooCompositeKey_Version]), FOREIGN KEY ([FooRelationshipA_Id]) REFERENCES ""FooRelationshipAs""([Id]) ON DELETE CASCADE, FOREIGN KEY ([FooCompositeKey_Id], [FooCompositeKey_Version]) REFERENCES ""FooCompositeKeys""([Id], [Version]) ON DELETE CASCADE);
28+
CREATE TABLE ""FooRelationshipBFooCompositeKeys"" ([FooRelationshipB_Id] int NOT NULL, [FooCompositeKey_Id] int NOT NULL, [FooCompositeKey_Version] nvarchar (20) NOT NULL COLLATE custom_collate, PRIMARY KEY([FooRelationshipB_Id], [FooCompositeKey_Id], [FooCompositeKey_Version]), FOREIGN KEY ([FooRelationshipB_Id]) REFERENCES ""FooRelationshipBs""([Id]) ON DELETE CASCADE, FOREIGN KEY ([FooCompositeKey_Id], [FooCompositeKey_Version]) REFERENCES ""FooCompositeKeys""([Id], [Version]) ON DELETE CASCADE);
29+
CREATE INDEX ""IX_MyTable_Id"" ON ""MyTable"" (""Id"");
30+
CREATE INDEX ""IX_Team_TeamsName"" ON ""MyTable"" (""Name"");
31+
CREATE INDEX ""IX_TeamPlayer_Number"" ON ""TeamPlayer"" (""Number"");
32+
CREATE UNIQUE INDEX ""IX_TeamPlayer_NumberPerTeam"" ON ""TeamPlayer"" (""Number"", ""TeamId"");
33+
CREATE INDEX ""IX_TeamPlayer_Mentor_Id"" ON ""TeamPlayer"" (""Mentor_Id"");
34+
CREATE UNIQUE INDEX ""IX_Stadion_Main"" ON ""Stadions"" (""Street"", ""Name"");
35+
CREATE UNIQUE INDEX ""ReservedKeyWordTest"" ON ""Stadions"" (""Order"");
36+
CREATE INDEX ""IX_Stadion_Team_Id"" ON ""Stadions"" (""Team_Id"");
37+
CREATE INDEX ""IX_Foo_FooSelf1Id"" ON ""Foos"" (""FooSelf1Id"");
38+
CREATE INDEX ""IX_Foo_FooSelf2Id"" ON ""Foos"" (""FooSelf2Id"");
39+
CREATE INDEX ""IX_Foo_FooSelf3Id"" ON ""Foos"" (""FooSelf3Id"");
40+
CREATE INDEX ""IX_FooSelf_FooId"" ON ""FooSelves"" (""FooId"");
41+
CREATE INDEX ""IX_FooStep_FooId"" ON ""FooSteps"" (""FooId"");
42+
CREATE INDEX ""IX_FooRelationshipAFooCompositeKey_FooRelationshipA_Id"" ON ""FooRelationshipAFooCompositeKeys"" (""FooRelationshipA_Id"");
43+
CREATE INDEX ""IX_FooRelationshipAFooCompositeKey_FooCompositeKey_Id_FooCompositeKey_Version"" ON ""FooRelationshipAFooCompositeKeys"" (""FooCompositeKey_Id"", ""FooCompositeKey_Version"");
44+
CREATE INDEX ""IX_FooRelationshipBFooCompositeKey_FooRelationshipB_Id"" ON ""FooRelationshipBFooCompositeKeys"" (""FooRelationshipB_Id"");
45+
CREATE INDEX ""IX_FooRelationshipBFooCompositeKey_FooCompositeKey_Id_FooCompositeKey_Version"" ON ""FooRelationshipBFooCompositeKeys"" (""FooCompositeKey_Id"", ""FooCompositeKey_Version"");
46+
";
47+
48+
private static string generatedSql;
49+
50+
// Does not work on the build server. No clue why.
51+
52+
[TestMethod]
53+
public void SqliteSqlGeneratorWithDefaultCollationTest()
54+
{
55+
using (DbConnection connection = new SQLiteConnection("FullUri=file::memory:"))
56+
{
57+
// This is important! Else the in memory database will not work.
58+
connection.Open();
59+
60+
var defaultCollation = new Collation() { Function = CollationFunction.Custom, CustomFunction = "custom_collate" };
61+
using (var context = new DummyDbContext(connection, defaultCollation))
62+
{
63+
// ReSharper disable once UnusedVariable
64+
Player fo = context.Set<Player>().FirstOrDefault();
65+
66+
Assert.AreEqual(RemoveLineEndings(ReferenceSql), RemoveLineEndings(generatedSql));
67+
}
68+
}
69+
}
70+
71+
private static string RemoveLineEndings(string input)
72+
{
73+
string lineSeparator = ((char)0x2028).ToString();
74+
string paragraphSeparator = ((char)0x2029).ToString();
75+
return input.Replace("\r\n", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty).Replace(lineSeparator, string.Empty).Replace(paragraphSeparator, string.Empty);
76+
}
77+
78+
private class DummyDbContext : DbContext
79+
{
80+
private readonly Collation defaultCollation;
81+
82+
public DummyDbContext(DbConnection connection, Collation defaultCollation = null)
83+
: base(connection, false)
84+
{
85+
this.defaultCollation = defaultCollation;
86+
}
87+
88+
protected override void OnModelCreating(DbModelBuilder modelBuilder)
89+
{
90+
// This configuration contains all supported cases.
91+
// So it makes a perfect test to validate whether the
92+
// generated SQL is correct.
93+
ModelConfiguration.Configure(modelBuilder);
94+
var initializer = new AssertInitializer(modelBuilder, defaultCollation);
95+
Database.SetInitializer(initializer);
96+
}
97+
98+
private class AssertInitializer : SqliteInitializerBase<DummyDbContext>
99+
{
100+
private readonly Collation defaultCollation;
101+
102+
public AssertInitializer(DbModelBuilder modelBuilder, Collation defaultCollation)
103+
: base(modelBuilder)
104+
{
105+
this.defaultCollation = defaultCollation;
106+
}
107+
108+
public override void InitializeDatabase(DummyDbContext context)
109+
{
110+
DbModel model = ModelBuilder.Build(context.Database.Connection);
111+
var sqliteSqlGenerator = new SqliteSqlGenerator(defaultCollation);
112+
generatedSql = sqliteSqlGenerator.Generate(model.StoreModel);
113+
base.InitializeDatabase(context);
114+
}
115+
}
116+
}
117+
}
118+
}

SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Data.Entity.Core.Metadata.Edm;
34
using System.Linq;
45
using SQLite.CodeFirst.Extensions;
@@ -11,11 +12,13 @@ internal class ColumnStatementCollectionBuilder : IStatementBuilder<ColumnStatem
1112
{
1213
private readonly IEnumerable<EdmProperty> properties;
1314
private readonly IEnumerable<EdmProperty> keyMembers;
15+
private readonly Collation defaultCollation;
1416

15-
public ColumnStatementCollectionBuilder(IEnumerable<EdmProperty> properties, IEnumerable<EdmProperty> keyMembers)
17+
public ColumnStatementCollectionBuilder(IEnumerable<EdmProperty> properties, IEnumerable<EdmProperty> keyMembers, Collation defaultCollation)
1618
{
1719
this.properties = properties;
1820
this.keyMembers = keyMembers;
21+
this.defaultCollation = defaultCollation;
1922
}
2023

2124
public ColumnStatementCollection BuildStatement()
@@ -39,7 +42,7 @@ private IEnumerable<ColumnStatement> CreateColumnStatements()
3942
AdjustDatatypeForAutogenerationIfNecessary(property, columnStatement);
4043
AddNullConstraintIfNecessary(property, columnStatement);
4144
AddUniqueConstraintIfNecessary(property, columnStatement);
42-
AddCollationConstraintIfNecessary(property, columnStatement);
45+
AddCollationConstraintIfNecessary(property, columnStatement, defaultCollation);
4346
AddPrimaryKeyConstraintAndAdjustTypeIfNecessary(property, columnStatement);
4447
AddDefaultValueConstraintIfNecessary(property, columnStatement);
4548

@@ -73,12 +76,25 @@ private static void AddNullConstraintIfNecessary(EdmProperty property, ColumnSta
7376
}
7477
}
7578

76-
private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement)
79+
private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement, Collation defaultCollation)
7780
{
78-
var value = property.GetCustomAnnotation<CollateAttribute>();
79-
if (value != null)
81+
var collateAttribute = property.GetCustomAnnotation<CollateAttribute>();
82+
if (property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String)
83+
{
84+
// The column is a string type. Check if we have an explicit or default collation.
85+
// If we have both, the explicitly chosen collation takes precedence.
86+
var value = collateAttribute == null ? defaultCollation : collateAttribute.Collation;
87+
if (value != null)
88+
{
89+
columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.Function, CustomCollationFunction = value.CustomFunction });
90+
}
91+
}
92+
else if (collateAttribute != null)
8093
{
81-
columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.Collation, CustomCollationFunction = value.Function });
94+
// Only string columns can be explicitly decorated with CollateAttribute.
95+
var name = $"{property.DeclaringType.Name}.{property.Name}";
96+
var errorMessage = $"CollateAttribute cannot be used on non-string property: {name} (underlying type is {property.PrimitiveType.PrimitiveTypeKind})";
97+
throw new InvalidOperationException(errorMessage);
8298
}
8399
}
84100

SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ namespace SQLite.CodeFirst.Builder
99
internal class CreateDatabaseStatementBuilder : IStatementBuilder<CreateDatabaseStatement>
1010
{
1111
private readonly EdmModel edmModel;
12+
private readonly Collation defaultCollation;
1213

13-
public CreateDatabaseStatementBuilder(EdmModel edmModel)
14+
public CreateDatabaseStatementBuilder(EdmModel edmModel, Collation defaultCollation)
1415
{
1516
this.edmModel = edmModel;
17+
this.defaultCollation = defaultCollation;
1618
}
1719

1820
public CreateDatabaseStatement BuildStatement()
@@ -30,7 +32,7 @@ private IEnumerable<CreateTableStatement> GetCreateTableStatements()
3032

3133
foreach (var entitySet in edmModel.Container.EntitySets)
3234
{
33-
var tableStatementBuilder = new CreateTableStatementBuilder(entitySet, associationTypeContainer);
35+
var tableStatementBuilder = new CreateTableStatementBuilder(entitySet, associationTypeContainer, defaultCollation);
3436
yield return tableStatementBuilder.BuildStatement();
3537
}
3638
}

SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ internal class CreateTableStatementBuilder : IStatementBuilder<CreateTableStatem
1212
{
1313
private readonly EntitySet entitySet;
1414
private readonly AssociationTypeContainer associationTypeContainer;
15+
private readonly Collation defaultCollation;
1516

16-
public CreateTableStatementBuilder(EntitySet entitySet, AssociationTypeContainer associationTypeContainer)
17+
public CreateTableStatementBuilder(EntitySet entitySet, AssociationTypeContainer associationTypeContainer, Collation defaultCollation)
1718
{
1819
this.entitySet = entitySet;
1920
this.associationTypeContainer = associationTypeContainer;
21+
this.defaultCollation = defaultCollation;
2022
}
2123

2224
public CreateTableStatement BuildStatement()
@@ -31,7 +33,7 @@ public CreateTableStatement BuildStatement()
3133
compositePrimaryKeyStatement = new CompositePrimaryKeyStatementBuilder(keyMembers).BuildStatement();
3234
}
3335

34-
var simpleColumnCollection = new ColumnStatementCollectionBuilder(entitySet.ElementType.Properties, keyMembers).BuildStatement();
36+
var simpleColumnCollection = new ColumnStatementCollectionBuilder(entitySet.ElementType.Properties, keyMembers, defaultCollation).BuildStatement();
3537
var foreignKeyCollection = new ForeignKeyStatementBuilder(associationTypeContainer.GetAssociationTypes(entitySet.Name)).BuildStatement();
3638

3739
var columnStatements = new List<IStatement>();

SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,19 @@ public sealed class CollateAttribute : Attribute
1212
{
1313
public CollateAttribute()
1414
{
15-
Collation = CollationFunction.None;
15+
Collation = new Collation();
1616
}
1717

18-
public CollateAttribute(CollationFunction collation)
18+
public CollateAttribute(CollationFunction function)
1919
{
20-
if (collation == CollationFunction.Custom)
21-
{
22-
throw new ArgumentException("If the collation is set to CollationFunction.Custom a function must be specified.", nameof(collation));
23-
}
24-
25-
Collation = collation;
20+
Collation = new Collation(function);
2621
}
27-
public CollateAttribute(CollationFunction collation, string function)
28-
{
29-
if (collation != CollationFunction.Custom && !string.IsNullOrEmpty(function))
30-
{
31-
throw new ArgumentException("If the collation is not set to CollationFunction.Custom a function must not be specified.", nameof(function));
32-
}
33-
34-
if (collation == CollationFunction.Custom && string.IsNullOrEmpty(function))
35-
{
36-
throw new ArgumentException("If the collation is set to CollationFunction.Custom a function must be specified.", nameof(function));
37-
}
3822

39-
Collation = collation;
40-
Function = function;
23+
public CollateAttribute(CollationFunction function, string customFunction)
24+
{
25+
Collation = new Collation(function, customFunction);
4126
}
4227

43-
public CollationFunction Collation { get; }
44-
45-
/// <summary>
46-
/// The name of the custom collating function to use (CollationFunction.Custom).
47-
/// </summary>
48-
public string Function { get; }
28+
public Collation Collation { get; }
4929
}
5030
}

SQLite.CodeFirst/Public/Collation.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
3+
namespace SQLite.CodeFirst
4+
{
5+
/// <summary>
6+
/// This class can be used to specify the default collation for the database. Explicit Collate attributes will take precendence.
7+
/// When SQLite compares two strings, it uses a collating sequence or collating function (two words for the same thing)
8+
/// to determine which string is greater or if the two strings are equal. SQLite has three built-in collating functions (see <see cref="Function"/>).
9+
/// Set <see cref="Function"/> to <see cref="CollationFunction.Custom"/> and specify the name using the function parameter.
10+
/// </summary>
11+
public class Collation
12+
{
13+
public Collation()
14+
: this(CollationFunction.None)
15+
{
16+
}
17+
18+
public Collation(CollationFunction function)
19+
: this(function, null)
20+
{
21+
}
22+
23+
public Collation(CollationFunction function, string customFunction)
24+
{
25+
if (function != CollationFunction.Custom && !string.IsNullOrEmpty(customFunction))
26+
{
27+
throw new ArgumentException("If the collation is not set to CollationFunction.Custom a function must not be specified.", nameof(function));
28+
}
29+
30+
if (function == CollationFunction.Custom && string.IsNullOrEmpty(customFunction))
31+
{
32+
throw new ArgumentException("If the collation is set to CollationFunction.Custom a function must be specified.", nameof(function));
33+
}
34+
35+
CustomFunction = customFunction;
36+
Function = function;
37+
}
38+
39+
public CollationFunction Function { get; set; }
40+
41+
/// <summary>
42+
/// The name of the custom collating function to use (CollationFunction.Custom).
43+
/// </summary>
44+
public string CustomFunction { get; set; }
45+
}
46+
}

SQLite.CodeFirst/Public/Attributes/CollationFunction.cs renamed to SQLite.CodeFirst/Public/CollationFunction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
/// <summary>
44
/// The collation function to use for this column.
5-
/// Is used together with the <see cref="CollateAttribute" />.
5+
/// Is used together with the <see cref="CollateAttribute" />, and when setting a default collation for the database.
66
/// </summary>
77
public enum CollationFunction
88
{

SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ private string GetHashFromModel(DbConnection connection)
172172
private string GetSqlFromModel(DbConnection connection)
173173
{
174174
var model = ModelBuilder.Build(connection);
175-
var sqliteSqlGenerator = new SqliteSqlGenerator();
175+
var sqliteSqlGenerator = new SqliteSqlGenerator(DefaultCollation);
176176
return sqliteSqlGenerator.Generate(model.StoreModel);
177177
}
178178
}

0 commit comments

Comments
 (0)