using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IStoryMemoryRepository { Task ClearDerivedStateAsync(int importSessionId); Task> ListProcessedSceneResultIdsAsync(int importSessionId, string processorVersion); Task MarkSceneResultProcessedAsync(int importSessionId, int sceneResultId, string processorVersion, string snapshotHash); Task UpsertCharacterAsync(StoryMemoryCharacterSave request); Task AddCharacterAliasAsync(int characterId, string alias, string normalisedAlias, string reason, int sceneResultId, decimal confidence); Task AddCharacterAttributeAsync(int characterId, string attributeType, string value, decimal confidence, bool isExplicit, string evidenceSummary, int sceneResultId); Task UpsertLocationAsync(StoryMemoryLocationSave request); Task UpsertAssetAsync(StoryMemoryAssetSave request); Task UpsertRelationshipAsync(int importSessionId, string sourceEntityType, int sourceEntityId, string targetEntityType, int targetEntityId, string relationshipType, decimal confidence, bool isExplicit, int sceneResultId); Task> ListCharactersAsync(int importSessionId); Task> ListCharacterAttributesAsync(int importSessionId); Task> ListLocationsAsync(int importSessionId); Task> ListAssetsAsync(int importSessionId); Task> ListAssignmentsAsync(int importSessionId); Task GetAssignmentAsync(int importSessionId, string entityType, int entityId); Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request); Task InvalidateAssignmentAsync(int assignmentId, string reason); Task UpsertDemandAsync(StoryMemoryIllustrationDemandSave request); Task GetAppearancePreferenceAsync(int importSessionId); Task SetAppearancePreferenceAsync(int importSessionId, string preference); Task GetDiagnosticsAsync(int importSessionId); } public sealed class StoryMemoryRepository(ISqlConnectionFactory connectionFactory) : IStoryMemoryRepository { public async Task ClearDerivedStateAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ DELETE FROM dbo.StoryMemoryIllustrationDemands WHERE ImportSessionID = @ImportSessionID; DELETE FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID; DELETE FROM dbo.StoryMemoryRelationships WHERE ImportSessionID = @ImportSessionID; DELETE a FROM dbo.StoryMemoryCharacterAttributes a INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID WHERE c.ImportSessionID = @ImportSessionID; DELETE aliasRows FROM dbo.StoryMemoryCharacterAliases aliasRows INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = aliasRows.StoryMemoryCharacterID WHERE c.ImportSessionID = @ImportSessionID; DELETE FROM dbo.StoryMemoryAssets WHERE ImportSessionID = @ImportSessionID; DELETE FROM dbo.StoryMemoryLocations WHERE ImportSessionID = @ImportSessionID; DELETE FROM dbo.StoryMemoryCharacters WHERE ImportSessionID = @ImportSessionID; DELETE FROM dbo.StoryMemoryProcessedSceneResults WHERE ImportSessionID = @ImportSessionID; """, new { ImportSessionID = importSessionId }); } public async Task> ListProcessedSceneResultIdsAsync(int importSessionId, string processorVersion) { using var connection = connectionFactory.CreateConnection(); var ids = await connection.QueryAsync( """ SELECT SceneResultID FROM dbo.StoryMemoryProcessedSceneResults WHERE ImportSessionID = @ImportSessionID AND ProcessorVersion = @ProcessorVersion; """, new { ImportSessionID = importSessionId, ProcessorVersion = processorVersion }); return ids.ToHashSet(); } public async Task MarkSceneResultProcessedAsync(int importSessionId, int sceneResultId, string processorVersion, string snapshotHash) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ MERGE dbo.StoryMemoryProcessedSceneResults WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID, @SceneResultID AS SceneResultID, @ProcessorVersion AS ProcessorVersion) AS source ON target.ImportSessionID = source.ImportSessionID AND target.SceneResultID = source.SceneResultID AND target.ProcessorVersion = source.ProcessorVersion WHEN MATCHED THEN UPDATE SET SnapshotHash = @SnapshotHash, ProcessedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, SceneResultID, ProcessorVersion, SnapshotHash) VALUES (@ImportSessionID, @SceneResultID, @ProcessorVersion, @SnapshotHash); """, new { ImportSessionID = importSessionId, SceneResultID = sceneResultId, ProcessorVersion = processorVersion, SnapshotHash = snapshotHash }); } public async Task UpsertCharacterAsync(StoryMemoryCharacterSave request) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( """ MERGE dbo.StoryMemoryCharacters WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID, @CanonicalIdentityKey AS CanonicalIdentityKey) AS source ON target.ImportSessionID = source.ImportSessionID AND target.CanonicalIdentityKey = source.CanonicalIdentityKey WHEN MATCHED THEN UPDATE SET DisplayName = CASE WHEN LEN(@DisplayName) > LEN(target.DisplayName) THEN @DisplayName ELSE target.DisplayName END, EntityContext = @EntityContext, SourceSectionType = @SourceSectionType, IsNamed = CASE WHEN @IsNamed = 1 THEN 1 ELSE target.IsNamed END, IsGroupEntity = CASE WHEN @IsGroupEntity = 1 THEN 1 ELSE target.IsGroupEntity END, IsResolved = CASE WHEN @IsResolved = 1 THEN 1 ELSE target.IsResolved END, IsNarrator = CASE WHEN @IsNarrator = 1 THEN 1 ELSE target.IsNarrator END, IsPrimaryPOV = CASE WHEN @IsPrimaryPOV = 1 THEN 1 ELSE target.IsPrimaryPOV END, Significance = CASE WHEN @Significance > target.Significance THEN @Significance ELSE target.Significance END, LastUpdatedSceneResultID = @SceneResultID, EvidenceVersion = target.EvidenceVersion + 1, UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, DisplayName, EntityContext, SourceSectionType, IsNamed, IsGroupEntity, IsResolved, IsNarrator, IsPrimaryPOV, Significance, CreatedSceneResultID, LastUpdatedSceneResultID) VALUES (@ImportSessionID, @ProjectID, @BookID, @CanonicalIdentityKey, @DisplayName, @EntityContext, @SourceSectionType, @IsNamed, @IsGroupEntity, @IsResolved, @IsNarrator, @IsPrimaryPOV, @Significance, @SceneResultID, @SceneResultID); SELECT * FROM dbo.StoryMemoryCharacters WHERE ImportSessionID = @ImportSessionID AND CanonicalIdentityKey = @CanonicalIdentityKey; """, request); } public async Task AddCharacterAliasAsync(int characterId, string alias, string normalisedAlias, string reason, int sceneResultId, decimal confidence) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ MERGE dbo.StoryMemoryCharacterAliases WITH (HOLDLOCK) AS target USING (SELECT @StoryMemoryCharacterID AS StoryMemoryCharacterID, @NormalisedAlias AS NormalisedAlias, @SourceSceneResultID AS SourceSceneResultID) AS source ON target.StoryMemoryCharacterID = source.StoryMemoryCharacterID AND target.NormalisedAlias = source.NormalisedAlias AND ISNULL(target.SourceSceneResultID, -1) = ISNULL(source.SourceSceneResultID, -1) WHEN NOT MATCHED THEN INSERT (StoryMemoryCharacterID, Alias, NormalisedAlias, ResolutionReason, SourceSceneResultID, Confidence) VALUES (@StoryMemoryCharacterID, @Alias, @NormalisedAlias, @ResolutionReason, @SourceSceneResultID, @Confidence); """, new { StoryMemoryCharacterID = characterId, Alias = alias, NormalisedAlias = normalisedAlias, ResolutionReason = reason, SourceSceneResultID = sceneResultId, Confidence = confidence }); } public async Task AddCharacterAttributeAsync(int characterId, string attributeType, string value, decimal confidence, bool isExplicit, string evidenceSummary, int sceneResultId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ MERGE dbo.StoryMemoryCharacterAttributes WITH (HOLDLOCK) AS target USING (SELECT @StoryMemoryCharacterID AS StoryMemoryCharacterID, @AttributeType AS AttributeType, @NormalisedValue AS NormalisedValue, @SourceSceneResultID AS SourceSceneResultID) AS source ON target.StoryMemoryCharacterID = source.StoryMemoryCharacterID AND target.AttributeType = source.AttributeType AND target.NormalisedValue = source.NormalisedValue AND ISNULL(target.SourceSceneResultID, -1) = ISNULL(source.SourceSceneResultID, -1) AND target.SupersededUtc IS NULL WHEN MATCHED THEN UPDATE SET Confidence = CASE WHEN @Confidence > target.Confidence THEN @Confidence ELSE target.Confidence END, IsExplicit = CASE WHEN @IsExplicit = 1 THEN 1 ELSE target.IsExplicit END, EvidenceSummary = @EvidenceSummary WHEN NOT MATCHED THEN INSERT (StoryMemoryCharacterID, AttributeType, NormalisedValue, Confidence, IsExplicit, EvidenceSummary, SourceSceneResultID) VALUES (@StoryMemoryCharacterID, @AttributeType, @NormalisedValue, @Confidence, @IsExplicit, @EvidenceSummary, @SourceSceneResultID); """, new { StoryMemoryCharacterID = characterId, AttributeType = attributeType, NormalisedValue = value, Confidence = confidence, IsExplicit = isExplicit, EvidenceSummary = evidenceSummary, SourceSceneResultID = sceneResultId }); } public async Task UpsertLocationAsync(StoryMemoryLocationSave request) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( """ MERGE dbo.StoryMemoryLocations WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID, @CanonicalIdentityKey AS CanonicalIdentityKey) AS source ON target.ImportSessionID = source.ImportSessionID AND target.CanonicalIdentityKey = source.CanonicalIdentityKey WHEN MATCHED THEN UPDATE SET DisplayName = @DisplayName, SemanticType = @SemanticType, Subtype = @Subtype, InteriorExterior = @InteriorExterior, LastSeenSceneResultID = @SceneResultID, UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, DisplayName, SemanticType, Subtype, InteriorExterior, FirstSeenSceneResultID, LastSeenSceneResultID) VALUES (@ImportSessionID, @ProjectID, @BookID, @CanonicalIdentityKey, @DisplayName, @SemanticType, @Subtype, @InteriorExterior, @SceneResultID, @SceneResultID); SELECT * FROM dbo.StoryMemoryLocations WHERE ImportSessionID = @ImportSessionID AND CanonicalIdentityKey = @CanonicalIdentityKey; """, request); } public async Task UpsertAssetAsync(StoryMemoryAssetSave request) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( """ MERGE dbo.StoryMemoryAssets WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID, @CanonicalIdentityKey AS CanonicalIdentityKey) AS source ON target.ImportSessionID = source.ImportSessionID AND target.CanonicalIdentityKey = source.CanonicalIdentityKey WHEN MATCHED THEN UPDATE SET DisplayName = @DisplayName, SemanticType = @SemanticType, Subtype = @Subtype, Colour = @Colour, Era = @Era, LastSeenSceneResultID = @SceneResultID, UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, DisplayName, SemanticType, Subtype, Colour, Era, FirstSeenSceneResultID, LastSeenSceneResultID) VALUES (@ImportSessionID, @ProjectID, @BookID, @CanonicalIdentityKey, @DisplayName, @SemanticType, @Subtype, @Colour, @Era, @SceneResultID, @SceneResultID); SELECT * FROM dbo.StoryMemoryAssets WHERE ImportSessionID = @ImportSessionID AND CanonicalIdentityKey = @CanonicalIdentityKey; """, request); } public async Task UpsertRelationshipAsync(int importSessionId, string sourceEntityType, int sourceEntityId, string targetEntityType, int targetEntityId, string relationshipType, decimal confidence, bool isExplicit, int sceneResultId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ MERGE dbo.StoryMemoryRelationships WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID, @SourceEntityType AS SourceEntityType, @SourceEntityID AS SourceEntityID, @TargetEntityType AS TargetEntityType, @TargetEntityID AS TargetEntityID, @RelationshipType AS RelationshipType) AS source ON target.ImportSessionID = source.ImportSessionID AND target.SourceEntityType = source.SourceEntityType AND target.SourceEntityID = source.SourceEntityID AND target.TargetEntityType = source.TargetEntityType AND target.TargetEntityID = source.TargetEntityID AND target.RelationshipType = source.RelationshipType AND target.Active = 1 WHEN MATCHED THEN UPDATE SET Confidence = CASE WHEN @Confidence > target.Confidence THEN @Confidence ELSE target.Confidence END, IsExplicit = CASE WHEN @IsExplicit = 1 THEN 1 ELSE target.IsExplicit END, LastUpdatedSceneResultID = @SceneResultID, UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, SourceEntityType, SourceEntityID, TargetEntityType, TargetEntityID, RelationshipType, Confidence, IsExplicit, FirstSeenSceneResultID, LastUpdatedSceneResultID) VALUES (@ImportSessionID, @SourceEntityType, @SourceEntityID, @TargetEntityType, @TargetEntityID, @RelationshipType, @Confidence, @IsExplicit, @SceneResultID, @SceneResultID); """, new { ImportSessionID = importSessionId, SourceEntityType = sourceEntityType, SourceEntityID = sourceEntityId, TargetEntityType = targetEntityType, TargetEntityID = targetEntityId, RelationshipType = relationshipType, Confidence = confidence, IsExplicit = isExplicit, SceneResultID = sceneResultId }); } public async Task> ListCharactersAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("SELECT * FROM dbo.StoryMemoryCharacters WHERE ImportSessionID = @ImportSessionID ORDER BY Significance DESC, DisplayName;", new { ImportSessionID = importSessionId })).ToList(); } public async Task> ListCharacterAttributesAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync( """ SELECT a.* FROM dbo.StoryMemoryCharacterAttributes a INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID WHERE c.ImportSessionID = @ImportSessionID AND a.SupersededUtc IS NULL; """, new { ImportSessionID = importSessionId })).ToList(); } public async Task> ListLocationsAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("SELECT * FROM dbo.StoryMemoryLocations WHERE ImportSessionID = @ImportSessionID ORDER BY DisplayName;", new { ImportSessionID = importSessionId })).ToList(); } public async Task> ListAssetsAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("SELECT * FROM dbo.StoryMemoryAssets WHERE ImportSessionID = @ImportSessionID ORDER BY DisplayName;", new { ImportSessionID = importSessionId })).ToList(); } public async Task> ListAssignmentsAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("SELECT * FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND InvalidatedUtc IS NULL;", new { ImportSessionID = importSessionId })).ToList(); } public async Task GetAssignmentAsync(int importSessionId, string entityType, int entityId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "SELECT * FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND EntityType = @EntityType AND EntityID = @EntityID AND InvalidatedUtc IS NULL;", new { ImportSessionID = importSessionId, EntityType = entityType, EntityID = entityId }); } public async Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ MERGE dbo.StoryMemoryIllustrationAssignments WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID, @EntityType AS EntityType, @EntityID AS EntityID) AS source ON target.ImportSessionID = source.ImportSessionID AND target.EntityType = source.EntityType AND target.EntityID = source.EntityID AND target.InvalidatedUtc IS NULL WHEN MATCHED THEN UPDATE SET IllustrationLibraryItemID = @IllustrationLibraryItemID, StableCode = @StableCode, AssignmentStatus = @AssignmentStatus, IsFallback = @IsFallback, MatchScore = @MatchScore, Confidence = @Confidence, AssignmentReason = @AssignmentReason, EvidenceVersion = @EvidenceVersion, LastValidatedUtc = SYSUTCDATETIME(), UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, EntityType, EntityID, IllustrationLibraryItemID, StableCode, AssignmentStatus, IsFallback, MatchScore, Confidence, AssignmentReason, EvidenceVersion) VALUES (@ImportSessionID, @EntityType, @EntityID, @IllustrationLibraryItemID, @StableCode, @AssignmentStatus, @IsFallback, @MatchScore, @Confidence, @AssignmentReason, @EvidenceVersion); """, request); } public async Task InvalidateAssignmentAsync(int assignmentId, string reason) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ UPDATE dbo.StoryMemoryIllustrationAssignments SET InvalidatedUtc = SYSUTCDATETIME(), InvalidationReason = @Reason, UpdatedUtc = SYSUTCDATETIME() WHERE StoryMemoryIllustrationAssignmentID = @AssignmentID AND InvalidatedUtc IS NULL; """, new { AssignmentID = assignmentId, Reason = reason }); } public async Task UpsertDemandAsync(StoryMemoryIllustrationDemandSave request) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( """ MERGE dbo.StoryMemoryIllustrationDemands WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID, @EntityType AS EntityType, @ArchetypeKey AS ArchetypeKey) AS source ON target.ImportSessionID = source.ImportSessionID AND target.EntityType = source.EntityType AND target.ArchetypeKey = source.ArchetypeKey WHEN MATCHED THEN UPDATE SET DemandCount = DemandCount + 1, WaitingEntityCount = @WaitingEntityCount, Status = @Status, QueuedIllustrationLibraryItemID = COALESCE(@QueuedIllustrationLibraryItemID, target.QueuedIllustrationLibraryItemID), GeneratedIllustrationLibraryItemID = COALESCE(@GeneratedIllustrationLibraryItemID, target.GeneratedIllustrationLibraryItemID), UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, ProjectID, EntityType, ArchetypeKey, WaitingEntityCount, Status, QueuedIllustrationLibraryItemID, GeneratedIllustrationLibraryItemID) VALUES (@ImportSessionID, @ProjectID, @EntityType, @ArchetypeKey, @WaitingEntityCount, @Status, @QueuedIllustrationLibraryItemID, @GeneratedIllustrationLibraryItemID); SELECT * FROM dbo.StoryMemoryIllustrationDemands WHERE ImportSessionID = @ImportSessionID AND EntityType = @EntityType AND ArchetypeKey = @ArchetypeKey; """, request); } public async Task GetAppearancePreferenceAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "SELECT AppearancePreference FROM dbo.StoryMemoryImportPreferences WHERE ImportSessionID = @ImportSessionID;", new { ImportSessionID = importSessionId }) ?? StoryMemoryAppearancePreferences.Balanced; } public async Task SetAppearancePreferenceAsync(int importSessionId, string preference) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( """ MERGE dbo.StoryMemoryImportPreferences WITH (HOLDLOCK) AS target USING (SELECT @ImportSessionID AS ImportSessionID) AS source ON target.ImportSessionID = source.ImportSessionID WHEN MATCHED THEN UPDATE SET AppearancePreference = @AppearancePreference, UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, AppearancePreference) VALUES (@ImportSessionID, @AppearancePreference); """, new { ImportSessionID = importSessionId, AppearancePreference = preference }); } public async Task GetDiagnosticsAsync(int importSessionId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( """ SELECT @ImportSessionID AS ImportSessionID, (SELECT COUNT(*) FROM dbo.StoryMemoryCharacters WHERE ImportSessionID = @ImportSessionID) AS CharacterCount, (SELECT COUNT(*) FROM dbo.StoryMemoryLocations WHERE ImportSessionID = @ImportSessionID) AS LocationCount, (SELECT COUNT(*) FROM dbo.StoryMemoryAssets WHERE ImportSessionID = @ImportSessionID) AS AssetCount, (SELECT COUNT(*) FROM dbo.StoryMemoryCharacterAliases a INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID WHERE c.ImportSessionID = @ImportSessionID) AS AliasCount, (SELECT COUNT(*) FROM dbo.StoryMemoryCharacterAttributes a INNER JOIN dbo.StoryMemoryCharacters c ON c.StoryMemoryCharacterID = a.StoryMemoryCharacterID WHERE c.ImportSessionID = @ImportSessionID AND a.SupersededUtc IS NULL) AS AttributeCount, (SELECT COUNT(*) FROM dbo.StoryMemoryRelationships WHERE ImportSessionID = @ImportSessionID AND Active = 1) AS RelationshipCount, (SELECT COUNT(*) FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND InvalidatedUtc IS NULL) AS AssignmentCount, (SELECT COUNT(*) FROM dbo.StoryMemoryIllustrationAssignments WHERE ImportSessionID = @ImportSessionID AND InvalidatedUtc IS NULL AND IsFallback = 1) AS FallbackCount, (SELECT COUNT(*) FROM dbo.StoryMemoryIllustrationDemands WHERE ImportSessionID = @ImportSessionID) AS DemandCount, (SELECT COUNT(*) FROM dbo.StoryMemoryProcessedSceneResults WHERE ImportSessionID = @ImportSessionID) AS ProcessedSceneCount, (SELECT MAX(SceneResultID) FROM dbo.StoryMemoryProcessedSceneResults WHERE ImportSessionID = @ImportSessionID) AS LastProcessedSceneResultID, COALESCE((SELECT AppearancePreference FROM dbo.StoryMemoryImportPreferences WHERE ImportSessionID = @ImportSessionID), N'Balanced') AS AppearancePreference; """, new { ImportSessionID = importSessionId }); } }