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
@@ -97,6 +97,94 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
}
}
/// <summary>
/// Ensures the PostgreSQL database exists, creating it if necessary.
/// </summary>
/// <param name="databaseConfiguration">The database configuration.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task EnsureDatabaseExistsAsync(DatabaseConfigurationOptions databaseConfiguration, CancellationToken cancellationToken = default)
{
static T? GetOption<T>(ICollection<CustomDatabaseOption>? options, string key, Func<string, T> converter, Func<T>? defaultValue = null)
{
if (options is null)
{
return defaultValue is not null ? defaultValue() : default;
}
var value = options.FirstOrDefault(e => e.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (value is null)
{
return defaultValue is not null ? defaultValue() : default;
}
return converter(value.Value);
}
var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
var host = GetOption(customOptions, "host", e => e, () => "localhost")!;
var port = GetOption(customOptions, "port", int.Parse, () => 5432);
var database = GetOption(customOptions, "database", e => e, () => "jellyfin")!;
var username = GetOption(customOptions, "username", e => e, () => "jellyfin")!;
var password = GetOption(customOptions, "password", e => e, () => string.Empty)!;
var timeout = GetOption(customOptions, "connection-timeout", int.Parse, () => 15);
// Connect to 'postgres' maintenance database to check if target database exists
var maintenanceConnectionBuilder = new NpgsqlConnectionStringBuilder
{
Host = host,
Port = port,
Database = "postgres", // Connect to maintenance database
Username = username,
Password = password,
Timeout = timeout,
Pooling = false // Don't pool maintenance connections
};
try
{
await using var connection = new NpgsqlConnection(maintenanceConnectionBuilder.ToString());
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
// Check if database exists
var checkQuery = "SELECT 1 FROM pg_database WHERE datname = @databaseName";
await using var checkCommand = new NpgsqlCommand(checkQuery, connection);
checkCommand.Parameters.AddWithValue("databaseName", database);
var exists = await checkCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
if (exists is null)
{
// Database doesn't exist, create it
logger.LogInformation("PostgreSQL database '{Database}' does not exist. Creating...", database);
// Use identifier quoting for safety
var createQuery = $"CREATE DATABASE \"{database}\" OWNER \"{username}\"";
await using var createCommand = new NpgsqlCommand(createQuery, connection);
await createCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("PostgreSQL database '{Database}' created successfully", database);
// Grant privileges
var grantQuery = $"GRANT ALL PRIVILEGES ON DATABASE \"{database}\" TO \"{username}\"";
await using var grantCommand = new NpgsqlCommand(grantQuery, connection);
await grantCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Granted all privileges on database '{Database}' to user '{Username}'", database, username);
}
else
{
logger.LogInformation("PostgreSQL database '{Database}' already exists", database);
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to ensure PostgreSQL database '{Database}' exists. Error: {Message}", database, ex.Message);
throw;
}
}
/// <inheritdoc/>
public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
{