Skip to content

Commit a305e99

Browse files
magnusbakken-zetadisplaymsallin
authored andcommitted
Added support for a default collation for all tables.
1 parent 732c7a0 commit a305e99

File tree

11 files changed

+177
-13
lines changed

11 files changed

+177
-13
lines changed
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 CollationData() { Collation = CollationFunction.Custom, Function = "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 ICollationData defaultCollation;
81+
82+
public DummyDbContext(DbConnection connection, ICollationData 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 ICollationData defaultCollation;
101+
102+
public AssertInitializer(DbModelBuilder modelBuilder, ICollationData 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: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ internal class ColumnStatementCollectionBuilder : IStatementBuilder<ColumnStatem
1111
{
1212
private readonly IEnumerable<EdmProperty> properties;
1313
private readonly IEnumerable<EdmProperty> keyMembers;
14+
private readonly ICollationData defaultCollation;
1415

15-
public ColumnStatementCollectionBuilder(IEnumerable<EdmProperty> properties, IEnumerable<EdmProperty> keyMembers)
16+
public ColumnStatementCollectionBuilder(IEnumerable<EdmProperty> properties, IEnumerable<EdmProperty> keyMembers, ICollationData defaultCollation)
1617
{
1718
this.properties = properties;
1819
this.keyMembers = keyMembers;
20+
this.defaultCollation = defaultCollation;
1921
}
2022

2123
public ColumnStatementCollection BuildStatement()
@@ -39,7 +41,7 @@ private IEnumerable<ColumnStatement> CreateColumnStatements()
3941
AdjustDatatypeForAutogenerationIfNecessary(property, columnStatement);
4042
AddNullConstraintIfNecessary(property, columnStatement);
4143
AddUniqueConstraintIfNecessary(property, columnStatement);
42-
AddCollationConstraintIfNecessary(property, columnStatement);
44+
AddCollationConstraintIfNecessary(property, columnStatement, defaultCollation);
4345
AddPrimaryKeyConstraintAndAdjustTypeIfNecessary(property, columnStatement);
4446
AddDefaultValueConstraintIfNecessary(property, columnStatement);
4547

@@ -73,9 +75,15 @@ private static void AddNullConstraintIfNecessary(EdmProperty property, ColumnSta
7375
}
7476
}
7577

76-
private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement)
78+
private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement, ICollationData defaultCollation)
7779
{
78-
var value = property.GetCustomAnnotation<CollateAttribute>();
80+
ICollationData value = property.GetCustomAnnotation<CollateAttribute>();
81+
if (value == null && defaultCollation != null && property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String)
82+
{
83+
// Use default collation if one is given and the property is a string.
84+
value = defaultCollation;
85+
}
86+
7987
if (value != null)
8088
{
8189
columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.Collation, CustomCollationFunction = value.Function });

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 ICollationData defaultCollation;
1213

13-
public CreateDatabaseStatementBuilder(EdmModel edmModel)
14+
public CreateDatabaseStatementBuilder(EdmModel edmModel, ICollationData 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 ICollationData defaultCollation;
1516

16-
public CreateTableStatementBuilder(EntitySet entitySet, AssociationTypeContainer associationTypeContainer)
17+
public CreateTableStatementBuilder(EntitySet entitySet, AssociationTypeContainer associationTypeContainer, ICollationData 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace SQLite.CodeFirst
88
/// It is possible to specify a custom collating function. Set <see cref="CollationFunction"/> to <see cref="CollationFunction.Custom"/> and specify the name using the function parameter.
99
/// </summary>
1010
[AttributeUsage(AttributeTargets.Property)]
11-
public sealed class CollateAttribute : Attribute
11+
public sealed class CollateAttribute : Attribute, ICollationData
1212
{
1313
public CollateAttribute()
1414
{
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace SQLite.CodeFirst
2+
{
3+
public class CollationData : ICollationData
4+
{
5+
public CollationFunction Collation { get; set; }
6+
7+
public string Function { get; set; }
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace SQLite.CodeFirst
2+
{
3+
public interface ICollationData
4+
{
5+
CollationFunction Collation { get; }
6+
7+
string Function { get; }
8+
}
9+
}

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
}

SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ protected SqliteInitializerBase(DbModelBuilder modelBuilder)
5555
}
5656
}
5757

58+
public ICollationData DefaultCollation { get; set; }
59+
5860
protected DbModelBuilder ModelBuilder { get; }
5961

6062
/// <summary>
@@ -71,7 +73,7 @@ public virtual void InitializeDatabase(TContext context)
7173
string dbFile = GetDatabasePathFromContext(context);
7274
InMemoryAwareFile.CreateDirectory(dbFile);
7375

74-
var sqliteDatabaseCreator = new SqliteDatabaseCreator();
76+
var sqliteDatabaseCreator = new SqliteDatabaseCreator(DefaultCollation);
7577
sqliteDatabaseCreator.Create(context.Database, model);
7678

7779
Seed(context);

SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ namespace SQLite.CodeFirst
1616
/// </summary>
1717
public class SqliteDatabaseCreator : IDatabaseCreator
1818
{
19+
public SqliteDatabaseCreator(ICollationData defaultCollation = null)
20+
{
21+
DefaultCollation = defaultCollation;
22+
}
23+
24+
public ICollationData DefaultCollation { get; }
25+
1926
/// <summary>
2027
/// Creates the SQLite-Database.
2128
/// </summary>
@@ -24,7 +31,7 @@ public void Create(Database db, DbModel model)
2431
if (db == null) throw new ArgumentNullException("db");
2532
if (model == null) throw new ArgumentNullException("model");
2633

27-
var sqliteSqlGenerator = new SqliteSqlGenerator();
34+
var sqliteSqlGenerator = new SqliteSqlGenerator(DefaultCollation);
2835
string sql = sqliteSqlGenerator.Generate(model.StoreModel);
2936
Debug.Write(sql);
3037
db.ExecuteSqlCommand(TransactionalBehavior.EnsureTransaction, sql);

0 commit comments

Comments
 (0)