Files
ersatztv/ErsatzTV.Core/Domain/Library/LibraryFolder.cs
T
timothy 9a4f3e832d fix(491): unique index on LibraryFolder(LibraryPathId, PathHash) + tolerate concurrent insert
GetOrAddFolder was a check-then-insert with no unique constraint behind it,
so two callers racing the same folder could both miss the lookup and both
insert. Enforce identity in the schema and make the loser adopt the winner.

- LibraryFolder gains a SHA-256 PathHash (the MediaFile.Path/PathHash
  precedent): Path is MySQL longtext, which cannot be indexed without a
  prefix length and collates case-insensitively, so the unique index is on
  (LibraryPathId, PathHash) instead.
- GetOrAddFolder and SetEtag catch a classified unique violation via the
  existing TvContext.IsUniqueConstraintViolation seam (#308) and re-read.
- Dual-provider migration audits and collapses pre-existing duplicates
  (repointing MediaFile, ParentId and ImageFolderDuration) before creating
  the index; legacy rows keep a null hash and heal on the next scan.
- Tests: deterministic cross-connection race, 8x10 barrier stress with an
  insert-attempt vacuity guard, classifier-inversion negative control, and
  a real-migration dedupe test.

Refs #488 #308
fix #491
2026-07-25 21:13:22 +02:00

26 lines
1.1 KiB
C#

namespace ErsatzTV.Core.Domain;
public class LibraryFolder
{
public int Id { get; set; }
public string Path { get; set; }
/// <summary>
/// SHA-256 hex of <see cref="Path" /> (<see cref="ErsatzTV.Core.PathUtils.GetPathHash" />), the
/// indexable stand-in for the unbounded <see cref="Path" /> column that backs the unique
/// <c>(LibraryPathId, PathHash)</c> constraint — the same shape as <c>MediaFile.PathHash</c>.
/// Nullable: rows created before ersatztv#491 carry <c>null</c> until a scan heals them, and a
/// unique index treats nulls as distinct so those legacy rows never collide.
/// </summary>
public string PathHash { get; set; }
public int LibraryPathId { get; set; }
public LibraryPath LibraryPath { get; set; }
public int? ParentId { get; set; }
public LibraryFolder Parent { get; set; }
public ICollection<LibraryFolder> Children { get; set; }
public ICollection<MediaFile> MediaFiles { get; set; }
public ImageFolderDuration ImageFolderDuration { get; set; }
public string Etag { get; set; }
}