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
@@ -176,12 +176,23 @@ public sealed class BaseItemRepository
/// <inheritdoc />
public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
{
return GetItemIdsListAsync(filter, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc />
public async Task<IReadOnlyList<Guid>> GetItemIdsListAsync(InternalItemsQuery filter, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filter);
PrepareFilterQuery(filter);
using var context = _dbProvider.CreateDbContext();
return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, filter).Select(e => e.Id).ToArray();
await using var context = _dbProvider.CreateDbContext();
return await ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, filter)
.Select(e => e.Id)
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
}
/// <inheritdoc />
@@ -252,11 +263,19 @@ public sealed class BaseItemRepository
/// <inheritdoc />
public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
{
return GetItemsAsync(filter, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc />
public async Task<QueryResult<BaseItemDto>> GetItemsAsync(InternalItemsQuery filter, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filter);
if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
{
var returnList = GetItemList(filter);
var returnList = await GetItemListAsync(filter, cancellationToken).ConfigureAwait(false);
return new QueryResult<BaseItemDto>(
filter.StartIndex,
returnList.Count,
@@ -266,7 +285,7 @@ public sealed class BaseItemRepository
PrepareFilterQuery(filter);
var result = new QueryResult<BaseItemDto>();
using var context = _dbProvider.CreateDbContext();
await using var context = _dbProvider.CreateDbContext();
IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
@@ -275,24 +294,42 @@ public sealed class BaseItemRepository
if (filter.EnableTotalRecordCount)
{
result.TotalRecordCount = dbQuery.Count();
result.TotalRecordCount = await dbQuery
.CountAsync(cancellationToken)
.ConfigureAwait(false);
}
dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
var items = await dbQuery
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
result.Items = items
.Where(e => e is not null)
.Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
.Where(dto => dto is not null)
.ToArray()!;
result.StartIndex = filter.StartIndex ?? 0;
return result;
}
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
{
return GetItemListAsync(filter, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc />
public async Task<IReadOnlyList<BaseItemDto>> GetItemListAsync(InternalItemsQuery filter, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filter);
PrepareFilterQuery(filter);
using var context = _dbProvider.CreateDbContext();
await using var context = _dbProvider.CreateDbContext();
IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
dbQuery = TranslateQuery(dbQuery, context, filter);
@@ -303,14 +340,21 @@ public sealed class BaseItemRepository
var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
if (hasRandomSort)
{
var orderedIds = dbQuery.Select(e => e.Id).ToList();
var orderedIds = await dbQuery
.Select(e => e.Id)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
if (orderedIds.Count == 0)
{
return Array.Empty<BaseItemDto>();
}
var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter)
.AsEnumerable()
var items = await ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var itemsById = items
.Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
.Where(dto => dto is not null)
.ToDictionary(i => i!.Id);
@@ -320,7 +364,15 @@ public sealed class BaseItemRepository
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
var results = await dbQuery
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return results
.Where(e => e is not null)
.Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
.Where(dto => dto is not null)
.ToArray()!;
}
/// <inheritdoc/>
@@ -496,30 +548,47 @@ public sealed class BaseItemRepository
/// <inheritdoc/>
public int GetCount(InternalItemsQuery filter)
{
ArgumentNullException.ThrowIfNull(filter);
// Hack for right now since we currently don't support filtering out these duplicates within a query
PrepareFilterQuery(filter);
using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
return dbQuery.Count();
return GetCountAsync(filter, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc />
public ItemCounts GetItemCounts(InternalItemsQuery filter)
/// <inheritdoc/>
public async Task<int> GetCountAsync(InternalItemsQuery filter, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filter);
// Hack for right now since we currently don't support filtering out these duplicates within a query
PrepareFilterQuery(filter);
using var context = _dbProvider.CreateDbContext();
await using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
var counts = dbQuery
return await dbQuery.CountAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public ItemCounts GetItemCounts(InternalItemsQuery filter)
{
return GetItemCountsAsync(filter, CancellationToken.None)
.GetAwaiter()
.GetResult();
}
/// <inheritdoc />
public async Task<ItemCounts> GetItemCountsAsync(InternalItemsQuery filter, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filter);
// Hack for right now since we currently don't support filtering out these duplicates within a query
PrepareFilterQuery(filter);
await using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
var counts = await dbQuery
.GroupBy(x => x.Type)
.Select(x => new { x.Key, Count = x.Count() })
.ToArray();
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
var lookup = _itemTypeLookup.BaseItemKindNames;
var result = new ItemCounts();