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; }