From a3ce37c66c307c36ca5b735cfb9e12208cd15efd Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Tue, 23 Jun 2026 20:09:31 +0100 Subject: [PATCH] Converted all inline SQL to stored procedures. --- PlotLine/Data/ImportRepository.cs | 104 +-- PlotLine/Data/ProjectBackupRepository.cs | 10 +- PlotLine/Data/Repositories.cs | 558 +++------------- PlotLine/Data/WordCompanionRepository.cs | 18 +- .../088_InlineSqlStoredProcedureRefactor.sql | 619 ++++++++++++++++++ 5 files changed, 747 insertions(+), 562 deletions(-) create mode 100644 PlotLine/Sql/088_InlineSqlStoredProcedureRefactor.sql diff --git a/PlotLine/Data/ImportRepository.cs b/PlotLine/Data/ImportRepository.cs index 6da35c2..e10eaa6 100644 --- a/PlotLine/Data/ImportRepository.cs +++ b/PlotLine/Data/ImportRepository.cs @@ -24,16 +24,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : { using var connection = connectionFactory.CreateConnection(); var count = await connection.ExecuteScalarAsync( - """ - SELECT COUNT(1) - FROM dbo.Projects p - INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID - WHERE p.IsArchived = 0 - AND p.ProjectName = @ProjectName - AND pua.UserID = @UserID - AND pua.IsActive = 1; - """, - new { ProjectName = projectTitle, UserID = userId }); + "dbo.Project_ExistsByNameForUser", + new { ProjectName = projectTitle, UserID = userId }, + commandType: CommandType.StoredProcedure); return count > 0; } @@ -329,9 +322,10 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : if (povCharacterId.HasValue) { await connection.ExecuteAsync( - "UPDATE dbo.Scenes SET POVCharacterID = @POVCharacterID, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @SceneID;", + "dbo.Scene_UpdatePovCharacter", new { SceneID = sceneId, POVCharacterID = povCharacterId.Value }, - transaction); + transaction, + commandType: CommandType.StoredProcedure); } foreach (var appearance in BuildSceneAppearances(scene, povCharacterId, characterMap, package.Aliases.Characters)) @@ -666,68 +660,35 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : private static async Task GetLookupIdsAsync(IDbConnection connection, IDbTransaction transaction) { - var revisionStatusId = await connection.QuerySingleOrDefaultAsync( - "SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder;", - transaction: transaction); - var timeModeId = await connection.QuerySingleOrDefaultAsync( - "SELECT TOP (1) TimeModeID FROM dbo.TimeModes WHERE TimeModeName = N'Relative' ORDER BY SortOrder;", - transaction: transaction); - var timeConfidenceId = await connection.QuerySingleOrDefaultAsync( - "SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences WHERE TimeConfidenceName = N'Unknown' ORDER BY SortOrder;", - transaction: transaction); + using var result = await connection.QueryMultipleAsync( + "dbo.Import_LookupIds", + transaction: transaction, + commandType: CommandType.StoredProcedure); + var requiredLookups = await result.ReadSingleAsync(); + var locationTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var roleTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var presenceTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var plotLineTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var plotImportance = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var threadTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var threadStatuses = (await result.ReadAsync()).ToDictionary(x => x.StatusName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var threadEventTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var sceneDependencyTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var assetKinds = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var assetStates = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var assetEventTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var assetCustodyEventTypes = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var custodyRoles = (await result.ReadAsync()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var locationTypes = (await connection.QueryAsync( - "SELECT LocationTypeID AS LookupID, TypeName FROM dbo.LocationTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var roleTypes = (await connection.QueryAsync( - "SELECT CharacterRoleInSceneTypeID AS LookupID, TypeName FROM dbo.CharacterRoleInSceneTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var presenceTypes = (await connection.QueryAsync( - "SELECT PresenceTypeID AS LookupID, TypeName FROM dbo.PresenceTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var plotLineTypes = (await connection.QueryAsync( - "SELECT PlotLineTypeID AS LookupID, TypeName FROM dbo.PlotLineTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var plotImportance = (await connection.QueryAsync( - "SELECT PlotImportanceID AS LookupID, ImportanceName AS TypeName FROM dbo.PlotImportance WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var threadTypes = (await connection.QueryAsync( - "SELECT ThreadTypeID AS LookupID, TypeName FROM dbo.ThreadTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var threadStatuses = (await connection.QueryAsync( - "SELECT ThreadStatusID AS LookupID, StatusName FROM dbo.ThreadStatuses WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.StatusName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var threadEventTypes = (await connection.QueryAsync( - "SELECT ThreadEventTypeID AS LookupID, TypeName FROM dbo.ThreadEventTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var sceneDependencyTypes = (await connection.QueryAsync( - "SELECT SceneDependencyTypeID AS LookupID, TypeName FROM dbo.SceneDependencyTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var assetKinds = (await connection.QueryAsync( - "SELECT AssetKindID AS LookupID, KindName AS TypeName FROM dbo.AssetKinds WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var assetStates = (await connection.QueryAsync( - "SELECT AssetStateID AS LookupID, StateName AS TypeName FROM dbo.AssetStates WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var assetEventTypes = (await connection.QueryAsync( - "SELECT AssetEventTypeID AS LookupID, TypeName FROM dbo.AssetEventTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var assetCustodyEventTypes = (await connection.QueryAsync( - "SELECT AssetCustodyEventTypeID AS LookupID, TypeName FROM dbo.AssetCustodyEventTypes WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - var custodyRoles = (await connection.QueryAsync( - "SELECT CustodyRoleID AS LookupID, RoleName AS TypeName FROM dbo.CustodyRoles WHERE IsActive = 1;", - transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); - - if (!revisionStatusId.HasValue || !timeModeId.HasValue || !timeConfidenceId.HasValue) + if (!requiredLookups.RevisionStatusID.HasValue || !requiredLookups.TimeModeID.HasValue || !requiredLookups.TimeConfidenceID.HasValue) { throw new InvalidOperationException("Import lookup values are missing. Run the core database script before importing."); } return new ImportLookupIds( - revisionStatusId.Value, - timeModeId.Value, - timeConfidenceId.Value, + requiredLookups.RevisionStatusID.Value, + requiredLookups.TimeModeID.Value, + requiredLookups.TimeConfidenceID.Value, locationTypes, roleTypes, presenceTypes, @@ -1432,6 +1393,13 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : public string TypeName { get; set; } = string.Empty; } + private sealed class ImportRequiredLookupRow + { + public int? RevisionStatusID { get; set; } + public int? TimeModeID { get; set; } + public int? TimeConfidenceID { get; set; } + } + private sealed class ImportStatusLookupRow { public int LookupID { get; set; } diff --git a/PlotLine/Data/ProjectBackupRepository.cs b/PlotLine/Data/ProjectBackupRepository.cs index b890e0e..becdfa6 100644 --- a/PlotLine/Data/ProjectBackupRepository.cs +++ b/PlotLine/Data/ProjectBackupRepository.cs @@ -81,13 +81,9 @@ public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFact } var bookMetadata = (await connection.QueryAsync( - """ - SELECT BookID, Subtitle, Tagline, ShortDescription, Status, TargetWordCount, WritingStartDate, TargetCompletionDate, PublicationDate - , CoverThumbnailPath, CoverStandardPath - FROM dbo.Books - WHERE ProjectID = @ProjectID; - """, - new { ProjectID = projectId })).ToDictionary(x => x.BookID); + "dbo.ProjectBackup_BookMetadata_List", + new { ProjectID = projectId }, + commandType: CommandType.StoredProcedure)).ToDictionary(x => x.BookID); foreach (var book in data.Books) { if (!bookMetadata.TryGetValue(book.BookID, out var metadata)) diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index b27f095..0088970 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -792,44 +792,9 @@ public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT TOP (@Count) - cw.ContinuityWarningID, - cw.ValidationRunID, - cw.ProjectID, - cw.BookID, - b.BookTitle, - b.Subtitle AS BookSubtitle, - cw.ChapterID, - c.ChapterNumber, - c.ChapterTitle, - cw.SceneID, - s.SceneNumber, - s.SceneTitle, - cw.EntityType, - cw.EntityID, - cw.WarningTypeID, - wt.TypeName AS WarningTypeName, - cw.WarningSeverityID, - ws.SeverityName, - cw.Message, - cw.Details, - cw.IsDismissed, - cw.IsIntentional, - cw.CreatedDate, - cw.LastDetectedDate - FROM dbo.ContinuityWarnings cw - INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID - INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID - LEFT JOIN dbo.Books b ON b.BookID = cw.BookID - LEFT JOIN dbo.Chapters c ON c.ChapterID = cw.ChapterID - LEFT JOIN dbo.Scenes s ON s.SceneID = cw.SceneID - WHERE cw.ProjectID = @ProjectID - AND cw.IsDismissed = 0 - AND cw.IsIntentional = 0 - ORDER BY ws.SortOrder DESC, cw.LastDetectedDate DESC, cw.ContinuityWarningID DESC; - """, - new { ProjectID = projectId, Count = count }); + "dbo.ContinuityWarning_ListDashboardUnresolved", + new { ProjectID = projectId, Count = count }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -1349,45 +1314,9 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT wp.WritingPlanID, - wp.UserID, - wp.ProjectID, - p.ProjectName, - wp.BookID, - b.BookTitle, - b.Subtitle AS BookSubtitle, - wp.PlanName, - wp.GoalType, - wp.StartDate, - wp.DeadlineDate, - wp.AvailableDaysJson, - wp.SessionLengthMinutes, - wp.IsActive, - SUM(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN 1 ELSE 0 END) AS PlannedTaskCount, - MAX(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN wsi.ScheduledDate ELSE NULL END) AS ProjectedFinishDate - FROM dbo.WritingPlans wp - INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID - LEFT JOIN dbo.Books b ON b.BookID = wp.BookID - LEFT JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID - WHERE wp.UserID = @UserID - GROUP BY wp.WritingPlanID, - wp.UserID, - wp.ProjectID, - p.ProjectName, - wp.BookID, - b.BookTitle, - b.Subtitle, - wp.PlanName, - wp.GoalType, - wp.StartDate, - wp.DeadlineDate, - wp.AvailableDaysJson, - wp.SessionLengthMinutes, - wp.IsActive - ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID; - """, - new { UserID = userId }); + "dbo.WritingPlan_ListForManagement", + new { UserID = userId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -1395,29 +1324,9 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT s.SceneID, - rs.StatusName AS RevisionStatusName, - sw.EstimatedWordCount, - sw.Priority, - COALESCE(sw.IsBlocked, 0) AS IsBlocked, - b.SortOrder AS BookSortOrder, - c.SortOrder AS ChapterSortOrder, - s.SortOrder AS SceneSortOrder - FROM dbo.Projects p - INNER JOIN dbo.Books b ON b.ProjectID = p.ProjectID AND b.IsArchived = 0 - INNER JOIN dbo.Chapters c ON c.BookID = b.BookID AND c.IsArchived = 0 - INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0 - INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID - LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID - WHERE p.ProjectID = @ProjectID - AND p.IsArchived = 0 - AND (@BookID IS NULL OR b.BookID = @BookID) - AND rs.StatusName IN (N'Outlined', N'Needs Work', N'Revised') - AND (@IncludeBlockedScenes = 1 OR COALESCE(sw.IsBlocked, 0) = 0) - ORDER BY COALESCE(sw.Priority, 0) DESC, b.SortOrder, c.SortOrder, s.SortOrder, s.SceneID; - """, - new { ProjectID = projectId, BookID = bookId, IncludeBlockedScenes = includeBlockedScenes }); + "dbo.WritingScheduleScene_ListForPlanning", + new { ProjectID = projectId, BookID = bookId, IncludeBlockedScenes = includeBlockedScenes }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -1430,25 +1339,15 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa try { await connection.ExecuteAsync( - "DELETE FROM dbo.WritingScheduleItems WHERE WritingPlanID = @WritingPlanID;", + "dbo.WritingScheduleItem_DeleteByPlan", new { WritingPlanID = writingPlanId }, - transaction); + transaction, + commandType: CommandType.StoredProcedure); foreach (var item in items) { await connection.ExecuteAsync( - """ - INSERT dbo.WritingScheduleItems - ( - WritingPlanID, SceneID, ScheduledDate, TaskType, EstimatedMinutes, TargetWords, - ScheduleStatus, CompletedDateUtc, Notes - ) - VALUES - ( - @WritingPlanID, @SceneID, @ScheduledDate, @TaskType, @EstimatedMinutes, @TargetWords, - @ScheduleStatus, @CompletedDateUtc, @Notes - ); - """, + "dbo.CreateScheduleItem", new { item.WritingPlanID, @@ -1461,7 +1360,8 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa item.CompletedDateUtc, item.Notes }, - transaction); + transaction, + commandType: CommandType.StoredProcedure); } transaction.Commit(); @@ -1477,25 +1377,9 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT wp.WritingPlanID, - wp.UserID, - wp.ProjectID, - p.ProjectName, - wp.BookID, - b.BookTitle, - b.Subtitle AS BookSubtitle, - wp.PlanName, - wp.DeadlineDate, - wp.IsActive - FROM dbo.WritingPlans wp - INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID - LEFT JOIN dbo.Books b ON b.BookID = wp.BookID - WHERE wp.UserID = @UserID - AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID) - ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID; - """, - new { UserID = userId, ProjectID = projectId }); + "dbo.WritingPlanSummary_ListByUser", + new { UserID = userId, ProjectID = projectId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -1503,33 +1387,9 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT wsi.WritingScheduleItemID, - wsi.WritingPlanID, - wsi.SceneID, - wsi.ScheduledDate, - wsi.TaskType, - b.BookTitle, - b.Subtitle AS BookSubtitle, - c.ChapterNumber, - c.ChapterTitle, - s.SceneNumber, - s.SceneTitle, - sw.Priority, - wsi.EstimatedMinutes, - wsi.TargetWords, - wsi.ScheduleStatus, - wsi.Notes, - COALESCE(sw.IsBlocked, 0) AS IsBlocked - FROM dbo.WritingScheduleItems wsi - INNER JOIN dbo.Scenes s ON s.SceneID = wsi.SceneID - INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID - INNER JOIN dbo.Books b ON b.BookID = c.BookID - LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID - WHERE wsi.WritingPlanID = @WritingPlanID - ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID; - """, - new { WritingPlanID = writingPlanId }); + "dbo.WritingSchedulePreview_ListByPlan", + new { WritingPlanID = writingPlanId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -1537,46 +1397,18 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( - "DELETE FROM dbo.WritingScheduleItems WHERE WritingPlanID = @WritingPlanID;", - new { WritingPlanID = writingPlanId }); + "dbo.WritingScheduleItem_DeleteByPlan", + new { WritingPlanID = writingPlanId }, + commandType: CommandType.StoredProcedure); } public async Task> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT wsi.WritingScheduleItemID, - wsi.WritingPlanID, - wp.PlanName, - wsi.SceneID, - wsi.ScheduledDate, - wsi.TaskType, - b.BookTitle, - b.Subtitle AS BookSubtitle, - c.ChapterNumber, - c.ChapterTitle, - c.SortOrder AS ChapterSortOrder, - s.SceneNumber, - s.SceneTitle, - s.SortOrder AS SceneSortOrder, - sw.Priority, - wsi.EstimatedMinutes, - wsi.TargetWords, - wsi.ScheduleStatus, - COALESCE(sw.IsBlocked, 0) AS IsBlocked - FROM dbo.WritingPlans wp - INNER JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID - INNER JOIN dbo.Scenes s ON s.SceneID = wsi.SceneID - INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID - INNER JOIN dbo.Books b ON b.BookID = c.BookID - LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID - WHERE wp.UserID = @UserID - AND wp.IsActive = 1 - AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID) - ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID; - """, - new { UserID = userId, ProjectID = projectId }); + "dbo.WritingScheduleDashboard_ListActive", + new { UserID = userId, ProjectID = projectId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -1584,59 +1416,24 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa { using var connection = connectionFactory.CreateConnection(); return await connection.ExecuteAsync( - """ - UPDATE wsi - SET ScheduleStatus = @ScheduleStatus, - CompletedDateUtc = @CompletedDateUtc, - ModifiedDateUtc = SYSUTCDATETIME() - FROM dbo.WritingScheduleItems wsi - INNER JOIN dbo.WritingPlans wp ON wp.WritingPlanID = wsi.WritingPlanID - WHERE wsi.WritingScheduleItemID = @WritingScheduleItemID - AND wp.UserID = @UserID - AND wp.IsActive = 1 - AND wsi.ScheduleStatus = N'Planned'; - """, + "dbo.WritingScheduleItem_UpdatePlannedStatusForUser", new { WritingScheduleItemID = writingScheduleItemId, UserID = userId, ScheduleStatus = scheduleStatus, CompletedDateUtc = completedDateUtc - }); + }, + commandType: CommandType.StoredProcedure); } public async Task> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT wp.WritingPlanID, - wp.UserID, - wp.ProjectID, - p.ProjectName, - wp.BookID, - b.BookTitle, - b.Subtitle AS BookSubtitle, - wp.PlanName, - wp.DeadlineDate, - wp.IsActive - FROM dbo.WritingPlans wp - INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID - LEFT JOIN dbo.Books b ON b.BookID = wp.BookID - WHERE wp.UserID = @UserID - AND wp.IsActive = 1 - AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID) - AND EXISTS - ( - SELECT 1 - FROM dbo.WritingScheduleItems wsi - WHERE wsi.WritingPlanID = wp.WritingPlanID - AND wsi.ScheduleStatus = N'Planned' - AND wsi.ScheduledDate < @Today - ) - ORDER BY wp.DeadlineDate, wp.PlanName, wp.WritingPlanID; - """, - new { UserID = userId, Today = today.Date, ProjectID = projectId }); + "dbo.WritingPlanSummary_ListRequiringRebalance", + new { UserID = userId, Today = today.Date, ProjectID = projectId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -1656,21 +1453,15 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa foreach (var change in changes) { await connection.ExecuteAsync( - """ - UPDATE dbo.WritingScheduleItems - SET ScheduledDate = @ScheduledDate, - ModifiedDateUtc = SYSUTCDATETIME() - WHERE WritingPlanID = @WritingPlanID - AND WritingScheduleItemID = @WritingScheduleItemID - AND ScheduleStatus = N'Planned'; - """, + "dbo.WritingScheduleItem_UpdatePlannedDate", new { WritingPlanID = writingPlanId, change.WritingScheduleItemID, ScheduledDate = change.ScheduledDate.Date }, - transaction); + transaction, + commandType: CommandType.StoredProcedure); } transaction.Commit(); @@ -2095,62 +1886,18 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( - """ - SELECT - (SELECT COUNT(1) FROM dbo.Characters WHERE ProjectID = @ProjectID AND IsArchived = 0) AS CharacterCount, - (SELECT COUNT(1) FROM dbo.Locations WHERE ProjectID = @ProjectID AND IsArchived = 0) AS LocationCount, - ( - SELECT COUNT(1) - FROM dbo.PlotThreads pt - INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID - INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID - WHERE pl.ProjectID = @ProjectID - AND pl.IsArchived = 0 - AND pt.IsArchived = 0 - AND ts.IsOpenStatus = 1 - ) AS ActivePlotThreadCount, - ( - SELECT COUNT(1) - FROM dbo.ContinuityWarnings - WHERE ProjectID = @ProjectID - AND IsDismissed = 0 - AND IsIntentional = 0 - ) AS ContinuityWarningCount; - """, - new { ProjectID = projectId }); + "dbo.ProjectDashboard_Metrics", + new { ProjectID = projectId }, + commandType: CommandType.StoredProcedure); } public async Task> ListDashboardAttentionThreadsAsync(int projectId, int count) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT TOP (@Count) - pt.PlotThreadID, - pt.ThreadTitle, - pt.PlotLineID, - pl.PlotLineName, - pt.Importance, - ts.StatusName AS ThreadStatusName, - ts.IsOpenStatus, - ts.IsResolvedStatus - FROM dbo.PlotThreads pt - INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID - INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID - WHERE pl.ProjectID = @ProjectID - AND pl.IsArchived = 0 - AND pt.IsArchived = 0 - AND ( - ts.StatusName = N'Dormant' - OR (ts.IsOpenStatus = 1 AND pt.Importance >= 8) - ) - ORDER BY - CASE WHEN ts.StatusName = N'Dormant' THEN 0 ELSE 1 END, - pt.Importance DESC, - pt.UpdatedDate DESC, - pt.PlotThreadID DESC; - """, - new { ProjectID = projectId, Count = count }); + "dbo.ProjectDashboard_AttentionThreads", + new { ProjectID = projectId, Count = count }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -2333,25 +2080,9 @@ public sealed class ProjectActivityRepository(ISqlConnectionFactory connectionFa { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT TOP (@Count) - pa.ProjectActivityID, - pa.ProjectID, - pa.UserID, - u.DisplayName, - u.Email, - pa.ActivityType, - pa.EntityType, - pa.EntityID, - pa.EntityName, - pa.Description, - pa.CreatedDateUTC - FROM dbo.ProjectActivity pa - LEFT JOIN dbo.AppUser u ON u.UserID = pa.UserID - WHERE pa.ProjectID = @ProjectID - ORDER BY pa.CreatedDateUTC DESC, pa.ProjectActivityID DESC; - """, - new { ProjectID = projectId, Count = count }); + "dbo.ProjectActivity_ListRecent", + new { ProjectID = projectId, Count = count }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -2393,54 +2124,9 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT - b.BookID, - b.ProjectID, - b.BookTitle, - b.Subtitle, - b.Tagline, - b.ShortDescription, - b.Status, - b.BookNumber, - b.Description, - b.TargetWordCount, - b.WritingStartDate, - b.TargetCompletionDate, - b.PublicationDate, - b.CoverThumbnailPath, - b.CoverStandardPath, - ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount, - ISNULL(counts.ChapterCount, 0) AS ChapterCount, - ISNULL(counts.SceneCount, 0) AS SceneCount, - b.SortOrder, - b.CreatedDate, - b.UpdatedDate, - b.IsArchived, - b.ArchivedDate, - b.ArchivedReason - FROM dbo.Books b - OUTER APPLY - ( - SELECT - COUNT(DISTINCT c.ChapterID) AS ChapterCount, - COUNT(s.SceneID) AS SceneCount - FROM dbo.Chapters c - LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0 - WHERE c.BookID = b.BookID AND c.IsArchived = 0 - ) counts - OUTER APPLY - ( - SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount - FROM dbo.Chapters c - INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0 - LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID - WHERE c.BookID = b.BookID AND c.IsArchived = 0 - ) wordCounts - WHERE b.ProjectID IN @ProjectIDs AND b.IsArchived = 0 - ORDER BY b.ProjectID, b.SortOrder, b.BookNumber, b.BookTitle; - """, - new { ProjectIDs = ids }); + "dbo.Book_ListByProjects", + new { ProjectIDs = string.Join(',', ids) }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -2454,12 +2140,9 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( - """ - SELECT BookID, ProjectID, CoverThumbnailPath, CoverStandardPath - FROM dbo.Books - WHERE BookID = @BookID; - """, - new { BookID = bookId }); + "dbo.Book_GetCoverPaths", + new { BookID = bookId }, + commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Book book) @@ -2632,17 +2315,9 @@ public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : I { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( - """ - MERGE dbo.SceneMetricValues AS target - USING (SELECT @SceneID AS SceneID, @MetricTypeID AS MetricTypeID) AS source - ON target.SceneID = source.SceneID AND target.MetricTypeID = source.MetricTypeID - WHEN MATCHED THEN - UPDATE SET Value = @Value - WHEN NOT MATCHED THEN - INSERT (SceneID, MetricTypeID, Value, Notes) - VALUES (@SceneID, @MetricTypeID, @Value, NULL); - """, - new { SceneID = sceneId, MetricTypeID = metricTypeId, Value = value }); + "dbo.SceneMetric_SaveValue", + new { SceneID = sceneId, MetricTypeID = metricTypeId, Value = value, Notes = (string?)null }, + commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int sceneId) @@ -2824,8 +2499,9 @@ public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory) } var targetRows = await connection.QueryAsync( - "SELECT PlotEventID, PlotLineID FROM dbo.PlotEventTargetPlotLines WHERE PlotEventID IN @ThreadEventIDs;", - new { ThreadEventIDs = threadEventIds }); + "dbo.ThreadEventTarget_ListByEvents", + new { ThreadEventIDs = string.Join(',', threadEventIds) }, + commandType: CommandType.StoredProcedure); var targetsByEvent = targetRows .GroupBy(x => x.PlotEventID) .ToDictionary(x => x.Key, x => (IReadOnlyList)x.Select(row => row.PlotLineID).ToList()); @@ -3159,8 +2835,9 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP } var targetRows = await connection.QueryAsync( - "SELECT PlotEventID, PlotLineID FROM dbo.PlotEventTargetPlotLines WHERE PlotEventID IN @ThreadEventIDs;", - new { ThreadEventIDs = threadEventIds }); + "dbo.ThreadEventTarget_ListByEvents", + new { ThreadEventIDs = string.Join(',', threadEventIds) }, + commandType: CommandType.StoredProcedure); var targetsByEvent = targetRows .GroupBy(x => x.PlotEventID) .ToDictionary(x => x.Key, x => (IReadOnlyList)x.Select(row => row.PlotLineID).ToList()); @@ -3204,17 +2881,9 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP using var connection = connectionFactory.CreateConnection(); var targetIds = plotLineIds.Where(id => id > 0).Distinct().ToList(); await connection.ExecuteAsync( - "DELETE FROM dbo.PlotEventTargetPlotLines WHERE PlotEventID = @ThreadEventID;", - new { ThreadEventID = threadEventId }); - - if (targetIds.Count == 0) - { - return; - } - - await connection.ExecuteAsync( - "INSERT dbo.PlotEventTargetPlotLines (PlotEventID, PlotLineID) VALUES (@ThreadEventID, @PlotLineID);", - targetIds.Select(plotLineId => new { ThreadEventID = threadEventId, PlotLineID = plotLineId })); + "dbo.ThreadEventTarget_SaveForEvent", + new { ThreadEventID = threadEventId, PlotLineIDs = string.Join(',', targetIds) }, + commandType: CommandType.StoredProcedure); } public async Task DeleteThreadEventAsync(int threadEventId) @@ -3387,26 +3056,9 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT acec.AssetCustodyEventID, - ace.StoryAssetID, - ace.SceneID, - acec.CharacterID, - c.CharacterName, - acec.CharacterNameText, - cr.RoleName, - ace.UpdatedDate - FROM dbo.AssetCustodyEventCharacters acec - INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID - INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID - LEFT JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID - LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID - WHERE sa.ProjectID = @ProjectID - AND sa.IsArchived = 0 - AND acec.CharacterID IS NOT NULL - AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL; - """, - new { ProjectID = projectId }); + "dbo.AssetCustodyCharacter_ListByProject", + new { ProjectID = projectId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -3419,18 +3071,9 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I var eventIds = rows.Select(x => x.AssetCustodyEventID).Distinct().ToList(); var validCustodians = await connection.QueryAsync( - """ - SELECT acec.AssetCustodyEventID, - c.CharacterName, - cr.RoleName - FROM dbo.AssetCustodyEventCharacters acec - INNER JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID - LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID - WHERE acec.AssetCustodyEventID IN @AssetCustodyEventIDs - AND acec.CharacterID IS NOT NULL - AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL; - """, - new { AssetCustodyEventIDs = eventIds }); + "dbo.AssetCustodySummary_ListByEvents", + new { AssetCustodyEventIDs = string.Join(',', eventIds) }, + commandType: CommandType.StoredProcedure); var summaries = validCustodians .GroupBy(x => x.AssetCustodyEventID) @@ -3565,14 +3208,9 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT CharacterSexValueID, OwnerUserID, SexName, IsSystem, SortOrder, IsActive - FROM dbo.CharacterSexValues - WHERE IsActive = 1 - AND (IsSystem = 1 OR OwnerUserID = @OwnerUserID) - ORDER BY IsSystem DESC, SortOrder, SexName; - """, - new { OwnerUserID = ownerUserId }); + "dbo.CharacterSexValue_ListForOwner", + new { OwnerUserID = ownerUserId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -3580,14 +3218,9 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( - """ - SELECT CharacterSexValueID, OwnerUserID, SexName, IsSystem, SortOrder, IsActive - FROM dbo.CharacterSexValues - WHERE CharacterSexValueID = @CharacterSexValueID - AND IsActive = 1 - AND (IsSystem = 1 OR OwnerUserID = @OwnerUserID); - """, - new { CharacterSexValueID = characterSexValueId, OwnerUserID = ownerUserId }); + "dbo.CharacterSexValue_Get", + new { CharacterSexValueID = characterSexValueId, OwnerUserID = ownerUserId }, + commandType: CommandType.StoredProcedure); } public async Task GetOrCreateSexValueAsync(int ownerUserId, string sexName) @@ -3627,13 +3260,9 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT CharacterImageID, CharacterID, ProjectID, ImagePath, ThumbnailPath, Caption, SortOrder, UploadedDateUTC, IsDeleted - FROM dbo.CharacterImages - WHERE CharacterID = @CharacterID AND IsDeleted = 0 - ORDER BY SortOrder, UploadedDateUTC, CharacterImageID; - """, - new { CharacterID = characterId }); + "dbo.CharacterImage_ListByCharacter", + new { CharacterID = characterId }, + commandType: CommandType.StoredProcedure); return rows.ToList(); } @@ -3641,26 +3270,16 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( - """ - SELECT CharacterImageID, CharacterID, ProjectID, ImagePath, ThumbnailPath, Caption, SortOrder, UploadedDateUTC, IsDeleted - FROM dbo.CharacterImages - WHERE CharacterImageID = @CharacterImageID AND IsDeleted = 0; - """, - new { CharacterImageID = characterImageId }); + "dbo.CharacterImage_Get", + new { CharacterImageID = characterImageId }, + commandType: CommandType.StoredProcedure); } public async Task CreateCharacterImageAsync(CharacterImage image) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( - """ - DECLARE @SortOrder int = COALESCE(@InputSortOrder, (SELECT ISNULL(MAX(SortOrder), 0) + 1 FROM dbo.CharacterImages WHERE CharacterID = @CharacterID)); - - INSERT dbo.CharacterImages (CharacterID, ProjectID, ImagePath, ThumbnailPath, Caption, SortOrder) - VALUES (@CharacterID, @ProjectID, @ImagePath, @ThumbnailPath, @Caption, @SortOrder); - - SELECT CAST(SCOPE_IDENTITY() AS int); - """, + "dbo.CharacterImage_Create", new { image.CharacterID, @@ -3669,20 +3288,17 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) image.ThumbnailPath, image.Caption, InputSortOrder = image.SortOrder == 0 ? (int?)null : image.SortOrder - }); + }, + commandType: CommandType.StoredProcedure); } public async Task DeleteCharacterImageAsync(int characterImageId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( - """ - UPDATE dbo.CharacterImages - SET IsDeleted = 1, - DeletedDateUTC = SYSUTCDATETIME() - WHERE CharacterImageID = @CharacterImageID; - """, - new { CharacterImageID = characterImageId }); + "dbo.CharacterImage_Delete", + new { CharacterImageID = characterImageId }, + commandType: CommandType.StoredProcedure); } public async Task ArchiveCharacterAsync(int characterId) diff --git a/PlotLine/Data/WordCompanionRepository.cs b/PlotLine/Data/WordCompanionRepository.cs index 603ddb9..5a78dd7 100644 --- a/PlotLine/Data/WordCompanionRepository.cs +++ b/PlotLine/Data/WordCompanionRepository.cs @@ -36,23 +36,9 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( - """ - SELECT b.BookID AS BookId, - b.BookTitle AS Title, - b.Subtitle, - b.SortOrder - FROM dbo.Books b - INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = b.ProjectID - INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID - WHERE b.ProjectID = @ProjectID - AND pua.UserID = @UserID - AND pua.IsActive = 1 - AND p.IsArchived = 0 - AND b.IsArchived = 0 - ORDER BY b.SortOrder, b.BookNumber, b.BookTitle, b.BookID; - """, + "dbo.WordCompanion_Book_ListByProject", new { ProjectID = projectId, UserID = userId }, - commandType: CommandType.Text); + commandType: CommandType.StoredProcedure); return rows.ToList(); } diff --git a/PlotLine/Sql/088_InlineSqlStoredProcedureRefactor.sql b/PlotLine/Sql/088_InlineSqlStoredProcedureRefactor.sql new file mode 100644 index 0000000..94f25d3 --- /dev/null +++ b/PlotLine/Sql/088_InlineSqlStoredProcedureRefactor.sql @@ -0,0 +1,619 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.Project_ExistsByNameForUser + @ProjectName nvarchar(200), + @UserID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT COUNT(1) + FROM dbo.Projects p + INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID + WHERE p.IsArchived = 0 + AND p.ProjectName = @ProjectName + AND pua.UserID = @UserID + AND pua.IsActive = 1; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.Scene_UpdatePovCharacter + @SceneID int, + @POVCharacterID int +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.Scenes + SET POVCharacterID = @POVCharacterID, + UpdatedDate = SYSUTCDATETIME() + WHERE SceneID = @SceneID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.Import_LookupIds +AS +BEGIN + SET NOCOUNT ON; + + SELECT + (SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder) AS RevisionStatusID, + (SELECT TOP (1) TimeModeID FROM dbo.TimeModes WHERE TimeModeName = N'Relative' ORDER BY SortOrder) AS TimeModeID, + (SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences WHERE TimeConfidenceName = N'Unknown' ORDER BY SortOrder) AS TimeConfidenceID; + + SELECT LocationTypeID AS LookupID, TypeName FROM dbo.LocationTypes WHERE IsActive = 1; + SELECT CharacterRoleInSceneTypeID AS LookupID, TypeName FROM dbo.CharacterRoleInSceneTypes WHERE IsActive = 1; + SELECT PresenceTypeID AS LookupID, TypeName FROM dbo.PresenceTypes WHERE IsActive = 1; + SELECT PlotLineTypeID AS LookupID, TypeName FROM dbo.PlotLineTypes WHERE IsActive = 1; + SELECT PlotImportanceID AS LookupID, ImportanceName AS TypeName FROM dbo.PlotImportance WHERE IsActive = 1; + SELECT ThreadTypeID AS LookupID, TypeName FROM dbo.ThreadTypes WHERE IsActive = 1; + SELECT ThreadStatusID AS LookupID, StatusName FROM dbo.ThreadStatuses WHERE IsActive = 1; + SELECT ThreadEventTypeID AS LookupID, TypeName FROM dbo.ThreadEventTypes WHERE IsActive = 1; + SELECT SceneDependencyTypeID AS LookupID, TypeName FROM dbo.SceneDependencyTypes WHERE IsActive = 1; + SELECT AssetKindID AS LookupID, KindName AS TypeName FROM dbo.AssetKinds WHERE IsActive = 1; + SELECT AssetStateID AS LookupID, StateName AS TypeName FROM dbo.AssetStates WHERE IsActive = 1; + SELECT AssetEventTypeID AS LookupID, TypeName FROM dbo.AssetEventTypes WHERE IsActive = 1; + SELECT AssetCustodyEventTypeID AS LookupID, TypeName FROM dbo.AssetCustodyEventTypes WHERE IsActive = 1; + SELECT CustodyRoleID AS LookupID, RoleName AS TypeName FROM dbo.CustodyRoles WHERE IsActive = 1; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectBackup_BookMetadata_List + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT BookID, Subtitle, Tagline, ShortDescription, Status, TargetWordCount, WritingStartDate, TargetCompletionDate, PublicationDate, + CoverThumbnailPath, CoverStandardPath + FROM dbo.Books + WHERE ProjectID = @ProjectID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_ListByProject + @ProjectID int, + @UserID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT b.BookID AS BookId, + b.BookTitle AS Title, + b.Subtitle, + b.SortOrder + FROM dbo.Books b + INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = b.ProjectID + INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID + WHERE b.ProjectID = @ProjectID + AND pua.UserID = @UserID + AND pua.IsActive = 1 + AND p.IsArchived = 0 + AND b.IsArchived = 0 + ORDER BY b.SortOrder, b.BookNumber, b.BookTitle, b.BookID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_ListDashboardUnresolved + @ProjectID int, + @Count int +AS +BEGIN + SET NOCOUNT ON; + + SELECT TOP (@Count) + cw.ContinuityWarningID, + cw.ValidationRunID, + cw.ProjectID, + cw.BookID, + b.BookTitle, + b.Subtitle AS BookSubtitle, + cw.ChapterID, + c.ChapterNumber, + c.ChapterTitle, + cw.SceneID, + s.SceneNumber, + s.SceneTitle, + cw.EntityType, + cw.EntityID, + cw.WarningTypeID, + wt.TypeName AS WarningTypeName, + cw.WarningSeverityID, + ws.SeverityName, + cw.Message, + cw.Details, + cw.IsDismissed, + cw.IsIntentional, + cw.CreatedDate, + cw.LastDetectedDate + FROM dbo.ContinuityWarnings cw + INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID + INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID + LEFT JOIN dbo.Books b ON b.BookID = cw.BookID + LEFT JOIN dbo.Chapters c ON c.ChapterID = cw.ChapterID + LEFT JOIN dbo.Scenes s ON s.SceneID = cw.SceneID + WHERE cw.ProjectID = @ProjectID + AND cw.IsDismissed = 0 + AND cw.IsIntentional = 0 + ORDER BY ws.SortOrder DESC, cw.LastDetectedDate DESC, cw.ContinuityWarningID DESC; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingPlan_ListForManagement + @UserID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT wp.WritingPlanID, wp.UserID, wp.ProjectID, p.ProjectName, wp.BookID, b.BookTitle, b.Subtitle AS BookSubtitle, + wp.PlanName, wp.GoalType, wp.StartDate, wp.DeadlineDate, wp.AvailableDaysJson, wp.SessionLengthMinutes, wp.IsActive, + SUM(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN 1 ELSE 0 END) AS PlannedTaskCount, + MAX(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN wsi.ScheduledDate ELSE NULL END) AS ProjectedFinishDate + FROM dbo.WritingPlans wp + INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID + LEFT JOIN dbo.Books b ON b.BookID = wp.BookID + LEFT JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID + WHERE wp.UserID = @UserID + GROUP BY wp.WritingPlanID, wp.UserID, wp.ProjectID, p.ProjectName, wp.BookID, b.BookTitle, b.Subtitle, + wp.PlanName, wp.GoalType, wp.StartDate, wp.DeadlineDate, wp.AvailableDaysJson, wp.SessionLengthMinutes, wp.IsActive + ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingScheduleScene_ListForPlanning + @ProjectID int, + @BookID int = NULL, + @IncludeBlockedScenes bit = 0 +AS +BEGIN + SET NOCOUNT ON; + + SELECT s.SceneID, rs.StatusName AS RevisionStatusName, sw.EstimatedWordCount, sw.Priority, + COALESCE(sw.IsBlocked, 0) AS IsBlocked, b.SortOrder AS BookSortOrder, + c.SortOrder AS ChapterSortOrder, s.SortOrder AS SceneSortOrder + FROM dbo.Projects p + INNER JOIN dbo.Books b ON b.ProjectID = p.ProjectID AND b.IsArchived = 0 + INNER JOIN dbo.Chapters c ON c.BookID = b.BookID AND c.IsArchived = 0 + INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0 + INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID + LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID + WHERE p.ProjectID = @ProjectID + AND p.IsArchived = 0 + AND (@BookID IS NULL OR b.BookID = @BookID) + AND rs.StatusName IN (N'Outlined', N'Needs Work', N'Revised') + AND (@IncludeBlockedScenes = 1 OR COALESCE(sw.IsBlocked, 0) = 0) + ORDER BY COALESCE(sw.Priority, 0) DESC, b.SortOrder, c.SortOrder, s.SortOrder, s.SceneID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingScheduleItem_DeleteByPlan + @WritingPlanID int +AS +BEGIN + SET NOCOUNT ON; + + DELETE FROM dbo.WritingScheduleItems + WHERE WritingPlanID = @WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingPlanSummary_ListByUser + @UserID int, + @ProjectID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + SELECT wp.WritingPlanID, wp.UserID, wp.ProjectID, p.ProjectName, wp.BookID, b.BookTitle, b.Subtitle AS BookSubtitle, + wp.PlanName, wp.DeadlineDate, wp.IsActive + FROM dbo.WritingPlans wp + INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID + LEFT JOIN dbo.Books b ON b.BookID = wp.BookID + WHERE wp.UserID = @UserID + AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID) + ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingSchedulePreview_ListByPlan + @WritingPlanID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT wsi.WritingScheduleItemID, wsi.WritingPlanID, wsi.SceneID, wsi.ScheduledDate, wsi.TaskType, + b.BookTitle, b.Subtitle AS BookSubtitle, c.ChapterNumber, c.ChapterTitle, s.SceneNumber, s.SceneTitle, + sw.Priority, wsi.EstimatedMinutes, wsi.TargetWords, wsi.ScheduleStatus, wsi.Notes, + COALESCE(sw.IsBlocked, 0) AS IsBlocked + FROM dbo.WritingScheduleItems wsi + INNER JOIN dbo.Scenes s ON s.SceneID = wsi.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID + WHERE wsi.WritingPlanID = @WritingPlanID + ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingScheduleDashboard_ListActive + @UserID int, + @ProjectID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + SELECT wsi.WritingScheduleItemID, wsi.WritingPlanID, wp.PlanName, wsi.SceneID, wsi.ScheduledDate, wsi.TaskType, + b.BookTitle, b.Subtitle AS BookSubtitle, c.ChapterNumber, c.ChapterTitle, c.SortOrder AS ChapterSortOrder, + s.SceneNumber, s.SceneTitle, s.SortOrder AS SceneSortOrder, sw.Priority, wsi.EstimatedMinutes, + wsi.TargetWords, wsi.ScheduleStatus, COALESCE(sw.IsBlocked, 0) AS IsBlocked + FROM dbo.WritingPlans wp + INNER JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID + INNER JOIN dbo.Scenes s ON s.SceneID = wsi.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID + WHERE wp.UserID = @UserID + AND wp.IsActive = 1 + AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID) + ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingScheduleItem_UpdatePlannedStatusForUser + @WritingScheduleItemID int, + @UserID int, + @ScheduleStatus nvarchar(50), + @CompletedDateUtc datetime2 = NULL +AS +BEGIN + SET NOCOUNT ON; + + UPDATE wsi + SET ScheduleStatus = @ScheduleStatus, + CompletedDateUtc = @CompletedDateUtc, + ModifiedDateUtc = SYSUTCDATETIME() + FROM dbo.WritingScheduleItems wsi + INNER JOIN dbo.WritingPlans wp ON wp.WritingPlanID = wsi.WritingPlanID + WHERE wsi.WritingScheduleItemID = @WritingScheduleItemID + AND wp.UserID = @UserID + AND wp.IsActive = 1 + AND wsi.ScheduleStatus = N'Planned'; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingPlanSummary_ListRequiringRebalance + @UserID int, + @Today date, + @ProjectID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + SELECT wp.WritingPlanID, wp.UserID, wp.ProjectID, p.ProjectName, wp.BookID, b.BookTitle, b.Subtitle AS BookSubtitle, + wp.PlanName, wp.DeadlineDate, wp.IsActive + FROM dbo.WritingPlans wp + INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID + LEFT JOIN dbo.Books b ON b.BookID = wp.BookID + WHERE wp.UserID = @UserID + AND wp.IsActive = 1 + AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID) + AND EXISTS + ( + SELECT 1 + FROM dbo.WritingScheduleItems wsi + WHERE wsi.WritingPlanID = wp.WritingPlanID + AND wsi.ScheduleStatus = N'Planned' + AND wsi.ScheduledDate < @Today + ) + ORDER BY wp.DeadlineDate, wp.PlanName, wp.WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.WritingScheduleItem_UpdatePlannedDate + @WritingPlanID int, + @WritingScheduleItemID int, + @ScheduledDate date +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.WritingScheduleItems + SET ScheduledDate = @ScheduledDate, + ModifiedDateUtc = SYSUTCDATETIME() + WHERE WritingPlanID = @WritingPlanID + AND WritingScheduleItemID = @WritingScheduleItemID + AND ScheduleStatus = N'Planned'; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectDashboard_Metrics + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT + (SELECT COUNT(1) FROM dbo.Characters WHERE ProjectID = @ProjectID AND IsArchived = 0) AS CharacterCount, + (SELECT COUNT(1) FROM dbo.Locations WHERE ProjectID = @ProjectID AND IsArchived = 0) AS LocationCount, + ( + SELECT COUNT(1) + FROM dbo.PlotThreads pt + INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID + INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID + WHERE pl.ProjectID = @ProjectID + AND pl.IsArchived = 0 + AND pt.IsArchived = 0 + AND ts.IsOpenStatus = 1 + ) AS ActivePlotThreadCount, + ( + SELECT COUNT(1) + FROM dbo.ContinuityWarnings + WHERE ProjectID = @ProjectID + AND IsDismissed = 0 + AND IsIntentional = 0 + ) AS ContinuityWarningCount; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectDashboard_AttentionThreads + @ProjectID int, + @Count int +AS +BEGIN + SET NOCOUNT ON; + + SELECT TOP (@Count) pt.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, + pt.Importance, ts.StatusName AS ThreadStatusName, ts.IsOpenStatus, ts.IsResolvedStatus + FROM dbo.PlotThreads pt + INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID + INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID + WHERE pl.ProjectID = @ProjectID + AND pl.IsArchived = 0 + AND pt.IsArchived = 0 + AND (ts.StatusName = N'Dormant' OR (ts.IsOpenStatus = 1 AND pt.Importance >= 8)) + ORDER BY CASE WHEN ts.StatusName = N'Dormant' THEN 0 ELSE 1 END, + pt.Importance DESC, pt.UpdatedDate DESC, pt.PlotThreadID DESC; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectActivity_ListRecent + @ProjectID int, + @Count int +AS +BEGIN + SET NOCOUNT ON; + + SELECT TOP (@Count) pa.ProjectActivityID, pa.ProjectID, pa.UserID, u.DisplayName, u.Email, + pa.ActivityType, pa.EntityType, pa.EntityID, pa.EntityName, pa.Description, pa.CreatedDateUTC + FROM dbo.ProjectActivity pa + LEFT JOIN dbo.AppUser u ON u.UserID = pa.UserID + WHERE pa.ProjectID = @ProjectID + ORDER BY pa.CreatedDateUTC DESC, pa.ProjectActivityID DESC; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.Book_ListByProjects + @ProjectIDs nvarchar(max) +AS +BEGIN + SET NOCOUNT ON; + + ;WITH ProjectIDs AS + ( + SELECT DISTINCT TRY_CONVERT(int, value) AS ProjectID + FROM STRING_SPLIT(COALESCE(@ProjectIDs, N''), N',') + WHERE TRY_CONVERT(int, value) IS NOT NULL + ) + SELECT b.BookID, b.ProjectID, b.BookTitle, b.Subtitle, b.Tagline, b.ShortDescription, b.Status, b.BookNumber, + b.Description, b.TargetWordCount, b.WritingStartDate, b.TargetCompletionDate, b.PublicationDate, + b.CoverThumbnailPath, b.CoverStandardPath, ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount, + ISNULL(counts.ChapterCount, 0) AS ChapterCount, ISNULL(counts.SceneCount, 0) AS SceneCount, + b.SortOrder, b.CreatedDate, b.UpdatedDate, b.IsArchived, b.ArchivedDate, b.ArchivedReason + FROM dbo.Books b + INNER JOIN ProjectIDs ids ON ids.ProjectID = b.ProjectID + OUTER APPLY + ( + SELECT COUNT(DISTINCT c.ChapterID) AS ChapterCount, COUNT(s.SceneID) AS SceneCount + FROM dbo.Chapters c + LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0 + WHERE c.BookID = b.BookID AND c.IsArchived = 0 + ) counts + OUTER APPLY + ( + SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount + FROM dbo.Chapters c + INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0 + LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID + WHERE c.BookID = b.BookID AND c.IsArchived = 0 + ) wordCounts + WHERE b.IsArchived = 0 + ORDER BY b.ProjectID, b.SortOrder, b.BookNumber, b.BookTitle; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.Book_GetCoverPaths + @BookID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT BookID, ProjectID, CoverThumbnailPath, CoverStandardPath + FROM dbo.Books + WHERE BookID = @BookID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ThreadEventTarget_ListByEvents + @ThreadEventIDs nvarchar(max) +AS +BEGIN + SET NOCOUNT ON; + + ;WITH ThreadEventIDs AS + ( + SELECT DISTINCT TRY_CONVERT(int, value) AS ThreadEventID + FROM STRING_SPLIT(COALESCE(@ThreadEventIDs, N''), N',') + WHERE TRY_CONVERT(int, value) IS NOT NULL + ) + SELECT petpl.PlotEventID, petpl.PlotLineID + FROM dbo.PlotEventTargetPlotLines petpl + INNER JOIN ThreadEventIDs ids ON ids.ThreadEventID = petpl.PlotEventID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ThreadEventTarget_SaveForEvent + @ThreadEventID int, + @PlotLineIDs nvarchar(max) = NULL +AS +BEGIN + SET NOCOUNT ON; + + DELETE FROM dbo.PlotEventTargetPlotLines + WHERE PlotEventID = @ThreadEventID; + + ;WITH PlotLineIDs AS + ( + SELECT DISTINCT TRY_CONVERT(int, value) AS PlotLineID + FROM STRING_SPLIT(COALESCE(@PlotLineIDs, N''), N',') + WHERE TRY_CONVERT(int, value) IS NOT NULL + AND TRY_CONVERT(int, value) > 0 + ) + INSERT dbo.PlotEventTargetPlotLines (PlotEventID, PlotLineID) + SELECT @ThreadEventID, PlotLineID + FROM PlotLineIDs; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.AssetCustodyCharacter_ListByProject + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT acec.AssetCustodyEventID, ace.StoryAssetID, ace.SceneID, acec.CharacterID, c.CharacterName, + acec.CharacterNameText, cr.RoleName, ace.UpdatedDate + FROM dbo.AssetCustodyEventCharacters acec + INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID + INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID + LEFT JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID + LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID + WHERE sa.ProjectID = @ProjectID + AND sa.IsArchived = 0 + AND acec.CharacterID IS NOT NULL + AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.AssetCustodySummary_ListByEvents + @AssetCustodyEventIDs nvarchar(max) +AS +BEGIN + SET NOCOUNT ON; + + ;WITH AssetCustodyEventIDs AS + ( + SELECT DISTINCT TRY_CONVERT(int, value) AS AssetCustodyEventID + FROM STRING_SPLIT(COALESCE(@AssetCustodyEventIDs, N''), N',') + WHERE TRY_CONVERT(int, value) IS NOT NULL + ) + SELECT acec.AssetCustodyEventID, c.CharacterName, cr.RoleName + FROM dbo.AssetCustodyEventCharacters acec + INNER JOIN AssetCustodyEventIDs ids ON ids.AssetCustodyEventID = acec.AssetCustodyEventID + INNER JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID + LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID + WHERE acec.CharacterID IS NOT NULL + AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterSexValue_ListForOwner + @OwnerUserID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT CharacterSexValueID, OwnerUserID, SexName, IsSystem, SortOrder, IsActive + FROM dbo.CharacterSexValues + WHERE IsActive = 1 + AND (IsSystem = 1 OR OwnerUserID = @OwnerUserID) + ORDER BY IsSystem DESC, SortOrder, SexName; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterSexValue_Get + @CharacterSexValueID int, + @OwnerUserID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT CharacterSexValueID, OwnerUserID, SexName, IsSystem, SortOrder, IsActive + FROM dbo.CharacterSexValues + WHERE CharacterSexValueID = @CharacterSexValueID + AND IsActive = 1 + AND (IsSystem = 1 OR OwnerUserID = @OwnerUserID); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterImage_ListByCharacter + @CharacterID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT CharacterImageID, CharacterID, ProjectID, ImagePath, ThumbnailPath, Caption, SortOrder, UploadedDateUTC, IsDeleted + FROM dbo.CharacterImages + WHERE CharacterID = @CharacterID + AND IsDeleted = 0 + ORDER BY SortOrder, UploadedDateUTC, CharacterImageID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterImage_Get + @CharacterImageID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT CharacterImageID, CharacterID, ProjectID, ImagePath, ThumbnailPath, Caption, SortOrder, UploadedDateUTC, IsDeleted + FROM dbo.CharacterImages + WHERE CharacterImageID = @CharacterImageID + AND IsDeleted = 0; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterImage_Create + @CharacterID int, + @ProjectID int, + @ImagePath nvarchar(500), + @ThumbnailPath nvarchar(500) = NULL, + @Caption nvarchar(500) = NULL, + @InputSortOrder int = NULL +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @SortOrder int = COALESCE(@InputSortOrder, (SELECT ISNULL(MAX(SortOrder), 0) + 1 FROM dbo.CharacterImages WHERE CharacterID = @CharacterID)); + + INSERT dbo.CharacterImages (CharacterID, ProjectID, ImagePath, ThumbnailPath, Caption, SortOrder) + VALUES (@CharacterID, @ProjectID, @ImagePath, @ThumbnailPath, @Caption, @SortOrder); + + SELECT CAST(SCOPE_IDENTITY() AS int); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterImage_Delete + @CharacterImageID int +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.CharacterImages + SET IsDeleted = 1, + DeletedDateUTC = SYSUTCDATETIME() + WHERE CharacterImageID = @CharacterImageID; +END; +GO