PlotDirector/PlotLine/Sql/193_LocationAndAssetMerge.sql

210 lines
15 KiB
Transact-SQL

-- Merge duplicate locations and story assets without deleting their archived source records.
-- Keep the selected target details; move canonical references and preserve known names.
CREATE OR ALTER PROCEDURE dbo.Location_Merge
@SourceID int,
@TargetID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @SourceID IS NULL OR @TargetID IS NULL OR @SourceID = @TargetID
THROW 51000, 'Choose two different locations to merge.', 1;
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @SourceName nvarchar(200), @TargetName nvarchar(200), @Now datetime2 = SYSUTCDATETIME();
SELECT @ProjectID = ProjectID FROM dbo.Locations WHERE LocationID = @SourceID;
-- Serialize merges in a project before locking individual entities.
DECLARE @LockedProjectID int;
SELECT @LockedProjectID = ProjectID FROM dbo.Projects WITH (UPDLOCK, HOLDLOCK) WHERE ProjectID = @ProjectID;
IF @UserID IS NULL OR NOT EXISTS
(SELECT 1 FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND UserID = @UserID AND IsActive = 1)
THROW 51000, 'You do not have access to this project.', 1;
SELECT @SourceName = LocationName FROM dbo.Locations WITH (UPDLOCK, HOLDLOCK)
WHERE LocationID = @SourceID AND IsArchived = 0;
SELECT @TargetName = LocationName FROM dbo.Locations WITH (UPDLOCK, HOLDLOCK)
WHERE LocationID = @TargetID AND ProjectID = @ProjectID AND IsArchived = 0;
IF @SourceName IS NULL OR @TargetName IS NULL
THROW 51000, 'Both locations must be active and belong to the same project.', 1;
-- Merging an ancestor into its descendant would create a hierarchy cycle.
DECLARE @AncestorID int = @TargetID;
DECLARE @Visited TABLE (LocationID int PRIMARY KEY);
WHILE @AncestorID IS NOT NULL
BEGIN
IF @AncestorID = @SourceID OR EXISTS (SELECT 1 FROM @Visited WHERE LocationID = @AncestorID)
THROW 51000, 'Cannot merge a location into one of its descendants. Choose another location or change its parent first.', 1;
INSERT @Visited VALUES (@AncestorID);
SELECT @AncestorID = ParentLocationID FROM dbo.Locations WITH (UPDLOCK, HOLDLOCK) WHERE LocationID = @AncestorID;
END;
INSERT dbo.LocationAliases (LocationID, Alias, SortOrder)
SELECT @TargetID, AliasValue, ISNULL((SELECT MAX(SortOrder) FROM dbo.LocationAliases WHERE LocationID = @TargetID), 0)
+ ROW_NUMBER() OVER (ORDER BY AliasValue) * 10
FROM (SELECT LTRIM(RTRIM(@SourceName)) AS AliasValue
UNION SELECT LTRIM(RTRIM(Alias)) FROM dbo.LocationAliases WHERE LocationID = @SourceID) names
WHERE NULLIF(AliasValue, N'') IS NOT NULL AND AliasValue <> @TargetName
AND NOT EXISTS (SELECT 1 FROM dbo.LocationAliases existing WHERE existing.LocationID = @TargetID
AND UPPER(LTRIM(RTRIM(existing.Alias))) = UPPER(AliasValue));
DELETE sourceRows FROM dbo.WordCompanionSceneLocations sourceRows
WHERE sourceRows.LocationID = @SourceID
AND EXISTS (SELECT 1 FROM dbo.WordCompanionSceneLocations targetRows WHERE targetRows.LocationID = @TargetID
AND targetRows.SceneID = sourceRows.SceneID);
DELETE sourceRows FROM dbo.SceneLocationSuggestions sourceRows
WHERE sourceRows.LocationID = @SourceID AND sourceRows.Status = N'Pending'
AND EXISTS (SELECT 1 FROM dbo.SceneLocationSuggestions targetRows WHERE targetRows.LocationID = @TargetID
AND targetRows.SceneID = sourceRows.SceneID AND targetRows.Status = sourceRows.Status);
UPDATE dbo.Locations SET ParentLocationID = @TargetID WHERE ParentLocationID = @SourceID;
UPDATE dbo.Scenes SET PrimaryLocationID = @TargetID WHERE PrimaryLocationID = @SourceID;
UPDATE dbo.SceneCharacters SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.SceneCharacters SET EntryLocationID = @TargetID WHERE EntryLocationID = @SourceID;
UPDATE dbo.SceneCharacters SET ExitLocationID = @TargetID WHERE ExitLocationID = @SourceID;
UPDATE dbo.StoryAssets SET CurrentLocationID = @TargetID WHERE CurrentLocationID = @SourceID;
UPDATE dbo.SceneAssetLocations SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.SceneFloorPlanOccupancy SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.WordCompanionSceneLocations SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.SceneLocationSuggestions SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.FloorPlans SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.FloorPlanFloors SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.FloorPlanBlocks SET LocationID = @TargetID WHERE LocationID = @SourceID;
UPDATE dbo.FloorPlanTransitions SET FromLocationID = @TargetID WHERE FromLocationID = @SourceID;
UPDATE dbo.FloorPlanTransitions SET ToLocationID = @TargetID WHERE ToLocationID = @SourceID;
UPDATE dbo.CoreImportBasicLocationCandidates SET ExistingLocationID = @TargetID WHERE ExistingLocationID = @SourceID;
UPDATE dbo.CoreImportBasicLocationCandidates SET ParentLocationID = @TargetID WHERE ParentLocationID = @SourceID;
UPDATE dbo.CoreImportBasicLocationCandidates SET ResultLocationID = @TargetID WHERE ResultLocationID = @SourceID;
UPDATE dbo.CoreImportBasicLocationCandidates SET SameAsLocationID = @TargetID WHERE SameAsLocationID = @SourceID;
-- Keep self-links archived with their original endpoints, as in character merging.
UPDATE dbo.LocationRelationships SET IsArchived = 1, UpdatedDate = @Now
WHERE (FromLocationID = @SourceID AND ToLocationID = @TargetID) OR (FromLocationID = @TargetID AND ToLocationID = @SourceID);
UPDATE dbo.LocationRelationships SET FromLocationID = CASE WHEN FromLocationID = @SourceID THEN @TargetID ELSE FromLocationID END,
ToLocationID = CASE WHEN ToLocationID = @SourceID THEN @TargetID ELSE ToLocationID END, UpdatedDate = @Now
WHERE IsArchived = 0 AND (FromLocationID = @SourceID OR ToLocationID = @SourceID);
-- Collapse identical active links while retaining links with different evidence/settings.
UPDATE duplicate SET IsArchived = 1, UpdatedDate = @Now
FROM dbo.LocationRelationships duplicate
WHERE duplicate.IsArchived = 0 AND (duplicate.FromLocationID = @TargetID OR duplicate.ToLocationID = @TargetID)
AND EXISTS (SELECT 1 FROM dbo.LocationRelationships kept WHERE kept.IsArchived = 0
AND kept.LocationRelationshipID < duplicate.LocationRelationshipID
AND kept.FromLocationID = duplicate.FromLocationID AND kept.ToLocationID = duplicate.ToLocationID
AND (kept.LocationRelationshipTypeID = duplicate.LocationRelationshipTypeID OR (kept.LocationRelationshipTypeID IS NULL AND duplicate.LocationRelationshipTypeID IS NULL))
AND (kept.IsBidirectional = duplicate.IsBidirectional OR (kept.IsBidirectional IS NULL AND duplicate.IsBidirectional IS NULL))
AND (kept.Strength = duplicate.Strength OR (kept.Strength IS NULL AND duplicate.Strength IS NULL))
AND (kept.CanHearNormalSpeech = duplicate.CanHearNormalSpeech OR (kept.CanHearNormalSpeech IS NULL AND duplicate.CanHearNormalSpeech IS NULL))
AND (kept.CanHearLoudNoise = duplicate.CanHearLoudNoise OR (kept.CanHearLoudNoise IS NULL AND duplicate.CanHearLoudNoise IS NULL))
AND (kept.CanSeeMovement = duplicate.CanSeeMovement OR (kept.CanSeeMovement IS NULL AND duplicate.CanSeeMovement IS NULL))
AND (kept.CanSeeClearly = duplicate.CanSeeClearly OR (kept.CanSeeClearly IS NULL AND duplicate.CanSeeClearly IS NULL))
AND (kept.CanTravelDirectly = duplicate.CanTravelDirectly OR (kept.CanTravelDirectly IS NULL AND duplicate.CanTravelDirectly IS NULL))
AND (kept.Notes = duplicate.Notes OR (kept.Notes IS NULL AND duplicate.Notes IS NULL)));
UPDATE dbo.Locations
SET IsArchived = 1, ArchivedDate = @Now,
ArchivedReason = CONCAT(N'Merged into ', @TargetName, N' (LocationID ', @TargetID, N').'),
UpdatedDate = @Now, ModifiedByUserID = @UserID, ModifiedDateUTC = @Now
WHERE LocationID = @SourceID;
COMMIT TRANSACTION;
SELECT @ProjectID AS ProjectID, @TargetID AS TargetID, @SourceName AS SourceName, @TargetName AS TargetName;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_Merge
@SourceID int,
@TargetID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @SourceID IS NULL OR @TargetID IS NULL OR @SourceID = @TargetID
THROW 51000, 'Choose two different assets to merge.', 1;
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @SourceName nvarchar(200), @TargetName nvarchar(200), @Now datetime2 = SYSUTCDATETIME();
SELECT @ProjectID = ProjectID FROM dbo.StoryAssets WHERE StoryAssetID = @SourceID;
-- Serialize merges in a project before locking individual entities.
DECLARE @LockedProjectID int;
SELECT @LockedProjectID = ProjectID FROM dbo.Projects WITH (UPDLOCK, HOLDLOCK) WHERE ProjectID = @ProjectID;
IF @UserID IS NULL OR NOT EXISTS
(SELECT 1 FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND UserID = @UserID AND IsActive = 1)
THROW 51000, 'You do not have access to this project.', 1;
SELECT @SourceName = AssetName FROM dbo.StoryAssets WITH (UPDLOCK, HOLDLOCK)
WHERE StoryAssetID = @SourceID AND IsArchived = 0;
SELECT @TargetName = AssetName FROM dbo.StoryAssets WITH (UPDLOCK, HOLDLOCK)
WHERE StoryAssetID = @TargetID AND ProjectID = @ProjectID AND IsArchived = 0;
IF @SourceName IS NULL OR @TargetName IS NULL
THROW 51000, 'Both assets must be active and belong to the same project.', 1;
INSERT dbo.AssetAliases (StoryAssetID, Alias, SortOrder)
SELECT @TargetID, AliasValue, ISNULL((SELECT MAX(SortOrder) FROM dbo.AssetAliases WHERE StoryAssetID = @TargetID), 0)
+ ROW_NUMBER() OVER (ORDER BY AliasValue) * 10
FROM (SELECT LTRIM(RTRIM(@SourceName)) AS AliasValue
UNION SELECT LTRIM(RTRIM(Alias)) FROM dbo.AssetAliases WHERE StoryAssetID = @SourceID) names
WHERE NULLIF(AliasValue, N'') IS NOT NULL AND AliasValue <> @TargetName
AND NOT EXISTS (SELECT 1 FROM dbo.AssetAliases existing WHERE existing.StoryAssetID = @TargetID
AND UPPER(LTRIM(RTRIM(existing.Alias))) = UPPER(AliasValue));
DELETE sourceRows FROM dbo.WordCompanionSceneAssets sourceRows
WHERE sourceRows.StoryAssetID = @SourceID
AND EXISTS (SELECT 1 FROM dbo.WordCompanionSceneAssets targetRows WHERE targetRows.StoryAssetID = @TargetID
AND targetRows.SceneID = sourceRows.SceneID);
DELETE sourceRows FROM dbo.SceneAssetSuggestions sourceRows
WHERE sourceRows.AssetID = @SourceID AND sourceRows.Status = N'Pending'
AND EXISTS (SELECT 1 FROM dbo.SceneAssetSuggestions targetRows WHERE targetRows.AssetID = @TargetID
AND targetRows.SceneID = sourceRows.SceneID AND targetRows.Status = sourceRows.Status);
DELETE sourceRows FROM dbo.SceneFloorPlanOccupancy sourceRows
WHERE sourceRows.StoryAssetID = @SourceID
AND EXISTS (SELECT 1 FROM dbo.SceneFloorPlanOccupancy targetRows WHERE targetRows.StoryAssetID = @TargetID
AND targetRows.SceneID = sourceRows.SceneID);
DELETE sourceRows FROM dbo.ContinuityWarningAcknowledgements sourceRows
WHERE sourceRows.AssetID = @SourceID
AND EXISTS (SELECT 1 FROM dbo.ContinuityWarningAcknowledgements targetRows WHERE targetRows.AssetID = @TargetID
AND targetRows.ProjectID = sourceRows.ProjectID AND targetRows.WarningType = sourceRows.WarningType AND targetRows.SceneIDKey = sourceRows.SceneIDKey AND targetRows.CharacterIDKey = sourceRows.CharacterIDKey);
UPDATE dbo.AssetEvents SET StoryAssetID = @TargetID WHERE StoryAssetID = @SourceID;
UPDATE dbo.AssetCustodyEvents SET StoryAssetID = @TargetID WHERE StoryAssetID = @SourceID;
UPDATE dbo.CharacterKnowledge SET StoryAssetID = @TargetID WHERE StoryAssetID = @SourceID;
UPDATE dbo.SceneAssetLocations SET StoryAssetID = @TargetID WHERE StoryAssetID = @SourceID;
UPDATE dbo.SceneFloorPlanOccupancy SET StoryAssetID = @TargetID WHERE StoryAssetID = @SourceID;
UPDATE dbo.WordCompanionSceneAssets SET StoryAssetID = @TargetID WHERE StoryAssetID = @SourceID;
UPDATE dbo.SceneAssetSuggestions SET AssetID = @TargetID WHERE AssetID = @SourceID;
UPDATE dbo.ContinuityWarningAcknowledgements SET AssetID = @TargetID WHERE AssetID = @SourceID;
-- Keep self-links archived with their original endpoints, as in character merging.
UPDATE dbo.AssetDependencies SET IsArchived = 1, UpdatedDate = @Now
WHERE (SourceAssetID = @SourceID AND TargetAssetID = @TargetID) OR (SourceAssetID = @TargetID AND TargetAssetID = @SourceID);
UPDATE dbo.AssetDependencies SET SourceAssetID = CASE WHEN SourceAssetID = @SourceID THEN @TargetID ELSE SourceAssetID END,
TargetAssetID = CASE WHEN TargetAssetID = @SourceID THEN @TargetID ELSE TargetAssetID END, UpdatedDate = @Now
WHERE IsArchived = 0 AND (SourceAssetID = @SourceID OR TargetAssetID = @SourceID);
-- Collapse identical active links while retaining links with different evidence/settings.
UPDATE duplicate SET IsArchived = 1, UpdatedDate = @Now
FROM dbo.AssetDependencies duplicate
WHERE duplicate.IsArchived = 0 AND (duplicate.SourceAssetID = @TargetID OR duplicate.TargetAssetID = @TargetID)
AND EXISTS (SELECT 1 FROM dbo.AssetDependencies kept WHERE kept.IsArchived = 0
AND kept.AssetDependencyID < duplicate.AssetDependencyID
AND kept.SourceAssetID = duplicate.SourceAssetID AND kept.TargetAssetID = duplicate.TargetAssetID
AND (kept.AssetDependencyTypeID = duplicate.AssetDependencyTypeID OR (kept.AssetDependencyTypeID IS NULL AND duplicate.AssetDependencyTypeID IS NULL))
AND (kept.SourceRequiredStateID = duplicate.SourceRequiredStateID OR (kept.SourceRequiredStateID IS NULL AND duplicate.SourceRequiredStateID IS NULL))
AND (kept.TargetBlockedStateID = duplicate.TargetBlockedStateID OR (kept.TargetBlockedStateID IS NULL AND duplicate.TargetBlockedStateID IS NULL))
AND (kept.Description = duplicate.Description OR (kept.Description IS NULL AND duplicate.Description IS NULL)));
UPDATE dbo.StoryAssets
SET IsArchived = 1, ArchivedDate = @Now,
ArchivedReason = CONCAT(N'Merged into ', @TargetName, N' (StoryAssetID ', @TargetID, N').'),
UpdatedDate = @Now, ModifiedByUserID = @UserID, ModifiedDateUTC = @Now
WHERE StoryAssetID = @SourceID;
COMMIT TRANSACTION;
SELECT @ProjectID AS ProjectID, @TargetID AS TargetID, @SourceName AS SourceName, @TargetName AS TargetName;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
END;
GO