Files
pgsql-jellyfin/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
T
wjones 3e5d29225a Refactor SQLite Database Provider
- Removed unused classes and files related to SQLite database provider, including SqliteDesignTimeJellyfinDbFactory, ModelBuilderExtensions, PragmaConnectionInterceptor, AssemblyInfo, SqliteDatabaseProvider, DateTimeKindValueConverter.
- Updated tests to remove dependencies on removed classes and adjusted mocking for configuration sections.
- Added Microsoft.EntityFrameworkCore.Sqlite package reference to test project.
- Improved string handling in tests for better consistency and clarity.
- Refactored logging methods in JellyfinApplicationFactory for better readability and maintainability.
2026-05-03 09:39:00 -04:00

461 lines
17 KiB
C#

// <copyright file="JellyfinDbContext.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable SA1202
namespace Jellyfin.Database.Implementations;
using System;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
using Jellyfin.Database.Implementations.Entities.Views;
using Jellyfin.Database.Implementations.Interfaces;
using Jellyfin.Database.Implementations.Locking;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
/// <inheritdoc/>
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
/// </summary>
/// <param name="options">The database context options.</param>
/// <param name="logger">Logger.</param>
/// <param name="jellyfinDatabaseProvider">The provider for the database engine specific operations.</param>
/// <param name="entityFrameworkCoreLocking">The locking behavior.</param>
public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger, IJellyfinDatabaseProvider jellyfinDatabaseProvider, IEntityFrameworkCoreLockingBehavior entityFrameworkCoreLocking) : DbContext(options)
{
private static readonly Action<ILogger, Exception?> SaveChangesError =
LoggerMessage.Define(
LogLevel.Error,
new EventId(1, nameof(SaveChanges)),
"Error trying to save changes.");
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
/// </summary>
public DbSet<AccessSchedule> AccessSchedules => this.Set<AccessSchedule>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the activity logs.
/// </summary>
public DbSet<ActivityLog> ActivityLogs => this.Set<ActivityLog>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the API keys.
/// </summary>
public DbSet<ApiKey> ApiKeys => this.Set<ApiKey>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the devices.
/// </summary>
public DbSet<Device> Devices => this.Set<Device>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the device options.
/// </summary>
public DbSet<DeviceOptions> DeviceOptions => this.Set<DeviceOptions>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the display preferences.
/// </summary>
public DbSet<DisplayPreferences> DisplayPreferences => this.Set<DisplayPreferences>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the image infos.
/// </summary>
public DbSet<ImageInfo> ImageInfos => this.Set<ImageInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the item display preferences.
/// </summary>
public DbSet<ItemDisplayPreferences> ItemDisplayPreferences => this.Set<ItemDisplayPreferences>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the custom item display preferences.
/// </summary>
public DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences => this.Set<CustomItemDisplayPreferences>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the permissions.
/// </summary>
public DbSet<Permission> Permissions => this.Set<Permission>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the preferences.
/// </summary>
public DbSet<Preference> Preferences => this.Set<Preference>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the users.
/// </summary>
public DbSet<User> Users => this.Set<User>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
/// </summary>
public DbSet<TrickplayInfo> TrickplayInfos => this.Set<TrickplayInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the media segments.
/// </summary>
public DbSet<MediaSegment> MediaSegments => this.Set<MediaSegment>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<UserData> UserData => this.Set<UserData>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<AncestorId> AncestorIds => this.Set<AncestorId>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<AttachmentStreamInfo> AttachmentStreamInfos => this.Set<AttachmentStreamInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<BaseItemEntity> BaseItems => this.Set<BaseItemEntity>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<Chapter> Chapters => this.Set<Chapter>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<ItemValue> ItemValues => this.Set<ItemValue>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<ItemValueMap> ItemValuesMap => this.Set<ItemValueMap>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<MediaStreamInfo> MediaStreamInfos => this.Set<MediaStreamInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing audio-specific stream details.
/// </summary>
public DbSet<AudioStreamDetails> AudioStreamDetails => this.Set<AudioStreamDetails>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing video-specific stream details.
/// </summary>
public DbSet<VideoStreamDetails> VideoStreamDetails => this.Set<VideoStreamDetails>();
// ── Read-only view projections (PostgreSQL only) ──────────────────────
/// <summary>
/// Gets the flat view over MediaStreamInfos joined with audio and video detail tables.
/// Maps to library."MediaStreamInfos_v".
/// </summary>
public DbSet<MediaStreamInfoView> MediaStreamInfoViews => this.Set<MediaStreamInfoView>();
/// <summary>
/// Gets the view of video items joined with their primary stream details.
/// Maps to library."VideoItems_v".
/// </summary>
public DbSet<VideoItemView> VideoItemViews => this.Set<VideoItemView>();
/// <summary>
/// Gets the view of per-user watch history with calculated progress.
/// Maps to library."UserPlaybackHistory_v".
/// </summary>
public DbSet<UserPlaybackHistoryView> UserPlaybackHistoryViews => this.Set<UserPlaybackHistoryView>();
/// <summary>
/// Gets the view of items with pivoted external provider IDs.
/// Maps to library."ItemProviders_v".
/// </summary>
public DbSet<ItemProviderView> ItemProviderViews => this.Set<ItemProviderView>();
/// <summary>
/// Gets the view of cast and crew entries linked to their items.
/// Maps to library."ItemPeople_v".
/// </summary>
public DbSet<ItemPersonView> ItemPersonViews => this.Set<ItemPersonView>();
/// <summary>
/// Gets the aggregate library statistics view, one row per item type.
/// Maps to library."LibrarySummary_v".
/// </summary>
public DbSet<LibrarySummaryView> LibrarySummaryViews => this.Set<LibrarySummaryView>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<People> Peoples => this.Set<People>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => this.Set<PeopleBaseItemMap>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
/// </summary>
public DbSet<BaseItemProvider> BaseItemProviders => this.Set<BaseItemProvider>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<BaseItemImageInfo> BaseItemImageInfos => this.Set<BaseItemImageInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<BaseItemMetadataField> BaseItemMetadataFields => this.Set<BaseItemMetadataField>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => this.Set<BaseItemTrailerType>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<KeyframeData> KeyframeData => this.Set<KeyframeData>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing persisted library options.
/// </summary>
public DbSet<LibraryOptionsEntity> LibraryOptions => this.Set<LibraryOptionsEntity>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing TV-specific extension data for BaseItems.
/// </summary>
public DbSet<BaseItemTvExtras> BaseItemTvExtras => this.Set<BaseItemTvExtras>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing LiveTV-specific extension data for BaseItems.
/// </summary>
public DbSet<BaseItemLiveTvExtras> BaseItemLiveTvExtras => this.Set<BaseItemLiveTvExtras>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing music-specific extension data for BaseItems.
/// </summary>
public DbSet<BaseItemAudioExtras> BaseItemAudioExtras => this.Set<BaseItemAudioExtras>();
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
public DbSet<BookMetadata> BookMetadata => Set<BookMetadata>();
public DbSet<Chapter> Chapters => Set<Chapter>();
public DbSet<Collection> Collections => Set<Collection>();
public DbSet<CollectionItem> CollectionItems => Set<CollectionItem>();
public DbSet<Company> Companies => Set<Company>();
public DbSet<CompanyMetadata> CompanyMetadata => Set<CompanyMetadata>();
public DbSet<CustomItem> CustomItems => Set<CustomItem>();
public DbSet<CustomItemMetadata> CustomItemMetadata => Set<CustomItemMetadata>();
public DbSet<Episode> Episodes => Set<Episode>();
public DbSet<EpisodeMetadata> EpisodeMetadata => Set<EpisodeMetadata>();
public DbSet<Genre> Genres => Set<Genre>();
public DbSet<Group> Groups => Set<Groups>();
public DbSet<Library> Libraries => Set<Library>();
public DbSet<LibraryItem> LibraryItems => Set<LibraryItems>();
public DbSet<LibraryRoot> LibraryRoot => Set<LibraryRoot>();
public DbSet<MediaFile> MediaFiles => Set<MediaFiles>();
public DbSet<MediaFileStream> MediaFileStream => Set<MediaFileStream>();
public DbSet<Metadata> Metadata => Set<Metadata>();
public DbSet<MetadataProvider> MetadataProviders => Set<MetadataProvider>();
public DbSet<MetadataProviderId> MetadataProviderIds => Set<MetadataProviderId>();
public DbSet<Movie> Movies => Set<Movie>();
public DbSet<MovieMetadata> MovieMetadata => Set<MovieMetadata>();
public DbSet<MusicAlbum> MusicAlbums => Set<MusicAlbum>();
public DbSet<MusicAlbumMetadata> MusicAlbumMetadata => Set<MusicAlbumMetadata>();
public DbSet<Person> People => Set<Person>();
public DbSet<PersonRole> PersonRoles => Set<PersonRole>();
public DbSet<Photo> Photo => Set<Photo>();
public DbSet<PhotoMetadata> PhotoMetadata => Set<PhotoMetadata>();
public DbSet<ProviderMapping> ProviderMappings => Set<ProviderMapping>();
public DbSet<Rating> Ratings => Set<Rating>();
/// <summary>
/// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
/// store review ratings, not age ratings.
/// </summary>
public DbSet<RatingSource> RatingSources => Set<RatingSource>();
public DbSet<Release> Releases => Set<Release>();
public DbSet<Season> Seasons => Set<Season>();
public DbSet<SeasonMetadata> SeasonMetadata => Set<SeasonMetadata>();
public DbSet<Series> Series => Set<Series>();
public DbSet<SeriesMetadata> SeriesMetadata => Set<SeriesMetadata();
public DbSet<Track> Tracks => Set<Track>();
public DbSet<TrackMetadata> TrackMetadata => Set<TrackMetadata>();*/
/// <inheritdoc/>
public override async Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
this.HandleConcurrencyToken();
try
{
var result = -1;
await entityFrameworkCoreLocking.OnSaveChangesAsync(this, async () =>
{
result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
return result;
}
catch (DbUpdateException ex)
{
// Check if it's a constraint violation (works across all database providers)
var isConstraintViolation = ex.InnerException?.GetType().Name.Contains("Exception", StringComparison.Ordinal) == true &&
(ex.Message.Contains("constraint", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("duplicate", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("unique", StringComparison.OrdinalIgnoreCase));
if (isConstraintViolation)
{
// Log constraint violations as warnings (expected in some concurrent scenarios)
#pragma warning disable CA1848 // Use LoggerMessage delegates - exception type is dynamic
logger.LogWarning(
ex,
"Database constraint violation: Attempted to insert or update data that violates a database constraint. " +
"This may indicate a concurrency issue or a bug in the update logic. " +
"Inner exception: {InnerExceptionType}",
ex.InnerException?.GetType().Name ?? "unknown");
#pragma warning restore CA1848
}
else
{
// Other DbUpdateExceptions are true errors
SaveChangesError(logger, ex);
}
throw;
}
catch (Exception e)
{
SaveChangesError(logger, e);
throw;
}
}
/// <inheritdoc/>
public override int SaveChanges(bool acceptAllChangesOnSuccess) // SaveChanges(bool) is beeing called by SaveChanges() with default to false.
{
this.HandleConcurrencyToken();
try
{
var result = -1;
entityFrameworkCoreLocking.OnSaveChanges(this, () =>
{
result = base.SaveChanges(acceptAllChangesOnSuccess);
});
return result;
}
catch (Exception e)
{
SaveChangesError(logger, e);
throw;
}
}
private void HandleConcurrencyToken()
{
foreach (var saveEntity in this.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.Select(entry => entry.Entity)
.OfType<IHasConcurrencyToken>())
{
saveEntity.OnSavingChanges();
}
}
/// <summary>
/// Gets the recommended batch size for bulk operations based on the number of items to process.
/// Smaller batches during library scans reduce dead tuple generation in PostgreSQL.
/// </summary>
/// <param name="entityCount">The total number of entities being processed.</param>
/// <returns>The recommended batch size for optimal performance.</returns>
public int GetBatchSize(int entityCount)
{
// Smaller batches during media scans = fewer dead tuples
// < 100 items: batch 10
// 100-1000: batch 50
// 1000-10000: batch 100
// > 10000: batch 200
return entityCount switch
{
< 100 => 10,
< 1000 => 50,
< 10000 => 100,
_ => 200
};
}
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
jellyfinDatabaseProvider.OnModelCreating(modelBuilder);
base.OnModelCreating(modelBuilder);
ArgumentNullException.ThrowIfNull(modelBuilder);
// Configuration for each entity is in its own class inside 'ModelConfiguration'.
modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
}
/// <inheritdoc />
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
jellyfinDatabaseProvider.ConfigureConventions(configurationBuilder);
base.ConfigureConventions(configurationBuilder);
}
}