From dc5232378fc7b85cc1c3838f5f3121700cb68781 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Wed, 26 Aug 2026 19:16:49 +0000 Subject: [PATCH] Fix permanent project hard delete dependencies --- PlotLine.Tests/Program.cs | 21 +- PlotLine/Data/Repositories.cs | 137 +---------- ...25E_ProjectHardDeleteDependencyCleanup.sql | 218 ++++++++++++++++++ 3 files changed, 236 insertions(+), 140 deletions(-) create mode 100644 PlotLine/Sql/160_Phase25E_ProjectHardDeleteDependencyCleanup.sql diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 62c94b1..a735f82 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -180,16 +180,23 @@ static void ProjectHardDeleteClearsStoryIntelligenceDependenciesInTransaction() var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); var repository = File.ReadAllText(Path.Combine(root, "Data/Repositories.cs")); var cascadeMigration = File.ReadAllText(Path.Combine(root, "Sql/159_Phase25D_ManuscriptSourceHardDeleteCascades.sql")); + var dependencyCleanupMigration = File.ReadAllText(Path.Combine(root, "Sql/160_Phase25E_ProjectHardDeleteDependencyCleanup.sql")); Assert(repository.Contains("using var transaction = connection.BeginTransaction()", StringComparison.Ordinal), "Project hard delete should be wrapped by a repository transaction."); - Assert(repository.Contains("ProjectHardDeleteStoryIntelligenceDependencyCleanupSql", StringComparison.Ordinal), "Project hard delete should run scoped Story Intelligence dependency cleanup before the stored procedure."); + Assert(repository.Contains("\"dbo.ProjectHardDelete_PrepareDependencies\"", StringComparison.Ordinal), "Project hard delete should run scoped dependency cleanup through a stored procedure."); + Assert(!repository.Contains("ProjectHardDeleteStoryIntelligenceDependencyCleanupSql", StringComparison.Ordinal), "Project hard delete should not carry dependency cleanup as inline SQL."); Assert(repository.Contains("AllowActiveProject = false", StringComparison.Ordinal), "Archived-project permanent delete must refuse active projects before cleanup."); - Assert(repository.Contains("THROW 52002, 'Only archived projects can be permanently deleted.'", StringComparison.Ordinal), "Hard delete cleanup should enforce the archive gate before deleting child rows."); - Assert(repository.Contains("CharacterIntelligenceEvidenceEvents", StringComparison.Ordinal), "Hard delete cleanup should remove character intelligence evidence rows."); - Assert(repository.Contains("CharacterIntelligenceProfiles", StringComparison.Ordinal), "Hard delete cleanup should remove character intelligence profile rows."); - Assert(repository.Contains("StoryMemoryProcessedSceneResults", StringComparison.Ordinal), "Hard delete cleanup should remove durable story memory scene-result rows."); - Assert(repository.Contains("StoryMemoryImportPreferences", StringComparison.Ordinal), "Hard delete cleanup should remove durable story memory import rows."); - Assert(repository.Contains("WHERE ProjectID = @ProjectID", StringComparison.Ordinal), "Hard delete cleanup must be scoped by project id."); + Assert(dependencyCleanupMigration.Contains("THROW 52002, 'Only archived projects can be permanently deleted.'", StringComparison.Ordinal), "Hard delete cleanup should enforce the archive gate before deleting child rows."); + Assert(dependencyCleanupMigration.Contains("CharacterIntelligenceEvidenceEvents", StringComparison.Ordinal), "Hard delete cleanup should remove character intelligence evidence rows."); + Assert(dependencyCleanupMigration.Contains("CharacterIntelligenceProfiles", StringComparison.Ordinal), "Hard delete cleanup should remove character intelligence profile rows."); + Assert(dependencyCleanupMigration.Contains("StoryMemoryProcessedSceneResults", StringComparison.Ordinal), "Hard delete cleanup should remove durable story memory scene-result rows."); + Assert(dependencyCleanupMigration.Contains("StoryMemoryImportPreferences", StringComparison.Ordinal), "Hard delete cleanup should remove durable story memory import rows."); + Assert(dependencyCleanupMigration.Contains("SceneCharacterSuggestions", StringComparison.Ordinal), "Hard delete cleanup should remove scene character suggestion rows."); + Assert(dependencyCleanupMigration.Contains("SceneLocationSuggestions", StringComparison.Ordinal), "Hard delete cleanup should remove scene location suggestion rows."); + Assert(dependencyCleanupMigration.Contains("SceneAssetSuggestions", StringComparison.Ordinal), "Hard delete cleanup should remove scene asset suggestion rows."); + Assert(dependencyCleanupMigration.Contains("WordCompanionSceneLocations", StringComparison.Ordinal), "Hard delete cleanup should remove Word Companion scene location rows."); + Assert(dependencyCleanupMigration.Contains("WordCompanionSceneAssets", StringComparison.Ordinal), "Hard delete cleanup should remove Word Companion scene asset rows."); + Assert(dependencyCleanupMigration.Contains("WHERE ProjectID = @ProjectID", StringComparison.Ordinal), "Hard delete cleanup must be scoped by project id."); Assert(cascadeMigration.Contains("FK_SceneManuscriptSources_Scenes", StringComparison.Ordinal) && cascadeMigration.Contains("REFERENCES dbo.Scenes(SceneID) ON DELETE CASCADE", StringComparison.Ordinal), "Scene manuscript source text should not block permanent scene deletion."); Assert(cascadeMigration.Contains("FK_ManuscriptStructuralChapterSources_Chapters", StringComparison.Ordinal) diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index 46ad49d..4f22ff4 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -2112,9 +2112,10 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : try { await connection.ExecuteAsync( - ProjectHardDeleteStoryIntelligenceDependencyCleanupSql, + "dbo.ProjectHardDelete_PrepareDependencies", new { ProjectID = projectId, UserID = userId, AllowActiveProject = false }, transaction, + commandType: CommandType.StoredProcedure, commandTimeout: 120); await connection.ExecuteAsync( @@ -2132,137 +2133,6 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : } } - private const string ProjectHardDeleteStoryIntelligenceDependencyCleanupSql = """ -SET NOCOUNT ON; - -IF NOT EXISTS -( - SELECT 1 - FROM dbo.Projects p - INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID - WHERE p.ProjectID = @ProjectID - AND pua.UserID = @UserID - AND pua.AccessRole = N'Owner' - AND pua.IsActive = 1 -) - THROW 52001, 'Project was not found for this owner.', 1; - -IF @AllowActiveProject = 0 AND EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0) - THROW 52002, 'Only archived projects can be permanently deleted.', 1; - -DECLARE @BookIDs TABLE (BookID int NOT NULL PRIMARY KEY); -DECLARE @ChapterIDs TABLE (ChapterID int NOT NULL PRIMARY KEY); -DECLARE @SceneIDs TABLE (SceneID int NOT NULL PRIMARY KEY); -DECLARE @StoryIntelligenceRunIDs TABLE (StoryIntelligenceRunID int NOT NULL PRIMARY KEY); -DECLARE @StoryIntelligenceChapterResultIDs TABLE (ChapterResultID int NOT NULL PRIMARY KEY); -DECLARE @StoryIntelligenceSceneResultIDs TABLE (SceneResultID int NOT NULL PRIMARY KEY); -DECLARE @ImportSessionIDs TABLE (ImportSessionID int NOT NULL PRIMARY KEY); -DECLARE @CharacterIntelligenceProfileIDs TABLE (CharacterIntelligenceProfileID int NOT NULL PRIMARY KEY); - -INSERT @BookIDs (BookID) -SELECT BookID FROM dbo.Books WHERE ProjectID = @ProjectID; - -INSERT @ChapterIDs (ChapterID) -SELECT c.ChapterID -FROM dbo.Chapters c -INNER JOIN @BookIDs b ON b.BookID = c.BookID; - -INSERT @SceneIDs (SceneID) -SELECT s.SceneID -FROM dbo.Scenes s -INNER JOIN @ChapterIDs c ON c.ChapterID = s.ChapterID; - -INSERT @ImportSessionIDs (ImportSessionID) -SELECT StoryIntelligenceBookPipelineID -FROM dbo.StoryIntelligenceBookPipelines pipeline -WHERE pipeline.ProjectID = @ProjectID - OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = pipeline.BookID); - -INSERT @StoryIntelligenceRunIDs (StoryIntelligenceRunID) -SELECT DISTINCT r.StoryIntelligenceRunID -FROM dbo.StoryIntelligenceRuns r -WHERE r.ProjectID = @ProjectID - OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = r.BookID) - OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = r.ChapterID) - OR EXISTS (SELECT 1 FROM @SceneIDs s INNER JOIN dbo.Scenes scene ON scene.SceneID = s.SceneID WHERE scene.ImportRunID = r.StoryIntelligenceRunID); - -INSERT @StoryIntelligenceChapterResultIDs (ChapterResultID) -SELECT DISTINCT cr.ChapterResultID -FROM dbo.StoryIntelligenceChapterResults cr -WHERE cr.ProjectID = @ProjectID - OR EXISTS (SELECT 1 FROM @StoryIntelligenceRunIDs r WHERE r.StoryIntelligenceRunID = cr.StoryIntelligenceRunID) - OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = cr.BookID) - OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = cr.ChapterID); - -INSERT @StoryIntelligenceSceneResultIDs (SceneResultID) -SELECT DISTINCT sr.SceneResultID -FROM dbo.StoryIntelligenceSceneResults sr -WHERE sr.ProjectID = @ProjectID - OR EXISTS (SELECT 1 FROM @StoryIntelligenceRunIDs r WHERE r.StoryIntelligenceRunID = sr.StoryIntelligenceRunID) - OR EXISTS (SELECT 1 FROM @StoryIntelligenceChapterResultIDs cr WHERE cr.ChapterResultID = sr.ChapterResultID) - OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = sr.BookID) - OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = sr.ChapterID) - OR EXISTS (SELECT 1 FROM @SceneIDs s WHERE s.SceneID = sr.SceneID); - -INSERT @CharacterIntelligenceProfileIDs (CharacterIntelligenceProfileID) -SELECT DISTINCT profile.CharacterIntelligenceProfileID -FROM dbo.CharacterIntelligenceProfiles profile -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs sessionIDs WHERE sessionIDs.ImportSessionID = profile.ImportSessionID) - OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = profile.LastUpdatedSceneResultID); - -DELETE evidence -FROM dbo.CharacterIntelligenceEvidenceEvents evidence -WHERE EXISTS (SELECT 1 FROM @CharacterIntelligenceProfileIDs profileIDs WHERE profileIDs.CharacterIntelligenceProfileID = evidence.CharacterIntelligenceProfileID) - OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = evidence.SceneResultID); - -DELETE profileRows -FROM dbo.CharacterIntelligenceProfiles profileRows -WHERE EXISTS (SELECT 1 FROM @CharacterIntelligenceProfileIDs profileIDs WHERE profileIDs.CharacterIntelligenceProfileID = profileRows.CharacterIntelligenceProfileID); - -DELETE FROM dbo.StoryMemoryIllustrationAssignments -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationAssignments.ImportSessionID); - -DELETE FROM dbo.StoryMemoryIllustrationDemands -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationDemands.ImportSessionID); - -DELETE FROM dbo.StoryMemoryRelationships -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryRelationships.ImportSessionID); - -DELETE FROM dbo.StoryMemoryCharacterAttributes -WHERE EXISTS -( - SELECT 1 - FROM dbo.StoryMemoryCharacters c - INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID - WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAttributes.StoryMemoryCharacterID -); - -DELETE FROM dbo.StoryMemoryCharacterAliases -WHERE EXISTS -( - SELECT 1 - FROM dbo.StoryMemoryCharacters c - INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID - WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAliases.StoryMemoryCharacterID -); - -DELETE FROM dbo.StoryMemoryAssets -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryAssets.ImportSessionID); - -DELETE FROM dbo.StoryMemoryLocations -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryLocations.ImportSessionID); - -DELETE FROM dbo.StoryMemoryCharacters -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryCharacters.ImportSessionID); - -DELETE FROM dbo.StoryMemoryProcessedSceneResults -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryProcessedSceneResults.ImportSessionID) - OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = StoryMemoryProcessedSceneResults.SceneResultID); - -DELETE FROM dbo.StoryMemoryImportPreferences -WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryImportPreferences.ImportSessionID); -"""; - public async Task> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); @@ -2282,9 +2152,10 @@ WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = St try { await connection.ExecuteAsync( - ProjectHardDeleteStoryIntelligenceDependencyCleanupSql, + "dbo.ProjectHardDelete_PrepareDependencies", new { ProjectID = projectId, UserID = userId, AllowActiveProject = true }, transaction, + commandType: CommandType.StoredProcedure, commandTimeout: 120); await connection.ExecuteAsync( diff --git a/PlotLine/Sql/160_Phase25E_ProjectHardDeleteDependencyCleanup.sql b/PlotLine/Sql/160_Phase25E_ProjectHardDeleteDependencyCleanup.sql new file mode 100644 index 0000000..4b0d797 --- /dev/null +++ b/PlotLine/Sql/160_Phase25E_ProjectHardDeleteDependencyCleanup.sql @@ -0,0 +1,218 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectHardDelete_PrepareDependencies + @ProjectID int, + @UserID int, + @AllowActiveProject bit = 0 +AS +BEGIN + SET NOCOUNT ON; + + IF NOT EXISTS + ( + SELECT 1 + FROM dbo.Projects p + INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID + WHERE p.ProjectID = @ProjectID + AND pua.UserID = @UserID + AND pua.AccessRole = N'Owner' + AND pua.IsActive = 1 + ) + THROW 52001, 'Project was not found for this owner.', 1; + + IF @AllowActiveProject = 0 AND EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0) + THROW 52002, 'Only archived projects can be permanently deleted.', 1; + + DECLARE @BookIDs TABLE (BookID int NOT NULL PRIMARY KEY); + DECLARE @ChapterIDs TABLE (ChapterID int NOT NULL PRIMARY KEY); + DECLARE @SceneIDs TABLE (SceneID int NOT NULL PRIMARY KEY); + DECLARE @StoryIntelligenceRunIDs TABLE (StoryIntelligenceRunID int NOT NULL PRIMARY KEY); + DECLARE @StoryIntelligenceChapterResultIDs TABLE (ChapterResultID int NOT NULL PRIMARY KEY); + DECLARE @StoryIntelligenceSceneResultIDs TABLE (SceneResultID int NOT NULL PRIMARY KEY); + DECLARE @ImportSessionIDs TABLE (ImportSessionID int NOT NULL PRIMARY KEY); + DECLARE @CharacterIntelligenceProfileIDs TABLE (CharacterIntelligenceProfileID int NOT NULL PRIMARY KEY); + + INSERT @BookIDs (BookID) + SELECT BookID FROM dbo.Books WHERE ProjectID = @ProjectID; + + INSERT @ChapterIDs (ChapterID) + SELECT c.ChapterID + FROM dbo.Chapters c + INNER JOIN @BookIDs b ON b.BookID = c.BookID; + + INSERT @SceneIDs (SceneID) + SELECT s.SceneID + FROM dbo.Scenes s + INNER JOIN @ChapterIDs c ON c.ChapterID = s.ChapterID; + + INSERT @ImportSessionIDs (ImportSessionID) + SELECT StoryIntelligenceBookPipelineID + FROM dbo.StoryIntelligenceBookPipelines pipeline + WHERE pipeline.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = pipeline.BookID); + + INSERT @StoryIntelligenceRunIDs (StoryIntelligenceRunID) + SELECT DISTINCT r.StoryIntelligenceRunID + FROM dbo.StoryIntelligenceRuns r + WHERE r.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = r.BookID) + OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = r.ChapterID) + OR EXISTS (SELECT 1 FROM @SceneIDs s INNER JOIN dbo.Scenes scene ON scene.SceneID = s.SceneID WHERE scene.ImportRunID = r.StoryIntelligenceRunID); + + INSERT @StoryIntelligenceChapterResultIDs (ChapterResultID) + SELECT DISTINCT cr.ChapterResultID + FROM dbo.StoryIntelligenceChapterResults cr + WHERE cr.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @StoryIntelligenceRunIDs r WHERE r.StoryIntelligenceRunID = cr.StoryIntelligenceRunID) + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = cr.BookID) + OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = cr.ChapterID); + + INSERT @StoryIntelligenceSceneResultIDs (SceneResultID) + SELECT DISTINCT sr.SceneResultID + FROM dbo.StoryIntelligenceSceneResults sr + WHERE sr.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @StoryIntelligenceRunIDs r WHERE r.StoryIntelligenceRunID = sr.StoryIntelligenceRunID) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceChapterResultIDs cr WHERE cr.ChapterResultID = sr.ChapterResultID) + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = sr.BookID) + OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = sr.ChapterID) + OR EXISTS (SELECT 1 FROM @SceneIDs s WHERE s.SceneID = sr.SceneID); + + INSERT @CharacterIntelligenceProfileIDs (CharacterIntelligenceProfileID) + SELECT DISTINCT profile.CharacterIntelligenceProfileID + FROM dbo.CharacterIntelligenceProfiles profile + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs sessionIDs WHERE sessionIDs.ImportSessionID = profile.ImportSessionID) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = profile.LastUpdatedSceneResultID); + + DELETE suggestion + FROM dbo.SceneCharacterSuggestions suggestion + LEFT JOIN @SceneIDs scene ON scene.SceneID = suggestion.SceneID + LEFT JOIN dbo.Characters characterRows ON characterRows.CharacterID = suggestion.CharacterID + WHERE scene.SceneID IS NOT NULL + OR characterRows.ProjectID = @ProjectID; + + DELETE suggestion + FROM dbo.SceneLocationSuggestions suggestion + LEFT JOIN @SceneIDs scene ON scene.SceneID = suggestion.SceneID + LEFT JOIN dbo.Locations locationRows ON locationRows.LocationID = suggestion.LocationID + WHERE scene.SceneID IS NOT NULL + OR locationRows.ProjectID = @ProjectID; + + DELETE suggestion + FROM dbo.SceneAssetSuggestions suggestion + LEFT JOIN @SceneIDs scene ON scene.SceneID = suggestion.SceneID + LEFT JOIN dbo.StoryAssets assetRows ON assetRows.StoryAssetID = suggestion.AssetID + WHERE scene.SceneID IS NOT NULL + OR assetRows.ProjectID = @ProjectID; + + DELETE sceneAssets + FROM dbo.WordCompanionSceneAssets sceneAssets + LEFT JOIN @SceneIDs scene ON scene.SceneID = sceneAssets.SceneID + LEFT JOIN dbo.StoryAssets assetRows ON assetRows.StoryAssetID = sceneAssets.StoryAssetID + WHERE scene.SceneID IS NOT NULL + OR assetRows.ProjectID = @ProjectID; + + DELETE sceneLocations + FROM dbo.WordCompanionSceneLocations sceneLocations + LEFT JOIN @SceneIDs scene ON scene.SceneID = sceneLocations.SceneID + LEFT JOIN dbo.Locations locationRows ON locationRows.LocationID = sceneLocations.LocationID + WHERE scene.SceneID IS NOT NULL + OR locationRows.ProjectID = @ProjectID; + + DELETE evidence + FROM dbo.CharacterIntelligenceEvidenceEvents evidence + WHERE EXISTS (SELECT 1 FROM @CharacterIntelligenceProfileIDs profileIDs WHERE profileIDs.CharacterIntelligenceProfileID = evidence.CharacterIntelligenceProfileID) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = evidence.SceneResultID); + + DELETE profileRows + FROM dbo.CharacterIntelligenceProfiles profileRows + WHERE EXISTS (SELECT 1 FROM @CharacterIntelligenceProfileIDs profileIDs WHERE profileIDs.CharacterIntelligenceProfileID = profileRows.CharacterIntelligenceProfileID); + + DELETE FROM dbo.StoryMemoryIllustrationAssignments + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationAssignments.ImportSessionID); + + DELETE FROM dbo.StoryMemoryIllustrationDemands + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationDemands.ImportSessionID); + + DELETE FROM dbo.StoryMemoryRelationships + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryRelationships.ImportSessionID); + + DELETE FROM dbo.StoryMemoryCharacterAttributes + WHERE EXISTS + ( + SELECT 1 + FROM dbo.StoryMemoryCharacters c + INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID + WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAttributes.StoryMemoryCharacterID + ); + + DELETE FROM dbo.StoryMemoryCharacterAliases + WHERE EXISTS + ( + SELECT 1 + FROM dbo.StoryMemoryCharacters c + INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID + WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAliases.StoryMemoryCharacterID + ); + + DELETE FROM dbo.StoryMemoryAssets + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryAssets.ImportSessionID); + + DELETE FROM dbo.StoryMemoryLocations + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryLocations.ImportSessionID); + + DELETE FROM dbo.StoryMemoryCharacters + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryCharacters.ImportSessionID); + + DELETE FROM dbo.StoryMemoryProcessedSceneResults + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryProcessedSceneResults.ImportSessionID) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = StoryMemoryProcessedSceneResults.SceneResultID); + + DELETE FROM dbo.StoryMemoryImportPreferences + WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryImportPreferences.ImportSessionID); +END; +GO + +DECLARE @ProjectHardDeleteDefinition nvarchar(max) = OBJECT_DEFINITION(OBJECT_ID(N'dbo.ProjectHardDelete_ForOwner')); + +IF @ProjectHardDeleteDefinition IS NOT NULL + AND @ProjectHardDeleteDefinition NOT LIKE N'%ProjectHardDelete_PrepareDependencies%' +BEGIN + SET @ProjectHardDeleteDefinition = REPLACE + ( + @ProjectHardDeleteDefinition, + N' BEGIN TRANSACTION;', + N' BEGIN TRANSACTION; + + EXEC dbo.ProjectHardDelete_PrepareDependencies + @ProjectID = @ProjectID, + @UserID = @UserID, + @AllowActiveProject = 0;' + ); + + IF @ProjectHardDeleteDefinition NOT LIKE N'%ProjectHardDelete_PrepareDependencies%' + THROW 52012, 'Project hard-delete dependency cleanup call was not inserted.', 1; + + DECLARE @CreateIndex int = CHARINDEX(N'CREATE', @ProjectHardDeleteDefinition); + DECLARE @ProcedureIndex int = CHARINDEX(N'PROCEDURE', @ProjectHardDeleteDefinition); + + IF @CreateIndex = 0 OR @ProcedureIndex = 0 OR @ProcedureIndex <= @CreateIndex + THROW 52013, 'Project hard-delete procedure definition could not be patched.', 1; + + SET @ProjectHardDeleteDefinition = STUFF + ( + @ProjectHardDeleteDefinition, + @CreateIndex, + @ProcedureIndex - @CreateIndex, + N'ALTER ' + ); + + EXEC sys.sp_executesql @ProjectHardDeleteDefinition; +END; +GO + +IF OBJECT_DEFINITION(OBJECT_ID(N'dbo.ProjectHardDelete_ForOwner')) NOT LIKE N'%ProjectHardDelete_PrepareDependencies%' + THROW 52014, 'Project hard-delete procedure does not call dependency cleanup.', 1; +GO