Converted all inline SQL to stored procedures.

This commit is contained in:
Nick Beckley 2026-06-23 20:09:31 +01:00
parent d255407193
commit a3ce37c66c
5 changed files with 747 additions and 562 deletions

View File

@ -24,16 +24,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
{
using var connection = connectionFactory.CreateConnection();
var count = await connection.ExecuteScalarAsync<int>(
"""
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<ImportLookupIds> GetLookupIdsAsync(IDbConnection connection, IDbTransaction transaction)
{
var revisionStatusId = await connection.QuerySingleOrDefaultAsync<int?>(
"SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder;",
transaction: transaction);
var timeModeId = await connection.QuerySingleOrDefaultAsync<int?>(
"SELECT TOP (1) TimeModeID FROM dbo.TimeModes WHERE TimeModeName = N'Relative' ORDER BY SortOrder;",
transaction: transaction);
var timeConfidenceId = await connection.QuerySingleOrDefaultAsync<int?>(
"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<ImportRequiredLookupRow>();
var locationTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var roleTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var presenceTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var plotLineTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var plotImportance = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var threadTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var threadStatuses = (await result.ReadAsync<ImportStatusLookupRow>()).ToDictionary(x => x.StatusName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var threadEventTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var sceneDependencyTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var assetKinds = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var assetStates = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var assetEventTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var assetCustodyEventTypes = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var custodyRoles = (await result.ReadAsync<ImportLookupRow>()).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var locationTypes = (await connection.QueryAsync<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportStatusLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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<ImportLookupRow>(
"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; }

View File

@ -81,13 +81,9 @@ public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFact
}
var bookMetadata = (await connection.QueryAsync<Book>(
"""
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))

View File

@ -792,44 +792,9 @@ public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) :
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ContinuityWarning>(
"""
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<WritingPlanManagementItem>(
"""
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<WritingScheduleScene>(
"""
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<WritingPlanSummary>(
"""
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<WritingSchedulePreviewItem>(
"""
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<IReadOnlyList<WritingScheduleDashboardItem>> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingScheduleDashboardItem>(
"""
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<IReadOnlyList<WritingPlanSummary>> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingPlanSummary>(
"""
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<ProjectDashboardMetrics>(
"""
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<IReadOnlyList<ProjectDashboardAttentionThread>> ListDashboardAttentionThreadsAsync(int projectId, int count)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ProjectDashboardAttentionThread>(
"""
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<ProjectActivity>(
"""
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<Book>(
"""
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<Book>(
"""
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<int> 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<ThreadEventTargetRow>(
"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<int>)x.Select(row => row.PlotLineID).ToList());
@ -3159,8 +2835,9 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP
}
var targetRows = await connection.QueryAsync<ThreadEventTargetRow>(
"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<int>)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<AssetCustodyCharacter>(
"""
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<AssetCustodySummaryRow>(
"""
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<CharacterSexValue>(
"""
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<CharacterSexValue>(
"""
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<int> 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<CharacterImage>(
"""
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<CharacterImage>(
"""
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<int> CreateCharacterImageAsync(CharacterImage image)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"""
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)

View File

@ -36,23 +36,9 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WordCompanionBookDto>(
"""
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();
}

View File

@ -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