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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ The following features are supported:
- Tables from classes (supported annotations: `Table`)
- Columns from properties (supported annotations: `Column`, `Key`, `MaxLength`, `Required`, `NotMapped`, `DatabaseGenerated`, `Index`)
- PrimaryKey constraint (`Key` annotation, key composites are supported)
- ForeignKey constraint (1-n relationships, support for 'Cascade on delete')
- ForeignKey constraint (1-n relationships, support for 'Cascade on delete' and 'Cascade on update')
- Not Null constraint
- Auto increment (An int PrimaryKey will automatically be incremented and you can explicitly set the "AUTOINCREMENT" constraint to a PrimaryKey using the Autoincrement-Attribute)
- 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 your 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)
- Cascade on update (Decorate the foreign key property with the `CascadeOnUpdateAttribute`, which is part of this library. Entity Framework cannot express `ON UPDATE CASCADE`, so this opt-in attribute must be placed on the dependent foreign key property, e.g. `TeamId`, not on the navigation property. It requires an explicit foreign key property.)

## Install

Expand Down
125 changes: 125 additions & 0 deletions SQLite.CodeFirst.Test/IntegrationTests/CascadeOnUpdateTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Collections.Generic;
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;

namespace SQLite.CodeFirst.Test.IntegrationTests
{
/// <summary>
/// Verifies that the <see cref="CascadeOnUpdateAttribute"/> placed on a foreign key property
/// results in 'ON UPDATE CASCADE' being emitted for the corresponding foreign key constraint.
/// This exercises the full pipeline: the attribute is registered as a column annotation, survives
/// the EF model build and is read back from the store model when the SQL is generated.
/// </summary>
[TestClass]
public class CascadeOnUpdateTest
{
private static string generatedSql;

[TestMethod]
public void CascadeOnUpdateAttributeEmitsOnUpdateCascade()
{
using (DbConnection connection = new SQLiteConnection("FullUri=file::memory:"))
{
// This is important! Else the in memory database will not work.
connection.Open();

using (var context = new CascadeDbContext(connection))
{
// Touching the set forces the initializer (and therefore the SQL generation) to run.
context.Set<Order>().FirstOrDefault();
}
}

// Decorated FK with cascade-on-delete: both keywords, delete before update.
StringAssert.Contains(generatedSql, "FOREIGN KEY ([CustomerId]) REFERENCES \"Customers\"([Id]) ON DELETE CASCADE ON UPDATE CASCADE");

// Decorated FK without cascade-on-delete: only the update keyword.
StringAssert.Contains(generatedSql, "FOREIGN KEY ([WarehouseId]) REFERENCES \"Warehouses\"([Id]) ON UPDATE CASCADE");

// Undecorated FK: no cascade keyword at all.
Assert.IsFalse(generatedSql.Contains("[RegionId]) REFERENCES \"Regions\"([Id]) ON"),
"The foreign key without the attribute must not emit any cascade clause.");
}

private class Customer
{
public int Id { get; set; }
}

private class Warehouse
{
public int Id { get; set; }
}

private class Region
{
public int Id { get; set; }
}

private class Order
{
public int Id { get; set; }

[CascadeOnUpdate]
public int CustomerId { get; set; }
public Customer Customer { get; set; }

[CascadeOnUpdate]
public int? WarehouseId { get; set; }
public Warehouse Warehouse { get; set; }

public int? RegionId { get; set; }
public Region Region { get; set; }
}

private class CascadeDbContext : DbContext
{
public CascadeDbContext(DbConnection connection)
: base(connection, false)
{
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasRequired(o => o.Customer)
.WithMany()
.HasForeignKey(o => o.CustomerId)
.WillCascadeOnDelete(true);

modelBuilder.Entity<Order>()
.HasOptional(o => o.Warehouse)
.WithMany()
.HasForeignKey(o => o.WarehouseId)
.WillCascadeOnDelete(false);

modelBuilder.Entity<Order>()
.HasOptional(o => o.Region)
.WithMany()
.HasForeignKey(o => o.RegionId)
.WillCascadeOnDelete(false);

Database.SetInitializer(new CaptureInitializer(modelBuilder));
}
}

private class CaptureInitializer : SqliteInitializerBase<CascadeDbContext>
{
public CaptureInitializer(DbModelBuilder modelBuilder)
: base(modelBuilder)
{
}

public override void InitializeDatabase(CascadeDbContext context)
{
DbModel model = ModelBuilder.Build(context.Database.Connection);
generatedSql = new SqliteSqlGenerator().Generate(model.StoreModel);
base.InitializeDatabase(context);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,37 @@ public void CreateStatementOneForeignKeyCascadeDeleteTest()
Assert.AreEqual("FOREIGN KEY ([dummyForeignKey1]) REFERENCES dummyForeignTable([dummForeignPrimaryKey1]) ON DELETE CASCADE", output);
}

[TestMethod]
public void CreateStatementOneForeignKeyCascadeUpdateTest()
{
var foreignKeyStatement = new ForeignKeyStatement
{
CascadeUpdate = true,
ForeignKey = new List<string> { "dummyForeignKey1" },
ForeignPrimaryKey = new List<string> { "dummForeignPrimaryKey1" },
ForeignTable = "dummyForeignTable"
};

string output = foreignKeyStatement.CreateStatement();
Assert.AreEqual("FOREIGN KEY ([dummyForeignKey1]) REFERENCES dummyForeignTable([dummForeignPrimaryKey1]) ON UPDATE CASCADE", output);
}

[TestMethod]
public void CreateStatementOneForeignKeyCascadeDeleteAndUpdateTest()
{
var foreignKeyStatement = new ForeignKeyStatement
{
CascadeDelete = true,
CascadeUpdate = true,
ForeignKey = new List<string> { "dummyForeignKey1" },
ForeignPrimaryKey = new List<string> { "dummForeignPrimaryKey1" },
ForeignTable = "dummyForeignTable"
};

string output = foreignKeyStatement.CreateStatement();
Assert.AreEqual("FOREIGN KEY ([dummyForeignKey1]) REFERENCES dummyForeignTable([dummForeignPrimaryKey1]) ON DELETE CASCADE ON UPDATE CASCADE", output);
}

[TestMethod]
public void CreateStatementTwoForeignKeyTest()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ private IEnumerable<ForeignKeyStatement> GetForeignKeyStatements()
ForeignKey = associationType.ForeignKey,
ForeignTable = associationType.FromTableName,
ForeignPrimaryKey = associationType.ForeignPrimaryKey,
CascadeDelete = associationType.CascadeDelete
CascadeDelete = associationType.CascadeDelete,
CascadeUpdate = associationType.CascadeUpdate
};
}
}
Expand Down
6 changes: 6 additions & 0 deletions SQLite.CodeFirst/Internal/Statement/ForeignKeyStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ internal class ForeignKeyStatement : IStatement
{
private const string Template = "FOREIGN KEY ({foreign-key}) REFERENCES {referenced-table}({referenced-id})";
private const string CascadeDeleteStatement = "ON DELETE CASCADE";
private const string CascadeUpdateStatement = "ON UPDATE CASCADE";

public IEnumerable<string> ForeignKey { get; set; }
public string ForeignTable { get; set; }
public IEnumerable<string> ForeignPrimaryKey { get; set; }
public bool CascadeDelete { get; set; }
public bool CascadeUpdate { get; set; }

public string CreateStatement()
{
Expand All @@ -25,6 +27,10 @@ public string CreateStatement()
{
sb.Append(" " + CascadeDeleteStatement);
}
if (CascadeUpdate)
{
sb.Append(" " + CascadeUpdateStatement);
}

return sb.ToString();
}
Expand Down
9 changes: 9 additions & 0 deletions SQLite.CodeFirst/Internal/Utility/SqliteAssociationType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Data.Entity.Core.Metadata.Edm;
using System.Linq;
using SQLite.CodeFirst.Builder.NameCreators;
using SQLite.CodeFirst.Extensions;

namespace SQLite.CodeFirst.Utility
{
Expand Down Expand Up @@ -30,6 +31,13 @@ public SqliteAssociationType(AssociationType associationType, EntityContainer co
ForeignKey = associationType.Constraint.ToProperties.Select(x => x.Name);
ForeignPrimaryKey = associationType.Constraint.FromProperties.Select(x => x.Name);
CascadeDelete = associationType.Constraint.FromRole.DeleteBehavior == OperationAction.Cascade;

// EF has no notion of update-cascade, so it is opt-in via the CascadeOnUpdateAttribute placed on the
// dependent foreign key property. The attribute is registered as a column annotation and therefore
// available on the store model foreign key columns (the constraint's ToProperties).
CascadeUpdate = associationType.Constraint.ToProperties
.Select(property => property.GetCustomAnnotation<CascadeOnUpdateAttribute>())
.Any(attribute => attribute != null && attribute.CanCascade);
}

private static bool IsSelfReferencing(AssociationType associationType)
Expand All @@ -47,5 +55,6 @@ private static bool IsSelfReferencing(AssociationType associationType)
public string ToTableName { get; }
public IEnumerable<string> ForeignPrimaryKey { get; }
public bool CascadeDelete { get; }
public bool CascadeUpdate { get; }
}
}
28 changes: 28 additions & 0 deletions SQLite.CodeFirst/Public/Attributes/CascadeOnUpdateAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;

