diff --git a/.editorconfig b/.editorconfig
index 247dc1d43b..ac434a5cf3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,6 +2,7 @@
root = true
[*]
+end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 4
diff --git a/documentation/documentation/documents/configuration/foreign_keys.md b/documentation/documentation/documents/configuration/foreign_keys.md
index 6ae11d5fcf..940e54f247 100644
--- a/documentation/documentation/documents/configuration/foreign_keys.md
+++ b/documentation/documentation/documents/configuration/foreign_keys.md
@@ -32,6 +32,30 @@ In the `Issue` referencing `User` example above, this means that if you create a
`Issue` in the same session, when you call `IDocumentSession.SaveChanges()/SaveChangesAsync()`, Marten will know
to save the new user first so that the issue will not fail with referential integrity violations.
+## Foreign Keys to non-Marten tables
+
+Marten can also create a foreign key to tables that are not managed by Marten. Continuing the our sample
+of `Issue`, we can create a foreign key from our `Issue` to our external bug tracking system:
+
+<[sample:configure-external-foreign-key]>
+
+With the configuration above, Marten will generate a foreign key constraint from the `Issue` to a table in the
+`bug-tracker` schema called `bugs` on the `id` column. The constraint would be defined as:
+
+
+ALTER TABLE public.mt_doc_issue
+ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+REFERENCES bug-tracker.bugs (id);
+
+
+## Cascading deletes
+
+Marten can also cascade deletes on the foreign keys that it creates. The `ForeignKeyDefinition` has a
+`CascadeDeletes` property that indicates whether the foreign key should enable cascading deletes. One way
+to enable this is to use a configuration function like:
+
+<[sample:cascade_deletes_with_config_func]>
+
## Configuring with Attributes
You can optionally configure properties or fields as foreign key relationships with the `[ForeignKey]` attribute:
diff --git a/src/Marten.Testing/Acceptance/foreign_keys.cs b/src/Marten.Testing/Acceptance/foreign_keys.cs
new file mode 100644
index 0000000000..c68797ddd0
--- /dev/null
+++ b/src/Marten.Testing/Acceptance/foreign_keys.cs
@@ -0,0 +1,236 @@
+using System;
+using Marten.Testing.Documents;
+using Shouldly;
+using Xunit;
+
+namespace Marten.Testing.Acceptance
+{
+ public class foreign_keys: IntegratedFixture
+ {
+ [Fact]
+ public void can_insert_document_with_null_value_of_foreign_key()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(false);
+
+ var issue = new Issue();
+
+ ShouldProperlySave(issue);
+ }
+
+ [Fact]
+ public void can_insert_document_with_existing_value_of_foreign_key()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(false);
+
+ var user = new User();
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(user);
+ session.SaveChanges();
+ }
+
+ var issue = new Issue { AssigneeId = user.Id };
+
+ ShouldProperlySave(issue);
+ }
+
+ [Fact]
+ public void cannot_insert_document_with_non_existing_value_of_foreign_key()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(false);
+
+ var issue = new Issue { AssigneeId = Guid.NewGuid() };
+
+ Should.Throw(() =>
+ {
+ using (var session = theStore.OpenSession())
+ {
+ session.Insert(issue);
+ session.SaveChanges();
+ }
+ });
+ }
+
+ [Fact]
+ public void can_update_document_with_existing_value_of_foreign_key_to_other_existing_value()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(false);
+
+ var user = new User();
+ var otherUser = new User();
+ var issue = new Issue { AssigneeId = user.Id };
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(user, otherUser);
+ session.Store(issue);
+ session.SaveChanges();
+ }
+
+ issue.AssigneeId = otherUser.Id;
+
+ ShouldProperlySave(issue);
+ }
+
+ [Fact]
+ public void can_update_document_with_existing_value_of_foreign_key_to_null()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(false);
+
+ var user = new User();
+ var otherUser = new User();
+ var issue = new Issue { AssigneeId = user.Id };
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(user, otherUser);
+ session.Store(issue);
+ session.SaveChanges();
+ }
+
+ issue.AssigneeId = null;
+
+ ShouldProperlySave(issue);
+ }
+
+ [Fact]
+ public void cannot_update_document_with_existing_value_of_foreign_key_to_not_existing()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(false);
+
+ var user = new User();
+ var otherUser = new User();
+ var issue = new Issue { AssigneeId = user.Id };
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(user, otherUser);
+ session.Store(issue);
+ session.SaveChanges();
+ }
+
+ issue.AssigneeId = Guid.NewGuid();
+
+ Should.Throw(() =>
+ {
+ using (var session = theStore.OpenSession())
+ {
+ session.Update(issue);
+ session.SaveChanges();
+ }
+ });
+ }
+
+ [Fact]
+ public void can_delete_document_with_foreign_key()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(true);
+
+ var user = new User();
+ var issue = new Issue { AssigneeId = user.Id };
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(user);
+ session.Store(issue);
+ session.SaveChanges();
+ }
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Delete(issue);
+ session.SaveChanges();
+ }
+
+ using (var query = theStore.QuerySession())
+ {
+ query.Load(issue.Id).ShouldBeNull();
+ query.Load(user.Id).ShouldNotBeNull();
+ }
+ }
+
+ [Fact]
+ public void can_delete_document_that_is_referenced_by_foreignkey_with_cascadedeletes_from_other_document()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(true);
+
+ var user = new User();
+ var issue = new Issue { AssigneeId = user.Id };
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(user);
+ session.Store(issue);
+ session.SaveChanges();
+ }
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Delete(user);
+ session.SaveChanges();
+ }
+
+ using (var query = theStore.QuerySession())
+ {
+ query.Load(issue.Id).ShouldBeNull();
+ query.Load(user.Id).ShouldBeNull();
+ }
+ }
+
+ [Fact]
+ public void cannot_delete_document_that_is_referenced_by_foreignkey_without_cascadedeletes_from_other_document()
+ {
+ ConfigureForeignKeyWithCascadingDeletes(false);
+
+ var user = new User();
+ var issue = new Issue { AssigneeId = user.Id };
+
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(user);
+ session.Store(issue);
+ session.SaveChanges();
+ }
+
+ Should.Throw(() =>
+ {
+ using (var session = theStore.OpenSession())
+ {
+ session.Delete(user);
+ session.SaveChanges();
+ }
+ });
+
+ using (var query = theStore.QuerySession())
+ {
+ query.Load(issue.Id).ShouldNotBeNull();
+ query.Load(user.Id).ShouldNotBeNull();
+ }
+ }
+
+ private void ConfigureForeignKeyWithCascadingDeletes(bool hasCascadeDeletes)
+ {
+ StoreOptions(options =>
+ {
+ options.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = hasCascadeDeletes);
+ });
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+ }
+
+ private void ShouldProperlySave(Issue issue)
+ {
+ using (var session = theStore.OpenSession())
+ {
+ session.Store(issue);
+ session.SaveChanges();
+ }
+
+ using (var query = theStore.QuerySession())
+ {
+ var documentFromDb = query.Load(issue.Id);
+
+ documentFromDb.ShouldNotBeNull();
+ }
+ }
+ }
+}
diff --git a/src/Marten.Testing/Bugs/Bug_1258_cannot_derive_updates_for_objects.cs b/src/Marten.Testing/Bugs/Bug_1258_cannot_derive_updates_for_objects.cs
index 46e74850c4..e93dfb1bdc 100644
--- a/src/Marten.Testing/Bugs/Bug_1258_cannot_derive_updates_for_objects.cs
+++ b/src/Marten.Testing/Bugs/Bug_1258_cannot_derive_updates_for_objects.cs
@@ -8,11 +8,12 @@
namespace Marten.Testing.Bugs
{
-public class Bug_1258_cannot_derive_updates_for_objects : IntegratedFixture
+ public class Bug_1258_cannot_derive_updates_for_objects: IntegratedFixture
{
[Fact]
public void can_properly_detect_changes_when_user_defined_type()
{
+ theStore.Advanced.Clean.CompletelyRemoveAll();
StoreOptions(_ =>
{
_.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate;
@@ -118,7 +119,7 @@ CREATE FUNCTION cust_type_ne(cust_type, cust_type) RETURNS boolean IMMUTABLE LAN
RIGHTARG = cust_type,
COMMUTATOR = ~>=~,
NEGATOR = ~>~
- );
+ );
CREATE OPERATOR ~<~ (
PROCEDURE = cust_type_lt,
LEFTARG = cust_type,
@@ -230,4 +231,4 @@ public class IssueForUserWithCustomType
public Guid? UserId { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Marten.Testing/Documents/Issue.cs b/src/Marten.Testing/Documents/Issue.cs
index d497753a6f..e868f7d4c7 100644
--- a/src/Marten.Testing/Documents/Issue.cs
+++ b/src/Marten.Testing/Documents/Issue.cs
@@ -21,6 +21,26 @@ public Issue()
public Guid? AssigneeId { get; set; }
public Guid? ReporterId { get; set; }
+
+ public Guid? BugId { get; set; }
+ }
+ // ENDSAMPLE
+
+ // SAMPLE: Bug
+ public class Bug
+ {
+ public Bug()
+ {
+ Id = Guid.NewGuid();
+ }
+
+ public Guid Id { get; set; }
+
+ public string Type { get; set; }
+
+ public string Title { get; set; }
+
+ public int IssueTrackerId { get; set; }
}
// ENDSAMPLE
}
\ No newline at end of file
diff --git a/src/Marten.Testing/Examples/ForeignKeyExamples.cs b/src/Marten.Testing/Examples/ForeignKeyExamples.cs
index cbbe4daaf8..59b6a97d0e 100644
--- a/src/Marten.Testing/Examples/ForeignKeyExamples.cs
+++ b/src/Marten.Testing/Examples/ForeignKeyExamples.cs
@@ -1,5 +1,4 @@
-using System;
-using Marten.Testing.Documents;
+using Marten.Testing.Documents;
namespace Marten.Testing.Examples
{
@@ -8,19 +7,47 @@ public class ForeignKeyExamples
public void configuration()
{
// SAMPLE: configure-foreign-key
-var store = DocumentStore.For(_ =>
-{
- _.Connection("some database connection");
+ var store = DocumentStore
+ .For(_ =>
+ {
+ _.Connection("some database connection");
- // In the following line of code, I'm setting
- // up a foreign key relationship to the User document
- _.Schema.For().ForeignKey(x => x.AssigneeId);
-});
+ // In the following line of code, I'm setting
+ // up a foreign key relationship to the User document
+ _.Schema.For().ForeignKey(x => x.AssigneeId);
+ });
// ENDSAMPLE
//var sql = store.Schema.ToDDL();
//Console.WriteLine(sql);
+ }
+
+ public void external_fkey()
+ {
+ // SAMPLE: configure-external-foreign-key
+ var store = DocumentStore
+ .For(_ =>
+ {
+ _.Connection("some database connection");
+
+ // Here we create a foreign key to table that is not
+ // created or managed by marten
+ _.Schema.For().ForeignKey(i => i.BugId, "bugtracker", "bugs", "id");
+ });
+ // ENDSAMPLE
+ }
+ public void cascade_deletes_with_config_func()
+ {
+ // SAMPLE: cascade_deletes_with_config_func
+ var store = DocumentStore
+ .For(_ =>
+ {
+ _.Connection("some database connection");
+
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
+ });
+ // ENDSAMPLE
}
}
}
\ No newline at end of file
diff --git a/src/Marten.Testing/Schema/DocumentMapping_schema_patch_foreign_key_writing.cs b/src/Marten.Testing/Schema/DocumentMapping_schema_patch_foreign_key_writing.cs
new file mode 100644
index 0000000000..a1d13dbba6
--- /dev/null
+++ b/src/Marten.Testing/Schema/DocumentMapping_schema_patch_foreign_key_writing.cs
@@ -0,0 +1,425 @@
+using Marten.Testing.Documents;
+using Marten.Util;
+using Xunit;
+
+namespace Marten.Testing.Schema
+{
+ public class DocumentMapping_schema_patch_foreign_key_writing: IntegratedFixture
+ {
+ [Fact]
+ public void can_update_foreignkeys_todocuments_with_cascade_delete_that_were_added()
+ {
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES public.mt_doc_user (id);", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES mt_doc_user(id)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_update_foreignkeys_todocuments_without_cascade_delete_that_were_added()
+ {
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES public.mt_doc_user (id)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES mt_doc_user(id);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_create_and_drop_foreignkeys_toexternaltable_that_were_added()
+ {
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs (bugid)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_update_foreignkeys_toexternaltable_with_cascade_delete_that_were_added()
+ {
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs (bugid)", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs(bugid)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_update_foreignkeys_toexternaltable_without_cascade_delete_that_were_added()
+ {
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.AutoCreateSchemaObjects = AutoCreate.All;
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs (bugid)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs(bugid);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_add_foreignkeys_toexternaltable_and_delete_that_were_added_asdocument_and_does_not_exist()
+ {
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.AutoCreateSchemaObjects = AutoCreate.All;
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs (bugid)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES mt_doc_user(id);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_add_foreignkeys_todocuments_and_delete_that_were_added_asexternaltable_and_does_not_exist()
+ {
+ theStore.Advanced.Clean.CompletelyRemoveAll();
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.AutoCreateSchemaObjects = AutoCreate.All;
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES public.mt_doc_user (id)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs(bugid);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_add_foreignkeys_toexternaltable_and_keep_that_were_added_asdocument_and_exists()
+ {
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.AutoCreateSchemaObjects = AutoCreate.All;
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = false);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldNotContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
+ REFERENCES bugtracker.bugs (bugid)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldNotContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES mt_doc_user(id);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_add_foreignkeys_todocuments_and_keep_that_were_added_asexternaltable_and_exists()
+ {
+ theStore.Advanced.Clean.CompletelyRemoveAll();
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.AutoCreateSchemaObjects = AutoCreate.All;
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.BugId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = false);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldNotContain("DROP CONSTRAINT mt_doc_issue_bug_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES public.mt_doc_user (id)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+ patch.RollbackDDL.ShouldNotContain(@"ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES bugtracker.bugs(bugid);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_replace_foreignkeys_from_todocuments_to_asexternaltable()
+ {
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.AutoCreateSchemaObjects = AutoCreate.All;
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.AssigneeId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES bugtracker.bugs (bugid)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES mt_doc_user(id);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ [Fact]
+ public void can_replace_foreignkeys_from_asexternaltable_to_todocuments()
+ {
+ theStore.Advanced.Clean.CompletelyRemoveAll();
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+ CreateNonMartenTable();
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(i => i.AssigneeId, "bugs", "bugid", "bugtracker", fkd => fkd.CascadeDeletes = false);
+ }))
+ {
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.AutoCreateSchemaObjects = AutoCreate.All;
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+
+ patch.UpdateDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES public.mt_doc_user (id)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain("DROP CONSTRAINT mt_doc_issue_assignee_id_fkey;", StringComparisonOption.NormalizeWhitespaces);
+
+ patch.RollbackDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES bugtracker.bugs(bugid);", StringComparisonOption.NormalizeWhitespaces);
+
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
+ }
+ }
+
+ private void CreateNonMartenTable()
+ {
+ using (var sesion = theStore.OpenSession())
+ {
+ sesion.Connection.RunSql(@"CREATE SCHEMA IF NOT EXISTS bugtracker;");
+ sesion.Connection.RunSql(
+ @"CREATE TABLE IF NOT EXISTS bugtracker.bugs (
+ bugid uuid CONSTRAINT pk_mt_streams PRIMARY KEY,
+ name varchar(100) NULL
+ )");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Marten.Testing/Schema/DocumentMapping_schema_patch_writing.cs b/src/Marten.Testing/Schema/DocumentMapping_schema_patch_writing.cs
index d4b0ac2099..3ff13d8dbd 100644
--- a/src/Marten.Testing/Schema/DocumentMapping_schema_patch_writing.cs
+++ b/src/Marten.Testing/Schema/DocumentMapping_schema_patch_writing.cs
@@ -1,10 +1,10 @@
-using Marten.Schema;
+using Marten.Schema;
using Marten.Testing.Documents;
using Xunit;
namespace Marten.Testing.Schema
{
- public class DocumentMapping_schema_patch_writing : IntegratedFixture
+ public class DocumentMapping_schema_patch_writing: IntegratedFixture
{
[Fact]
public void creates_the_table_in_update_ddl_if_all_new()
@@ -111,13 +111,11 @@ public void can_revert_indexes_that_changed()
var patch = store.Schema.ToPatch();
patch.RollbackDDL.ShouldContain("drop index");
-
- patch.RollbackDDL.ShouldContain("CREATE INDEX mt_doc_user_idx_user_name");
-
+ patch.RollbackDDL.ShouldContain("CREATE INDEX mt_doc_user_idx_user_name");
}
}
-
+
[Fact]
public void can_revert_indexes_that_changed_in_non_public_schema()
{
@@ -140,10 +138,29 @@ public void can_revert_indexes_that_changed_in_non_public_schema()
var patch = store.Schema.ToPatch();
patch.RollbackDDL.ShouldContain("drop index other.mt_doc_user_idx_user_name;");
-
+
patch.RollbackDDL.ShouldContain("CREATE INDEX mt_doc_user_idx_user_name ON other.mt_doc_user USING btree (user_name);");
+ }
+ }
+
+ [Fact]
+ public void can_create_and_drop_foreignkeys_todocuments_that_were_added()
+ {
+ theStore.Tenancy.Default.EnsureStorageExists(typeof(User));
+
+ using (var store = DocumentStore.For(_ =>
+ {
+ _.Connection(ConnectionSource.ConnectionString);
+ _.Schema.For().ForeignKey(x => x.AssigneeId, fkd => fkd.CascadeDeletes = true);
+ }))
+ {
+ var patch = store.Schema.ToPatch();
+ patch.UpdateDDL.ShouldContain(@"ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
+ REFERENCES public.mt_doc_user (id)
+ ON DELETE CASCADE;", StringComparisonOption.NormalizeWhitespaces);
+ store.Schema.ApplyAllConfiguredChangesToDatabase();
}
}
}
diff --git a/src/Marten.Testing/Schema/ForeignKeyDefinitionTests.cs b/src/Marten.Testing/Schema/ForeignKeyDefinitionTests.cs
index c9478afa86..856ce84c47 100644
--- a/src/Marten.Testing/Schema/ForeignKeyDefinitionTests.cs
+++ b/src/Marten.Testing/Schema/ForeignKeyDefinitionTests.cs
@@ -29,6 +29,21 @@ public void generate_ddl()
.ShouldBe(expected);
}
+ [Fact]
+ public void generate_ddl_with_cascade()
+ {
+ var expected = string.Join(Environment.NewLine,
+ "ALTER TABLE public.mt_doc_issue",
+ "ADD CONSTRAINT mt_doc_issue_user_id_fkey FOREIGN KEY (user_id)",
+ "REFERENCES public.mt_doc_user (id)",
+ "ON DELETE CASCADE;");
+
+ new ForeignKeyDefinition("user_id", _issueMapping, _userMapping)
+ { CascadeDeletes = true }
+ .ToDDL()
+ .ShouldBe(expected);
+ }
+
[Fact]
public void generate_ddl_on_other_schema()
{
@@ -43,6 +58,24 @@ public void generate_ddl_on_other_schema()
new ForeignKeyDefinition("user_id", issueMappingOtherSchema, userMappingOtherSchema).ToDDL()
.ShouldBe(expected);
}
+
+ [Fact]
+ public void generate_ddl_on_other_schema_with_cascade()
+ {
+ var issueMappingOtherSchema = DocumentMapping.For("schema1");
+ var userMappingOtherSchema = DocumentMapping.For("schema2");
+
+ var expected = string.Join(Environment.NewLine,
+ "ALTER TABLE schema1.mt_doc_issue",
+ "ADD CONSTRAINT mt_doc_issue_user_id_fkey FOREIGN KEY (user_id)",
+ "REFERENCES schema2.mt_doc_user (id)",
+ "ON DELETE CASCADE;");
+
+ new ForeignKeyDefinition("user_id", issueMappingOtherSchema, userMappingOtherSchema)
+ { CascadeDeletes = true }
+ .ToDDL()
+ .ShouldBe(expected);
+ }
}
public class ExternalForeignKeyDefinitionTests
@@ -50,7 +83,7 @@ public class ExternalForeignKeyDefinitionTests
private readonly DocumentMapping _userMapping = DocumentMapping.For();
[Fact]
- public void generate_ddl()
+ public void generate_ddl_without_cascade()
{
var expected = string.Join(Environment.NewLine,
"ALTER TABLE public.mt_doc_user",
@@ -62,5 +95,21 @@ public void generate_ddl()
.ToDDL()
.ShouldBe(expected);
}
+
+ [Fact]
+ public void generate_ddl_with_cascade()
+ {
+ var expected = string.Join(Environment.NewLine,
+ "ALTER TABLE public.mt_doc_user",
+ "ADD CONSTRAINT mt_doc_user_user_id_fkey FOREIGN KEY (user_id)",
+ "REFERENCES external_schema.external_table (external_id)",
+ "ON DELETE CASCADE;");
+
+ new ExternalForeignKeyDefinition("user_id", _userMapping,
+ "external_schema", "external_table", "external_id")
+ { CascadeDeletes = true }
+ .ToDDL()
+ .ShouldBe(expected);
+ }
}
}
\ No newline at end of file
diff --git a/src/Marten.Testing/Session/document_session_find_json_Tests.cs b/src/Marten.Testing/Session/document_session_find_json_Tests.cs
index 4b5f2965d9..14f08bce60 100644
--- a/src/Marten.Testing/Session/document_session_find_json_Tests.cs
+++ b/src/Marten.Testing/Session/document_session_find_json_Tests.cs
@@ -6,7 +6,7 @@
namespace Marten.Testing.Session
{
- public class document_session_find_json_Tests : DocumentSessionFixture
+ public class document_session_find_json_Tests: DocumentSessionFixture
{
// SAMPLE: find-json-by-id
[Fact]
@@ -18,8 +18,9 @@ public void when_find_then_a_json_should_be_returned()
theSession.SaveChanges();
var json = theSession.Json.FindById(issue.Id);
- json.ShouldBe($"{{\"Id\": \"{issue.Id}\", \"Tags\": null, \"Title\": \"Issue 1\", \"Number\": 0, \"AssigneeId\": null, \"ReporterId\": null}}");
+ json.ShouldBe($"{{\"Id\": \"{issue.Id}\", \"Tags\": null, \"BugId\": null, \"Title\": \"Issue 1\", \"Number\": 0, \"AssigneeId\": null, \"ReporterId\": null}}");
}
+
// ENDSAMPLE
[Fact]
diff --git a/src/Marten.Testing/Session/document_session_find_json_async_Tests.cs b/src/Marten.Testing/Session/document_session_find_json_async_Tests.cs
index 7d98eb0118..ca2e9a91fe 100644
--- a/src/Marten.Testing/Session/document_session_find_json_async_Tests.cs
+++ b/src/Marten.Testing/Session/document_session_find_json_async_Tests.cs
@@ -7,7 +7,7 @@
namespace Marten.Testing.Session
{
- public class document_session_find_json_async_Tests : DocumentSessionFixture
+ public class document_session_find_json_async_Tests: DocumentSessionFixture
{
// SAMPLE: find-json-by-id-async
[Fact]
@@ -19,8 +19,9 @@ public async Task when_find_then_a_json_should_be_returned()
await theSession.SaveChangesAsync().ConfigureAwait(false);
var json = await theSession.Json.FindByIdAsync(issue.Id).ConfigureAwait(false);
- json.ShouldBe($"{{\"Id\": \"{issue.Id}\", \"Tags\": null, \"Title\": \"Issue 2\", \"Number\": 0, \"AssigneeId\": null, \"ReporterId\": null}}");
+ json.ShouldBe($"{{\"Id\": \"{issue.Id}\", \"Tags\": null, \"BugId\": null, \"Title\": \"Issue 2\", \"Number\": 0, \"AssigneeId\": null, \"ReporterId\": null}}");
}
+
// ENDSAMPLE
[Fact]
diff --git a/src/Marten.Testing/SpecificationExtensions.cs b/src/Marten.Testing/SpecificationExtensions.cs
index 5defd1879b..074719e33c 100644
--- a/src/Marten.Testing/SpecificationExtensions.cs
+++ b/src/Marten.Testing/SpecificationExtensions.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Baseline;
using Marten.Schema;
@@ -152,13 +153,23 @@ public static string ShouldNotBeEmpty(this string aString)
return aString;
}
- public static void ShouldContain(this string actual, string expected)
+ public static void ShouldContain(this string actual, string expected, StringComparisonOption options = StringComparisonOption.Default)
{
+ if (options == StringComparisonOption.NormalizeWhitespaces)
+ {
+ actual = Regex.Replace(actual, @"\s+", " ");
+ expected = Regex.Replace(expected, @"\s+", " ");
+ }
actual.Contains(expected).ShouldBeTrue($"Actual: {actual}{Environment.NewLine}Expected: {expected}");
}
- public static string ShouldNotContain(this string actual, string expected)
+ public static string ShouldNotContain(this string actual, string expected, StringComparisonOption options = StringComparisonOption.NormalizeWhitespaces)
{
+ if (options == StringComparisonOption.NormalizeWhitespaces)
+ {
+ actual = Regex.Replace(actual, @"\s+", " ");
+ expected = Regex.Replace(expected, @"\s+", " ");
+ }
actual.Contains(expected).ShouldBeFalse($"Actual: {actual}{Environment.NewLine}Expected: {expected}");
return actual;
}
@@ -209,4 +220,10 @@ public static void ShouldContain(this DbObjectName[] names, string qualifiedName
names.ShouldContain(function);
}
}
+
+ public enum StringComparisonOption
+ {
+ Default,
+ NormalizeWhitespaces
+ }
}
\ No newline at end of file
diff --git a/src/Marten/MartenRegistry.cs b/src/Marten/MartenRegistry.cs
index 457661bc32..292ab9f892 100644
--- a/src/Marten/MartenRegistry.cs
+++ b/src/Marten/MartenRegistry.cs
@@ -293,6 +293,14 @@ public DocumentMappingExpression ForeignKey(
return this;
}
+ public DocumentMappingExpression ForeignKey(Expression> expression, string schemaName, string tableName, string columnName,
+ Action foreignKeyConfiguration = null)
+ {
+ alter = m => m.ForeignKey(expression, schemaName, tableName, columnName, foreignKeyConfiguration);
+
+ return this;
+ }
+
///
/// Overrides the Hilo sequence increment and "maximum low" number for document types that
/// use numeric id's and the Hilo Id assignment
diff --git a/src/Marten/Schema/ActualForeignKey.cs b/src/Marten/Schema/ActualForeignKey.cs
new file mode 100644
index 0000000000..51239bd6b5
--- /dev/null
+++ b/src/Marten/Schema/ActualForeignKey.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace Marten.Schema
+{
+ public class ActualForeignKey
+ {
+ public DbObjectName Table { get; }
+ public string Name { get; }
+ public string DDL { get; }
+
+ public bool DoesCascadeDeletes()
+ {
+ // NOTE: Use .IndexOf() so it's not effected by whitespace
+ return DDL.IndexOf("on delete cascade", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ public ActualForeignKey(DbObjectName table, string name, string ddl)
+ {
+ Table = table;
+ Name = name;
+ DDL = ddl;
+ }
+
+ public override string ToString()
+ {
+ return $"Table: {Table}, Name: {Name}, DDL: {DDL}";
+ }
+
+ protected bool Equals(ActualIndex other)
+ {
+ return string.Equals(Name, other.Name) && string.Equals(DDL, other.DDL);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((ActualForeignKey)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ (DDL != null ? DDL.GetHashCode() : 0);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Marten/Schema/DocumentMapping.cs b/src/Marten/Schema/DocumentMapping.cs
index 37eaee2661..e9829b831d 100644
--- a/src/Marten/Schema/DocumentMapping.cs
+++ b/src/Marten/Schema/DocumentMapping.cs
@@ -879,6 +879,13 @@ public FullTextIndex FullTextIndex(string regConfig, params Expression
+ /// Adds foreign key index to other marten document
+ ///
+ /// Document type
+ /// Field selector
+ /// customize foreign key configuration
+ /// customize index configuration
public void ForeignKey(
Expression> expression,
Action foreignKeyConfiguration = null,
@@ -893,15 +900,31 @@ public void ForeignKey(
var indexDefinition = AddIndex(foreignKeyDefinition.ColumnName);
indexConfiguration?.Invoke(indexDefinition);
}
-
- public void ForeignKey(Expression> expression, string schemaName, string tableName, string columnName)
+
+ ///
+ /// Adds foreign key index to non-marten table
+ ///
+ /// Field selector
+ /// external table name
+ /// referenced column to external table
+ /// external table schema name, if not provided then DatabaseSchemaName from store options will be used
+ /// customize foreign key configuration
+ public void ForeignKey(
+ Expression> expression,
+ string tableName,
+ string columnName,
+ string schemaName = null,
+ Action foreignKeyConfiguration = null)
{
+ schemaName = schemaName ?? DatabaseSchemaName;
+
var visitor = new FindMembers();
visitor.Visit(expression);
var duplicateField = DuplicateField(visitor.Members.ToArray());
var foreignKey = new ExternalForeignKeyDefinition(duplicateField.ColumnName, this, schemaName, tableName, columnName);
+ foreignKeyConfiguration?.Invoke(foreignKey);
ForeignKeys.Add(foreignKey);
}
}
diff --git a/src/Marten/Schema/ForeignKeyDefinition.cs b/src/Marten/Schema/ForeignKeyDefinition.cs
index 2d5cca19e5..da5790d693 100644
--- a/src/Marten/Schema/ForeignKeyDefinition.cs
+++ b/src/Marten/Schema/ForeignKeyDefinition.cs
@@ -1,5 +1,6 @@
using System;
using System.Text;
+using Baseline;
namespace Marten.Schema
{
@@ -8,22 +9,38 @@ public class ForeignKeyDefinition
private readonly DocumentMapping _parent;
private readonly DocumentMapping _reference;
private string _keyName;
- private Func _fkeyTableRefFunc;
+ private readonly Func _fkeyTableRefFunc;
private readonly Func _fkeyColumnRefFunc;
+ private readonly Func _fkeyExtraFunc;
- public ForeignKeyDefinition(string columnName, DocumentMapping parent, DocumentMapping reference)
- : this(columnName, parent, fkd => reference.Table.QualifiedName, fkd => "(id)")
+ public ForeignKeyDefinition(
+ string columnName,
+ DocumentMapping parent,
+ DocumentMapping reference
+ ) : this(
+ columnName,
+ parent,
+ fkd => reference.Table.QualifiedName,
+ fkd => "(id)",
+ GenerateOnDeleteClause
+ )
{
_reference = reference;
}
- protected ForeignKeyDefinition(string columnName, DocumentMapping parent, Func fkeyTableRefFunc,
- Func fkeyColumnRefFunc)
+ protected ForeignKeyDefinition(
+ string columnName,
+ DocumentMapping parent,
+ Func fkeyTableRefFunc,
+ Func fkeyColumnRefFunc,
+ Func fkeyExtraFunc
+ )
{
ColumnName = columnName;
_parent = parent;
_fkeyTableRefFunc = fkeyTableRefFunc;
_fkeyColumnRefFunc = fkeyColumnRefFunc;
+ _fkeyExtraFunc = fkeyExtraFunc;
}
public string KeyName
@@ -34,6 +51,8 @@ public string KeyName
public string ColumnName { get; }
+ public bool CascadeDeletes { get; set; }
+
public Type ReferenceDocumentType => _reference?.DocumentType;
public string ToDDL()
@@ -42,17 +61,37 @@ public string ToDDL()
sb.AppendLine($"ALTER TABLE {_parent.Table.QualifiedName}");
sb.AppendLine($"ADD CONSTRAINT {KeyName} FOREIGN KEY ({ColumnName})");
- sb.Append($"REFERENCES {_fkeyTableRefFunc.Invoke(this)} {_fkeyColumnRefFunc.Invoke(this)};");
+ sb.Append($"REFERENCES {_fkeyTableRefFunc.Invoke(this)} {_fkeyColumnRefFunc.Invoke(this)}");
+
+ var extra = _fkeyExtraFunc?.Invoke(this);
+ if (extra.IsNotEmpty())
+ {
+ sb.AppendLine();
+ sb.Append(extra);
+ }
+ sb.Append(";");
return sb.ToString();
}
+
+ protected static string GenerateOnDeleteClause(ForeignKeyDefinition fkd) => fkd.CascadeDeletes ? "ON DELETE CASCADE" : string.Empty;
}
public class ExternalForeignKeyDefinition : ForeignKeyDefinition
{
- public ExternalForeignKeyDefinition(string columnName, DocumentMapping parent, string externalSchemaName, string externalTableName,
- string externalColumnName)
- : base(columnName, parent, _ => $"{externalSchemaName}.{externalTableName}", _ => $"({externalColumnName})")
+ public ExternalForeignKeyDefinition(
+ string columnName,
+ DocumentMapping parent,
+ string externalSchemaName,
+ string externalTableName,
+ string externalColumnName
+ ) : base(
+ columnName,
+ parent,
+ _ => $"{externalSchemaName}.{externalTableName}",
+ _ => $"({externalColumnName})",
+ GenerateOnDeleteClause
+ )
{
}
}
diff --git a/src/Marten/Storage/StorageFeatures.cs b/src/Marten/Storage/StorageFeatures.cs
index a0bceeb141..ae119e0158 100644
--- a/src/Marten/Storage/StorageFeatures.cs
+++ b/src/Marten/Storage/StorageFeatures.cs
@@ -74,7 +74,7 @@ public void Add() where T : IFeatureSchema
public IEnumerable AllMappings => _documentMappings.Value.Enumerate().Select(x => x.Value).Union(_mappings.Value.Enumerate().Select(x => x.Value));
public DocumentMapping MappingFor(Type documentType)
- {
+ {
if (!_documentMappings.Value.TryFind(documentType, out var value))
{
value = typeof(DocumentMapping<>).CloseAndBuildAs(_options, documentType);
@@ -99,7 +99,7 @@ internal IDocumentMapping FindMapping(Type documentType)
}
internal void AddMapping(IDocumentMapping mapping)
- {
+ {
_mappings.Swap(d => d.AddOrUpdate(mapping.DocumentType, mapping));
}
@@ -164,14 +164,14 @@ internal void PostProcessConfiguration()
Add(_options.Events);
_features[typeof(StreamState)] = _options.Events;
_features[typeof(EventStream)] = _options.Events;
- _features[typeof(IEvent)] = _options.Events;
-
+ _features[typeof(IEvent)] = _options.Events;
+
_mappings.Swap(d => d.AddOrUpdate(typeof(IEvent), new EventQueryMapping(_options)));
foreach (var mapping in _documentMappings.Value.Enumerate().Select(x => x.Value))
{
foreach (var subClass in mapping.SubClasses)
- {
+ {
_mappings.Swap(d => d.AddOrUpdate(subClass.DocumentType, subClass));
_features[subClass.DocumentType] = subClass.Parent;
}
@@ -203,6 +203,7 @@ public IEnumerable AllActiveFeatures(ITenant tenant)
return m.ForeignKeys
.Where(x => x.ReferenceDocumentType != m.DocumentType)
.Select(keyDefinition => keyDefinition.ReferenceDocumentType)
+ .Where(keyDefinition => keyDefinition != null)
.Select(MappingFor);
});
diff --git a/src/Marten/Storage/Table.cs b/src/Marten/Storage/Table.cs
index 69dba8a1fe..71dd07e601 100644
--- a/src/Marten/Storage/Table.cs
+++ b/src/Marten/Storage/Table.cs
@@ -32,7 +32,7 @@ public IEnumerable AllNames()
yield return new DbObjectName(Identifier.Schema, index.IndexName);
}
- foreach (var fk in ForeignKeys)
+ foreach (var fk in ForeignKeys)
{
yield return new DbObjectName(Identifier.Schema, fk.KeyName);
}
@@ -159,7 +159,6 @@ public virtual void Write(DdlRules rules, StringWriter writer)
public List PrimaryKeys { get; } = new List();
-
public void WriteDropStatement(DdlRules rules, StringWriter writer)
{
writer.WriteLine($"DROP TABLE IF EXISTS {Identifier} CASCADE;");
@@ -178,12 +177,11 @@ public void ConfigureQueryCommand(CommandBuilder builder)
select a.attname, format_type(a.atttypid, a.atttypmod) as data_type
from pg_index i
join pg_attribute a on a.attrelid = i.indrelid and a.attnum = ANY(i.indkey)
-where attrelid = (select pg_class.oid
- from pg_class
+where attrelid = (select pg_class.oid
+ from pg_class
join pg_catalog.pg_namespace n ON n.oid = pg_class.relnamespace
where n.nspname = :{schemaParam} and relname = :{nameParam})
-and i.indisprimary;
-
+and i.indisprimary;
SELECT
U.usename AS user_name,
@@ -212,16 +210,26 @@ JOIN pg_am AS am
JOIN pg_user AS U ON i.relowner = U.usesysid
WHERE
nspname = :{schemaParam} AND
- NOT nspname LIKE 'pg%' AND
+ NOT nspname LIKE 'pg%' AND
i.relname like 'mt_%';
-select constraint_name
-from information_schema.table_constraints as c
-where
- c.constraint_name LIKE 'mt_%' and
- c.constraint_type = 'FOREIGN KEY' and
- c.table_schema = :{schemaParam} and
- c.table_name = :{nameParam};
+SELECT c.conname AS constraint_name,
+ c.contype AS constraint_type,
+ sch.nspname AS schema_name,
+ tbl.relname AS table_name,
+ ARRAY_AGG(col.attname ORDER BY u.attposition) AS columns,
+ pg_get_constraintdef(c.oid) AS definition
+FROM pg_constraint c
+ JOIN LATERAL UNNEST(c.conkey) WITH ORDINALITY AS u(attnum, attposition) ON TRUE
+ JOIN pg_class tbl ON tbl.oid = c.conrelid
+ JOIN pg_namespace sch ON sch.oid = tbl.relnamespace
+ JOIN pg_attribute col ON (col.attrelid = tbl.oid AND col.attnum = u.attnum)
+WHERE
+ c.conname like 'mt_%' and
+ c.contype = 'f' and
+ sch.nspname = :{schemaParam} and
+ tbl.relname = :{nameParam}
+GROUP BY constraint_name, constraint_type, schema_name, table_name, definition;
");
}
@@ -269,7 +277,11 @@ public SchemaPatchDifference CreatePatch(DbDataReader reader, SchemaPatch patch,
if (delta.Extras.Any() || delta.Different.Any())
{
if (autoCreate == AutoCreate.All)
- {
+ {
+ delta.ForeignKeyMissing.Each(x => patch.Updates.Apply(this, x));
+ delta.ForeignKeyRollbacks.Each(x => patch.Rollbacks.Apply(this, x));
+ delta.ForeignKeyMissingRollbacks.Each(x => patch.Rollbacks.Apply(this, x));
+
Write(patch.Rules, patch.UpWriter);
return SchemaPatchDifference.Create;
@@ -292,7 +304,9 @@ public SchemaPatchDifference CreatePatch(DbDataReader reader, SchemaPatch patch,
delta.IndexChanges.Each(x => patch.Updates.Apply(this, x));
delta.IndexRollbacks.Each(x => patch.Rollbacks.Apply(this, x));
- delta.MissingForeignKeys.Each(x => patch.Updates.Apply(this, x.ToDDL()));
+ delta.ForeignKeyChanges.Each(x => patch.Updates.Apply(this, x));
+ delta.ForeignKeyRollbacks.Each(x => patch.Rollbacks.Apply(this, x));
+ delta.ForeignKeyMissingRollbacks.Each(x => patch.Rollbacks.Apply(this, x));
return SchemaPatchDifference.Update;
}
@@ -304,7 +318,6 @@ private Table readExistingTable(DbDataReader reader)
var indexes = readIndexes(reader);
var constraints = readConstraints(reader);
-
if (!columns.Any()) return null;
var existing = new Table(Identifier);
@@ -321,22 +334,20 @@ private Table readExistingTable(DbDataReader reader)
existing.ActualIndices = indexes;
existing.ActualForeignKeys = constraints;
-
return existing;
}
- public List ActualForeignKeys { get; set; } = new List();
+ public List ActualForeignKeys { get; set; } = new List();
public Dictionary ActualIndices { get; set; } = new Dictionary();
-
- private static List readConstraints(DbDataReader reader)
+ private List readConstraints(DbDataReader reader)
{
reader.NextResult();
- var constraints = new List();
+ var constraints = new List();
while (reader.Read())
{
- constraints.Add(reader.GetString(0));
+ constraints.Add(new ActualForeignKey(Identifier, reader.GetString(0), reader.GetString(5)));
}
return constraints;
@@ -361,8 +372,6 @@ private Dictionary readIndexes(DbDataReader reader)
dict.Add(index.Name, index);
}
-
-
}
return dict;
@@ -433,7 +442,7 @@ public override bool Equals(object obj)
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (!obj.GetType().CanBeCastTo()) return false;
- return Equals((Table) obj);
+ return Equals((Table)obj);
}
public override int GetHashCode()
diff --git a/src/Marten/Storage/TableDelta.cs b/src/Marten/Storage/TableDelta.cs
index 29c908ce46..7eb2ca58d6 100644
--- a/src/Marten/Storage/TableDelta.cs
+++ b/src/Marten/Storage/TableDelta.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Baseline;
using Marten.Schema;
namespace Marten.Storage
@@ -22,8 +21,7 @@ public TableDelta(Table expected, Table actual)
compareIndices(expected, actual);
- var missingFKs = expected.ForeignKeys.Where(x => !actual.ActualForeignKeys.Contains(x.KeyName));
- MissingForeignKeys.AddRange(missingFKs);
+ compareForeignKeys(expected, actual);
}
private void compareIndices(Table expected, Table actual)
@@ -40,11 +38,11 @@ private void compareIndices(Table expected, Table actual)
if (!index.Name.EndsWith("pkey"))
{
IndexChanges.Add($"drop index concurrently if exists {schemaName}.{index.Name};");
- }
- /* else
- {
- IndexChanges.Add($"alter table {_tableName} drop constraint if exists {schemaName}.{index.Name};");
- }*/
+ }
+ /* else
+ {
+ IndexChanges.Add($"alter table {_tableName} drop constraint if exists {schemaName}.{index.Name};");
+ }*/
}
foreach (var index in expected.Indexes)
@@ -66,8 +64,45 @@ private void compareIndices(Table expected, Table actual)
}
}
+ private void compareForeignKeys(Table expected, Table actual)
+ {
+ var schemaName = expected.Identifier.Schema;
+ var tableName = expected.Identifier.Name;
+
+ // Locate FKs that exist, but aren't defined
+ var obsoleteFkeys = actual.ActualForeignKeys.Where(afk => expected.ForeignKeys.All(fk => fk.KeyName != afk.Name));
+ foreach (var fkey in obsoleteFkeys)
+ {
+ ForeignKeyMissing.Add($"ALTER TABLE {schemaName}.{tableName} DROP CONSTRAINT {fkey.Name};");
+ ForeignKeyMissingRollbacks.Add($"ALTER TABLE {schemaName}.{tableName} ADD CONSTRAINT {fkey.Name} {fkey.DDL};");
+ }
+
+ // Detect changes
+ foreach (var fkey in expected.ForeignKeys)
+ {
+ var actualFkey = actual.ActualForeignKeys.SingleOrDefault(afk => afk.Name == fkey.KeyName);
+ if (actualFkey != null && fkey.CascadeDeletes != actualFkey.DoesCascadeDeletes())
+ {
+ // The fkey cascading has changed, drop and re-create the key
+ ForeignKeyChanges.Add($"ALTER TABLE {schemaName}.{tableName} DROP CONSTRAINT {actualFkey.Name}; {fkey.ToDDL()};");
+ ForeignKeyRollbacks.Add($"ALTER TABLE {schemaName}.{tableName} DROP CONSTRAINT {fkey.KeyName}; ALTER TABLE {schemaName}.{tableName} ADD CONSTRAINT {actualFkey.Name} {actualFkey.DDL};");
+ }
+ else if (actualFkey == null)// The foreign key is missing
+ {
+ ForeignKeyChanges.Add(fkey.ToDDL());
+ ForeignKeyRollbacks.Add($"ALTER TABLE {schemaName}.{tableName} DROP CONSTRAINT {fkey.KeyName};");
+ }
+ }
+ }
+
public readonly IList IndexChanges = new List();
- public readonly IList IndexRollbacks = new List();
+ public readonly IList IndexRollbacks = new List();
+
+ public readonly IList ForeignKeyMissing = new List();
+ public readonly IList ForeignKeyMissingRollbacks = new List();
+
+ public readonly IList ForeignKeyChanges = new List();
+ public readonly IList ForeignKeyRollbacks = new List();
public TableColumn[] Different { get; set; }
@@ -77,16 +112,20 @@ private void compareIndices(Table expected, Table actual)
public TableColumn[] Missing { get; set; }
- public IList MissingForeignKeys { get; } = new List();
-
public bool Matches
{
get
{
- if (Missing.Any()) return false;
- if (Extras.Any()) return false;
- if (Different.Any()) return false;
- if (IndexChanges.Any()) return false;
+ if (Missing.Any())
+ return false;
+ if (Extras.Any())
+ return false;
+ if (Different.Any())
+ return false;
+ if (IndexChanges.Any())
+ return false;
+ if (ForeignKeyChanges.Any())
+ return false;
return true;
}