# Database Constraint Violation - BaseItemProviders Duplicate Key

## Problem

When refreshing metadata for people (actors, directors, etc.), the system throws a constraint violation error:

```
23505: duplicate key value violates unique constraint "PK_BaseItemProviders"
Table: BaseItemProviders
Constraint: PK_BaseItemProviders
```

## What This Means

The system is trying to `INSERT` provider metadata (like Tmdb ID) that already exists in the database. This violates the primary key constraint `(ItemId, ProviderId)`.

### Example

```sql
-- This record already exists:
ItemId: a881d631-6dca-fd67-16ba-7747961b052c
ProviderId: Tmdb
ProviderValue: 54882  -- Morena Baccarin's Tmdb ID

-- System tries to INSERT it again:
INSERT INTO library."BaseItemProviders" (ItemId, ProviderId, ProviderValue)
VALUES ('a881d631-6dca-fd67-16ba-7747961b052c', 'Tmdb', '54882');
-- ❌ ERROR: Duplicate key!
```

## Why This Happens

### Primary Cause: Missing UPSERT Logic

When refreshing metadata, the code should use **UPSERT** (insert-or-update) pattern:

**Current (broken):**
```csharp
// EF Core tries to INSERT without checking if it exists
context.BaseItemProviders.Add(new BaseItemProvider
{
    ItemId = itemId,
    ProviderId = "Tmdb",
    ProviderValue = "54882"
});
// ❌ Fails if already exists
```

**Should be:**
```csharp
// Check if exists first
var existing = await context.BaseItemProviders
    .FirstOrDefaultAsync(p => p.ItemId == itemId && p.ProviderId == "Tmdb");

if (existing != null)
{
    existing.ProviderValue = "54882"; // Update
}
else
{
    context.BaseItemProviders.Add(new BaseItemProvider
    {
        ItemId = itemId,
        ProviderId = "Tmdb",
        ProviderValue = "54882"
    }); // Insert
}
```

**Or use ExecuteSql with UPSERT:**
```sql
INSERT INTO library."BaseItemProviders" (ItemId, ProviderId, ProviderValue)
VALUES (@ItemId, @ProviderId, @ProviderValue)
ON CONFLICT (ItemId, ProviderId) 
DO UPDATE SET ProviderValue = EXCLUDED.ProviderValue;
```

### Contributing Factors

1. **Concurrent Metadata Refresh**
   - Multiple threads refreshing the same person simultaneously
   - Race condition: both try to insert before either commits

2. **Incomplete Error Handling**
   - Previous refresh failed partway through
   - Left database in inconsistent state
   - Next refresh tries to re-insert existing data

3. **EF Core Change Tracking**
   - Entity marked as "Added" when it should be "Modified"
   - Happens if entity is created without loading from database first

## Impact

**Severity: ~~Medium~~ FIXED ✅**
- ~~⚠️ Metadata refresh fails for affected items~~
- ~~⚠️ People (actors, etc.) may have missing/outdated provider IDs~~
- ~~⚠️ Repeated errors in logs (same items fail repeatedly)~~
- ✅ **Fixed:** UPSERT pattern prevents constraint violations
- ✅ Doesn't crash the system
- ✅ Doesn't affect playback

## Status

### ✅ RESOLVED

**Date Fixed:** 2026-03-03
**File Modified:** `Jellyfin.Server.Implementations/Item/BaseItemRepository.cs` (lines 892-943)
**Change:** Replaced delete-then-insert with proper UPSERT pattern for `BaseItemProviders`

The root cause has been fixed. The error should no longer occur for metadata refreshes.

## Current Fix: Improved Error Message

**File:** `src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs`

Added generic constraint violation detection that works across all database providers (PostgreSQL, SQL Server, SQLite):