namespace SQLite.CodeFirst
{
/// <summary>
/// Adds 'ON UPDATE CASCADE' to the foreign key constraint of the relationship the decorated property belongs to.
/// Entity Framework has no concept of update-cascade (it assumes primary keys are immutable), so neither the
/// <see cref="System.ComponentModel.DataAnnotations.Schema.ForeignKeyAttribute"/> nor the fluent API can express it.
/// This opt-in attribute is the only way to emit the keyword.
/// Place it on the dependent foreign key property (e.g. the 'TeamId' property), not on the navigation property (e.g. 'Team').
/// Requires an explicit foreign key property; relationships with a shadow foreign key column cannot be decorated.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class CascadeOnUpdateAttribute : Attribute
{
public CascadeOnUpdateAttribute()
{
CanCascade = true;
}

public CascadeOnUpdateAttribute(bool canCascade)
{
CanCascade = canCascade;
}

public bool CanCascade { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ protected SqliteInitializerBase(DbModelBuilder modelBuilder, Collation defaultCo
modelBuilder.RegisterAttributeAsColumnAnnotation<CollateAttribute>();
modelBuilder.RegisterAttributeAsColumnAnnotation<AutoincrementAttribute>();
modelBuilder.RegisterAttributeAsColumnAnnotation<SqlDefaultValueAttribute>();
modelBuilder.RegisterAttributeAsColumnAnnotation<CascadeOnUpdateAttribute>();

// By default there is a 'ForeignKeyIndexConvention' but it can be removed.
// And there is no "Contains" and no way to enumerate the ConventionsCollection.
Expand Down