Merge critical imports without overwriting curation
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using RolemasterDb.App.Data;
|
||||
using RolemasterDb.App.Domain;
|
||||
using RolemasterDb.ImportTool.Parsing;
|
||||
|
||||
namespace RolemasterDb.ImportTool.Tests;
|
||||
|
||||
public sealed class CriticalImportMergeIntegrationTests
|
||||
{
|
||||
private static readonly PdfXmlExtractor Extractor = new();
|
||||
private static readonly StandardCriticalTableParser StandardParser = new();
|
||||
|
||||
[Fact]
|
||||
public async Task Reimport_updates_uncurated_results_and_refreshes_source_metadata()
|
||||
{
|
||||
var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
|
||||
var databasePath = CreateEmptyDatabasePath();
|
||||
var loader = new CriticalImportLoader(databasePath);
|
||||
|
||||
await loader.LoadAsync(parseResult.Table);
|
||||
|
||||
await using (var dbContext = CreateDbContext(databasePath))
|
||||
{
|
||||
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
||||
result.IsCurated = false;
|
||||
result.RawCellText = "Temporary raw text";
|
||||
result.DescriptionText = "Temporary description";
|
||||
result.RawAffixText = "+99H";
|
||||
result.ParseStatus = "manually_curated";
|
||||
result.SourcePageNumber = null;
|
||||
result.SourceImagePath = null;
|
||||
result.SourceImageCropJson = null;
|
||||
|
||||
dbContext.CriticalEffects.RemoveRange(result.Effects);
|
||||
foreach (var branch in result.Branches)
|
||||
{
|
||||
dbContext.CriticalEffects.RemoveRange(branch.Effects);
|
||||
}
|
||||
|
||||
dbContext.CriticalBranches.RemoveRange(result.Branches);
|
||||
result.Effects.Clear();
|
||||
result.Branches.Clear();
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await loader.LoadAsync(parseResult.Table);
|
||||
|
||||
await using (var dbContext = CreateDbContext(databasePath))
|
||||
{
|
||||
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
||||
|
||||
Assert.NotEqual("Temporary raw text", result.RawCellText);
|
||||
Assert.NotEqual("Temporary description", result.DescriptionText);
|
||||
Assert.NotEqual("+99H", result.RawAffixText);
|
||||
Assert.NotEqual("manually_curated", result.ParseStatus);
|
||||
Assert.NotNull(result.SourcePageNumber);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
|
||||
Assert.NotEmpty(result.Branches);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reimport_preserves_curated_results_and_child_rows()
|
||||
{
|
||||
var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
|
||||
var databasePath = CreateEmptyDatabasePath();
|
||||
var loader = new CriticalImportLoader(databasePath);
|
||||
|
||||
await loader.LoadAsync(parseResult.Table);
|
||||
|
||||
await using (var dbContext = CreateDbContext(databasePath))
|
||||
{
|
||||
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
||||
|
||||
result.IsCurated = true;
|
||||
result.RawCellText = "Curated raw text";
|
||||
result.DescriptionText = "Curated result prose.";
|
||||
result.RawAffixText = "+12H";
|
||||
result.ParseStatus = "manually_curated";
|
||||
result.ParsedJson = "{\"reviewed\":true}";
|
||||
|
||||
dbContext.CriticalEffects.RemoveRange(result.Effects);
|
||||
foreach (var branch in result.Branches)
|
||||
{
|
||||
dbContext.CriticalEffects.RemoveRange(branch.Effects);
|
||||
}
|
||||
|
||||
dbContext.CriticalBranches.RemoveRange(result.Branches);
|
||||
result.Effects.Clear();
|
||||
result.Branches.Clear();
|
||||
|
||||
result.Effects.Add(new CriticalEffect
|
||||
{
|
||||
EffectCode = CriticalEffectCodes.DirectHits,
|
||||
ValueInteger = 12,
|
||||
IsPermanent = false,
|
||||
SourceType = "manual",
|
||||
SourceText = "+12H"
|
||||
});
|
||||
|
||||
result.Branches.Add(new CriticalBranch
|
||||
{
|
||||
BranchKind = "conditional",
|
||||
ConditionKey = "with_shield",
|
||||
ConditionText = "with shield",
|
||||
ConditionJson = "{}",
|
||||
RawText = "with shield: glancing blow.",
|
||||
DescriptionText = "Glancing blow.",
|
||||
ParsedJson = "{}",
|
||||
SortOrder = 1,
|
||||
Effects =
|
||||
[
|
||||
new CriticalEffect
|
||||
{
|
||||
EffectCode = CriticalEffectCodes.MustParryRounds,
|
||||
DurationRounds = 2,
|
||||
IsPermanent = false,
|
||||
SourceType = "manual",
|
||||
SourceText = "2mp"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await loader.LoadAsync(parseResult.Table);
|
||||
|
||||
await using (var dbContext = CreateDbContext(databasePath))
|
||||
{
|
||||
var result = await LoadResultAsync(dbContext, "36-45", "B");
|
||||
|
||||
Assert.True(result.IsCurated);
|
||||
Assert.Equal("Curated raw text", result.RawCellText);
|
||||
Assert.Equal("Curated result prose.", result.DescriptionText);
|
||||
Assert.Equal("+12H", result.RawAffixText);
|
||||
Assert.Equal("manually_curated", result.ParseStatus);
|
||||
Assert.Equal("{\"reviewed\":true}", result.ParsedJson);
|
||||
Assert.Single(result.Effects);
|
||||
Assert.Equal(12, result.Effects[0].ValueInteger);
|
||||
Assert.Single(result.Branches);
|
||||
Assert.Equal("with_shield", result.Branches[0].ConditionKey);
|
||||
Assert.Single(result.Branches[0].Effects);
|
||||
Assert.Equal(2, result.Branches[0].Effects[0].DurationRounds);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reimport_deletes_only_unmatched_uncurated_results()
|
||||
{
|
||||
var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
|
||||
var databasePath = CreateEmptyDatabasePath();
|
||||
var loader = new CriticalImportLoader(databasePath);
|
||||
|
||||
await loader.LoadAsync(parseResult.Table);
|
||||
|
||||
await using (var dbContext = CreateDbContext(databasePath))
|
||||
{
|
||||
var curatedMissing = await LoadResultAsync(dbContext, "01-05", "A");
|
||||
curatedMissing.IsCurated = true;
|
||||
curatedMissing.DescriptionText = "Keep me.";
|
||||
|
||||
var uncuratedMissing = await LoadResultAsync(dbContext, "01-05", "B");
|
||||
uncuratedMissing.IsCurated = false;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var trimmedTable = CreateTrimmedTable(
|
||||
parseResult.Table,
|
||||
("01-05", "A"),
|
||||
("01-05", "B"));
|
||||
|
||||
await loader.LoadAsync(trimmedTable);
|
||||
|
||||
await using (var dbContext = CreateDbContext(databasePath))
|
||||
{
|
||||
var results = await dbContext.CriticalResults
|
||||
.Include(item => item.CriticalTable)
|
||||
.Include(item => item.CriticalColumn)
|
||||
.Include(item => item.CriticalRollBand)
|
||||
.Where(item => item.CriticalTable.Slug == "slash" && item.CriticalRollBand.Label == "01-05")
|
||||
.ToListAsync();
|
||||
|
||||
Assert.Contains(results, item => item.CriticalColumn.ColumnKey == "A" && item.IsCurated && item.DescriptionText == "Keep me.");
|
||||
Assert.DoesNotContain(results, item => item.CriticalColumn.ColumnKey == "B");
|
||||
}
|
||||
}
|
||||
|
||||
private static ParsedCriticalTable CreateTrimmedTable(
|
||||
ParsedCriticalTable table,
|
||||
params (string RollBandLabel, string ColumnKey)[] excludedResults)
|
||||
{
|
||||
var excludedKeys = excludedResults
|
||||
.Select(item => $"{item.RollBandLabel}|{item.ColumnKey}")
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
return new ParsedCriticalTable(
|
||||
table.Slug,
|
||||
table.DisplayName,
|
||||
table.Family,
|
||||
table.SourceDocument,
|
||||
table.Notes,
|
||||
table.Groups,
|
||||
table.Columns,
|
||||
table.RollBands,
|
||||
table.Results
|
||||
.Where(item => !excludedKeys.Contains($"{item.RollBandLabel}|{item.ColumnKey}"))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private static async Task<(CriticalTableParseResult ParseResult, ImportArtifactPaths ArtifactPaths)> LoadPreparedSlashParseResultAsync()
|
||||
{
|
||||
var entry = LoadManifest().Tables.Single(item => item.Slug == "slash");
|
||||
var xmlPath = Path.Combine(GetArtifactCacheRoot(), $"{entry.Slug}.xml");
|
||||
|
||||
if (!File.Exists(xmlPath))
|
||||
{
|
||||
await Extractor.ExtractAsync(Path.Combine(GetRepositoryRoot(), entry.PdfPath), xmlPath);
|
||||
}
|
||||
|
||||
var parseResult = StandardParser.Parse(entry, await File.ReadAllTextAsync(xmlPath));
|
||||
var artifactRoot = Path.Combine(GetArtifactCacheRoot(), Guid.NewGuid().ToString("N"));
|
||||
var artifactPaths = ImportArtifactPaths.Create(artifactRoot, entry.Slug);
|
||||
var generator = new CriticalSourceImageArtifactGenerator(new PdfXmlExtractor());
|
||||
|
||||
await generator.GenerateAsync(Path.Combine(GetRepositoryRoot(), entry.PdfPath), artifactPaths, parseResult);
|
||||
return (parseResult, artifactPaths);
|
||||
}
|
||||
|
||||
private static async Task<CriticalResult> LoadResultAsync(RolemasterDbContext dbContext, string rollBandLabel, string columnKey) =>
|
||||
await dbContext.CriticalResults
|
||||
.Include(item => item.CriticalTable)
|
||||
.Include(item => item.CriticalColumn)
|
||||
.Include(item => item.CriticalRollBand)
|
||||
.Include(item => item.Effects)
|
||||
.Include(item => item.Branches)
|
||||
.ThenInclude(branch => branch.Effects)
|
||||
.SingleAsync(item =>
|
||||
item.CriticalTable.Slug == "slash" &&
|
||||
item.CriticalRollBand.Label == rollBandLabel &&
|
||||
item.CriticalColumn.ColumnKey == columnKey);
|
||||
|
||||
private static ParsedCriticalResult FindResult(CriticalTableParseResult parseResult, string rollBandLabel, string columnKey) =>
|
||||
parseResult.Table.Results.Single(item =>
|
||||
item.GroupKey is null &&
|
||||
item.RollBandLabel == rollBandLabel &&
|
||||
item.ColumnKey == columnKey);
|
||||
|
||||
private static CriticalImportManifest LoadManifest() =>
|
||||
new CriticalImportManifestLoader().Load(Path.Combine(GetRepositoryRoot(), "sources", "critical-import-manifest.json"));
|
||||
|
||||
private static RolemasterDbContext CreateDbContext(string databasePath)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
|
||||
.UseSqlite($"Data Source={databasePath}")
|
||||
.Options;
|
||||
|
||||
return new RolemasterDbContext(options);
|
||||
}
|
||||
|
||||
private static string CreateEmptyDatabasePath() =>
|
||||
Path.Combine(GetArtifactCacheRoot(), $"critical-import-{Guid.NewGuid():N}.db");
|
||||
|
||||
private static string GetArtifactCacheRoot()
|
||||
{
|
||||
var cacheRoot = Path.Combine(Path.GetTempPath(), "RolemasterDb.ImportTool.MergeTests");
|
||||
Directory.CreateDirectory(cacheRoot);
|
||||
return cacheRoot;
|
||||
}
|
||||
|
||||
private static string GetRepositoryRoot()
|
||||
{
|
||||
var probe = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
||||
while (probe is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(probe.FullName, "RolemasterDB.slnx")))
|
||||
{
|
||||
return probe.FullName;
|
||||
}
|
||||
|
||||
probe = probe.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not find the repository root for integration tests.");
|
||||
}
|
||||
}
|
||||
@@ -42,85 +42,86 @@ public sealed class CriticalImportLoader(string databasePath)
|
||||
await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext, cancellationToken);
|
||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
await DeleteTableAsync(dbContext, table.Slug, cancellationToken);
|
||||
var entity = await dbContext.CriticalTables
|
||||
.AsSplitQuery()
|
||||
.Include(item => item.Groups)
|
||||
.Include(item => item.Columns)
|
||||
.Include(item => item.RollBands)
|
||||
.Include(item => item.Results)
|
||||
.ThenInclude(result => result.CriticalGroup)
|
||||
.Include(item => item.Results)
|
||||
.ThenInclude(result => result.CriticalColumn)
|
||||
.Include(item => item.Results)
|
||||
.ThenInclude(result => result.CriticalRollBand)
|
||||
.Include(item => item.Results)
|
||||
.ThenInclude(result => result.Effects)
|
||||
.Include(item => item.Results)
|
||||
.ThenInclude(result => result.Branches)
|
||||
.ThenInclude(branch => branch.Effects)
|
||||
.SingleOrDefaultAsync(item => item.Slug == table.Slug, cancellationToken);
|
||||
|
||||
var entity = new CriticalTable
|
||||
if (entity is null)
|
||||
{
|
||||
Slug = table.Slug,
|
||||
DisplayName = table.DisplayName,
|
||||
Family = table.Family,
|
||||
SourceDocument = table.SourceDocument,
|
||||
Notes = table.Notes
|
||||
entity = new CriticalTable
|
||||
{
|
||||
Slug = table.Slug
|
||||
};
|
||||
|
||||
entity.Groups = table.Groups
|
||||
.Select(item => new CriticalGroup
|
||||
{
|
||||
GroupKey = item.GroupKey,
|
||||
Label = item.Label,
|
||||
SortOrder = item.SortOrder
|
||||
})
|
||||
.ToList();
|
||||
|
||||
entity.Columns = table.Columns
|
||||
.Select(item => new CriticalColumn
|
||||
{
|
||||
ColumnKey = item.ColumnKey,
|
||||
Label = item.Label,
|
||||
Role = item.Role,
|
||||
SortOrder = item.SortOrder
|
||||
})
|
||||
.ToList();
|
||||
|
||||
entity.RollBands = table.RollBands
|
||||
.Select(item => new CriticalRollBand
|
||||
{
|
||||
Label = item.Label,
|
||||
MinRoll = item.MinRoll,
|
||||
MaxRoll = item.MaxRoll,
|
||||
SortOrder = item.SortOrder
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var groupsByKey = entity.Groups.ToDictionary(item => item.GroupKey, StringComparer.OrdinalIgnoreCase);
|
||||
var columnsByKey = entity.Columns.ToDictionary(item => item.ColumnKey, StringComparer.OrdinalIgnoreCase);
|
||||
var rollBandsByLabel = entity.RollBands.ToDictionary(item => item.Label, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
entity.Results = table.Results
|
||||
.Select(item => new CriticalResult
|
||||
{
|
||||
CriticalGroup = item.GroupKey is null ? null : groupsByKey[item.GroupKey],
|
||||
CriticalColumn = columnsByKey[item.ColumnKey],
|
||||
CriticalRollBand = rollBandsByLabel[item.RollBandLabel],
|
||||
RawCellText = item.RawCellText,
|
||||
DescriptionText = item.DescriptionText,
|
||||
RawAffixText = item.RawAffixText,
|
||||
ParsedJson = SerializeParsedEffects(item.Effects),
|
||||
ParseStatus = ResolveParseStatus(item.Effects, item.Branches),
|
||||
Effects = item.Effects
|
||||
.Select(CreateEffectEntity)
|
||||
.ToList(),
|
||||
Branches = item.Branches
|
||||
.Select(branch => new CriticalBranch
|
||||
{
|
||||
BranchKind = branch.BranchKind,
|
||||
ConditionKey = branch.ConditionKey,
|
||||
ConditionText = branch.ConditionText,
|
||||
ConditionJson = "{}",
|
||||
RawText = branch.RawText,
|
||||
DescriptionText = branch.DescriptionText,
|
||||
RawAffixText = branch.RawAffixText,
|
||||
ParsedJson = SerializeParsedEffects(branch.Effects),
|
||||
SortOrder = branch.SortOrder,
|
||||
Effects = branch.Effects
|
||||
.Select(CreateEffectEntity)
|
||||
.ToList()
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
dbContext.CriticalTables.Add(entity);
|
||||
}
|
||||
|
||||
entity.DisplayName = table.DisplayName;
|
||||
entity.Family = table.Family;
|
||||
entity.SourceDocument = table.SourceDocument;
|
||||
entity.Notes = table.Notes;
|
||||
|
||||
var groupsByKey = SynchronizeGroups(entity, table);
|
||||
var columnsByKey = SynchronizeColumns(entity, table);
|
||||
var rollBandsByLabel = SynchronizeRollBands(entity, table);
|
||||
var existingResultsByKey = entity.Results.ToDictionary(
|
||||
item => CreateResultKey(item.CriticalGroup?.GroupKey, item.CriticalColumn.ColumnKey, item.CriticalRollBand.Label),
|
||||
StringComparer.Ordinal);
|
||||
var importedKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var item in table.Results)
|
||||
{
|
||||
var resultKey = CreateResultKey(item.GroupKey, item.ColumnKey, item.RollBandLabel);
|
||||
importedKeys.Add(resultKey);
|
||||
|
||||
if (!existingResultsByKey.TryGetValue(resultKey, out var existingResult))
|
||||
{
|
||||
var newResult = CreateResultEntity(item, entity, groupsByKey, columnsByKey, rollBandsByLabel);
|
||||
entity.Results.Add(newResult);
|
||||
continue;
|
||||
}
|
||||
|
||||
existingResult.CriticalGroup = item.GroupKey is null ? null : groupsByKey[item.GroupKey];
|
||||
existingResult.CriticalColumn = columnsByKey[item.ColumnKey];
|
||||
existingResult.CriticalRollBand = rollBandsByLabel[item.RollBandLabel];
|
||||
ApplyImporterProvenance(existingResult, item);
|
||||
|
||||
if (existingResult.IsCurated)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyImportedContent(existingResult, item);
|
||||
ReplaceResultChildren(dbContext, existingResult, item);
|
||||
}
|
||||
|
||||
foreach (var unmatchedResult in entity.Results
|
||||
.Where(item => !importedKeys.Contains(CreateResultKey(item.CriticalGroup?.GroupKey, item.CriticalColumn.ColumnKey, item.CriticalRollBand.Label)))
|
||||
.ToList())
|
||||
{
|
||||
if (unmatchedResult.IsCurated)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RemoveResultGraph(dbContext, unmatchedResult);
|
||||
entity.Results.Remove(unmatchedResult);
|
||||
}
|
||||
|
||||
RemoveUnusedAxes(dbContext, entity, table);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
|
||||
@@ -136,52 +137,207 @@ public sealed class CriticalImportLoader(string databasePath)
|
||||
return new RolemasterDbContext(options);
|
||||
}
|
||||
|
||||
private static async Task DeleteTableAsync(
|
||||
RolemasterDbContext dbContext,
|
||||
string slug,
|
||||
CancellationToken cancellationToken)
|
||||
private static Dictionary<string, CriticalGroup> SynchronizeGroups(CriticalTable entity, ParsedCriticalTable table)
|
||||
{
|
||||
var tableId = await dbContext.CriticalTables
|
||||
.Where(item => item.Slug == slug)
|
||||
.Select(item => (int?)item.Id)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
var groupsByKey = entity.Groups.ToDictionary(item => item.GroupKey, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (tableId is null)
|
||||
foreach (var parsedGroup in table.Groups)
|
||||
{
|
||||
return;
|
||||
if (!groupsByKey.TryGetValue(parsedGroup.GroupKey, out var group))
|
||||
{
|
||||
group = new CriticalGroup
|
||||
{
|
||||
CriticalTable = entity,
|
||||
GroupKey = parsedGroup.GroupKey
|
||||
};
|
||||
entity.Groups.Add(group);
|
||||
groupsByKey[parsedGroup.GroupKey] = group;
|
||||
}
|
||||
|
||||
await dbContext.CriticalEffects
|
||||
.Where(item => item.CriticalBranch != null && item.CriticalBranch.CriticalResult.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
group.Label = parsedGroup.Label;
|
||||
group.SortOrder = parsedGroup.SortOrder;
|
||||
}
|
||||
|
||||
await dbContext.CriticalEffects
|
||||
.Where(item => item.CriticalResult != null && item.CriticalResult.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
return groupsByKey;
|
||||
}
|
||||
|
||||
await dbContext.CriticalBranches
|
||||
.Where(item => item.CriticalResult.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
private static Dictionary<string, CriticalColumn> SynchronizeColumns(CriticalTable entity, ParsedCriticalTable table)
|
||||
{
|
||||
var columnsByKey = entity.Columns.ToDictionary(item => item.ColumnKey, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
await dbContext.CriticalResults
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
foreach (var parsedColumn in table.Columns)
|
||||
{
|
||||
if (!columnsByKey.TryGetValue(parsedColumn.ColumnKey, out var column))
|
||||
{
|
||||
column = new CriticalColumn
|
||||
{
|
||||
CriticalTable = entity,
|
||||
ColumnKey = parsedColumn.ColumnKey
|
||||
};
|
||||
entity.Columns.Add(column);
|
||||
columnsByKey[parsedColumn.ColumnKey] = column;
|
||||
}
|
||||
|
||||
await dbContext.CriticalGroups
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
column.Label = parsedColumn.Label;
|
||||
column.Role = parsedColumn.Role;
|
||||
column.SortOrder = parsedColumn.SortOrder;
|
||||
}
|
||||
|
||||
await dbContext.CriticalColumns
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
return columnsByKey;
|
||||
}
|
||||
|
||||
await dbContext.CriticalRollBands
|
||||
.Where(item => item.CriticalTableId == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
private static Dictionary<string, CriticalRollBand> SynchronizeRollBands(CriticalTable entity, ParsedCriticalTable table)
|
||||
{
|
||||
var rollBandsByLabel = entity.RollBands.ToDictionary(item => item.Label, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
await dbContext.CriticalTables
|
||||
.Where(item => item.Id == tableId.Value)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
foreach (var parsedRollBand in table.RollBands)
|
||||
{
|
||||
if (!rollBandsByLabel.TryGetValue(parsedRollBand.Label, out var rollBand))
|
||||
{
|
||||
rollBand = new CriticalRollBand
|
||||
{
|
||||
CriticalTable = entity,
|
||||
Label = parsedRollBand.Label
|
||||
};
|
||||
entity.RollBands.Add(rollBand);
|
||||
rollBandsByLabel[parsedRollBand.Label] = rollBand;
|
||||
}
|
||||
|
||||
rollBand.MinRoll = parsedRollBand.MinRoll;
|
||||
rollBand.MaxRoll = parsedRollBand.MaxRoll;
|
||||
rollBand.SortOrder = parsedRollBand.SortOrder;
|
||||
}
|
||||
|
||||
return rollBandsByLabel;
|
||||
}
|
||||
|
||||
private static CriticalResult CreateResultEntity(
|
||||
ParsedCriticalResult item,
|
||||
CriticalTable entity,
|
||||
IReadOnlyDictionary<string, CriticalGroup> groupsByKey,
|
||||
IReadOnlyDictionary<string, CriticalColumn> columnsByKey,
|
||||
IReadOnlyDictionary<string, CriticalRollBand> rollBandsByLabel)
|
||||
{
|
||||
var result = new CriticalResult
|
||||
{
|
||||
CriticalTable = entity,
|
||||
CriticalGroup = item.GroupKey is null ? null : groupsByKey[item.GroupKey],
|
||||
CriticalColumn = columnsByKey[item.ColumnKey],
|
||||
CriticalRollBand = rollBandsByLabel[item.RollBandLabel],
|
||||
IsCurated = false
|
||||
};
|
||||
|
||||
ApplyImportedContent(result, item);
|
||||
ApplyImporterProvenance(result, item);
|
||||
result.Effects = item.Effects
|
||||
.Select(CreateEffectEntity)
|
||||
.ToList();
|
||||
result.Branches = item.Branches
|
||||
.Select(CreateBranchEntity)
|
||||
.ToList();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ApplyImportedContent(CriticalResult result, ParsedCriticalResult item)
|
||||
{
|
||||
result.RawCellText = item.RawCellText;
|
||||
result.DescriptionText = item.DescriptionText;
|
||||
result.RawAffixText = item.RawAffixText;
|
||||
result.ParsedJson = SerializeParsedEffects(item.Effects);
|
||||
result.ParseStatus = ResolveParseStatus(item.Effects, item.Branches);
|
||||
}
|
||||
|
||||
private static void ApplyImporterProvenance(CriticalResult result, ParsedCriticalResult item)
|
||||
{
|
||||
result.SourcePageNumber = item.SourceBounds.PageNumber;
|
||||
result.SourceImagePath = NormalizeOptionalText(item.SourceImagePath);
|
||||
result.SourceImageCropJson = item.SourceImageCrop is null
|
||||
? null
|
||||
: JsonSerializer.Serialize(item.SourceImageCrop, JsonOptions);
|
||||
}
|
||||
|
||||
private static void ReplaceResultChildren(RolemasterDbContext dbContext, CriticalResult result, ParsedCriticalResult item)
|
||||
{
|
||||
foreach (var branch in result.Branches)
|
||||
{
|
||||
dbContext.CriticalEffects.RemoveRange(branch.Effects);
|
||||
}
|
||||
|
||||
dbContext.CriticalEffects.RemoveRange(result.Effects);
|
||||
dbContext.CriticalBranches.RemoveRange(result.Branches);
|
||||
result.Effects.Clear();
|
||||
result.Branches.Clear();
|
||||
|
||||
foreach (var effect in item.Effects)
|
||||
{
|
||||
result.Effects.Add(CreateEffectEntity(effect));
|
||||
}
|
||||
|
||||
foreach (var branch in item.Branches)
|
||||
{
|
||||
result.Branches.Add(CreateBranchEntity(branch));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveResultGraph(RolemasterDbContext dbContext, CriticalResult result)
|
||||
{
|
||||
foreach (var branch in result.Branches)
|
||||
{
|
||||
dbContext.CriticalEffects.RemoveRange(branch.Effects);
|
||||
}
|
||||
|
||||
dbContext.CriticalEffects.RemoveRange(result.Effects);
|
||||
dbContext.CriticalBranches.RemoveRange(result.Branches);
|
||||
dbContext.CriticalResults.Remove(result);
|
||||
}
|
||||
|
||||
private static void RemoveUnusedAxes(RolemasterDbContext dbContext, CriticalTable entity, ParsedCriticalTable table)
|
||||
{
|
||||
var activeGroupKeys = entity.Results
|
||||
.Where(item => item.CriticalGroup is not null)
|
||||
.Select(item => item.CriticalGroup!.GroupKey)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var importedGroupKeys = table.Groups
|
||||
.Select(item => item.GroupKey)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var group in entity.Groups
|
||||
.Where(item => !activeGroupKeys.Contains(item.GroupKey) && !importedGroupKeys.Contains(item.GroupKey))
|
||||
.ToList())
|
||||
{
|
||||
dbContext.CriticalGroups.Remove(group);
|
||||
entity.Groups.Remove(group);
|
||||
}
|
||||
|
||||
var activeColumnKeys = entity.Results
|
||||
.Select(item => item.CriticalColumn.ColumnKey)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var importedColumnKeys = table.Columns
|
||||
.Select(item => item.ColumnKey)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var column in entity.Columns
|
||||
.Where(item => !activeColumnKeys.Contains(item.ColumnKey) && !importedColumnKeys.Contains(item.ColumnKey))
|
||||
.ToList())
|
||||
{
|
||||
dbContext.CriticalColumns.Remove(column);
|
||||
entity.Columns.Remove(column);
|
||||
}
|
||||
|
||||
var activeRollBandLabels = entity.Results
|
||||
.Select(item => item.CriticalRollBand.Label)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var importedRollBandLabels = table.RollBands
|
||||
.Select(item => item.Label)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var rollBand in entity.RollBands
|
||||
.Where(item => !activeRollBandLabels.Contains(item.Label) && !importedRollBandLabels.Contains(item.Label))
|
||||
.ToList())
|
||||
{
|
||||
dbContext.CriticalRollBands.Remove(rollBand);
|
||||
entity.RollBands.Remove(rollBand);
|
||||
}
|
||||
}
|
||||
|
||||
private static CriticalEffect CreateEffectEntity(ParsedCriticalEffect effect) =>
|
||||
@@ -200,6 +356,23 @@ public sealed class CriticalImportLoader(string databasePath)
|
||||
SourceText = effect.SourceText
|
||||
};
|
||||
|
||||
private static CriticalBranch CreateBranchEntity(ParsedCriticalBranch branch) =>
|
||||
new()
|
||||
{
|
||||
BranchKind = branch.BranchKind,
|
||||
ConditionKey = branch.ConditionKey,
|
||||
ConditionText = branch.ConditionText,
|
||||
ConditionJson = "{}",
|
||||
RawText = branch.RawText,
|
||||
DescriptionText = branch.DescriptionText,
|
||||
RawAffixText = branch.RawAffixText,
|
||||
ParsedJson = SerializeParsedEffects(branch.Effects),
|
||||
SortOrder = branch.SortOrder,
|
||||
Effects = branch.Effects
|
||||
.Select(CreateEffectEntity)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
private static string SerializeParsedEffects(IReadOnlyList<ParsedCriticalEffect> effects) =>
|
||||
effects.Count == 0
|
||||
? "{}"
|
||||
@@ -211,4 +384,13 @@ public sealed class CriticalImportLoader(string databasePath)
|
||||
effects.Count > 0 || branches.Any(branch => branch.Effects.Count > 0)
|
||||
? "partial"
|
||||
: "raw";
|
||||
|
||||
private static string? NormalizeOptionalText(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private static string CreateResultKey(string? groupKey, string columnKey, string rollBandLabel) =>
|
||||
$"{NormalizeKey(groupKey)}|{NormalizeKey(columnKey)}|{NormalizeKey(rollBandLabel)}";
|
||||
|
||||
private static string NormalizeKey(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user