SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'Quality') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD Quality nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'Size') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD Size nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'OutputFormat') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD OutputFormat nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ImageKind') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD ImageKind nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'CanonicalName') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD CanonicalName nvarchar(200) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'BaseType') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD BaseType nvarchar(80) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SpecificityLevel') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD SpecificityLevel nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'IsFallbackClassification') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD IsFallbackClassification bit NOT NULL CONSTRAINT DF_IllustrationLibraryItems_IsFallbackClassification DEFAULT 0; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ReuseScope') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD ReuseScope nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceProjectID') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD SourceProjectID int NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceBookID') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD SourceBookID int NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceChapterID') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD SourceChapterID int NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceSceneID') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD SourceSceneID int NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'LinkedEntityType') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD LinkedEntityType nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'LinkedEntityID') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD LinkedEntityID int NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'GenerationReason') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD GenerationReason nvarchar(200) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'GenerationStatus') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD GenerationStatus nvarchar(40) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'IsArchived') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD IsArchived bit NOT NULL CONSTRAINT DF_IllustrationLibraryItems_IsArchived DEFAULT 0; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ArchivedAt') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD ArchivedAt datetime2(0) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ArchiveReason') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD ArchiveReason nvarchar(500) NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SupersededByImageID') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD SupersededByImageID int NULL; GO IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ReuseOfImageID') IS NULL ALTER TABLE dbo.IllustrationLibraryItems ADD ReuseOfImageID int NULL; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_IllustrationLibraryItems_ArchiveReuse' AND object_id = OBJECT_ID(N'dbo.IllustrationLibraryItems')) CREATE INDEX IX_IllustrationLibraryItems_ArchiveReuse ON dbo.IllustrationLibraryItems(IsArchived, IsActive, Category, Status, BaseType, ReuseScope); GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_IllustrationLibraryItems_SourceContext' AND object_id = OBJECT_ID(N'dbo.IllustrationLibraryItems')) CREATE INDEX IX_IllustrationLibraryItems_SourceContext ON dbo.IllustrationLibraryItems(SourceProjectID, SourceBookID, SourceChapterID, SourceSceneID, LinkedEntityType, LinkedEntityID); GO UPDATE dbo.IllustrationLibraryItems SET ImageKind = COALESCE(ImageKind, Category), CanonicalName = COALESCE(CanonicalName, JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.canonicalName'), DisplayTitle), BaseType = COALESCE(BaseType, JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.baseType'), JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.locationType'), JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.locationType'), JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.objectType')), SpecificityLevel = COALESCE(SpecificityLevel, JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.specificityLevel'), CASE WHEN StableCode LIKE N'demand-%' THEN N'Archetype' ELSE N'Generic' END), IsFallbackClassification = CASE WHEN IsFallbackClassification = 1 THEN 1 WHEN Category = N'Location' AND (DisplayTitle LIKE N'%Unknown Interior%' OR StableCode LIKE N'%unknowninterior%' OR JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.locationType') = N'UnknownInterior') THEN 1 WHEN Category = N'Asset' AND (DisplayTitle LIKE N'%Unknown Object%' OR StableCode LIKE N'%unknownobject%' OR JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.objectType') = N'UnknownObject') THEN 1 ELSE 0 END, ReuseScope = COALESCE(ReuseScope, CASE WHEN StableCode LIKE N'demand-%' THEN N'Book' WHEN StableCode LIKE N'loc-%' OR StableCode LIKE N'char-%' OR StableCode LIKE N'asset-%' THEN N'Global' ELSE N'Project' END), GenerationStatus = COALESCE(GenerationStatus, Status), UpdatedUtc = SYSUTCDATETIME() WHERE IsActive = 1 AND ( ImageKind IS NULL OR CanonicalName IS NULL OR SpecificityLevel IS NULL OR ReuseScope IS NULL OR GenerationStatus IS NULL ); GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_List @Category nvarchar(40) = NULL, @Status nvarchar(40) = NULL, @Search nvarchar(120) = NULL AS BEGIN SET NOCOUNT ON; SELECT * FROM dbo.IllustrationLibraryItems WHERE IsArchived = 0 AND (@Category IS NULL OR Category = @Category) AND (@Status IS NULL OR Status = @Status) AND ( @Search IS NULL OR StableCode LIKE N'%' + @Search + N'%' OR DisplayTitle LIKE N'%' + @Search + N'%' OR ShortDescription LIKE N'%' + @Search + N'%' OR CanonicalName LIKE N'%' + @Search + N'%' OR BaseType LIKE N'%' + @Search + N'%' ) ORDER BY CASE Status WHEN N'Generating' THEN 1 WHEN N'Queued' THEN 2 WHEN N'Generated' THEN 3 WHEN N'Planned' THEN 4 WHEN N'Failed' THEN 5 WHEN N'Rejected' THEN 6 WHEN N'Approved' THEN 7 ELSE 8 END, UpdatedUtc DESC, IllustrationLibraryItemID DESC; END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_Summary AS BEGIN SET NOCOUNT ON; SELECT COUNT(1) AS TotalCount, ISNULL(SUM(CASE WHEN Status = N'Planned' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS PlannedCount, ISNULL(SUM(CASE WHEN Status = N'Queued' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS QueuedCount, ISNULL(SUM(CASE WHEN Status = N'Generating' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS GeneratingCount, ISNULL(SUM(CASE WHEN Status = N'Generated' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS GeneratedCount, ISNULL(SUM(CASE WHEN Status = N'Approved' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS ApprovedCount, ISNULL(SUM(CASE WHEN Status = N'Rejected' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS RejectedCount, ISNULL(SUM(CASE WHEN Status = N'Failed' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS FailedCount, ISNULL(SUM(CASE WHEN Status = N'Superseded' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS SupersededCount, ISNULL(SUM(CASE WHEN Status = N'Disabled' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS DisabledCount, ISNULL(SUM(CASE WHEN IsArchived = 1 THEN 1 ELSE 0 END), 0) AS ArchivedCount FROM dbo.IllustrationLibraryItems; END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_UpsertPlanned @Category nvarchar(40), @StableCode nvarchar(120), @DisplayTitle nvarchar(200), @ShortDescription nvarchar(500) = NULL, @MetadataJson nvarchar(max) = NULL, @SpecificationJson nvarchar(max) = NULL AS BEGIN SET NOCOUNT ON; IF EXISTS ( SELECT 1 FROM dbo.IllustrationLibraryItems WHERE StableCode = @StableCode AND ParentItemID IS NULL AND IsActive = 1 AND IsArchived = 0 AND Status IN (N'Planned', N'Queued', N'Generating', N'Generated', N'Approved') ) BEGIN SELECT * FROM dbo.IllustrationLibraryItems WHERE StableCode = @StableCode AND ParentItemID IS NULL AND IsActive = 1 AND IsArchived = 0 AND Status IN (N'Planned', N'Queued', N'Generating', N'Generated', N'Approved'); RETURN; END; INSERT dbo.IllustrationLibraryItems (Category, StableCode, DisplayTitle, ShortDescription, MetadataJson, SpecificationJson, Status, ImageKind, CanonicalName, BaseType, SpecificityLevel, IsFallbackClassification, ReuseScope, SourceProjectID, SourceBookID, SourceChapterID, SourceSceneID, LinkedEntityType, LinkedEntityID, GenerationReason, GenerationStatus) VALUES ( @Category, @StableCode, @DisplayTitle, @ShortDescription, @MetadataJson, @SpecificationJson, N'Planned', COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.imageKind'), @Category), COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.canonicalName'), @DisplayTitle), COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.baseType'), JSON_VALUE(CASE WHEN ISJSON(@SpecificationJson) = 1 THEN @SpecificationJson ELSE N'{}' END, CASE WHEN @Category = N'Location' THEN '$.locationType' WHEN @Category = N'Asset' THEN '$.objectType' ELSE '$.presentation' END)), COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.specificityLevel'), N'Archetype'), CASE WHEN JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.isFallbackClassification') IN (N'true', N'True', N'1') THEN 1 ELSE 0 END, COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.reuseScope'), N'Book'), TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceProjectId')), TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceBookId')), TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceChapterId')), TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceSceneId')), JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.linkedEntityType'), TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.linkedEntityId')), COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.generationReason'), N'Demand generation'), N'Planned' ); SELECT * FROM dbo.IllustrationLibraryItems WHERE IllustrationLibraryItemID = SCOPE_IDENTITY(); END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_Queue @IllustrationLibraryItemID int = NULL, @Category nvarchar(40) = NULL AS BEGIN SET NOCOUNT ON; UPDATE dbo.IllustrationLibraryItems SET Status = N'Queued', GenerationStatus = N'Queued', QueuedUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME(), LastError = NULL WHERE (@IllustrationLibraryItemID IS NULL OR IllustrationLibraryItemID = @IllustrationLibraryItemID) AND (@Category IS NULL OR Category = @Category) AND Status IN (N'Planned', N'Failed', N'Rejected') AND IsActive = 1 AND IsArchived = 0; SELECT @@ROWCOUNT AS AffectedRows; END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_ClaimNext AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRANSACTION; DECLARE @Claimed TABLE (IllustrationLibraryItemID int NOT NULL); ;WITH nextItem AS ( SELECT TOP (1) * FROM dbo.IllustrationLibraryItems WITH (UPDLOCK, READPAST, ROWLOCK) WHERE Status = N'Queued' AND IsActive = 1 AND IsArchived = 0 ORDER BY QueuedUtc, IllustrationLibraryItemID ) UPDATE nextItem SET Status = N'Generating', GenerationStatus = N'Generating', AttemptCount = AttemptCount + 1, StartedUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME() OUTPUT inserted.IllustrationLibraryItemID INTO @Claimed; SELECT item.* FROM dbo.IllustrationLibraryItems item INNER JOIN @Claimed claimed ON claimed.IllustrationLibraryItemID = item.IllustrationLibraryItemID; COMMIT TRANSACTION; END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_MarkGenerated @IllustrationLibraryItemID int, @FilePath nvarchar(500), @ThumbnailPath nvarchar(500), @Width int, @Height int, @FileSizeBytes bigint, @MimeType nvarchar(100), @Provider nvarchar(80), @Model nvarchar(120), @PromptText nvarchar(max), @PromptTemplateVersion nvarchar(40), @ProviderRequestID nvarchar(160) = NULL, @Quality nvarchar(40) = NULL, @Size nvarchar(40) = NULL, @OutputFormat nvarchar(40) = NULL AS BEGIN SET NOCOUNT ON; UPDATE dbo.IllustrationLibraryItems SET Status = N'Generated', GenerationStatus = N'Generated', FilePath = @FilePath, ThumbnailPath = @ThumbnailPath, Width = @Width, Height = @Height, FileSizeBytes = @FileSizeBytes, MimeType = @MimeType, Provider = @Provider, Model = @Model, Quality = @Quality, Size = @Size, OutputFormat = @OutputFormat, PromptText = @PromptText, PromptTemplateVersion = @PromptTemplateVersion, ProviderRequestID = @ProviderRequestID, CompletedUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME(), LastError = NULL WHERE IllustrationLibraryItemID = @IllustrationLibraryItemID AND Status = N'Generating' AND IsArchived = 0; END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_MarkFailed @IllustrationLibraryItemID int, @LastError nvarchar(1000) AS BEGIN SET NOCOUNT ON; UPDATE dbo.IllustrationLibraryItems SET Status = N'Failed', GenerationStatus = N'Failed', LastError = @LastError, CompletedUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME() WHERE IllustrationLibraryItemID = @IllustrationLibraryItemID AND Status = N'Generating' AND IsArchived = 0; END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_GetApproved AS BEGIN SET NOCOUNT ON; SELECT * FROM dbo.IllustrationLibraryItems WHERE Status = N'Approved' AND IsApproved = 1 AND IsActive = 1 AND IsArchived = 0 AND FilePath IS NOT NULL ORDER BY StableCode, ApprovedUtc DESC; END; GO CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_ArchiveGeneratedSet @ProjectID int = NULL, @BookID int = NULL, @ImportSessionID int = NULL, @CreatedBeforeUtc datetime2(0) = NULL, @StableCodePrefix nvarchar(120) = NULL, @Reason nvarchar(500) = NULL AS BEGIN SET NOCOUNT ON; UPDATE item SET IsArchived = 1, IsActive = 0, ArchivedAt = COALESCE(ArchivedAt, SYSUTCDATETIME()), ArchiveReason = COALESCE(@Reason, N'Archived generated illustration set out of active reuse pool.'), GenerationStatus = COALESCE(GenerationStatus, Status), UpdatedUtc = SYSUTCDATETIME() FROM dbo.IllustrationLibraryItems item WHERE item.IsArchived = 0 AND item.Status IN (N'Generated', N'Approved') AND item.Provider = N'OpenAI' AND (@ProjectID IS NULL OR item.SourceProjectID = @ProjectID) AND (@BookID IS NULL OR item.SourceBookID = @BookID) AND (@ImportSessionID IS NULL OR TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(item.MetadataJson) = 1 THEN item.MetadataJson ELSE N'{}' END, '$.sourceImportSessionId')) = @ImportSessionID) AND (@CreatedBeforeUtc IS NULL OR item.CreatedUtc < @CreatedBeforeUtc) AND (@StableCodePrefix IS NULL OR item.StableCode LIKE @StableCodePrefix + N'%'); SELECT @@ROWCOUNT AS ArchivedCount; END; GO