Add automatic PostgreSQL database creation and migration

- Added Jellyfin.Database.Providers.Postgres project with full EF Core migration support for PostgreSQL.
- Implemented automatic database creation and privilege assignment on startup.
- Generated initial migration and model snapshot for PostgreSQL schema.
- Updated build, test, and dependency files to include PostgreSQL provider and Npgsql packages.
- Added PowerShell script for generating and testing PostgreSQL migrations.
- No changes to application logic outside database provider/migration infrastructure.
This commit is contained in:
2026-02-22 18:51:24 -05:00
parent 0efab2cfb6
commit a3eb4b1b57
52 changed files with 9233 additions and 33 deletions
@@ -43,6 +43,8 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
private static ReaderWriterLockSlim DatabaseLock { get; } = new(LockRecursionPolicy.SupportsRecursion);
private static AsyncLocal<int> WriteLockDepth { get; } = new();
/// <inheritdoc/>
public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
{
@@ -83,56 +85,114 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result)
{
DbLock.BeginWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth == 0)
{
DbLock.BeginWriteLock(this._logger);
WriteLockDepth.Value = currentDepth + 1;
}
return base.TransactionStarting(connection, eventData, result);
}
public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result, CancellationToken cancellationToken = default)
{
DbLock.BeginWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth == 0)
{
DbLock.BeginWriteLock(this._logger);
WriteLockDepth.Value = currentDepth + 1;
}
return base.TransactionStartingAsync(connection, eventData, result, cancellationToken);
}
public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData)
{
DbLock.EndWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
WriteLockDepth.Value = currentDepth - 1;
if (currentDepth == 1)
{
DbLock.EndWriteLock(this._logger);
}
}
base.TransactionCommitted(transaction, eventData);
}
public override Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, CancellationToken cancellationToken = default)
{
DbLock.EndWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
WriteLockDepth.Value = currentDepth - 1;
if (currentDepth == 1)
{
DbLock.EndWriteLock(this._logger);
}
}
return base.TransactionCommittedAsync(transaction, eventData, cancellationToken);
}
public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData)
{
DbLock.EndWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
WriteLockDepth.Value = currentDepth - 1;
if (currentDepth == 1)
{
DbLock.EndWriteLock(this._logger);
}
}
base.TransactionFailed(transaction, eventData);
}
public override Task TransactionFailedAsync(DbTransaction transaction, TransactionErrorEventData eventData, CancellationToken cancellationToken = default)
{
DbLock.EndWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
WriteLockDepth.Value = currentDepth - 1;
if (currentDepth == 1)
{
DbLock.EndWriteLock(this._logger);
}
}
return base.TransactionFailedAsync(transaction, eventData, cancellationToken);
}
public override void TransactionRolledBack(DbTransaction transaction, TransactionEndEventData eventData)
{
DbLock.EndWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
WriteLockDepth.Value = currentDepth - 1;
if (currentDepth == 1)
{
DbLock.EndWriteLock(this._logger);
}
}
base.TransactionRolledBack(transaction, eventData);
}
public override Task TransactionRolledBackAsync(DbTransaction transaction, TransactionEndEventData eventData, CancellationToken cancellationToken = default)
{
DbLock.EndWriteLock(this._logger);
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
WriteLockDepth.Value = currentDepth - 1;
if (currentDepth == 1)
{
DbLock.EndWriteLock(this._logger);
}
}
return base.TransactionRolledBackAsync(transaction, eventData, cancellationToken);
}
@@ -206,6 +266,7 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
private readonly Action? _action;
private bool _disposed;
private bool _lockAcquired;
public DbLock(Action? action = null)
{
@@ -217,17 +278,23 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
#pragma warning restore IDISP015 // Member should not return created and cached instance
{
logger.LogTrace("Enter Write for {Caller}:{Line}", callerMemberName, callerNo);
if (DatabaseLock.IsWriteLockHeld)
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
logger.LogTrace("Write Already Held (Depth: {Depth}) {Caller}:{Line}", currentDepth, callerMemberName, callerNo);
return _noLock;
}
BeginWriteLock(logger, command, callerMemberName, callerNo);
WriteLockDepth.Value = currentDepth + 1;
return new DbLock(() =>
{
WriteLockDepth.Value = Math.Max(0, WriteLockDepth.Value - 1);
EndWriteLock(logger, callerMemberName, callerNo);
});
})
{
_lockAcquired = true
};
}
#pragma warning disable IDISP015 // Member should not return created and cached instance
@@ -235,9 +302,10 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
#pragma warning restore IDISP015 // Member should not return created and cached instance
{
logger.LogTrace("Enter Read {Caller}:{Line}", callerMemberName, callerNo);
if (DatabaseLock.IsWriteLockHeld)
var currentDepth = WriteLockDepth.Value;
if (currentDepth > 0)
{
logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
logger.LogTrace("Write Already Held (Depth: {Depth}) {Caller}:{Line}", currentDepth, callerMemberName, callerNo);
return _noLock;
}
@@ -245,7 +313,10 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
return new DbLock(() =>
{
ExitReadLock(logger, callerMemberName, callerNo);
});
})
{
_lockAcquired = true
};
}
public static void BeginWriteLock(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
@@ -299,7 +370,7 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
}
this._disposed = true;
if (this._action is not null)
if (this._lockAcquired && this._action is not null)
{
this._action();
}