From 264075fc3fa69fae165a90faaf4e9913ceb46077 Mon Sep 17 00:00:00 2001 From: Magnus Grindal Bakken Date: Thu, 29 Apr 2021 14:13:12 +0200 Subject: [PATCH 1/3] Added support for a default collation for all tables. --- .../SqlGenerationDefaultCollationTest.cs | 118 ++++++++++++++++++ .../ColumnStatementCollectionBuilder.cs | 16 ++- .../Builder/CreateDatabaseStatementBuilder.cs | 6 +- .../Builder/CreateTableStatementBuilder.cs | 6 +- .../Public/Attributes/CollateAttribute.cs | 2 +- .../Public/Attributes/CollationData.cs | 9 ++ .../Public/Attributes/ICollationData.cs | 9 ++ ...qliteDropCreateDatabaseWhenModelChanges.cs | 2 +- .../DbInitializers/SqliteInitializerBase.cs | 4 +- .../Public/SqliteDatabaseCreator.cs | 9 +- SQLite.CodeFirst/Public/SqliteSqlGenerator.cs | 9 +- 11 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs create mode 100644 SQLite.CodeFirst/Public/Attributes/CollationData.cs create mode 100644 SQLite.CodeFirst/Public/Attributes/ICollationData.cs diff --git a/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs b/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs new file mode 100644 index 0000000..31236aa --- /dev/null +++ b/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs @@ -0,0 +1,118 @@ +using System.Data.Common; +using System.Data.Entity; +using System.Data.Entity.Infrastructure; +using System.Data.SQLite; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SQLite.CodeFirst.Console; +using SQLite.CodeFirst.Console.Entity; + +namespace SQLite.CodeFirst.Test.IntegrationTests +{ + [TestClass] + public class SqlGenerationDefaultCollationTest + { + private const string ReferenceSql = + @" +CREATE TABLE ""MyTable"" ([Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, [Name] nvarchar NOT NULL COLLATE custom_collate, FOREIGN KEY ([Id]) REFERENCES ""Coaches""([Id])); +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'))); +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); +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); +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])); +CREATE TABLE ""FooSelves"" ([FooSelfId] INTEGER PRIMARY KEY, [FooId] int NOT NULL, [Number] int NOT NULL, FOREIGN KEY ([FooId]) REFERENCES ""Foos""([FooId]) ON DELETE CASCADE); +CREATE TABLE ""FooSteps"" ([FooStepId] INTEGER PRIMARY KEY, [FooId] int NOT NULL, [Number] int NOT NULL, FOREIGN KEY ([FooId]) REFERENCES ""Foos""([FooId]) ON DELETE CASCADE); +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])); +CREATE TABLE ""FooRelationshipAs"" ([Id] INTEGER PRIMARY KEY, [Name] nvarchar (255) COLLATE custom_collate); +CREATE TABLE ""FooRelationshipBs"" ([Id] INTEGER PRIMARY KEY, [Name] nvarchar (255) COLLATE custom_collate); +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); +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); +CREATE INDEX ""IX_MyTable_Id"" ON ""MyTable"" (""Id""); +CREATE INDEX ""IX_Team_TeamsName"" ON ""MyTable"" (""Name""); +CREATE INDEX ""IX_TeamPlayer_Number"" ON ""TeamPlayer"" (""Number""); +CREATE UNIQUE INDEX ""IX_TeamPlayer_NumberPerTeam"" ON ""TeamPlayer"" (""Number"", ""TeamId""); +CREATE INDEX ""IX_TeamPlayer_Mentor_Id"" ON ""TeamPlayer"" (""Mentor_Id""); +CREATE UNIQUE INDEX ""IX_Stadion_Main"" ON ""Stadions"" (""Street"", ""Name""); +CREATE UNIQUE INDEX ""ReservedKeyWordTest"" ON ""Stadions"" (""Order""); +CREATE INDEX ""IX_Stadion_Team_Id"" ON ""Stadions"" (""Team_Id""); +CREATE INDEX ""IX_Foo_FooSelf1Id"" ON ""Foos"" (""FooSelf1Id""); +CREATE INDEX ""IX_Foo_FooSelf2Id"" ON ""Foos"" (""FooSelf2Id""); +CREATE INDEX ""IX_Foo_FooSelf3Id"" ON ""Foos"" (""FooSelf3Id""); +CREATE INDEX ""IX_FooSelf_FooId"" ON ""FooSelves"" (""FooId""); +CREATE INDEX ""IX_FooStep_FooId"" ON ""FooSteps"" (""FooId""); +CREATE INDEX ""IX_FooRelationshipAFooCompositeKey_FooRelationshipA_Id"" ON ""FooRelationshipAFooCompositeKeys"" (""FooRelationshipA_Id""); +CREATE INDEX ""IX_FooRelationshipAFooCompositeKey_FooCompositeKey_Id_FooCompositeKey_Version"" ON ""FooRelationshipAFooCompositeKeys"" (""FooCompositeKey_Id"", ""FooCompositeKey_Version""); +CREATE INDEX ""IX_FooRelationshipBFooCompositeKey_FooRelationshipB_Id"" ON ""FooRelationshipBFooCompositeKeys"" (""FooRelationshipB_Id""); +CREATE INDEX ""IX_FooRelationshipBFooCompositeKey_FooCompositeKey_Id_FooCompositeKey_Version"" ON ""FooRelationshipBFooCompositeKeys"" (""FooCompositeKey_Id"", ""FooCompositeKey_Version""); +"; + + private static string generatedSql; + + // Does not work on the build server. No clue why. + + [TestMethod] + public void SqliteSqlGeneratorWithDefaultCollationTest() + { + using (DbConnection connection = new SQLiteConnection("FullUri=file::memory:")) + { + // This is important! Else the in memory database will not work. + connection.Open(); + + var defaultCollation = new CollationData() { Collation = CollationFunction.Custom, Function = "custom_collate" }; + using (var context = new DummyDbContext(connection, defaultCollation)) + { + // ReSharper disable once UnusedVariable + Player fo = context.Set().FirstOrDefault(); + + Assert.AreEqual(RemoveLineEndings(ReferenceSql), RemoveLineEndings(generatedSql)); + } + } + } + + private static string RemoveLineEndings(string input) + { + string lineSeparator = ((char)0x2028).ToString(); + string paragraphSeparator = ((char)0x2029).ToString(); + return input.Replace("\r\n", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty).Replace(lineSeparator, string.Empty).Replace(paragraphSeparator, string.Empty); + } + + private class DummyDbContext : DbContext + { + private readonly ICollationData defaultCollation; + + public DummyDbContext(DbConnection connection, ICollationData defaultCollation = null) + : base(connection, false) + { + this.defaultCollation = defaultCollation; + } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + // This configuration contains all supported cases. + // So it makes a perfect test to validate whether the + // generated SQL is correct. + ModelConfiguration.Configure(modelBuilder); + var initializer = new AssertInitializer(modelBuilder, defaultCollation); + Database.SetInitializer(initializer); + } + + private class AssertInitializer : SqliteInitializerBase + { + private readonly ICollationData defaultCollation; + + public AssertInitializer(DbModelBuilder modelBuilder, ICollationData defaultCollation) + : base(modelBuilder) + { + this.defaultCollation = defaultCollation; + } + + public override void InitializeDatabase(DummyDbContext context) + { + DbModel model = ModelBuilder.Build(context.Database.Connection); + var sqliteSqlGenerator = new SqliteSqlGenerator(defaultCollation); + generatedSql = sqliteSqlGenerator.Generate(model.StoreModel); + base.InitializeDatabase(context); + } + } + } + } +} \ No newline at end of file diff --git a/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs b/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs index 93043c2..038a23b 100644 --- a/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs +++ b/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs @@ -11,11 +11,13 @@ internal class ColumnStatementCollectionBuilder : IStatementBuilder properties; private readonly IEnumerable keyMembers; + private readonly ICollationData defaultCollation; - public ColumnStatementCollectionBuilder(IEnumerable properties, IEnumerable keyMembers) + public ColumnStatementCollectionBuilder(IEnumerable properties, IEnumerable keyMembers, ICollationData defaultCollation) { this.properties = properties; this.keyMembers = keyMembers; + this.defaultCollation = defaultCollation; } public ColumnStatementCollection BuildStatement() @@ -39,7 +41,7 @@ private IEnumerable CreateColumnStatements() AdjustDatatypeForAutogenerationIfNecessary(property, columnStatement); AddNullConstraintIfNecessary(property, columnStatement); AddUniqueConstraintIfNecessary(property, columnStatement); - AddCollationConstraintIfNecessary(property, columnStatement); + AddCollationConstraintIfNecessary(property, columnStatement, defaultCollation); AddPrimaryKeyConstraintAndAdjustTypeIfNecessary(property, columnStatement); AddDefaultValueConstraintIfNecessary(property, columnStatement); @@ -73,9 +75,15 @@ private static void AddNullConstraintIfNecessary(EdmProperty property, ColumnSta } } - private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement) + private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement, ICollationData defaultCollation) { - var value = property.GetCustomAnnotation(); + ICollationData value = property.GetCustomAnnotation(); + if (value == null && defaultCollation != null && property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String) + { + // Use default collation if one is given and the property is a string. + value = defaultCollation; + } + if (value != null) { columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.Collation, CustomCollationFunction = value.Function }); diff --git a/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs b/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs index 65862a2..4229d36 100644 --- a/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs +++ b/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs @@ -9,10 +9,12 @@ namespace SQLite.CodeFirst.Builder internal class CreateDatabaseStatementBuilder : IStatementBuilder { private readonly EdmModel edmModel; + private readonly ICollationData defaultCollation; - public CreateDatabaseStatementBuilder(EdmModel edmModel) + public CreateDatabaseStatementBuilder(EdmModel edmModel, ICollationData defaultCollation) { this.edmModel = edmModel; + this.defaultCollation = defaultCollation; } public CreateDatabaseStatement BuildStatement() @@ -30,7 +32,7 @@ private IEnumerable GetCreateTableStatements() foreach (var entitySet in edmModel.Container.EntitySets) { - var tableStatementBuilder = new CreateTableStatementBuilder(entitySet, associationTypeContainer); + var tableStatementBuilder = new CreateTableStatementBuilder(entitySet, associationTypeContainer, defaultCollation); yield return tableStatementBuilder.BuildStatement(); } } diff --git a/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs b/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs index b86f701..174f012 100644 --- a/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs +++ b/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs @@ -12,11 +12,13 @@ internal class CreateTableStatementBuilder : IStatementBuilder(); diff --git a/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs b/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs index 2dfc272..d3dd3e7 100644 --- a/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs +++ b/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs @@ -8,7 +8,7 @@ namespace SQLite.CodeFirst /// It is possible to specify a custom collating function. Set to and specify the name using the function parameter. /// [AttributeUsage(AttributeTargets.Property)] - public sealed class CollateAttribute : Attribute + public sealed class CollateAttribute : Attribute, ICollationData { public CollateAttribute() { diff --git a/SQLite.CodeFirst/Public/Attributes/CollationData.cs b/SQLite.CodeFirst/Public/Attributes/CollationData.cs new file mode 100644 index 0000000..9e1505b --- /dev/null +++ b/SQLite.CodeFirst/Public/Attributes/CollationData.cs @@ -0,0 +1,9 @@ +namespace SQLite.CodeFirst +{ + public class CollationData : ICollationData + { + public CollationFunction Collation { get; set; } + + public string Function { get; set; } + } +} diff --git a/SQLite.CodeFirst/Public/Attributes/ICollationData.cs b/SQLite.CodeFirst/Public/Attributes/ICollationData.cs new file mode 100644 index 0000000..8bcc072 --- /dev/null +++ b/SQLite.CodeFirst/Public/Attributes/ICollationData.cs @@ -0,0 +1,9 @@ +namespace SQLite.CodeFirst +{ + public interface ICollationData + { + CollationFunction Collation { get; } + + string Function { get; } + } +} \ No newline at end of file diff --git a/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs b/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs index d83c3a9..f8029fc 100644 --- a/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs +++ b/SQLite.CodeFirst/Public/DbInitializers/SqliteDropCreateDatabaseWhenModelChanges.cs @@ -172,7 +172,7 @@ private string GetHashFromModel(DbConnection connection) private string GetSqlFromModel(DbConnection connection) { var model = ModelBuilder.Build(connection); - var sqliteSqlGenerator = new SqliteSqlGenerator(); + var sqliteSqlGenerator = new SqliteSqlGenerator(DefaultCollation); return sqliteSqlGenerator.Generate(model.StoreModel); } } diff --git a/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs b/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs index a84cdf8..284bf5c 100644 --- a/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs +++ b/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs @@ -55,6 +55,8 @@ protected SqliteInitializerBase(DbModelBuilder modelBuilder) } } + public ICollationData DefaultCollation { get; set; } + protected DbModelBuilder ModelBuilder { get; } /// @@ -71,7 +73,7 @@ public virtual void InitializeDatabase(TContext context) string dbFile = GetDatabasePathFromContext(context); InMemoryAwareFile.CreateDirectory(dbFile); - var sqliteDatabaseCreator = new SqliteDatabaseCreator(); + var sqliteDatabaseCreator = new SqliteDatabaseCreator(DefaultCollation); sqliteDatabaseCreator.Create(context.Database, model); Seed(context); diff --git a/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs b/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs index 4fd325e..2dd6b40 100644 --- a/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs +++ b/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs @@ -16,6 +16,13 @@ namespace SQLite.CodeFirst /// public class SqliteDatabaseCreator : IDatabaseCreator { + public SqliteDatabaseCreator(ICollationData defaultCollation = null) + { + DefaultCollation = defaultCollation; + } + + public ICollationData DefaultCollation { get; } + /// /// Creates the SQLite-Database. /// @@ -24,7 +31,7 @@ public void Create(Database db, DbModel model) if (db == null) throw new ArgumentNullException("db"); if (model == null) throw new ArgumentNullException("model"); - var sqliteSqlGenerator = new SqliteSqlGenerator(); + var sqliteSqlGenerator = new SqliteSqlGenerator(DefaultCollation); string sql = sqliteSqlGenerator.Generate(model.StoreModel); Debug.Write(sql); db.ExecuteSqlCommand(TransactionalBehavior.EnsureTransaction, sql); diff --git a/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs b/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs index d0f248a..c8fd613 100644 --- a/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs +++ b/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs @@ -9,12 +9,19 @@ namespace SQLite.CodeFirst /// public class SqliteSqlGenerator : ISqlGenerator { + public SqliteSqlGenerator(ICollationData defaultCollation = null) + { + DefaultCollation = defaultCollation; + } + + public ICollationData DefaultCollation { get; } + /// /// Generates the SQL statement, based on the . /// public string Generate(EdmModel storeModel) { - IStatementBuilder statementBuilder = new CreateDatabaseStatementBuilder(storeModel); + IStatementBuilder statementBuilder = new CreateDatabaseStatementBuilder(storeModel, DefaultCollation); IStatement statement = statementBuilder.BuildStatement(); return statement.CreateStatement(); } From e65a08a1dbe1ded7cb5d6be3a9e04afe9ed1c071 Mon Sep 17 00:00:00 2001 From: Magnus Grindal Bakken Date: Thu, 6 May 2021 13:12:06 +0200 Subject: [PATCH 2/3] Adjustments: - Renamed CollationData to Collation. - Removed the ICollationData interface. - Moved Collation and CollationFunction out of Attributes. - Changed the way the default collation is passed around to use the Collation class. --- .../SqlGenerationDefaultCollationTest.cs | 10 +++++----- .../ColumnStatementCollectionBuilder.cs | 17 +++++++++-------- .../Builder/CreateDatabaseStatementBuilder.cs | 4 ++-- .../Builder/CreateTableStatementBuilder.cs | 4 ++-- .../Public/Attributes/CollateAttribute.cs | 15 +++++++-------- .../Public/Attributes/CollationData.cs | 9 --------- .../Public/Attributes/ICollationData.cs | 9 --------- SQLite.CodeFirst/Public/Collation.cs | 18 ++++++++++++++++++ .../{Attributes => }/CollationFunction.cs | 2 +- .../DbInitializers/SqliteInitializerBase.cs | 5 +++-- .../Public/SqliteDatabaseCreator.cs | 4 ++-- SQLite.CodeFirst/Public/SqliteSqlGenerator.cs | 4 ++-- 12 files changed, 51 insertions(+), 50 deletions(-) delete mode 100644 SQLite.CodeFirst/Public/Attributes/CollationData.cs delete mode 100644 SQLite.CodeFirst/Public/Attributes/ICollationData.cs create mode 100644 SQLite.CodeFirst/Public/Collation.cs rename SQLite.CodeFirst/Public/{Attributes => }/CollationFunction.cs (95%) diff --git a/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs b/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs index 31236aa..7e7488f 100644 --- a/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs +++ b/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs @@ -57,7 +57,7 @@ public void SqliteSqlGeneratorWithDefaultCollationTest() // This is important! Else the in memory database will not work. connection.Open(); - var defaultCollation = new CollationData() { Collation = CollationFunction.Custom, Function = "custom_collate" }; + var defaultCollation = new Collation() { CollationFunction = CollationFunction.Custom, Function = "custom_collate" }; using (var context = new DummyDbContext(connection, defaultCollation)) { // ReSharper disable once UnusedVariable @@ -77,9 +77,9 @@ private static string RemoveLineEndings(string input) private class DummyDbContext : DbContext { - private readonly ICollationData defaultCollation; + private readonly Collation defaultCollation; - public DummyDbContext(DbConnection connection, ICollationData defaultCollation = null) + public DummyDbContext(DbConnection connection, Collation defaultCollation = null) : base(connection, false) { this.defaultCollation = defaultCollation; @@ -97,9 +97,9 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) private class AssertInitializer : SqliteInitializerBase { - private readonly ICollationData defaultCollation; + private readonly Collation defaultCollation; - public AssertInitializer(DbModelBuilder modelBuilder, ICollationData defaultCollation) + public AssertInitializer(DbModelBuilder modelBuilder, Collation defaultCollation) : base(modelBuilder) { this.defaultCollation = defaultCollation; diff --git a/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs b/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs index 038a23b..61aca7d 100644 --- a/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs +++ b/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs @@ -11,9 +11,9 @@ internal class ColumnStatementCollectionBuilder : IStatementBuilder properties; private readonly IEnumerable keyMembers; - private readonly ICollationData defaultCollation; + private readonly Collation defaultCollation; - public ColumnStatementCollectionBuilder(IEnumerable properties, IEnumerable keyMembers, ICollationData defaultCollation) + public ColumnStatementCollectionBuilder(IEnumerable properties, IEnumerable keyMembers, Collation defaultCollation) { this.properties = properties; this.keyMembers = keyMembers; @@ -75,18 +75,19 @@ private static void AddNullConstraintIfNecessary(EdmProperty property, ColumnSta } } - private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement, ICollationData defaultCollation) + private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement, Collation defaultCollation) { - ICollationData value = property.GetCustomAnnotation(); - if (value == null && defaultCollation != null && property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String) + if (property.PrimitiveType.PrimitiveTypeKind != PrimitiveTypeKind.String) { - // Use default collation if one is given and the property is a string. - value = defaultCollation; + return; } + var collateAttribute = property.GetCustomAnnotation(); + var value = collateAttribute == null ? defaultCollation : collateAttribute.CollationData; + if (value != null) { - columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.Collation, CustomCollationFunction = value.Function }); + columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.CollationFunction, CustomCollationFunction = value.Function }); } } diff --git a/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs b/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs index 4229d36..ea87cca 100644 --- a/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs +++ b/SQLite.CodeFirst/Internal/Builder/CreateDatabaseStatementBuilder.cs @@ -9,9 +9,9 @@ namespace SQLite.CodeFirst.Builder internal class CreateDatabaseStatementBuilder : IStatementBuilder { private readonly EdmModel edmModel; - private readonly ICollationData defaultCollation; + private readonly Collation defaultCollation; - public CreateDatabaseStatementBuilder(EdmModel edmModel, ICollationData defaultCollation) + public CreateDatabaseStatementBuilder(EdmModel edmModel, Collation defaultCollation) { this.edmModel = edmModel; this.defaultCollation = defaultCollation; diff --git a/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs b/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs index 174f012..ac3198b 100644 --- a/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs +++ b/SQLite.CodeFirst/Internal/Builder/CreateTableStatementBuilder.cs @@ -12,9 +12,9 @@ internal class CreateTableStatementBuilder : IStatementBuilder to and specify the name using the function parameter. /// [AttributeUsage(AttributeTargets.Property)] - public sealed class CollateAttribute : Attribute, ICollationData + public sealed class CollateAttribute : Attribute { public CollateAttribute() + : this(CollationFunction.None) { - Collation = CollationFunction.None; } public CollateAttribute(CollationFunction collation) + : this(collation, null) { - if (collation == CollationFunction.Custom) - { - throw new ArgumentException("If the collation is set to CollationFunction.Custom a function must be specified.", nameof(collation)); - } - - Collation = collation; } + public CollateAttribute(CollationFunction collation, string function) { if (collation != CollationFunction.Custom && !string.IsNullOrEmpty(function)) @@ -38,6 +34,7 @@ public CollateAttribute(CollationFunction collation, string function) Collation = collation; Function = function; + CollationData = new Collation() { CollationFunction = collation, Function = function }; } public CollationFunction Collation { get; } @@ -46,5 +43,7 @@ public CollateAttribute(CollationFunction collation, string function) /// The name of the custom collating function to use (CollationFunction.Custom). /// public string Function { get; } + + public Collation CollationData { get; } } } \ No newline at end of file diff --git a/SQLite.CodeFirst/Public/Attributes/CollationData.cs b/SQLite.CodeFirst/Public/Attributes/CollationData.cs deleted file mode 100644 index 9e1505b..0000000 --- a/SQLite.CodeFirst/Public/Attributes/CollationData.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SQLite.CodeFirst -{ - public class CollationData : ICollationData - { - public CollationFunction Collation { get; set; } - - public string Function { get; set; } - } -} diff --git a/SQLite.CodeFirst/Public/Attributes/ICollationData.cs b/SQLite.CodeFirst/Public/Attributes/ICollationData.cs deleted file mode 100644 index 8bcc072..0000000 --- a/SQLite.CodeFirst/Public/Attributes/ICollationData.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace SQLite.CodeFirst -{ - public interface ICollationData - { - CollationFunction Collation { get; } - - string Function { get; } - } -} \ No newline at end of file diff --git a/SQLite.CodeFirst/Public/Collation.cs b/SQLite.CodeFirst/Public/Collation.cs new file mode 100644 index 0000000..acfada8 --- /dev/null +++ b/SQLite.CodeFirst/Public/Collation.cs @@ -0,0 +1,18 @@ +namespace SQLite.CodeFirst +{ + /// + /// This class can be used to specify the default collation for the database. Explicit Collate attributes will take precendence. + /// When SQLite compares two strings, it uses a collating sequence or collating function (two words for the same thing) + /// to determine which string is greater or if the two strings are equal. SQLite has three built-in collating functions (see ). + /// Set to and specify the name using the function parameter. + /// + public class Collation + { + public CollationFunction CollationFunction { get; set; } + + /// + /// The name of the custom collating function to use (CollationFunction.Custom). + /// + public string Function { get; set; } + } +} diff --git a/SQLite.CodeFirst/Public/Attributes/CollationFunction.cs b/SQLite.CodeFirst/Public/CollationFunction.cs similarity index 95% rename from SQLite.CodeFirst/Public/Attributes/CollationFunction.cs rename to SQLite.CodeFirst/Public/CollationFunction.cs index 1bb7f30..6bd054d 100644 --- a/SQLite.CodeFirst/Public/Attributes/CollationFunction.cs +++ b/SQLite.CodeFirst/Public/CollationFunction.cs @@ -2,7 +2,7 @@ { /// /// The collation function to use for this column. - /// Is used together with the . + /// Is used together with the , and when setting a default collation for the database. /// public enum CollationFunction { diff --git a/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs b/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs index 284bf5c..1309b1f 100644 --- a/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs +++ b/SQLite.CodeFirst/Public/DbInitializers/SqliteInitializerBase.cs @@ -24,9 +24,10 @@ namespace SQLite.CodeFirst public abstract class SqliteInitializerBase : IDatabaseInitializer where TContext : DbContext { - protected SqliteInitializerBase(DbModelBuilder modelBuilder) + protected SqliteInitializerBase(DbModelBuilder modelBuilder, Collation defaultCollation = null) { ModelBuilder = modelBuilder ?? throw new ArgumentNullException(nameof(modelBuilder)); + DefaultCollation = defaultCollation; // This convention will crash the SQLite Provider before "InitializeDatabase" gets called. // See https://github.com/msallin/SQLiteCodeFirst/issues/7 for details. @@ -55,7 +56,7 @@ protected SqliteInitializerBase(DbModelBuilder modelBuilder) } } - public ICollationData DefaultCollation { get; set; } + public Collation DefaultCollation { get; } protected DbModelBuilder ModelBuilder { get; } diff --git a/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs b/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs index 2dd6b40..0626c24 100644 --- a/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs +++ b/SQLite.CodeFirst/Public/SqliteDatabaseCreator.cs @@ -16,12 +16,12 @@ namespace SQLite.CodeFirst /// public class SqliteDatabaseCreator : IDatabaseCreator { - public SqliteDatabaseCreator(ICollationData defaultCollation = null) + public SqliteDatabaseCreator(Collation defaultCollation = null) { DefaultCollation = defaultCollation; } - public ICollationData DefaultCollation { get; } + public Collation DefaultCollation { get; } /// /// Creates the SQLite-Database. diff --git a/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs b/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs index c8fd613..944115e 100644 --- a/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs +++ b/SQLite.CodeFirst/Public/SqliteSqlGenerator.cs @@ -9,12 +9,12 @@ namespace SQLite.CodeFirst /// public class SqliteSqlGenerator : ISqlGenerator { - public SqliteSqlGenerator(ICollationData defaultCollation = null) + public SqliteSqlGenerator(Collation defaultCollation = null) { DefaultCollation = defaultCollation; } - public ICollationData DefaultCollation { get; } + public Collation DefaultCollation { get; } /// /// Generates the SQL statement, based on the . From 97fe47bd76f963eee7e7f68d04afbf41abb42256 Mon Sep 17 00:00:00 2001 From: Magnus Grindal Bakken Date: Mon, 10 May 2021 10:19:12 +0200 Subject: [PATCH 3/3] More changes for default collations. - Changed CollateAttribute to use Collation internally. - Throw an exception when CollateAttribute has been used on a non-string property. - Added a line about default collations to the readme. --- README.md | 1 + .../SqlGenerationDefaultCollationTest.cs | 2 +- .../ColumnStatementCollectionBuilder.cs | 25 +++++++----- .../Public/Attributes/CollateAttribute.cs | 31 +++------------ SQLite.CodeFirst/Public/Collation.cs | 38 ++++++++++++++++--- 5 files changed, 57 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fe628db..d3be8f2 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The following features are supported: - 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`) - Unique constraint (Decorate columns with the `UniqueAttribute`, which is part of this library) - Collate constraint (Decorate columns with the `CollateAttribute`, which is part of this library. Use `CollationFunction.Custom` to specify a own collation function.) +- Default collation (pass an instance of Collation as constructor parameter for an initializer to specify a default collation). - SQL default value (Decorate columns with the `SqlDefaultValueAttribute`, which is part of this library) ## Install diff --git a/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs b/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs index 7e7488f..d6cbd08 100644 --- a/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs +++ b/SQLite.CodeFirst.Test/IntegrationTests/SqlGenerationDefaultCollationTest.cs @@ -57,7 +57,7 @@ public void SqliteSqlGeneratorWithDefaultCollationTest() // This is important! Else the in memory database will not work. connection.Open(); - var defaultCollation = new Collation() { CollationFunction = CollationFunction.Custom, Function = "custom_collate" }; + var defaultCollation = new Collation() { Function = CollationFunction.Custom, CustomFunction = "custom_collate" }; using (var context = new DummyDbContext(connection, defaultCollation)) { // ReSharper disable once UnusedVariable diff --git a/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs b/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs index 61aca7d..3fba1bb 100644 --- a/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs +++ b/SQLite.CodeFirst/Internal/Builder/ColumnStatementCollectionBuilder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Data.Entity.Core.Metadata.Edm; using System.Linq; using SQLite.CodeFirst.Extensions; @@ -77,17 +78,23 @@ private static void AddNullConstraintIfNecessary(EdmProperty property, ColumnSta private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement, Collation defaultCollation) { - if (property.PrimitiveType.PrimitiveTypeKind != PrimitiveTypeKind.String) + var collateAttribute = property.GetCustomAnnotation(); + if (property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String) { - return; + // The column is a string type. Check if we have an explicit or default collation. + // If we have both, the explicitly chosen collation takes precedence. + var value = collateAttribute == null ? defaultCollation : collateAttribute.Collation; + if (value != null) + { + columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.Function, CustomCollationFunction = value.CustomFunction }); + } } - - var collateAttribute = property.GetCustomAnnotation(); - var value = collateAttribute == null ? defaultCollation : collateAttribute.CollationData; - - if (value != null) + else if (collateAttribute != null) { - columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.CollationFunction, CustomCollationFunction = value.Function }); + // Only string columns can be explicitly decorated with CollateAttribute. + var name = $"{property.DeclaringType.Name}.{property.Name}"; + var errorMessage = $"CollateAttribute cannot be used on non-string property: {name} (underlying type is {property.PrimitiveType.PrimitiveTypeKind})"; + throw new InvalidOperationException(errorMessage); } } diff --git a/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs b/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs index 8382352..636965a 100644 --- a/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs +++ b/SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs @@ -11,39 +11,20 @@ namespace SQLite.CodeFirst public sealed class CollateAttribute : Attribute { public CollateAttribute() - : this(CollationFunction.None) { + Collation = new Collation(); } - public CollateAttribute(CollationFunction collation) - : this(collation, null) + public CollateAttribute(CollationFunction function) { + Collation = new Collation(function); } - public CollateAttribute(CollationFunction collation, string function) + public CollateAttribute(CollationFunction function, string customFunction) { - if (collation != CollationFunction.Custom && !string.IsNullOrEmpty(function)) - { - throw new ArgumentException("If the collation is not set to CollationFunction.Custom a function must not be specified.", nameof(function)); - } - - if (collation == CollationFunction.Custom && string.IsNullOrEmpty(function)) - { - throw new ArgumentException("If the collation is set to CollationFunction.Custom a function must be specified.", nameof(function)); - } - - Collation = collation; - Function = function; - CollationData = new Collation() { CollationFunction = collation, Function = function }; + Collation = new Collation(function, customFunction); } - public CollationFunction Collation { get; } - - /// - /// The name of the custom collating function to use (CollationFunction.Custom). - /// - public string Function { get; } - - public Collation CollationData { get; } + public Collation Collation { get; } } } \ No newline at end of file diff --git a/SQLite.CodeFirst/Public/Collation.cs b/SQLite.CodeFirst/Public/Collation.cs index acfada8..0695e73 100644 --- a/SQLite.CodeFirst/Public/Collation.cs +++ b/SQLite.CodeFirst/Public/Collation.cs @@ -1,18 +1,46 @@ -namespace SQLite.CodeFirst +using System; + +namespace SQLite.CodeFirst { /// /// This class can be used to specify the default collation for the database. Explicit Collate attributes will take precendence. /// When SQLite compares two strings, it uses a collating sequence or collating function (two words for the same thing) - /// to determine which string is greater or if the two strings are equal. SQLite has three built-in collating functions (see ). - /// Set to and specify the name using the function parameter. + /// to determine which string is greater or if the two strings are equal. SQLite has three built-in collating functions (see ). + /// Set to and specify the name using the function parameter. /// public class Collation { - public CollationFunction CollationFunction { get; set; } + public Collation() + : this(CollationFunction.None) + { + } + + public Collation(CollationFunction function) + : this(function, null) + { + } + + public Collation(CollationFunction function, string customFunction) + { + if (function != CollationFunction.Custom && !string.IsNullOrEmpty(customFunction)) + { + throw new ArgumentException("If the collation is not set to CollationFunction.Custom a function must not be specified.", nameof(function)); + } + + if (function == CollationFunction.Custom && string.IsNullOrEmpty(customFunction)) + { + throw new ArgumentException("If the collation is set to CollationFunction.Custom a function must be specified.", nameof(function)); + } + + CustomFunction = customFunction; + Function = function; + } + + public CollationFunction Function { get; set; } /// /// The name of the custom collating function to use (CollationFunction.Custom). /// - public string Function { get; set; } + public string CustomFunction { get; set; } } }