Refactor PostgreSQL provider: multi-schema & async prep

- Refactor migrations and provider to use multiple PostgreSQL schemas, each matching a legacy SQLite database (activitylog, authentication, displaypreferences, library, users).
- All tables, foreign keys, and indexes are now schema-qualified; Down migration drops tables by schema.
- Provider ensures schemas exist before migrations; entities are mapped to correct schemas in OnModelCreating.
- Add support for max-pool-size, min-pool-size, and multiplexing connection options; update logging accordingly.
- VACUUM ANALYZE now runs per schema during scheduled optimization.
- TruncateAllTablesAsync now truncates tables with schema qualification.
- README updated with schema structure, new options, and multiplexing warnings.
- CacheDecorator now calls async repository methods using .GetAwaiter().GetResult(), with documentation.
- Lays groundwork for full async/await and multiplexing support in the database layer.
This commit is contained in:
2026-02-23 09:38:22 -05:00
parent ede6904433
commit 86883cd5c6
51 changed files with 6719 additions and 187 deletions
@@ -8,6 +8,8 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
@@ -37,7 +39,15 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
/// <inheritdoc/>
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter)
{
using var context = _dbProvider.CreateDbContext();
return GetPeopleAsync(filter, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc/>
public async Task<IReadOnlyList<PersonInfo>> GetPeopleAsync(InternalPeopleQuery filter, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter);
// Include PeopleBaseItemMap
@@ -58,26 +68,49 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
dbQuery = dbQuery.Take(filter.Limit);
}
return dbQuery.AsEnumerable().Select(Map).ToArray();
var results = await dbQuery
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
return results.Select(Map).ToArray();
}
/// <inheritdoc/>
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter)
{
using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
return GetPeopleNamesAsync(filter, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc/>
public async Task<IReadOnlyList<string>> GetPeopleNamesAsync(InternalPeopleQuery filter, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter)
.Select(e => e.Name)
.Distinct();
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
if (filter.Limit > 0)
{
dbQuery = dbQuery.Take(filter.Limit);
}
return dbQuery.ToArray();
return await dbQuery
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
}
/// <inheritdoc />
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
{
UpdatePeopleAsync(itemId, people, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc />
public async Task UpdatePeopleAsync(Guid itemId, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken = default)
{
foreach (var person in people)
{
@@ -89,27 +122,35 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray();
var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray();
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
var existingPersons = context.Peoples.Select(e => new
await using var context = _dbProvider.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
var existingPersons = await context.Peoples
.Select(e => new
{
item = e,
SelectionKey = e.Name + "-" + e.PersonType
})
.Where(p => personKeys.Contains(p.SelectionKey))
.Select(f => f.item)
.ToArray();
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
var toAdd = people
.Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
.Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
.Select(Map);
context.Peoples.AddRange(toAdd);
context.SaveChanges();
await context.Peoples.AddRangeAsync(toAdd, cancellationToken).ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
var personsEntities = toAdd.Concat(existingPersons).ToArray();
var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList();
var existingMaps = await context.PeopleBaseItemMap
.Include(e => e.People)
.Where(e => e.ItemId == itemId)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var listOrder = 0;
@@ -124,7 +165,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
if (existingMap is null)
{
context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
await context.PeopleBaseItemMap.AddAsync(new PeopleBaseItemMap()
{
Item = null!,
ItemId = itemId,
@@ -133,7 +174,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
ListOrder = listOrder,
SortOrder = person.SortOrder,
Role = person.Role
});
}, cancellationToken).ConfigureAwait(false);
}
else
{
@@ -149,8 +190,8 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
context.PeopleBaseItemMap.RemoveRange(existingMaps);
context.SaveChanges();
transaction.Commit();
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
private PersonInfo Map(People people)