feat: implement migration tracking and history management for PostgreSQL

This commit is contained in:
2026-05-04 12:47:12 -04:00
parent e67c191843
commit 4ccd84342b
12 changed files with 191 additions and 192 deletions
@@ -1157,18 +1157,17 @@ public sealed class BaseItemRepository
.ConfigureAwait(false);
}
// UPSERT all providers using raw SQL with ON CONFLICT
// This is atomic and handles concurrent operations correctly
foreach (var provider in providersToUpsert)
{
await context.Database.ExecuteSqlAsync(
$@"INSERT INTO library.base_item_providers (item_id, provider_id, provider_value)
VALUES ({entity.Id}, {provider.ProviderId}, {provider.ProviderValue})
ON CONFLICT (item_id, provider_id)
DO UPDATE SET provider_value = EXCLUDED.provider_value",
cancellationToken)
.ConfigureAwait(false);
}
// UPSERT all providers in a single UNNEST-based INSERT instead of one round-trip per provider
var providerItemIds = providersToUpsert.Select(_ => entity.Id).ToArray();
var providerIds = providersToUpsert.Select(p => p.ProviderId).ToArray();
var providerValues = providersToUpsert.Select(p => p.ProviderValue).ToArray();
await context.Database.ExecuteSqlAsync(
$@"INSERT INTO library.base_item_providers (item_id, provider_id, provider_value)
SELECT * FROM UNNEST({providerItemIds}, {providerIds}, {providerValues})
ON CONFLICT (item_id, provider_id)
DO UPDATE SET provider_value = EXCLUDED.provider_value",
cancellationToken)
.ConfigureAwait(false);
// CRITICAL: Clear the navigation property so EF Core doesn't try to track/save these
entity.Provider = null;
@@ -1291,7 +1290,6 @@ public sealed class BaseItemRepository
Value = f.Value
}).ToArray();
context.ItemValues.AddRange(missingItemValues);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
var valueMap = itemValueMaps
@@ -1332,19 +1330,36 @@ public sealed class BaseItemRepository
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
// Hoist ancestor queries outside the per-item loop to avoid N×2 DB round-trips
var ancestorItemIds = tuples
.Where(t => t.Item.SupportsAncestors && t.AncestorIds != null)
.Select(t => t.Item.Id)
.ToArray();
var allNeededAncestorIds = tuples
.Where(t => t.Item.SupportsAncestors && t.AncestorIds != null)
.SelectMany(t => t.AncestorIds!)
.Distinct()
.ToArray();
var allExistingAncestorIds = ancestorItemIds.Length > 0
? await context.AncestorIds
.Where(e => ancestorItemIds.Contains(e.ItemId))
.ToListAsync(cancellationToken)
.ConfigureAwait(false)
: [];
var validAncestorIdSet = allNeededAncestorIds.Length > 0
? (await context.BaseItems
.Where(e => allNeededAncestorIds.Contains(e.Id))
.Select(f => f.Id)
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false)).ToHashSet()
: [];
foreach (var item in tuples)
{
if (item.Item.SupportsAncestors && item.AncestorIds != null)
{
var existingAncestorIds = await context.AncestorIds
.Where(e => e.ItemId == item.Item.Id)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var validAncestorIds = await context.BaseItems
.Where(e => item.AncestorIds.Contains(e.Id))
.Select(f => f.Id)
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
var existingAncestorIds = allExistingAncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
var validAncestorIds = item.AncestorIds.Where(validAncestorIdSet.Contains).ToArray();
foreach (var ancestorId in validAncestorIds)
{
var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);