diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 925f6cbe..e59d91b2 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -127,65 +127,69 @@ public sealed class BaseItemRepository var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using (transaction.ConfigureAwait(false)) + var executionStrategy = context.Database.CreateExecutionStrategy(); + await executionStrategy.ExecuteAsync(async () => { - var date = (DateTime?)DateTime.UtcNow; + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + var date = (DateTime?)DateTime.UtcNow; - await EnsurePlaceholderItemAsync(context, cancellationToken).ConfigureAwait(false); + await EnsurePlaceholderItemAsync(context, cancellationToken).ConfigureAwait(false); - var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray(); + var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray(); - // Remove any UserData entries for the placeholder item that would conflict with the UserData - // being detached from the item being deleted. This is necessary because, during an update, - // UserData may be reattached to a new entry, but some entries can be left behind. - // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder. - await context.UserData - .Join( - context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId), - placeholder => new { placeholder.UserId, placeholder.CustomDataKey }, - userData => new { userData.UserId, userData.CustomDataKey }, - (placeholder, userData) => placeholder) - .Where(e => e.ItemId == PlaceholderId) - .ExecuteDeleteAsync(cancellationToken) - .ConfigureAwait(false); + // Remove any UserData entries for the placeholder item that would conflict with the UserData + // being detached from the item being deleted. This is necessary because, during an update, + // UserData may be reattached to a new entry, but some entries can be left behind. + // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder. + await context.UserData + .Join( + context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId), + placeholder => new { placeholder.UserId, placeholder.CustomDataKey }, + userData => new { userData.UserId, userData.CustomDataKey }, + (placeholder, userData) => placeholder) + .Where(e => e.ItemId == PlaceholderId) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); - // Detach all user watch data - await context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId) - .ExecuteUpdateAsync( - e => e - .SetProperty(f => f.RetentionDate, date) - .SetProperty(f => f.ItemId, PlaceholderId), - cancellationToken) - .ConfigureAwait(false); + // Detach all user watch data + await context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.RetentionDate, date) + .SetProperty(f => f.ItemId, PlaceholderId), + cancellationToken) + .ConfigureAwait(false); - await context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - var query = await context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId) - .Select(f => f.PeopleId) - .Distinct() - .ToArrayAsync(cancellationToken) - .ConfigureAwait(false); - await context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } + await context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + var query = await context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId) + .Select(f => f.PeopleId) + .Distinct() + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); + await context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + }).ConfigureAwait(false); } } catch (Npgsql.PostgresException ex) when (ex.SqlState == "40P01") // Deadlock detected @@ -1158,10 +1162,10 @@ public sealed class BaseItemRepository foreach (var provider in providersToUpsert) { await context.Database.ExecuteSqlAsync( - $@"INSERT INTO library.""BaseItemProviders"" (""ItemId"", ""ProviderId"", ""ProviderValue"") + $@"INSERT INTO library.base_item_providers (item_id, provider_id, provider_value) VALUES ({entity.Id}, {provider.ProviderId}, {provider.ProviderValue}) - ON CONFLICT (""ItemId"", ""ProviderId"") - DO UPDATE SET ""ProviderValue"" = EXCLUDED.""ProviderValue""", + ON CONFLICT (item_id, provider_id) + DO UPDATE SET provider_value = EXCLUDED.provider_value", cancellationToken) .ConfigureAwait(false); } @@ -1202,14 +1206,14 @@ public sealed class BaseItemRepository if (tvExtras is not null) { await context.Database.ExecuteSqlAsync( - $@"INSERT INTO library.""BaseItemTvExtras"" (""ItemId"", ""SeriesId"", ""SeasonId"", ""SeriesName"", ""SeasonName"", ""SeriesPresentationUniqueKey"") + $@"INSERT INTO library.base_item_tv_extras (item_id, series_id, season_id, series_name, season_name, series_presentation_unique_key) VALUES ({extItemId}, {tvExtras.SeriesId}, {tvExtras.SeasonId}, {tvExtras.SeriesName}, {tvExtras.SeasonName}, {tvExtras.SeriesPresentationUniqueKey}) - ON CONFLICT (""ItemId"") DO UPDATE SET - ""SeriesId"" = EXCLUDED.""SeriesId"", - ""SeasonId"" = EXCLUDED.""SeasonId"", - ""SeriesName"" = EXCLUDED.""SeriesName"", - ""SeasonName"" = EXCLUDED.""SeasonName"", - ""SeriesPresentationUniqueKey"" = EXCLUDED.""SeriesPresentationUniqueKey""", + ON CONFLICT (item_id) DO UPDATE SET + series_id = EXCLUDED.series_id, + season_id = EXCLUDED.season_id, + series_name = EXCLUDED.series_name, + season_name = EXCLUDED.season_name, + series_presentation_unique_key = EXCLUDED.series_presentation_unique_key", cancellationToken).ConfigureAwait(false); } else @@ -1222,16 +1226,16 @@ public sealed class BaseItemRepository if (liveTvExtras is not null) { await context.Database.ExecuteSqlAsync( - $@"INSERT INTO library.""BaseItemLiveTvExtras"" (""ItemId"", ""StartDate"", ""EndDate"", ""EpisodeTitle"", ""ShowId"", ""ExternalSeriesId"", ""ExternalServiceId"", ""Audio"") + $@"INSERT INTO library.base_item_live_tv_extras (item_id, start_date, end_date, episode_title, show_id, external_series_id, external_service_id, audio) VALUES ({extItemId}, {liveTvExtras.StartDate}, {liveTvExtras.EndDate}, {liveTvExtras.EpisodeTitle}, {liveTvExtras.ShowId}, {liveTvExtras.ExternalSeriesId}, {liveTvExtras.ExternalServiceId}, {(int?)liveTvExtras.Audio}) - ON CONFLICT (""ItemId"") DO UPDATE SET - ""StartDate"" = EXCLUDED.""StartDate"", - ""EndDate"" = EXCLUDED.""EndDate"", - ""EpisodeTitle"" = EXCLUDED.""EpisodeTitle"", - ""ShowId"" = EXCLUDED.""ShowId"", - ""ExternalSeriesId"" = EXCLUDED.""ExternalSeriesId"", - ""ExternalServiceId"" = EXCLUDED.""ExternalServiceId"", - ""Audio"" = EXCLUDED.""Audio""", + ON CONFLICT (item_id) DO UPDATE SET + start_date = EXCLUDED.start_date, + end_date = EXCLUDED.end_date, + episode_title = EXCLUDED.episode_title, + show_id = EXCLUDED.show_id, + external_series_id = EXCLUDED.external_series_id, + external_service_id = EXCLUDED.external_service_id, + audio = EXCLUDED.audio", cancellationToken).ConfigureAwait(false); } else @@ -1244,14 +1248,14 @@ public sealed class BaseItemRepository if (audioExtras is not null) { await context.Database.ExecuteSqlAsync( - $@"INSERT INTO library.""BaseItemAudioExtras"" (""ItemId"", ""Album"", ""Artists"", ""AlbumArtists"", ""LUFS"", ""NormalizationGain"") + $@"INSERT INTO library.base_item_audio_extras (item_id, album, artists, album_artists, lufs, normalization_gain) VALUES ({extItemId}, {audioExtras.Album}, {audioExtras.Artists}, {audioExtras.AlbumArtists}, {audioExtras.LUFS}, {audioExtras.NormalizationGain}) - ON CONFLICT (""ItemId"") DO UPDATE SET - ""Album"" = EXCLUDED.""Album"", - ""Artists"" = EXCLUDED.""Artists"", - ""AlbumArtists"" = EXCLUDED.""AlbumArtists"", - ""LUFS"" = EXCLUDED.""LUFS"", - ""NormalizationGain"" = EXCLUDED.""NormalizationGain""", + ON CONFLICT (item_id) DO UPDATE SET + album = EXCLUDED.album, + artists = EXCLUDED.artists, + album_artists = EXCLUDED.album_artists, + lufs = EXCLUDED.lufs, + normalization_gain = EXCLUDED.normalization_gain", cancellationToken).ConfigureAwait(false); } else diff --git a/DATABASE_MISMATCH_DETAILED_ENTITY_MAP.md b/docs/DATABASE_MISMATCH_DETAILED_ENTITY_MAP.md similarity index 100% rename from DATABASE_MISMATCH_DETAILED_ENTITY_MAP.md rename to docs/DATABASE_MISMATCH_DETAILED_ENTITY_MAP.md diff --git a/DATABASE_MISMATCH_VISUAL_COMPARISON.md b/docs/DATABASE_MISMATCH_VISUAL_COMPARISON.md similarity index 100% rename from DATABASE_MISMATCH_VISUAL_COMPARISON.md rename to docs/DATABASE_MISMATCH_VISUAL_COMPARISON.md diff --git a/DATABASE_SCHEMA_MISMATCH_REPORT.md b/docs/DATABASE_SCHEMA_MISMATCH_REPORT.md similarity index 100% rename from DATABASE_SCHEMA_MISMATCH_REPORT.md rename to docs/DATABASE_SCHEMA_MISMATCH_REPORT.md diff --git a/SCHEMA_COLUMN_CONVERSION_REFERENCE.md b/docs/SCHEMA_COLUMN_CONVERSION_REFERENCE.md similarity index 100% rename from SCHEMA_COLUMN_CONVERSION_REFERENCE.md rename to docs/SCHEMA_COLUMN_CONVERSION_REFERENCE.md diff --git a/SCHEMA_CONVERSION_GUIDE.md b/docs/SCHEMA_CONVERSION_GUIDE.md similarity index 100% rename from SCHEMA_CONVERSION_GUIDE.md rename to docs/SCHEMA_CONVERSION_GUIDE.md