Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>3.0.2.0</Version>
<AssemblyVersion>3.0.2.0</AssemblyVersion>
<FileVersion>3.0.2.0</FileVersion>
<Version>3.0.3.0</Version>
<AssemblyVersion>3.0.3.0</AssemblyVersion>
<FileVersion>3.0.3.0</FileVersion>
</PropertyGroup>
</Project>
95 changes: 95 additions & 0 deletions Jellyfin.Database.Providers.Postgres/Api/IPluginSchemaHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;

namespace Jellyfin.Database.Providers.Postgres.Api;

/// <summary>
/// Provides isolated PostgreSQL storage for a plugin-owned schema.
/// </summary>
/// <remarks>
/// This contract deliberately does not expose a connection string, an Npgsql connection, or Jellyfin's
/// <c>public</c> schema. Consumers must use Jellyfin's own contracts for Jellyfin data and use this host
/// only for state they own. Loaded plugins are trusted in-process code; this API is an isolation boundary
/// against accidental cross-plugin access, not a security boundary against malicious code.
/// </remarks>
public interface IPluginSchemaHost
{
/// <summary>
/// Opens a named private schema and binds it to its plugin owner.
/// </summary>
/// <param name="request">Schema name and immutable plugin owner.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An opaque handle for the plugin's schema.</returns>
Task<PluginSchema> OpenSchemaAsync(PluginSchemaRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Opens the generated private schema assigned to <paramref name="pluginId"/>, creating it when needed.
/// </summary>
/// <param name="pluginId">The immutable identifier of the consuming plugin.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An opaque handle for the plugin's schema.</returns>
Task<PluginSchema> OpenSchemaAsync(Guid pluginId, CancellationToken cancellationToken = default);

/// <summary>
/// Claims the immediate pre-registry schema matching the target plugin name and moves it to the
/// target private-schema name.
/// </summary>
/// <param name="request">The target schema name and immutable plugin owner.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The handle of the registered target schema.</returns>
/// <remarks>
/// The only legacy source accepted is the exact prefix of <paramref name="request"/>'s target
/// name: <c>name_guid</c> may claim only <c>name</c>. The target must not exist and the source must
/// be unregistered. This preserves the source tables by an atomic PostgreSQL schema rename.
/// </remarks>
Task<PluginSchema> MigratePreRegistrySchemaAsync(
PluginSchemaRequest request,
CancellationToken cancellationToken = default);

/// <summary>
/// Applies versioned DDL migrations in the schema represented by <paramref name="schema"/>.
/// </summary>
/// <param name="schema">Schema handle returned by <see cref="OpenSchemaAsync(PluginSchemaRequest, CancellationToken)"/>.</param>
/// <param name="migrations">Migrations to apply in their supplied order.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that completes after all migrations have been committed.</returns>
Task ApplyMigrationsAsync(
PluginSchema schema,
IReadOnlyList<PluginSchemaMigration> migrations,
CancellationToken cancellationToken = default);

/// <summary>
/// Executes one parameterized data command inside the plugin's schema.
/// </summary>
/// <param name="schema">Schema handle returned by <see cref="OpenSchemaAsync(PluginSchemaRequest, CancellationToken)"/>.</param>
/// <param name="sql">A single unqualified SQL statement.</param>
/// <param name="parameters">Parameters for <paramref name="sql"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The number of rows affected.</returns>
Task<int> ExecuteAsync(
PluginSchema schema,
string sql,
IReadOnlyList<PluginSqlParameter>? parameters = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Executes one parameterized query inside the plugin's schema and projects its rows without exposing
/// the underlying connection.
/// </summary>
/// <typeparam name="T">Projection result type.</typeparam>
/// <param name="schema">Schema handle returned by <see cref="OpenSchemaAsync(PluginSchemaRequest, CancellationToken)"/>.</param>
/// <param name="sql">A single unqualified SQL query.</param>
/// <param name="project">Synchronous projection of the open reader.</param>
/// <param name="parameters">Parameters for <paramref name="sql"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The projected result.</returns>
Task<T> QueryAsync<T>(
PluginSchema schema,
string sql,
Func<DbDataReader, T> project,
IReadOnlyList<PluginSqlParameter>? parameters = null,
CancellationToken cancellationToken = default);
}
22 changes: 22 additions & 0 deletions Jellyfin.Database.Providers.Postgres/Api/PluginSchema.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;

namespace Jellyfin.Database.Providers.Postgres.Api;

/// <summary>
/// Opaque reference to the private PostgreSQL schema assigned to a plugin.
/// </summary>
public sealed class PluginSchema
{
internal PluginSchema(Guid pluginId, string name)
{
PluginId = pluginId;
Name = name;
}

/// <summary>Gets the immutable identifier of the schema owner.</summary>
public Guid PluginId { get; }

/// <summary>Gets the generated schema name for diagnostics only.</summary>
/// <remarks>Consumers must not use this value to build SQL.</remarks>
public string Name { get; }
}
14 changes: 14 additions & 0 deletions Jellyfin.Database.Providers.Postgres/Api/PluginSchemaMigration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Jellyfin.Database.Providers.Postgres.Api;

/// <summary>
/// One immutable, plugin-owned schema migration.
/// </summary>
/// <param name="Id">Stable migration identifier, unique within the plugin.</param>
/// <param name="Sql">One unqualified DDL statement to execute.</param>
public sealed record PluginSchemaMigration(string Id, string Sql)
{
/// <summary>Gets a deterministic checksum used to detect modified applied migrations.</summary>
public string Checksum { get; } = PluginSchemaMigrationChecksum.Compute(Id, Sql);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Security.Cryptography;
using System.Text;

namespace Jellyfin.Database.Providers.Postgres.Api;

internal static class PluginSchemaMigrationChecksum
{
internal static string Compute(string id, string sql)
{
ArgumentNullException.ThrowIfNull(id);
ArgumentNullException.ThrowIfNull(sql);
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(string.Concat(id, "\n", sql)));
return Convert.ToHexString(bytes);
}
}
15 changes: 15 additions & 0 deletions Jellyfin.Database.Providers.Postgres/Api/PluginSchemaRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace Jellyfin.Database.Providers.Postgres.Api;

/// <summary>
/// Identifies a plugin-owned PostgreSQL schema.
/// </summary>
/// <param name="PluginId">Immutable identifier of the plugin that owns the schema.</param>
/// <param name="SchemaName">Stable lowercase PostgreSQL schema name, for example <c>jellytrend</c>.</param>
/// <param name="AdoptExistingSchema">Whether an existing unclaimed schema may be bound to <paramref name="PluginId"/>.</param>
/// <remarks>
/// Adoption exists only for a deliberate migration from a predecessor contract. Once claimed, the host
/// writes an ownership marker and rejects future requests by a different plugin identifier.
/// </remarks>
public sealed record PluginSchemaRequest(Guid PluginId, string SchemaName, bool AdoptExistingSchema = false);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Jellyfin.Database.Providers.Postgres.Api;

/// <summary>
/// A named value bound to a private-schema SQL statement.
/// </summary>
/// <param name="Name">Parameter name without SQL syntax, for example <c>itemId</c>.</param>
/// <param name="Value">Value to bind. A null value is sent as <see cref="System.DBNull.Value"/>.</param>
public sealed record PluginSqlParameter(string Name, object? Value);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Jellyfin.Database.Providers.Postgres.Api;

/// <summary>
/// Public, dependency-free marker for discovering the private-schema host from another Jellyfin plugin.
/// </summary>
/// <remarks>
/// A consumer obtains <see cref="IPluginSchemaHost"/> by asking Jellyfin's service provider for that
/// interface type from this loaded assembly. In a plugin load-context mismatch, consumers should first
/// locate PG Provider's live plugin assembly through <c>IPluginManager.Plugins</c>, then request the
/// type named by <see cref="ContractTypeName"/> from that assembly. Consumers must not construct an
/// internal provider implementation through reflection or <c>ActivatorUtilities</c>.
/// </remarks>
public static class PostgresSchemaHostCapability
{
/// <summary>Stable protocol identifier used by reflection-based consumers.</summary>
public const string ProtocolName = "jellyfin.postgres.private-schema-host";

/// <summary>Current backwards-compatible protocol version.</summary>
public const int ProtocolVersion = 1;

/// <summary>Full name of the service contract to request from Jellyfin DI.</summary>
public const string ContractTypeName = "Jellyfin.Database.Providers.Postgres.Api.IPluginSchemaHost";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Jellyfin.Database.Providers.Postgres.Controllers.Models;

/// <summary>Administrator-selected scheduled-maintenance policy for a private plugin schema.</summary>
public sealed class PluginSchemaMaintenancePolicyRequest
{
/// <summary>Gets or sets a value indicating whether scheduled provider maintenance refreshes planner statistics.</summary>
public bool IncludeAnalyze { get; set; }

/// <summary>Gets or sets a value indicating whether scheduled provider maintenance runs VACUUM (ANALYZE).</summary>
public bool IncludeVacuum { get; set; }

/// <summary>Gets or sets a value indicating whether scheduled provider maintenance rebuilds indexes concurrently.</summary>
public bool IncludeReindex { get; set; }
}
Loading
Loading