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:
Binary file not shown.
Binary file not shown.
+1
-1
@@ -14,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.CodeAnalysis")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8d39eb77e6f2c21547ee919c542c5f7bf520c9a5")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+ede6904433ad169c85d6740696e10c0df47e1a13")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.CodeAnalysis")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.CodeAnalysis")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
46f31a434996f3f948113e0a45d718aef3a886077a49d1b3e5b5870d402fb364
|
||||
2e432f4563c02bf83f64873f7a02c00c8dd4fb81d0140d7082d0a153c870cf29
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+186
-34
@@ -12,8 +12,16 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Create schemas to organize tables by their legacy database origin
|
||||
migrationBuilder.EnsureSchema(name: "activitylog");
|
||||
migrationBuilder.EnsureSchema(name: "authentication");
|
||||
migrationBuilder.EnsureSchema(name: "displaypreferences");
|
||||
migrationBuilder.EnsureSchema(name: "library");
|
||||
migrationBuilder.EnsureSchema(name: "users");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ActivityLogs",
|
||||
schema: "activitylog",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -35,6 +43,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiKeys",
|
||||
schema: "authentication",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -51,6 +60,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BaseItems",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -132,6 +142,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_BaseItems_BaseItems_ParentId",
|
||||
column: x => x.ParentId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -139,6 +150,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CustomItemDisplayPreferences",
|
||||
schema: "displaypreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -156,6 +168,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DeviceOptions",
|
||||
schema: "authentication",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -170,6 +183,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ItemValues",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemValueId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -184,6 +198,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MediaSegments",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -200,6 +215,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Peoples",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -213,6 +229,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TrickplayInfos",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -231,6 +248,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
schema: "users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -272,6 +290,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AncestorIds",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ParentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -283,12 +302,14 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_AncestorIds_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AncestorIds_BaseItems_ParentItemId",
|
||||
column: x => x.ParentItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -296,6 +317,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AttachmentStreamInfos",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -312,6 +334,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_AttachmentStreamInfos_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -319,6 +342,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BaseItemImageInfos",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -336,6 +360,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_BaseItemImageInfos_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -343,6 +368,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BaseItemMetadataFields",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
@@ -354,6 +380,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_BaseItemMetadataFields_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -361,6 +388,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BaseItemProviders",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -373,6 +401,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_BaseItemProviders_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -380,6 +409,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BaseItemTrailerTypes",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
@@ -391,6 +421,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_BaseItemTrailerTypes_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -398,6 +429,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Chapters",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -413,6 +445,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_Chapters_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -420,6 +453,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "KeyframeData",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -432,6 +466,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_KeyframeData_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -439,6 +474,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MediaStreamInfos",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -495,6 +531,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_MediaStreamInfos_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -502,6 +539,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ItemValuesMap",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
ItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
@@ -513,12 +551,14 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_ItemValuesMap_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ItemValuesMap_ItemValues_ItemValueId",
|
||||
column: x => x.ItemValueId,
|
||||
principalSchema: "library",
|
||||
principalTable: "ItemValues",
|
||||
principalColumn: "ItemValueId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -526,6 +566,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PeopleBaseItemMap",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Role = table.Column<string>(type: "text", nullable: false),
|
||||
@@ -540,12 +581,14 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_PeopleBaseItemMap_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_PeopleBaseItemMap_Peoples_PeopleId",
|
||||
column: x => x.PeopleId,
|
||||
principalSchema: "library",
|
||||
principalTable: "Peoples",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -553,6 +596,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AccessSchedules",
|
||||
schema: "users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -568,6 +612,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_AccessSchedules_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -575,6 +620,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Devices",
|
||||
schema: "authentication",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -596,6 +642,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_Devices_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -603,6 +650,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DisplayPreferences",
|
||||
schema: "displaypreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -627,6 +675,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_DisplayPreferences_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -634,6 +683,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ImageInfos",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -648,6 +698,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_ImageInfos_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -655,6 +706,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ItemDisplayPreferences",
|
||||
schema: "displaypreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -675,6 +727,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_ItemDisplayPreferences_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -682,6 +735,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Permissions",
|
||||
schema: "users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -698,6 +752,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_Permissions_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -705,6 +760,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Preferences",
|
||||
schema: "users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -721,6 +777,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_Preferences_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -728,6 +785,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserData",
|
||||
schema: "library",
|
||||
columns: table => new
|
||||
{
|
||||
CustomDataKey = table.Column<string>(type: "text", nullable: false),
|
||||
@@ -750,12 +808,14 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_UserData_BaseItems_ItemId",
|
||||
column: x => x.ItemId,
|
||||
principalSchema: "library",
|
||||
principalTable: "BaseItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserData_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "users",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
@@ -763,6 +823,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "HomeSection",
|
||||
schema: "displaypreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
@@ -777,234 +838,288 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
table.ForeignKey(
|
||||
name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId",
|
||||
column: x => x.DisplayPreferencesId,
|
||||
principalSchema: "displaypreferences",
|
||||
principalTable: "DisplayPreferences",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Id", "Album", "AlbumArtists", "Artists", "Audio", "ChannelId", "CleanName", "CommunityRating", "CriticRating", "CustomRating", "Data", "DateCreated", "DateLastMediaAdded", "DateLastRefreshed", "DateLastSaved", "DateModified", "EndDate", "EpisodeTitle", "ExternalId", "ExternalSeriesId", "ExternalServiceId", "ExtraIds", "ExtraType", "ForcedSortName", "Genres", "Height", "IndexNumber", "InheritedParentalRatingSubValue", "InheritedParentalRatingValue", "IsFolder", "IsInMixedFolder", "IsLocked", "IsMovie", "IsRepeat", "IsSeries", "IsVirtualItem", "LUFS", "MediaType", "Name", "NormalizationGain", "OfficialRating", "OriginalTitle", "Overview", "OwnerId", "ParentId", "ParentIndexNumber", "Path", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "PremiereDate", "PresentationUniqueKey", "PrimaryVersionId", "ProductionLocations", "ProductionYear", "RunTimeTicks", "SeasonId", "SeasonName", "SeriesId", "SeriesName", "SeriesPresentationUniqueKey", "ShowId", "Size", "SortName", "StartDate", "Studios", "Tagline", "Tags", "TopParentId", "TotalBitrate", "Type", "UnratedType", "Width" },
|
||||
values: new object[] { new Guid("00000000-0000-0000-0000-000000000001"), null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, false, false, false, false, false, false, null, null, "This is a placeholder item for UserData that has been detacted from its original item", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "PLACEHOLDER", null, null });
|
||||
// Insert placeholder BaseItem for orphaned UserData
|
||||
// Using raw SQL to avoid entity mapping issues with schema-qualified tables during migration
|
||||
migrationBuilder.Sql(@"
|
||||
INSERT INTO library.""BaseItems"" (
|
||||
""Id"", ""Type"", ""Name"", ""IsFolder"", ""IsInMixedFolder"", ""IsLocked"", ""IsMovie"",
|
||||
""IsRepeat"", ""IsSeries"", ""IsVirtualItem""
|
||||
)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
||||
'PLACEHOLDER',
|
||||
'This is a placeholder item for UserData that has been detacted from its original item',
|
||||
false, false, false, false, false, false, false
|
||||
);
|
||||
");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AccessSchedules_UserId",
|
||||
schema: "users",
|
||||
table: "AccessSchedules",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ActivityLogs_DateCreated",
|
||||
schema: "activitylog",
|
||||
table: "ActivityLogs",
|
||||
column: "DateCreated");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AncestorIds_ParentItemId",
|
||||
schema: "library",
|
||||
table: "AncestorIds",
|
||||
column: "ParentItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiKeys_AccessToken",
|
||||
schema: "authentication",
|
||||
table: "ApiKeys",
|
||||
column: "AccessToken",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItemImageInfos_ItemId",
|
||||
schema: "library",
|
||||
table: "BaseItemImageInfos",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItemMetadataFields_ItemId",
|
||||
schema: "library",
|
||||
table: "BaseItemMetadataFields",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId",
|
||||
schema: "library",
|
||||
table: "BaseItemProviders",
|
||||
columns: new[] { "ProviderId", "ProviderValue", "ItemId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Id", "Type", "IsFolder", "IsVirtualItem" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_IsFolder_TopParentId_IsVirtualItem_PresentationUn~",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_MediaType_TopParentId_IsVirtualItem_PresentationU~",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_ParentId",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
column: "ParentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Path",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
column: "Path");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_PresentationUniqueKey",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
column: "PresentationUniqueKey");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_TopParentId_Id",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "TopParentId", "Id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_IsFolder_IsVirtu~",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_PresentationUniq~",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Type_TopParentId_Id",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Type", "TopParentId", "Id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Type_TopParentId_IsVirtualItem_PresentationUnique~",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Type_TopParentId_PresentationUniqueKey",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Type", "TopParentId", "PresentationUniqueKey" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItems_Type_TopParentId_StartDate",
|
||||
schema: "library",
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Type", "TopParentId", "StartDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BaseItemTrailerTypes_ItemId",
|
||||
schema: "library",
|
||||
table: "BaseItemTrailerTypes",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustomItemDisplayPreferences_UserId_ItemId_Client_Key",
|
||||
schema: "displaypreferences",
|
||||
table: "CustomItemDisplayPreferences",
|
||||
columns: new[] { "UserId", "ItemId", "Client", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceOptions_DeviceId",
|
||||
schema: "authentication",
|
||||
table: "DeviceOptions",
|
||||
column: "DeviceId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Devices_AccessToken_DateLastActivity",
|
||||
schema: "authentication",
|
||||
table: "Devices",
|
||||
columns: new[] { "AccessToken", "DateLastActivity" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Devices_DeviceId",
|
||||
schema: "authentication",
|
||||
table: "Devices",
|
||||
column: "DeviceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Devices_DeviceId_DateLastActivity",
|
||||
schema: "authentication",
|
||||
table: "Devices",
|
||||
columns: new[] { "DeviceId", "DateLastActivity" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Devices_UserId_DeviceId",
|
||||
schema: "authentication",
|
||||
table: "Devices",
|
||||
columns: new[] { "UserId", "DeviceId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DisplayPreferences_UserId_ItemId_Client",
|
||||
schema: "displaypreferences",
|
||||
table: "DisplayPreferences",
|
||||
columns: new[] { "UserId", "ItemId", "Client" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_HomeSection_DisplayPreferencesId",
|
||||
schema: "displaypreferences",
|
||||
table: "HomeSection",
|
||||
column: "DisplayPreferencesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ImageInfos_UserId",
|
||||
schema: "library",
|
||||
table: "ImageInfos",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ItemDisplayPreferences_UserId",
|
||||
schema: "displaypreferences",
|
||||
table: "ItemDisplayPreferences",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ItemValues_Type_CleanValue",
|
||||
schema: "library",
|
||||
table: "ItemValues",
|
||||
columns: new[] { "Type", "CleanValue" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ItemValues_Type_Value",
|
||||
schema: "library",
|
||||
table: "ItemValues",
|
||||
columns: new[] { "Type", "Value" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ItemValuesMap_ItemId",
|
||||
schema: "library",
|
||||
table: "ItemValuesMap",
|
||||
column: "ItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaStreamInfos_StreamIndex",
|
||||
schema: "library",
|
||||
table: "MediaStreamInfos",
|
||||
column: "StreamIndex");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaStreamInfos_StreamIndex_StreamType",
|
||||
schema: "library",
|
||||
table: "MediaStreamInfos",
|
||||
columns: new[] { "StreamIndex", "StreamType" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language",
|
||||
schema: "library",
|
||||
table: "MediaStreamInfos",
|
||||
columns: new[] { "StreamIndex", "StreamType", "Language" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaStreamInfos_StreamType",
|
||||
schema: "library",
|
||||
table: "MediaStreamInfos",
|
||||
column: "StreamType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PeopleBaseItemMap_ItemId_ListOrder",
|
||||
schema: "library",
|
||||
table: "PeopleBaseItemMap",
|
||||
columns: new[] { "ItemId", "ListOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PeopleBaseItemMap_ItemId_SortOrder",
|
||||
schema: "library",
|
||||
table: "PeopleBaseItemMap",
|
||||
columns: new[] { "ItemId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PeopleBaseItemMap_PeopleId",
|
||||
schema: "library",
|
||||
table: "PeopleBaseItemMap",
|
||||
column: "PeopleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Peoples_Name",
|
||||
schema: "library",
|
||||
table: "Peoples",
|
||||
column: "Name");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Permissions_UserId_Kind",
|
||||
schema: "users",
|
||||
table: "Permissions",
|
||||
columns: new[] { "UserId", "Kind" },
|
||||
unique: true,
|
||||
@@ -1012,6 +1127,7 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Preferences_UserId_Kind",
|
||||
schema: "users",
|
||||
table: "Preferences",
|
||||
columns: new[] { "UserId", "Kind" },
|
||||
unique: true,
|
||||
@@ -1019,31 +1135,37 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserData_ItemId_UserId_IsFavorite",
|
||||
schema: "library",
|
||||
table: "UserData",
|
||||
columns: new[] { "ItemId", "UserId", "IsFavorite" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserData_ItemId_UserId_LastPlayedDate",
|
||||
schema: "library",
|
||||
table: "UserData",
|
||||
columns: new[] { "ItemId", "UserId", "LastPlayedDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserData_ItemId_UserId_PlaybackPositionTicks",
|
||||
schema: "library",
|
||||
table: "UserData",
|
||||
columns: new[] { "ItemId", "UserId", "PlaybackPositionTicks" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserData_ItemId_UserId_Played",
|
||||
schema: "library",
|
||||
table: "UserData",
|
||||
columns: new[] { "ItemId", "UserId", "Played" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserData_UserId",
|
||||
schema: "library",
|
||||
table: "UserData",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_Username",
|
||||
schema: "users",
|
||||
table: "Users",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
@@ -1053,94 +1175,124 @@ namespace Jellyfin.Database.Providers.Postgres.Migrations
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AccessSchedules");
|
||||
name: "AccessSchedules",
|
||||
schema: "users");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ActivityLogs");
|
||||
name: "ActivityLogs",
|
||||
schema: "activitylog");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AncestorIds");
|
||||
name: "AncestorIds",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiKeys");
|
||||
name: "ApiKeys",
|
||||
schema: "authentication");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AttachmentStreamInfos");
|
||||
name: "AttachmentStreamInfos",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BaseItemImageInfos");
|
||||
name: "BaseItemImageInfos",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BaseItemMetadataFields");
|
||||
name: "BaseItemMetadataFields",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BaseItemProviders");
|
||||
name: "BaseItemProviders",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BaseItemTrailerTypes");
|
||||
name: "BaseItemTrailerTypes",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Chapters");
|
||||
name: "Chapters",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CustomItemDisplayPreferences");
|
||||
name: "CustomItemDisplayPreferences",
|
||||
schema: "displaypreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DeviceOptions");
|
||||
name: "DeviceOptions",
|
||||
schema: "authentication");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Devices");
|
||||
name: "Devices",
|
||||
schema: "authentication");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "HomeSection");
|
||||
name: "HomeSection",
|
||||
schema: "displaypreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ImageInfos");
|
||||
name: "ImageInfos",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ItemDisplayPreferences");
|
||||
name: "ItemDisplayPreferences",
|
||||
schema: "displaypreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ItemValuesMap");
|
||||
name: "ItemValuesMap",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "KeyframeData");
|
||||
name: "KeyframeData",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MediaSegments");
|
||||
name: "MediaSegments",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MediaStreamInfos");
|
||||
name: "MediaStreamInfos",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PeopleBaseItemMap");
|
||||
name: "PeopleBaseItemMap",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Permissions");
|
||||
name: "Permissions",
|
||||
schema: "users");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Preferences");
|
||||
name: "Preferences",
|
||||
schema: "users");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TrickplayInfos");
|
||||
name: "TrickplayInfos",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserData");
|
||||
name: "UserData",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DisplayPreferences");
|
||||
name: "DisplayPreferences",
|
||||
schema: "displaypreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ItemValues");
|
||||
name: "ItemValues",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Peoples");
|
||||
name: "Peoples",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BaseItems");
|
||||
name: "BaseItems",
|
||||
schema: "library");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
name: "Users",
|
||||
schema: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+188
-6
@@ -2,6 +2,8 @@
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
#pragma warning disable SA1201 // Elements should appear in the correct order
|
||||
|
||||
namespace Jellyfin.Database.Providers.Postgres;
|
||||
|
||||
using System;
|
||||
@@ -12,6 +14,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.DbConfiguration;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Entities.Security;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -37,6 +41,42 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schema names mapping to legacy database files.
|
||||
/// </summary>
|
||||
public static class Schemas
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema for activity log tables (legacy: activitylog.db).
|
||||
/// </summary>
|
||||
public const string ActivityLog = "activitylog";
|
||||
|
||||
/// <summary>
|
||||
/// Schema for authentication tables (legacy: authentication.db).
|
||||
/// </summary>
|
||||
public const string Authentication = "authentication";
|
||||
|
||||
/// <summary>
|
||||
/// Schema for display preferences tables (legacy: displaypreferences.db).
|
||||
/// </summary>
|
||||
public const string DisplayPreferences = "displaypreferences";
|
||||
|
||||
/// <summary>
|
||||
/// Schema for library tables (legacy: library.db).
|
||||
/// </summary>
|
||||
public const string Library = "library";
|
||||
|
||||
/// <summary>
|
||||
/// Schema for user tables (legacy: users.db).
|
||||
/// </summary>
|
||||
public const string Users = "users";
|
||||
|
||||
/// <summary>
|
||||
/// Gets all schema names.
|
||||
/// </summary>
|
||||
public static string[] All => [ActivityLog, Authentication, DisplayPreferences, Library, Users];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
|
||||
|
||||
@@ -70,19 +110,24 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
Password = GetOption(customOptions, "password", e => e, () => string.Empty)!,
|
||||
Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true),
|
||||
CommandTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 30),
|
||||
Timeout = GetOption(customOptions, "connection-timeout", int.Parse, () => 15)
|
||||
Timeout = GetOption(customOptions, "connection-timeout", int.Parse, () => 15),
|
||||
Multiplexing = GetOption(customOptions, "multiplexing", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => false),
|
||||
MaxPoolSize = GetOption(customOptions, "max-pool-size", int.Parse, () => 100),
|
||||
MinPoolSize = GetOption(customOptions, "min-pool-size", int.Parse, () => 0)
|
||||
};
|
||||
|
||||
var connectionString = connectionBuilder.ToString();
|
||||
|
||||
// Log PostgreSQL connection parameters (without password)
|
||||
logger.LogInformation(
|
||||
"PostgreSQL connection: Host={Host}, Port={Port}, Database={Database}, Username={Username}, Pooling={Pooling}",
|
||||
"PostgreSQL connection: Host={Host}, Port={Port}, Database={Database}, Username={Username}, Pooling={Pooling}, MaxPoolSize={MaxPoolSize}, Multiplexing={Multiplexing}",
|
||||
connectionBuilder.Host,
|
||||
connectionBuilder.Port,
|
||||
connectionBuilder.Database,
|
||||
connectionBuilder.Username,
|
||||
connectionBuilder.Pooling);
|
||||
connectionBuilder.Pooling,
|
||||
connectionBuilder.MaxPoolSize,
|
||||
connectionBuilder.Multiplexing);
|
||||
|
||||
options
|
||||
.UseNpgsql(
|
||||
@@ -177,6 +222,9 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
{
|
||||
logger.LogInformation("PostgreSQL database '{Database}' already exists", database);
|
||||
}
|
||||
|
||||
// Now ensure all schemas exist in the database
|
||||
await EnsureSchemasExistAsync(host, port, database, username, password, timeout, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -185,14 +233,85 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all required schemas exist in the PostgreSQL database.
|
||||
/// </summary>
|
||||
private async Task EnsureSchemasExistAsync(
|
||||
string host,
|
||||
int port,
|
||||
string database,
|
||||
string username,
|
||||
string password,
|
||||
int timeout,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var connectionBuilder = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = host,
|
||||
Port = port,
|
||||
Database = database,
|
||||
Username = username,
|
||||
Password = password,
|
||||
Timeout = timeout,
|
||||
Pooling = false
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = new NpgsqlConnection(connectionBuilder.ToString());
|
||||
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var schema in Schemas.All)
|
||||
{
|
||||
// Check if schema exists
|
||||
var checkQuery = "SELECT 1 FROM information_schema.schemata WHERE schema_name = @schemaName";
|
||||
await using var checkCommand = new NpgsqlCommand(checkQuery, connection);
|
||||
checkCommand.Parameters.AddWithValue("schemaName", schema);
|
||||
|
||||
var exists = await checkCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (exists is null)
|
||||
{
|
||||
// Schema doesn't exist, create it
|
||||
logger.LogInformation("Creating PostgreSQL schema '{Schema}'...", schema);
|
||||
|
||||
var createQuery = $"CREATE SCHEMA \"{schema}\" AUTHORIZATION \"{username}\"";
|
||||
await using var createCommand = new NpgsqlCommand(createQuery, connection);
|
||||
await createCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Schema '{Schema}' created successfully", schema);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Schema '{Schema}' already exists", schema);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("All required PostgreSQL schemas are ready");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to ensure PostgreSQL schemas exist. Error: {Message}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
// Run PostgreSQL optimization commands
|
||||
await context.Database.ExecuteSqlRawAsync("VACUUM ANALYZE", cancellationToken).ConfigureAwait(false);
|
||||
// Run PostgreSQL optimization commands on all schemas
|
||||
// Note: VACUUM cannot be parameterized, but schema names are from const strings, not user input
|
||||
foreach (var schema in Schemas.All)
|
||||
{
|
||||
#pragma warning disable EF1002 // Schema names are internal constants, not user input
|
||||
await context.Database.ExecuteSqlRawAsync($"VACUUM ANALYZE \"{schema}\"", cancellationToken).ConfigureAwait(false);
|
||||
#pragma warning restore EF1002
|
||||
logger.LogDebug("Optimized PostgreSQL schema '{Schema}'", schema);
|
||||
}
|
||||
|
||||
logger.LogInformation("PostgreSQL database optimized successfully!");
|
||||
}
|
||||
}
|
||||
@@ -201,6 +320,56 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
public void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
|
||||
|
||||
// Assign entities to schemas based on their legacy database origin
|
||||
AssignEntitiesToSchemas(modelBuilder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns entities to PostgreSQL schemas based on their legacy database origin.
|
||||
/// </summary>
|
||||
/// <param name="modelBuilder">The model builder.</param>
|
||||
private static void AssignEntitiesToSchemas(ModelBuilder modelBuilder)
|
||||
{
|
||||
// ActivityLog schema (legacy: activitylog.db)
|
||||
modelBuilder.Entity<ActivityLog>().ToTable("ActivityLogs", Schemas.ActivityLog);
|
||||
|
||||
// Authentication schema (legacy: authentication.db)
|
||||
modelBuilder.Entity<ApiKey>().ToTable("ApiKeys", Schemas.Authentication);
|
||||
modelBuilder.Entity<Device>().ToTable("Devices", Schemas.Authentication);
|
||||
modelBuilder.Entity<DeviceOptions>().ToTable("DeviceOptions", Schemas.Authentication);
|
||||
|
||||
// DisplayPreferences schema (legacy: displaypreferences.db)
|
||||
modelBuilder.Entity<DisplayPreferences>().ToTable("DisplayPreferences", Schemas.DisplayPreferences);
|
||||
modelBuilder.Entity<ItemDisplayPreferences>().ToTable("ItemDisplayPreferences", Schemas.DisplayPreferences);
|
||||
modelBuilder.Entity<CustomItemDisplayPreferences>().ToTable("CustomItemDisplayPreferences", Schemas.DisplayPreferences);
|
||||
modelBuilder.Entity<HomeSection>().ToTable("HomeSections", Schemas.DisplayPreferences);
|
||||
|
||||
// Users schema (legacy: users.db)
|
||||
modelBuilder.Entity<User>().ToTable("Users", Schemas.Users);
|
||||
modelBuilder.Entity<Permission>().ToTable("Permissions", Schemas.Users);
|
||||
modelBuilder.Entity<Preference>().ToTable("Preferences", Schemas.Users);
|
||||
modelBuilder.Entity<AccessSchedule>().ToTable("AccessSchedules", Schemas.Users);
|
||||
|
||||
// Library schema (legacy: library.db) - All remaining entities
|
||||
modelBuilder.Entity<BaseItemEntity>().ToTable("BaseItems", Schemas.Library);
|
||||
modelBuilder.Entity<Chapter>().ToTable("Chapters", Schemas.Library);
|
||||
modelBuilder.Entity<MediaStreamInfo>().ToTable("MediaStreamInfos", Schemas.Library);
|
||||
modelBuilder.Entity<AttachmentStreamInfo>().ToTable("AttachmentStreamInfos", Schemas.Library);
|
||||
modelBuilder.Entity<ImageInfo>().ToTable("ImageInfos", Schemas.Library);
|
||||
modelBuilder.Entity<BaseItemImageInfo>().ToTable("BaseItemImageInfos", Schemas.Library);
|
||||
modelBuilder.Entity<BaseItemProvider>().ToTable("BaseItemProviders", Schemas.Library);
|
||||
modelBuilder.Entity<BaseItemMetadataField>().ToTable("BaseItemMetadataFields", Schemas.Library);
|
||||
modelBuilder.Entity<BaseItemTrailerType>().ToTable("BaseItemTrailerTypes", Schemas.Library);
|
||||
modelBuilder.Entity<ItemValue>().ToTable("ItemValues", Schemas.Library);
|
||||
modelBuilder.Entity<ItemValueMap>().ToTable("ItemValuesMap", Schemas.Library);
|
||||
modelBuilder.Entity<People>().ToTable("Peoples", Schemas.Library);
|
||||
modelBuilder.Entity<PeopleBaseItemMap>().ToTable("PeopleBaseItemMap", Schemas.Library);
|
||||
modelBuilder.Entity<UserData>().ToTable("UserData", Schemas.Library);
|
||||
modelBuilder.Entity<AncestorId>().ToTable("AncestorIds", Schemas.Library);
|
||||
modelBuilder.Entity<TrickplayInfo>().ToTable("TrickplayInfos", Schemas.Library);
|
||||
modelBuilder.Entity<MediaSegment>().ToTable("MediaSegments", Schemas.Library);
|
||||
modelBuilder.Entity<KeyframeData>().ToTable("KeyframeData", Schemas.Library);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -255,7 +424,20 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
var deleteQueries = new List<string>();
|
||||
foreach (var tableName in tableNames)
|
||||
{
|
||||
deleteQueries.Add($"TRUNCATE TABLE \"{tableName}\" CASCADE;");
|
||||
// Find the schema for this table
|
||||
var entityType = dbContext.Model.GetEntityTypes()
|
||||
.FirstOrDefault(et => et.GetTableName() == tableName);
|
||||
|
||||
if (entityType is not null)
|
||||
{
|
||||
var schema = entityType.GetSchema() ?? "public";
|
||||
deleteQueries.Add($"TRUNCATE TABLE \"{schema}\".\"{tableName}\" CASCADE;");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to public schema if entity not found
|
||||
deleteQueries.Add($"TRUNCATE TABLE \"public\".\"{tableName}\" CASCADE;");
|
||||
}
|
||||
}
|
||||
|
||||
var deleteAllQuery = string.Join('\n', deleteQueries);
|
||||
|
||||
@@ -26,6 +26,8 @@ To use PostgreSQL as the database backend, configure the following options in yo
|
||||
{ "Key": "username", "Value": "jellyfin" },
|
||||
{ "Key": "password", "Value": "your_secure_password" },
|
||||
{ "Key": "pooling", "Value": "true" },
|
||||
{ "Key": "max-pool-size", "Value": "100" },
|
||||
{ "Key": "min-pool-size", "Value": "0" },
|
||||
{ "Key": "command-timeout", "Value": "30" },
|
||||
{ "Key": "connection-timeout", "Value": "15" }
|
||||
]
|
||||
@@ -83,19 +85,46 @@ psql -U jellyfin -h localhost jellyfin < jellyfin_backup.sql
|
||||
| username | jellyfin | Database username |
|
||||
| password | (empty) | Database password |
|
||||
| pooling | true | Enable connection pooling |
|
||||
| max-pool-size | 100 | Maximum number of connections in the pool |
|
||||
| min-pool-size | 0 | Minimum number of connections in the pool |
|
||||
| command-timeout | 30 | Command timeout in seconds |
|
||||
| connection-timeout | 15 | Connection timeout in seconds |
|
||||
| multiplexing | false | ⚠️ **Advanced**: Enable command multiplexing (requires all async operations) |
|
||||
| EnableSensitiveDataLogging | false | Enable sensitive data logging (for debugging) |
|
||||
|
||||
### ⚠️ Multiplexing Warning
|
||||
|
||||
**Multiplexing is disabled by default** because it requires all database operations to be asynchronous. Enabling multiplexing will cause errors like:
|
||||
|
||||
```
|
||||
System.NotSupportedException: Synchronous command execution is not supported when multiplexing is on
|
||||
```
|
||||
|
||||
Only enable multiplexing if you have modified Jellyfin code to use fully async database operations.
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
For better performance, consider:
|
||||
|
||||
1. **Indexes**: The provider will create necessary indexes through migrations
|
||||
2. **Connection Pooling**: Enabled by default
|
||||
2. **Connection Pooling**: Enabled by default with max 100 connections
|
||||
- Adjust `max-pool-size` based on your concurrent user count
|
||||
- Higher values allow more simultaneous database operations but use more resources
|
||||
3. **Vacuum**: Scheduled optimization runs `VACUUM ANALYZE` periodically
|
||||
4. **PostgreSQL Configuration**: Adjust `shared_buffers`, `effective_cache_size`, etc. in postgresql.conf
|
||||
|
||||
### Connection Pool Sizing
|
||||
|
||||
A good rule of thumb for `max-pool-size`:
|
||||
- **Small deployments** (1-10 users): 20-50 connections
|
||||
- **Medium deployments** (10-50 users): 50-100 connections
|
||||
- **Large deployments** (50+ users): 100-200 connections
|
||||
|
||||
Monitor your PostgreSQL server's active connections with:
|
||||
```sql
|
||||
SELECT count(*) FROM pg_stat_activity WHERE datname = 'jellyfin';
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Migrations from SQLite are not automatically handled. You'll need to export data from SQLite and import into PostgreSQL manually.
|
||||
|
||||
@@ -46,7 +46,14 @@ public class CacheDecorator : IKeyframeExtractor
|
||||
/// <inheritdoc />
|
||||
public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
|
||||
{
|
||||
keyframeData = _keyframeRepository.GetKeyframeData(itemId).FirstOrDefault();
|
||||
// Note: This method is synchronous by interface design, but repository is async.
|
||||
// Using GetAwaiter().GetResult() here is acceptable as this is called during media scanning
|
||||
// which is not performance-critical. Consider making IKeyframeExtractor async in future.
|
||||
keyframeData = _keyframeRepository.GetKeyframeDataAsync(itemId, CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (keyframeData is null)
|
||||
{
|
||||
if (!_keyframeExtractor.TryExtractKeyframes(itemId, filePath, out var result))
|
||||
@@ -57,7 +64,9 @@ public class CacheDecorator : IKeyframeExtractor
|
||||
|
||||
_logger.LogDebug("Successfully extracted keyframes using {ExtractorName}", _keyframeExtractorName);
|
||||
keyframeData = result;
|
||||
_keyframeRepository.SaveKeyframeDataAsync(itemId, keyframeData, CancellationToken.None).GetAwaiter().GetResult();
|
||||
_keyframeRepository.SaveKeyframeDataAsync(itemId, keyframeData, CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user