Normalize OwnerId to GUID and add performance indexes

- Change OwnerId from string to Guid for proper foreign key relationships
- Add Owner/Extras navigation properties for extras relationship
- Add indexes on OwnerId and ExtraType columns for efficient queries
- Add optimized composite indexes for latest items queries sorted by DateCreated
- Update BaseItemRepository and migration to handle new Guid type
This commit is contained in:
Shadowghost
2026-01-17 15:06:10 +01:00
parent cc2ccd1bf3
commit 139d23ddc2
9 changed files with 3864 additions and 8 deletions

View File

@@ -856,7 +856,7 @@ public sealed class BaseItemRepository
dto.ChannelId = entity.ChannelId ?? Guid.Empty;
dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
dto.OwnerId = entity.OwnerId ?? Guid.Empty;
dto.Width = entity.Width.GetValueOrDefault();
dto.Height = entity.Height.GetValueOrDefault();
dto.UserData = entity.UserData;
@@ -1023,7 +1023,7 @@ public sealed class BaseItemRepository
entity.ChannelId = dto.ChannelId;
entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
entity.OwnerId = dto.OwnerId.ToString();
entity.OwnerId = dto.OwnerId == Guid.Empty ? null : dto.OwnerId;
entity.Width = dto.Width;
entity.Height = dto.Height;
entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()

View File

@@ -1216,9 +1216,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
entity.ShowId = showId;
}
if (reader.TryGetString(index++, out var ownerId))
if (reader.TryGetString(index++, out var ownerId) && Guid.TryParse(ownerId, out var ownerIdGuid))
{
entity.OwnerId = ownerId;
entity.OwnerId = ownerIdGuid;
}
if (reader.TryGetString(index++, out var mediaType))

View File

@@ -134,7 +134,17 @@ public class BaseItemEntity
public string? ShowId { get; set; }
public string? OwnerId { get; set; }
public Guid? OwnerId { get; set; }
/// <summary>
/// Gets or sets the owner item (for extras like trailers, theme songs, etc.).
/// </summary>
public BaseItemEntity? Owner { get; set; }
/// <summary>
/// Gets or sets the extras owned by this item (trailers, theme songs, behind the scenes, etc.).
/// </summary>
public ICollection<BaseItemEntity>? Extras { get; set; }
public int? Width { get; set; }

View File

@@ -28,12 +28,16 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
builder.HasMany(e => e.Parents);
builder.HasMany(e => e.Children);
builder.HasMany(e => e.DirectChildren).WithOne(e => e.DirectParent).HasForeignKey(e => e.ParentId).OnDelete(DeleteBehavior.Cascade);
builder.HasMany(e => e.Extras).WithOne(e => e.Owner).HasForeignKey(e => e.OwnerId).OnDelete(DeleteBehavior.NoAction);
builder.HasMany(e => e.LockedFields);
builder.HasMany(e => e.TrailerTypes);
builder.HasMany(e => e.Images);
builder.HasIndex(e => e.Path);
builder.HasIndex(e => e.ParentId);
builder.HasIndex(e => e.OwnerId);
builder.HasIndex(e => e.ExtraType);
builder.HasIndex(e => new { e.ExtraType, e.OwnerId });
builder.HasIndex(e => e.PresentationUniqueKey);
builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem });
@@ -53,6 +57,10 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
// latest items
builder.HasIndex(e => new { e.Type, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
// latest items - optimized for sorting by DateCreated (no PresentationUniqueKey breaking the sort)
builder.HasIndex(e => new { e.TopParentId, e.Type, e.IsVirtualItem, e.DateCreated });
builder.HasIndex(e => new { e.TopParentId, e.IsFolder, e.IsVirtualItem, e.DateCreated });
builder.HasIndex(e => new { e.TopParentId, e.MediaType, e.IsVirtualItem, e.DateCreated });
// resume
builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey });
@@ -60,7 +68,7 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
Type = "PLACEHOLDER",
Name = "This is a placeholder item for UserData that has been detacted from its original item",
Name = "This is a placeholder item for UserData that has been detached from its original item",
});
}
}

View File

