Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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