SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO IF COL_LENGTH(N'dbo.CharacterEnrichmentRuns', N'CharacterID') IS NULL BEGIN ALTER TABLE dbo.CharacterEnrichmentRuns ADD CharacterID int NULL; END; GO IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_CharacterEnrichmentRuns_Characters') BEGIN ALTER TABLE dbo.CharacterEnrichmentRuns ADD CONSTRAINT FK_CharacterEnrichmentRuns_Characters FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_CharacterEnrichmentRuns_ActiveCharacter' AND object_id = OBJECT_ID(N'dbo.CharacterEnrichmentRuns')) BEGIN CREATE UNIQUE INDEX UX_CharacterEnrichmentRuns_ActiveCharacter ON dbo.CharacterEnrichmentRuns(BookID, CharacterID) WHERE CharacterID IS NOT NULL AND IsCurrent = 1 AND Status IN (N'Queued', N'Running'); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_CharacterEnrichmentRuns_Character_Current' AND object_id = OBJECT_ID(N'dbo.CharacterEnrichmentRuns')) BEGIN CREATE INDEX IX_CharacterEnrichmentRuns_Character_Current ON dbo.CharacterEnrichmentRuns(CharacterID, IsCurrent, UpdatedUtc DESC) INCLUDE (BookID, UserID, Status, CompletedUtc); END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_QueueCharacter @CharacterID int, @UserID int, @BookID int = NULL AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @ProjectID int, @ResolvedBookID int, @PipelineID int, @ExistingID int, @Version nvarchar(120), @WasCoalesced bit = 1; SELECT TOP (1) @ProjectID = c.ProjectID, @ResolvedBookID = ch.BookID FROM dbo.Characters c INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID INNER JOIN dbo.SceneCharacters sc ON sc.CharacterID = c.CharacterID INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0 INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.IsArchived = 0 INNER JOIN dbo.SceneManuscriptSources sms ON sms.SceneID = s.SceneID WHERE c.CharacterID = @CharacterID AND c.IsArchived = 0 AND pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1 AND (@BookID IS NULL OR ch.BookID = @BookID) ORDER BY ch.SortOrder, ch.ChapterNumber, s.SortOrder, s.SceneNumber, s.SceneID; IF @ProjectID IS NULL OR @ResolvedBookID IS NULL THROW 52221, 'Character could not be queued for enrichment for this user and book.', 1; SELECT TOP (1) @PipelineID = StoryIntelligenceBookPipelineID FROM dbo.StoryIntelligenceBookPipelines WHERE ProjectID = @ProjectID AND BookID = @ResolvedBookID ORDER BY StoryIntelligenceBookPipelineID DESC; SET @Version = CONCAT(N'ManualCharacter:', @CharacterID, N':Book:', @ResolvedBookID); BEGIN TRANSACTION; SELECT TOP (1) @ExistingID = CharacterEnrichmentRunID FROM dbo.CharacterEnrichmentRuns WITH (UPDLOCK, HOLDLOCK) WHERE BookID = @ResolvedBookID AND CharacterID = @CharacterID AND IsCurrent = 1 AND Status IN (N'Queued', N'Running') ORDER BY CharacterEnrichmentRunID DESC; IF @ExistingID IS NULL BEGIN UPDATE dbo.CharacterEnrichmentRuns SET IsCurrent = 0, Status = N'Superseded', UpdatedUtc = SYSUTCDATETIME() WHERE BookID = @ResolvedBookID AND CharacterID = @CharacterID AND IsCurrent = 1; INSERT dbo.CharacterEnrichmentRuns (ProjectID, BookID, UserID, CharacterID, StoryIntelligenceBookPipelineID, CharacterResolutionVersion, Status, TotalCharacters, CurrentStage, CurrentMessage) VALUES (@ProjectID, @ResolvedBookID, @UserID, @CharacterID, @PipelineID, @Version, N'Queued', 1, N'Queued', N'Character reanalysis is queued.'); SET @ExistingID = CAST(SCOPE_IDENTITY() AS int); SET @WasCoalesced = 0; END; COMMIT TRANSACTION; SELECT runRows.*, WasCoalesced = @WasCoalesced FROM dbo.CharacterEnrichmentRuns runRows WHERE runRows.CharacterEnrichmentRunID = @ExistingID; END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_Retry @BookID int, @UserID int AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @PreviousID int, @ProjectID int, @PipelineID int, @Version nvarchar(120); SELECT TOP (1) @PreviousID = CharacterEnrichmentRunID, @ProjectID = ProjectID, @PipelineID = StoryIntelligenceBookPipelineID, @Version = CharacterResolutionVersion FROM dbo.CharacterEnrichmentRuns WHERE BookID = @BookID AND UserID = @UserID AND CharacterID IS NULL AND IsCurrent = 1 AND Status = N'Failed' ORDER BY CharacterEnrichmentRunID DESC; IF @PreviousID IS NULL BEGIN SELECT TOP (0) * FROM dbo.CharacterEnrichmentRuns; RETURN; END; UPDATE dbo.CharacterEnrichmentRuns SET IsCurrent = 0, Status = N'Superseded', UpdatedUtc = SYSUTCDATETIME() WHERE CharacterEnrichmentRunID = @PreviousID; EXEC dbo.CharacterEnrichmentRun_Queue @ProjectID = @ProjectID, @BookID = @BookID, @UserID = @UserID, @StoryIntelligenceBookPipelineID = @PipelineID, @CharacterResolutionVersion = @Version; END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_GetCurrentByBook @BookID int AS BEGIN SET NOCOUNT ON; SELECT TOP (1) * FROM dbo.CharacterEnrichmentRuns WHERE BookID = @BookID AND CharacterID IS NULL AND IsCurrent = 1 ORDER BY CharacterEnrichmentRunID DESC; END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_GetCurrentByCharacter @CharacterID int, @UserID int AS BEGIN SET NOCOUNT ON; SELECT TOP (1) runRows.* FROM dbo.CharacterEnrichmentRuns runRows INNER JOIN dbo.Characters c ON c.CharacterID = @CharacterID AND c.ProjectID = runRows.ProjectID INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID WHERE runRows.CharacterID = @CharacterID AND runRows.UserID = @UserID AND runRows.IsCurrent = 1 AND pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1 ORDER BY runRows.CharacterEnrichmentRunID DESC; END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichment_ContextGet @ProjectID int, @BookID int, @CharacterID int = NULL AS BEGIN SET NOCOUNT ON; SELECT BookTitle = CASE WHEN NULLIF(LTRIM(RTRIM(b.Subtitle)), N'') IS NULL THEN b.BookTitle ELSE CONCAT(b.BookTitle, N': ', b.Subtitle) END, b.StoryEra, SeriesStartDate = CAST(NULL AS date) FROM dbo.Books b WHERE b.BookID = @BookID AND b.ProjectID = @ProjectID AND b.IsArchived = 0; SELECT DISTINCT c.CharacterID, c.CharacterName, c.BirthDate, c.AgeAtSeriesStart, c.Height, c.EyeColour, c.DefaultDescription FROM dbo.Characters c INNER JOIN dbo.SceneCharacters sc ON sc.CharacterID = c.CharacterID INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0 INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.IsArchived = 0 INNER JOIN dbo.SceneManuscriptSources sms ON sms.SceneID = s.SceneID WHERE c.ProjectID = @ProjectID AND c.IsArchived = 0 AND ch.BookID = @BookID AND (@CharacterID IS NULL OR c.CharacterID = @CharacterID) ORDER BY c.CharacterName; SELECT alias.CharacterID, alias.Alias FROM dbo.CharacterAliases alias INNER JOIN dbo.Characters c ON c.CharacterID = alias.CharacterID WHERE c.ProjectID = @ProjectID AND c.IsArchived = 0 AND (@CharacterID IS NULL OR c.CharacterID = @CharacterID) ORDER BY alias.CharacterID, alias.SortOrder, alias.Alias; SELECT sc.CharacterID, s.SceneID, ch.ChapterID, ch.ChapterNumber, ch.ChapterTitle, s.SceneNumber, s.SceneTitle, sms.SourceText FROM dbo.SceneCharacters sc INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID AND c.ProjectID = @ProjectID AND c.IsArchived = 0 INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0 INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.BookID = @BookID AND ch.IsArchived = 0 INNER JOIN dbo.SceneManuscriptSources sms ON sms.SceneID = s.SceneID WHERE @CharacterID IS NULL OR sc.CharacterID = @CharacterID ORDER BY sc.CharacterID, ch.SortOrder, ch.ChapterNumber, s.SortOrder, s.SceneNumber, s.SceneID; END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentCharacterStatus_Get @CharacterID int, @UserID int AS BEGIN SET NOCOUNT ON; DECLARE @ProjectID int; SELECT @ProjectID = c.ProjectID FROM dbo.Characters c INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID WHERE c.CharacterID = @CharacterID AND c.IsArchived = 0 AND pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1; IF @ProjectID IS NULL BEGIN SELECT TOP (0) CharacterID = CAST(NULL AS int), BookID = CAST(NULL AS int), CharacterEnrichmentRunID = CAST(NULL AS int), Status = CAST(NULL AS nvarchar(30)), CurrentStage = CAST(NULL AS nvarchar(120)), CurrentMessage = CAST(NULL AS nvarchar(300)), LastAnalysedUtc = CAST(NULL AS datetime2), ErrorMessage = CAST(NULL AS nvarchar(max)), Summary = CAST(NULL AS nvarchar(max)), DateOfBirth = CAST(NULL AS nvarchar(500)), AgeAtStartOfSeries = CAST(NULL AS nvarchar(500)), Height = CAST(NULL AS nvarchar(500)), EyeColour = CAST(NULL AS nvarchar(500)), UpdatedUtc = CAST(NULL AS datetime2); RETURN; END; DECLARE @Attempt TABLE ( CharacterEnrichmentRunID int NULL, BookID int NULL, Status nvarchar(30) NULL, CurrentStage nvarchar(120) NULL, CurrentMessage nvarchar(300) NULL, ErrorMessage nvarchar(max) NULL, UpdatedUtc datetime2 NULL ); INSERT @Attempt SELECT TOP (1) runRows.CharacterEnrichmentRunID, runRows.BookID, runRows.Status, runRows.CurrentStage, runRows.CurrentMessage, runRows.ErrorMessage, runRows.UpdatedUtc FROM dbo.CharacterEnrichmentRuns runRows WHERE runRows.UserID = @UserID AND runRows.ProjectID = @ProjectID AND runRows.CharacterID = @CharacterID AND runRows.IsCurrent = 1 ORDER BY CASE runRows.Status WHEN N'Queued' THEN 0 WHEN N'Running' THEN 0 WHEN N'Failed' THEN 1 WHEN N'Completed' THEN 2 ELSE 9 END, runRows.UpdatedUtc DESC, runRows.CharacterEnrichmentRunID DESC; DECLARE @SuccessRunID int, @SuccessBookID int, @LastAnalysedUtc datetime2; SELECT TOP (1) @SuccessRunID = runRows.CharacterEnrichmentRunID, @SuccessBookID = runRows.BookID, @LastAnalysedUtc = runRows.CompletedUtc FROM dbo.CharacterEnrichmentRuns runRows WHERE runRows.UserID = @UserID AND runRows.ProjectID = @ProjectID AND runRows.Status = N'Completed' AND runRows.CompletedUtc IS NOT NULL AND ( runRows.CharacterID = @CharacterID OR EXISTS ( SELECT 1 FROM dbo.CharacterEnrichmentEvidence evidence WHERE evidence.CharacterEnrichmentRunID = runRows.CharacterEnrichmentRunID AND evidence.CharacterID = @CharacterID ) ) ORDER BY runRows.CompletedUtc DESC, runRows.CharacterEnrichmentRunID DESC; SELECT CharacterID = @CharacterID, BookID = COALESCE((SELECT TOP (1) BookID FROM @Attempt), @SuccessBookID), CharacterEnrichmentRunID = COALESCE((SELECT TOP (1) CharacterEnrichmentRunID FROM @Attempt), @SuccessRunID), Status = COALESCE((SELECT TOP (1) Status FROM @Attempt), CASE WHEN @SuccessRunID IS NULL THEN N'Not analysed' ELSE N'Completed' END), CurrentStage = (SELECT TOP (1) CurrentStage FROM @Attempt), CurrentMessage = (SELECT TOP (1) CurrentMessage FROM @Attempt), LastAnalysedUtc = @LastAnalysedUtc, ErrorMessage = (SELECT TOP (1) ErrorMessage FROM @Attempt), Summary = MAX(CASE WHEN evidence.FieldName = N'Summary' THEN evidence.Value END), DateOfBirth = MAX(CASE WHEN evidence.FieldName = N'DateOfBirth' THEN evidence.Value END), AgeAtStartOfSeries = MAX(CASE WHEN evidence.FieldName = N'AgeAtStartOfSeries' THEN evidence.Value END), Height = MAX(CASE WHEN evidence.FieldName = N'Height' THEN evidence.Value END), EyeColour = MAX(CASE WHEN evidence.FieldName = N'EyeColour' THEN evidence.Value END), UpdatedUtc = COALESCE((SELECT TOP (1) UpdatedUtc FROM @Attempt), @LastAnalysedUtc, SYSUTCDATETIME()) FROM (SELECT 1 AS Anchor) anchor LEFT JOIN dbo.CharacterEnrichmentEvidence evidence ON evidence.CharacterEnrichmentRunID = @SuccessRunID AND evidence.CharacterID = @CharacterID; END; GO