Skip to content

Commit

Permalink
#163 Add support for defining a default collation
Browse files Browse the repository at this point in the history
  • Loading branch information
magnusbakken-zetadisplay authored and msallin committed May 10, 2021
1 parent 732c7a0 commit 58b3371
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 44 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 Collation() { Function = CollationFunction.Custom, CustomFunction = "custom_collate" };
using (var context = new DummyDbContext(connection, defaultCollation))
{
// ReSharper disable once UnusedVariable
Player fo = context.Set<Player>().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 Collation defaultCollation;

public DummyDbContext(DbConnection connection, Collation 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<DummyDbContext>
{
private readonly Collation defaultCollation;

public AssertInitializer(DbModelBuilder modelBuilder, Collation 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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,11 +12,13 @@ internal class ColumnStatementCollectionBuilder : IStatementBuilder<ColumnStatem
{
private readonly IEnumerable<EdmProperty> properties;
private readonly IEnumerable<EdmProperty> keyMembers;
private readonly Collation defaultCollation;

public ColumnStatementCollectionBuilder(IEnumerable<EdmProperty> properties, IEnumerable<EdmProperty> keyMembers)
public ColumnStatementCollectionBuilder(IEnumerable<EdmProperty> properties, IEnumerable<EdmProperty> keyMembers, Collation defaultCollation)
{
this.properties = properties;
this.keyMembers = keyMembers;
this.defaultCollation = defaultCollation;
}

public ColumnStatementCollection BuildStatement()
Expand All @@ -39,7 +42,7 @@ private IEnumerable<ColumnStatement> CreateColumnStatements()
AdjustDatatypeForAutogenerationIfNecessary(property, columnStatement);
AddNullConstraintIfNecessary(property, columnStatement);
AddUniqueConstraintIfNecessary(property, columnStatement);
AddCollationConstraintIfNecessary(property, columnStatement);
AddCollationConstraintIfNecessary(property, columnStatement, defaultCollation);
AddPrimaryKeyConstraintAndAdjustTypeIfNecessary(property, columnStatement);
AddDefaultValueConstraintIfNecessary(property, columnStatement);

Expand Down Expand Up @@ -73,12 +76,25 @@ private static void AddNullConstraintIfNecessary(EdmProperty property, ColumnSta
}
}

private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement)
private static void AddCollationConstraintIfNecessary(EdmProperty property, ColumnStatement columnStatement, Collation defaultCollation)
{
var value = property.GetCustomAnnotation<CollateAttribute>();
if (value != null)
var collateAttribute = property.GetCustomAnnotation<CollateAttribute>();
if (property.PrimitiveType.PrimitiveTypeKind == PrimitiveTypeKind.String)
{
// 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 });
}
}
else if (collateAttribute != null)
{
columnStatement.ColumnConstraints.Add(new CollateConstraint { CollationFunction = value.Collation, 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ namespace SQLite.CodeFirst.Builder
internal class CreateDatabaseStatementBuilder : IStatementBuilder<CreateDatabaseStatement>
{
private readonly EdmModel edmModel;
private readonly Collation defaultCollation;

public CreateDatabaseStatementBuilder(EdmModel edmModel)
public CreateDatabaseStatementBuilder(EdmModel edmModel, Collation defaultCollation)
{
this.edmModel = edmModel;
this.defaultCollation = defaultCollation;
}

public CreateDatabaseStatement BuildStatement()
Expand All @@ -30,7 +32,7 @@ private IEnumerable<CreateTableStatement> 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();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ internal class CreateTableStatementBuilder : IStatementBuilder<CreateTableStatem
{
private readonly EntitySet entitySet;
private readonly AssociationTypeContainer associationTypeContainer;
private readonly Collation defaultCollation;

public CreateTableStatementBuilder(EntitySet entitySet, AssociationTypeContainer associationTypeContainer)
public CreateTableStatementBuilder(EntitySet entitySet, AssociationTypeContainer associationTypeContainer, Collation defaultCollation)
{
this.entitySet = entitySet;
this.associationTypeContainer = associationTypeContainer;
this.defaultCollation = defaultCollation;
}

public CreateTableStatement BuildStatement()
Expand All @@ -31,7 +33,7 @@ public CreateTableStatement BuildStatement()
compositePrimaryKeyStatement = new CompositePrimaryKeyStatementBuilder(keyMembers).BuildStatement();
}

var simpleColumnCollection = new ColumnStatementCollectionBuilder(entitySet.ElementType.Properties, keyMembers).BuildStatement();
var simpleColumnCollection = new ColumnStatementCollectionBuilder(entitySet.ElementType.Properties, keyMembers, defaultCollation).BuildStatement();
var foreignKeyCollection = new ForeignKeyStatementBuilder(associationTypeContainer.GetAssociationTypes(entitySet.Name)).BuildStatement();

var columnStatements = new List<IStatement>();
Expand Down
34 changes: 7 additions & 27 deletions SQLite.CodeFirst/Public/Attributes/CollateAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,19 @@ public sealed class CollateAttribute : Attribute
{
public CollateAttribute()
{
Collation = CollationFunction.None;
Collation = new Collation();
}

public CollateAttribute(CollationFunction collation)
public CollateAttribute(CollationFunction function)
{
if (collation == CollationFunction.Custom)
{
throw new ArgumentException("If the collation is set to CollationFunction.Custom a function must be specified.", nameof(collation));
}

Collation = collation;
Collation = new Collation(function);
}
public CollateAttribute(CollationFunction collation, string function)
{
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;
public CollateAttribute(CollationFunction function, string customFunction)
{
Collation = new Collation(function, customFunction);
}

public CollationFunction Collation { get; }

/// <summary>
/// The name of the custom collating function to use (CollationFunction.Custom).
/// </summary>
public string Function { get; }
public Collation Collation { get; }
}
}
46 changes: 46 additions & 0 deletions SQLite.CodeFirst/Public/Collation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;

namespace SQLite.CodeFirst
{
/// <summary>
/// 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 <see cref="Function"/>).
/// Set <see cref="Function"/> to <see cref="CollationFunction.Custom"/> and specify the name using the function parameter.
/// </summary>
public class Collation
{
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; }

/// <summary>
/// The name of the custom collating function to use (CollationFunction.Custom).
/// </summary>
public string CustomFunction { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
/// <summary>
/// The collation function to use for this column.
/// Is used together with the <see cref="CollateAttribute" />.
/// Is used together with the <see cref="CollateAttribute" />, and when setting a default collation for the database.
/// </summary>
public enum CollationFunction
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Loading

0 comments on commit 58b3371

Please sign in to comment.