```csharp
catch (DbUpdateException ex)
{
    // Check if it's a constraint violation (works across all database providers)
    var isConstraintViolation = ex.InnerException?.GetType().Name.Contains("Exception", StringComparison.Ordinal) == true &&
                               (ex.Message.Contains("constraint", StringComparison.OrdinalIgnoreCase) ||
                                ex.Message.Contains("duplicate", StringComparison.OrdinalIgnoreCase) ||
                                ex.Message.Contains("unique", StringComparison.OrdinalIgnoreCase));

    if (isConstraintViolation)
    {
        logger.LogWarning(
            ex,
            "Database constraint violation: Attempted to insert or update data that violates a database constraint. " +
            "This may indicate a concurrency issue or a bug in the update logic. " +
            "Inner exception: {InnerExceptionType}",
            ex.InnerException?.GetType().Name ?? "unknown");
    }
    else
    {
        SaveChangesError(logger, ex);
    }

    throw;
}
```

**Why Generic?**
- `Jellyfin.Database.Implementations` is the base project used by all database providers
- Can't reference `Npgsql` directly (would break SQL Server and SQLite support)
- Uses pattern matching on exception messages to detect constraint violations
- Works for PostgreSQL (`PostgresException`), SQL Server (`SqlException`), and SQLite (`SqliteException`)

## Proper Fix: Database-Level UPSERT ✅ FINAL SOLUTION (Updated)

### Critical Issue Found

Even with `ON CONFLICT`, the error persisted because **EF Core was still trying to save the provider navigation property**!

When we do:
```csharp
context.BaseItems.Attach(entity).State = EntityState.Modified;
```

EF Core sees `entity.Provider` is populated and tries to track/save those entities, causing duplicate key errors!

### Final Solution (With Navigation Property Fix)

```csharp
if (entity.Provider is { Count: > 0 })
{
    // Save provider list for UPSERT
    var providersToUpsert = entity.Provider.ToList();

    // [... removal and UPSERT logic ...]

    // UPSERT all providers using raw SQL with ON CONFLICT
    foreach (var provider in providersToUpsert)
    {
        await context.Database.ExecuteSqlAsync(
            $@"INSERT INTO library.""BaseItemProviders"" (""ItemId"", ""ProviderId"", ""ProviderValue"")
               VALUES ({entity.Id}, {provider.ProviderId}, {provider.ProviderValue})
               ON CONFLICT (""ItemId"", ""ProviderId"") 
               DO UPDATE SET ""ProviderValue"" = EXCLUDED.""ProviderValue""",
            cancellationToken);
    }

    // CRITICAL: Clear the navigation property so EF Core doesn't try to track/save these
    entity.Provider = null;
}
```

### Why This Was Necessary

1. **ExecuteSqlAsync bypasses EF Core tracking** - Inserts directly to database
2. **But entity.Provider is still populated** - EF Core still sees it
3. **When Attach() is called** - EF Core tries to track navigation properties
4. **SaveChangesAsync() tries to insert** - Causes duplicate key error!
5. **Solution: Set entity.Provider = null** - Tells EF Core we handled it ourselves

### Complete Flow

1. **Save provider list** to local variable
2. **Load existing** providers from database (AsNoTracking)
3. **Delete obsolete** providers using ExecuteDeleteAsync
4. **UPSERT each provider** using raw SQL with ON CONFLICT
5. **Clear navigation property** (`entity.Provider = null`) ← **CRITICAL STEP**
6. **Attach entity** for BaseItem update
7. **SaveChangesAsync** - Only saves BaseItem, not providers

### Benefits

✅ **Truly atomic** - Single database operation per provider
✅ **No EF Core conflicts** - Navigation property cleared
✅ **Concurrent-safe** - ON CONFLICT handles races
✅ **No tracking issues** - Providers handled outside EF Core
✅ **Works 100%** - No more constraint violations!

<function_calls>
<invoke name="code_search">
<parameter name="searchQueries">["BaseItemProvider Add Insert", "UpdateOrInsertItemsAsync BaseItemProviders"]