@@ -0,0 +1,156 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class ChangeOwnerIdToGuid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Normalize OwnerId to uppercase GUID format
migrationBuilder.Sql(
@"UPDATE BaseItems
SET OwnerId = UPPER(OwnerId)
WHERE OwnerId IS NOT NULL");
// Clear invalid OwnerId values (not 36 characters = not a valid GUID)
migrationBuilder.Sql(
@"UPDATE BaseItems
SET OwnerId = null
WHERE OwnerId IS NOT NULL AND length(OwnerId) != 36");
// Clear placeholder/empty GUIDs
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "OwnerId",
keyValue: new Guid("00000000-0000-0000-0000-000000000000"),
column: "OwnerId",
value: null);
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "OwnerId",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "OwnerId",
value: null);
migrationBuilder.AddColumn<Guid>(
name: "BaseItemEntityId",
table: "BaseItems",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
columns: new[] { "BaseItemEntityId", "Name", "OwnerId" },
values: new object[] { null, "This is a placeholder item for UserData that has been detached from its original item", null });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_ExtraType",
table: "BaseItems",
column: "ExtraType");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_ExtraType_OwnerId",
table: "BaseItems",
columns: new[] { "ExtraType", "OwnerId" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_OwnerId",
table: "BaseItems",
column: "OwnerId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_IsFolder_IsVirtualItem_DateCreated",
table: "BaseItems",
columns: new[] { "TopParentId", "IsFolder", "IsVirtualItem", "DateCreated" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_MediaType_IsVirtualItem_DateCreated",
table: "BaseItems",
columns: new[] { "TopParentId", "MediaType", "IsVirtualItem", "DateCreated" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_Type_IsVirtualItem_DateCreated",
table: "BaseItems",
columns: new[] { "TopParentId", "Type", "IsVirtualItem", "DateCreated" });
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId",
principalTable: "BaseItems",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "OwnerId",
value: null);
migrationBuilder.Sql(
@"UPDATE BaseItems
SET OwnerId = LOWER(OwnerId)
WHERE OwnerId IS NOT NULL");
migrationBuilder.DropForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_ExtraType",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_ExtraType_OwnerId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_OwnerId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_TopParentId_IsFolder_IsVirtualItem_DateCreated",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_TopParentId_MediaType_IsVirtualItem_DateCreated",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_TopParentId_Type_IsVirtualItem_DateCreated",
table: "BaseItems");
migrationBuilder.DropColumn(
name: "BaseItemEntityId",
table: "BaseItems");
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
columns: new[] { "Name", "OwnerId" },
values: new object[] { "This is a placeholder item for UserData that has been detacted from its original item", null });
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddForeignKeyToOwnerId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropColumn(
name: "BaseItemEntityId",
table: "BaseItems");
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_OwnerId",
table: "BaseItems",
column: "OwnerId",
principalTable: "BaseItems",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_BaseItems_BaseItems_OwnerId",
table: "BaseItems");
migrationBuilder.AddColumn<Guid>(
name: "BaseItemEntityId",
table: "BaseItems",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "BaseItemEntityId",
value: null);
migrationBuilder.CreateIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId");
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId",
principalTable: "BaseItems",
principalColumn: "Id");
}
}
}

View File

@@ -273,7 +273,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("OwnerId")
b.Property<Guid?>("OwnerId")
.HasColumnType("TEXT");
b.Property<Guid?>("ParentId")
@@ -363,12 +363,18 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("ExtraType");
b.HasIndex("OwnerId");
b.HasIndex("ParentId");
b.HasIndex("Path");
b.HasIndex("PresentationUniqueKey");
b.HasIndex("ExtraType", "OwnerId");
b.HasIndex("TopParentId", "Id");
b.HasIndex("Type", "TopParentId", "Id");
@@ -381,6 +387,12 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
b.HasIndex("TopParentId", "IsFolder", "IsVirtualItem", "DateCreated");
b.HasIndex("TopParentId", "MediaType", "IsVirtualItem", "DateCreated");
b.HasIndex("TopParentId", "Type", "IsVirtualItem", "DateCreated");
b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
@@ -404,7 +416,7 @@ namespace Jellyfin.Server.Implementations.Migrations
IsRepeat = false,
IsSeries = false,
IsVirtualItem = false,
Name = "This is a placeholder item for UserData that has been detacted from its original item",
Name = "This is a placeholder item for UserData that has been detached from its original item",
Type = "PLACEHOLDER"
});
});
@@ -1483,12 +1495,19 @@ namespace Jellyfin.Server.Implementations.Migrations
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
{
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
.WithMany("Extras")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.NoAction);
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
.WithMany("DirectChildren")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("DirectParent");
b.Navigation("Owner");
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
@@ -1714,6 +1733,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("DirectChildren");
b.Navigation("Extras");
b.Navigation("Images");
b.Navigation("ItemValues");