Add comprehensive documentation for PostgreSQL schema conversion

- Introduced DATABASE_SCHEMA_MISMATCH_REPORT.md to detail critical mismatches between SQL schema and C# code configurations, highlighting issues with naming conventions and potential runtime failures.
- Created SCHEMA_COLUMN_CONVERSION_REFERENCE.md to provide a reference for column name conversions from PascalCase to snake_case across all tables.
- Developed SCHEMA_CONVERSION_GUIDE.md outlining the conversion strategy for the SQL schema, including authoritative table mappings, column naming rules, and a validation checklist to ensure accuracy in the conversion process.
This commit is contained in:
2026-05-04 10:16:17 -04:00
parent 3e5d29225a
commit e67c191843
6 changed files with 83 additions and 79 deletions
@@ -0,0 +1,234 @@
# Database Schema Mismatch - Detailed Entity Mapping
## Quick Reference: All Mismatches
### ACTIVITYLOG Schema (1 table)
| Entity | SQL Table Name | C# Expected Name | Columns Match? | Status |
|--------|----------------|------------------|-----------------|--------|
| ActivityLog | `"ActivityLogs"` | `activity_logs` | NO (11 cols) | ❌ CRITICAL |
**SQL Columns:**
- "Id", "Name", "Overview", "ShortOverview", "Type", "UserId", "ItemId", "DateCreated", "LogSeverity", "RowVersion"
**C# Expected Columns (via SnakeCaseNamingConvention):**
- id, name, overview, short_overview, type, user_id, item_id, date_created, log_severity, row_version
---
### AUTHENTICATION Schema (3 tables)
| Entity | SQL Table Name | C# Expected Name | Columns Match? | Status |
|--------|----------------|------------------|-----------------|--------|
| ApiKey | `"ApiKeys"` | `api_keys` | NO (5 cols) | ❌ CRITICAL |
| Device | `"Devices"` | `devices` | NO (11 cols) | ❌ CRITICAL |
| DeviceOptions | `"DeviceOptions"` | `device_options` | NO (3 cols) | ❌ CRITICAL |
#### ApiKey
**SQL Columns:** "Id", "DateCreated", "DateLastActivity", "Name", "AccessToken"
**C# Expected:** id, date_created, date_last_activity, name, access_token
#### Device
**SQL Columns:** "Id", "UserId", "AccessToken", "AppName", "AppVersion", "DeviceName", "DeviceId", "IsActive", "DateCreated", "DateModified", "DateLastActivity"
**C# Expected:** id, user_id, access_token, app_name, app_version, device_name, device_id, is_active, date_created, date_modified, date_last_activity
#### DeviceOptions
**SQL Columns:** "Id", "DeviceId", "CustomName"
**C# Expected:** id, device_id, custom_name
---
### DISPLAYPREFERENCES Schema (4 tables)
| Entity | SQL Table Name | C# Expected Name | Columns Match? | Status |
|--------|----------------|------------------|-----------------|--------|
| CustomItemDisplayPreferences | `"CustomItemDisplayPreferences"` | `custom_item_display_preferences` | NO (6 cols) | ❌ CRITICAL |
| DisplayPreferences | `"DisplayPreferences"` | `display_preferences` | NO (14 cols) | ❌ CRITICAL |
| HomeSection | `"HomeSection"` | `home_sections` | NO (4 cols) | ❌ CRITICAL |
| ItemDisplayPreferences | `"ItemDisplayPreferences"` | `item_display_preferences` | NO (10 cols) | ❌ CRITICAL |
#### CustomItemDisplayPreferences
**SQL Columns:** "Id", "UserId", "ItemId", "Client", "Key", "Value"
**C# Expected:** id, user_id, item_id, client, key, value
#### DisplayPreferences
**SQL Columns:** "Id", "UserId", "ItemId", "Client", "ShowSidebar", "ShowBackdrop", "ScrollDirection", "IndexBy", "SkipForwardLength", "SkipBackwardLength", "ChromecastVersion", "EnableNextVideoInfoOverlay", "DashboardTheme", "TvHome"
**C# Expected:** id, user_id, item_id, client, show_sidebar, show_backdrop, scroll_direction, index_by, skip_forward_length, skip_backward_length, chromecast_version, enable_next_video_info_overlay, dashboard_theme, tv_home
#### HomeSection
**SQL Columns:** "Id", "DisplayPreferencesId", "Order", "Type"
**C# Expected:** id, display_preferences_id, order, type
#### ItemDisplayPreferences
**SQL Columns:** "Id", "UserId", "ItemId", "Client", "ViewType", "RememberIndexing", "IndexBy", "RememberSorting", "SortBy", "SortOrder"
**C# Expected:** id, user_id, item_id, client, view_type, remember_indexing, index_by, remember_sorting, sort_by, sort_order
---
### LIBRARY Schema (15 tables - MOST CRITICAL)
| Entity | SQL Table Name | C# Expected Name | Columns Match? | Status |
|--------|----------------|------------------|-----------------|--------|
| AncestorId | `"AncestorIds"` | `ancestor_ids` | NO (2 cols) | ❌ |
| AttachmentStreamInfo | `"AttachmentStreamInfos"` | `attachment_stream_infos` | NO (7 cols) | ❌ |
| BaseItemImageInfo | `"BaseItemImageInfos"` | `base_item_image_infos` | NO (8 cols) | ❌ |
| BaseItemMetadataField | `"BaseItemMetadataFields"` | `base_item_metadata_fields` | NO (2 cols) | ❌ |
| BaseItemProvider | `"BaseItemProviders"` | `base_item_providers` | NO (3 cols) | ❌ |
| BaseItemTrailerType | `"BaseItemTrailerTypes"` | `base_item_trailer_types` | NO (2 cols) | ❌ |
| BaseItemEntity | `"BaseItems"` | `base_items` | NO (73 cols!) | ❌ SEVERE |
| Chapter | `"Chapters"` | `chapters` | NO (6 cols) | ❌ |
| ImageInfo | `"ImageInfos"` | `image_infos` | NO (4 cols) | ❌ |
| ItemValue | `"ItemValues"` | `item_values` | NO (4 cols) | ❌ |
| ItemValueMap | `"ItemValuesMap"` | `item_values_map` | NO (2 cols) | ❌ |
| KeyframeData | `"KeyframeData"` | `keyframe_data` | NO (3 cols) | ❌ |
| MediaSegment | `"MediaSegments"` | `media_segments` | NO (6 cols) | ❌ |
| MediaStreamInfo | `"MediaStreamInfos"` | `media_stream_infos` | NO (60+ cols) | ❌ SEVERE |
| PeopleBaseItemMap | `"PeopleBaseItemMap"` | `people_base_item_map` | NO (5 cols) | ❌ |
| People | `"Peoples"` | `peoples` | NO (3 cols) | ❌ |
| TrickplayInfo | `"TrickplayInfos"` | `trickplay_infos` | NO (8 cols) | ❌ |
| UserData | `"UserData"` | `user_data` | NO (13 cols) | ❌ |
#### BaseItemEntity (MOST CRITICAL - 73 columns)
**SQL Columns:**
"Id", "Type", "Data", "Path", "StartDate", "EndDate", "ChannelId", "IsMovie", "CommunityRating", "CustomRating", "IndexNumber", "IsLocked", "Name", "OfficialRating", "MediaType", "Overview", "ParentIndexNumber", "PremiereDate", "ProductionYear", "Genres", "SortName", "ForcedSortName", "RunTimeTicks", "DateCreated", "DateModified", "IsSeries", "EpisodeTitle", "IsRepeat", "PreferredMetadataLanguage", "PreferredMetadataCountryCode", "DateLastRefreshed", "DateLastSaved", "IsInMixedFolder", "Studios", "ExternalServiceId", "Tags", "IsFolder", "InheritedParentalRatingValue", "InheritedParentalRatingSubValue", "UnratedType", "CriticRating", "CleanName", "PresentationUniqueKey", "OriginalTitle", "PrimaryVersionId", "DateLastMediaAdded", "Album", "LUFS", "NormalizationGain", "IsVirtualItem", "SeriesName", "SeasonName", "ExternalSeriesId", "Tagline", "ProductionLocations", "ExtraIds", "TotalBitrate", "ExtraType", "Artists", "AlbumArtists", "ExternalId", "SeriesPresentationUniqueKey", "ShowId", "OwnerId", "Width", "Height", "Size", "Audio", "ParentId", "TopParentId", "SeasonId", "SeriesId"
**C# Expected (73 snake_case columns):**
id, type, data, path, start_date, end_date, channel_id, is_movie, community_rating, custom_rating, index_number, is_locked, name, official_rating, media_type, overview, parent_index_number, premiere_date, production_year, genres, sort_name, forced_sort_name, run_time_ticks, date_created, date_modified, is_series, episode_title, is_repeat, preferred_metadata_language, preferred_metadata_country_code, date_last_refreshed, date_last_saved, is_in_mixed_folder, studios, external_service_id, tags, is_folder, inherited_parental_rating_value, inherited_parental_rating_sub_value, unrated_type, critic_rating, clean_name, presentation_unique_key, original_title, primary_version_id, date_last_media_added, album, lufs, normalization_gain, is_virtual_item, series_name, season_name, external_series_id, tagline, production_locations, extra_ids, total_bitrate, extra_type, artists, album_artists, external_id, series_presentation_unique_key, show_id, owner_id, width, height, size, audio, parent_id, top_parent_id, season_id, series_id
---
### USERS Schema (4 tables)
| Entity | SQL Table Name | C# Expected Name | Columns Match? | Status |
|--------|----------------|------------------|-----------------|--------|
| AccessSchedule | `"AccessSchedules"` | `access_schedules` | NO (5 cols) | ❌ |
| Permission | `"Permissions"` | `permissions` | NO (6 cols) | ❌ |
| Preference | `"Preferences"` | `preferences` | NO (6 cols) | ❌ |
| User | `"Users"` | `users` | NO (31 cols) | ❌ |
#### AccessSchedule
**SQL Columns:** "Id", "UserId", "DayOfWeek", "StartHour", "EndHour"
**C# Expected:** id, user_id, day_of_week, start_hour, end_hour
#### Permission
**SQL Columns:** "Id", "UserId", "Kind", "Value", "RowVersion", "Permission_Permissions_Guid"
**C# Expected:** id, user_id, kind, value, row_version, permission_permissions_guid
#### Preference
**SQL Columns:** "Id", "UserId", "Kind", "Value", "RowVersion", "Preference_Preferences_Guid"
**C# Expected:** id, user_id, kind, value, row_version, preference_preferences_guid
#### User (31 columns)
**SQL Columns:**
"Id", "Username", "Password", "MustUpdatePassword", "AudioLanguagePreference", "AuthenticationProviderId", "PasswordResetProviderId", "InvalidLoginAttemptCount", "LastActivityDate", "LastLoginDate", "LoginAttemptsBeforeLockout", "MaxActiveSessions", "SubtitleMode", "PlayDefaultAudioTrack", "SubtitleLanguagePreference", "DisplayMissingEpisodes", "DisplayCollectionsView", "EnableLocalPassword", "HidePlayedInLatest", "RememberAudioSelections", "RememberSubtitleSelections", "EnableNextEpisodeAutoPlay", "EnableAutoLogin", "EnableUserPreferenceAccess", "MaxParentalRatingScore", "MaxParentalRatingSubScore", "RemoteClientBitrateLimit", "InternalId", "SyncPlayAccess", "CastReceiverId", "RowVersion"
**C# Expected (31 snake_case columns):**
id, username, password, must_update_password, audio_language_preference, authentication_provider_id, password_reset_provider_id, invalid_login_attempt_count, last_activity_date, last_login_date, login_attempts_before_lockout, max_active_sessions, subtitle_mode, play_default_audio_track, subtitle_language_preference, display_missing_episodes, display_collections_view, enable_local_password, hide_played_in_latest, remember_audio_selections, remember_subtitle_selections, enable_next_episode_auto_play, enable_auto_login, enable_user_preference_access, max_parental_rating_score, max_parental_rating_sub_score, remote_client_bitrate_limit, internal_id, sync_play_access, cast_receiver_id, row_version
---
## Statistics
- **Total Tables:** 31
- **Mismatched Table Names:** 31 (100%)
- **Total Columns Affected:** 200+ columns
- **Severity Level:** CRITICAL - Application will not run
### Breakdown by Schema
| Schema | Total Tables | Mismatched | Correct | Mismatch % |
|--------|--------------|-----------|---------|-----------|
| activitylog | 1 | 1 | 0 | 100% |
| authentication | 3 | 3 | 0 | 100% |
| displaypreferences | 4 | 4 | 0 | 100% |
| library | 15 | 15 | 0 | 100% |
| users | 4 | 4 | 0 | 100% |
| **TOTAL** | **31** | **31** | **0** | **100%** |
---
## Column Naming Convention Samples
The SnakeCaseNamingConvention applies these transformations:
### Examples from BaseItems Table
| Original (PascalCase) | Expected (snake_case) | SQL Has | Match? |
|----------------------|----------------------|---------|--------|
| Id | id | "Id" | ❌ |
| IsMovie | is_movie | "IsMovie" | ❌ |
| CommunityRating | community_rating | "CommunityRating" | ❌ |
| DateCreated | date_created | "DateCreated" | ❌ |
| IsVirtualItem | is_virtual_item | "IsVirtualItem" | ❌ |
| PresentationUniqueKey | presentation_unique_key | "PresentationUniqueKey" | ❌ |
| SeriesPresentationUniqueKey | series_presentation_unique_key | "SeriesPresentationUniqueKey" | ❌ |
| PreferredMetadataLanguage | preferred_metadata_language | "PreferredMetadataLanguage" | ❌ |
| TopParentId | top_parent_id | "TopParentId" | ❌ |
**Conversion Rule (from SnakeCaseNamingConvention.cs):**
```
DateLastMediaAdded → date_last_media_added
PreferredMetadataCountryCode → preferred_metadata_country_code
InheritedParentalRatingSubValue → inherited_parental_rating_sub_value
```
---
## Impact Timeline
### On Application Startup
1. ✓ Database connection succeeds
2. ✓ Migration history table found (`__EFMigrationsHistory`)
3. ✓ Migrations complete
4. ❌ First query to ActivityLog fails: `relation "activity_logs" does not exist`
5. ❌ Application crashes
### Error Messages You Will See
```
Npgsql.PostgresException: 42P01: relation "activity_logs" does not exist
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong()
at [Your Entity Query Code]
```
### Affected Operations
- ✓ Schema creation works (uses explicit table names in SQL)
- ❌ Any LINQ query fails
- ❌ Any repository method fails
- ❌ Any DbContext.Set<T>.ToList() fails
- ❌ Bulk operations fail
- ❌ Migrations that query tables fail
---
## Code Configuration References
### PostgresDatabaseProvider.cs (Lines 763-823)
Table mapping that creates the mismatch:
```csharp
public void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ActivityLog>().ToTable("activity_logs", Schemas.ActivityLog);
modelBuilder.Entity<ApiKey>().ToTable("api_keys", Schemas.Authentication);
// ... 31 mappings total ...
}
```
### SnakeCaseNamingConvention.cs (Lines 25-30)
Column name conversion that creates column mismatches:
```csharp
public void ProcessPropertyAdded(
IConventionPropertyBuilder propertyBuilder,
IConventionContext<IConventionPropertyBuilder> context)
{
propertyBuilder.HasColumnName(ToSnakeCase(propertyBuilder.Metadata.Name));
}
```
---
**Generated:** 2025-05-01
**Severity:** 🔴 CRITICAL - BLOCKING
**Action Required:** Immediate resolution needed before application can run
+335
View File
@@ -0,0 +1,335 @@
# Visual Schema Mismatch Comparison
## Side-by-Side Comparison Examples
### Example 1: ActivityLog Entity
```
┌─────────────────────────────────────────────────────────────────────────┐
│ ACTIVITYLOG Schema Mismatch │
├────────────────────────────────┬────────────────────────────────────────┤
│ SQL Schema File │ C# Code Configuration │
├────────────────────────────────┼────────────────────────────────────────┤
│ CREATE TABLE │ modelBuilder │
│ activitylog."ActivityLogs" │ .Entity<ActivityLog>() │
│ ( │ .ToTable("activity_logs", │
│ "Id" integer, │ Schemas.ActivityLog); │
│ "Name" varchar(512), │ │
│ "Overview" varchar(512), │ // Column Mapping (via │
│ "ShortOverview" varchar, │ // SnakeCaseNamingConvention) │
│ "Type" varchar(256), │ │
│ "UserId" uuid, │ public class ActivityLog │
│ "ItemId" varchar(256), │ { │
│ "DateCreated" timestamp, │ public int Id { get; set; } │
│ "LogSeverity" integer, │ public string Name { get; set; } │
│ "RowVersion" bigint │ public string Overview { get; set; } │
│ ); │ public string ShortOverview { get; } │
│ │ public string Type { get; set; } │
│ │ public Guid UserId { get; set; } │
│ │ public string ItemId { get; set; } │
│ │ public DateTime DateCreated { get; } │
│ │ public int LogSeverity { get; set; } │
│ │ public long RowVersion { get; set; } │
│ │ } │
└────────────────────────────────┴────────────────────────────────────────┘
QUERY EXECUTION FLOW:
┌─────────────────────────────────────────────────────────────────┐
│ C# Code: await context.ActivityLogs.ToListAsync(); │
├─────────────────────────────────────────────────────────────────┤
│ EF Core Translates To: │
│ SELECT * FROM activitylog.activity_logs │
│ (Lower case! ─────────────────) │
├─────────────────────────────────────────────────────────────────┤
│ PostgreSQL Searches For: activitylog.activity_logs │
│ PostgreSQL Finds: activitylog."ActivityLogs" │
│ (Quoted PascalCase!) │
├─────────────────────────────────────────────────────────────────┤
│ RESULT: ERROR: relation "activity_logs" does not exist │
│ ERROR CODE: 42P01 (UNDEFINED TABLE) │
└─────────────────────────────────────────────────────────────────┘
```
---
### Example 2: BaseItem Entity (Most Complex)
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ LIBRARY.BaseItems Schema Mismatch (73 Columns!) │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ SQL Schema Creates: │
│ CREATE TABLE library."BaseItems" ( │
│ "Id" uuid PRIMARY KEY, │
│ "Type" text NOT NULL, │
│ "IsMovie" boolean NOT NULL, │
│ "DateCreated" timestamp with time zone NOT NULL, │
│ "DateModified" timestamp with time zone NOT NULL, │
│ ... 68 more columns all in QUOTED PASCALCASE ... │
│ ); │
│ │
│ C# Code Expects: │
│ SELECT │
│ id, │
│ type, │
│ is_movie, ← Converted from IsMovie │
│ date_created, ← Converted from DateCreated │
│ date_modified, ← Converted from DateModified │
│ ... 68 more columns in snake_case ... │
│ FROM library.base_items; ← Also lowercase table! │
│ │
│ PostgreSQL Receives Query: │
│ SELECT id, type, is_movie, date_created, ... FROM library.base_items │
│ ↓ │
│ Searches for columns: id, type, is_movie, date_created │
│ (all lowercase) │
│ ↓ │
│ But table only has: "Id", "Type", "IsMovie", "DateCreated" │
│ (all QUOTED PASCALCASE) │
│ ↓ │
│ ERROR: column "id" does not exist │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
```
---
## Mismatch Pattern Visualization
```
SCHEMA LAYER:
┌─────────────────────────────────────────────────────────────────────────┐
│ PostgreSQL Database │
│ │
│ Schema: library │
│ ├── Table: "BaseItems" (quoted identifier - preserves case) │
│ │ ├── Column: "Id" │
│ │ ├── Column: "Type" │
│ │ ├── Column: "IsMovie" │
│ │ ├── Column: "DateCreated" │
│ │ └── ... 70 more columns in QUOTED PASCALCASE │
│ │ │
│ ├── Table: "ActivityLogs" (PascalCase) │
│ ├── Table: "ApiKeys" (PascalCase) │
│ ├── Table: "Devices" (PascalCase) │
│ └── ... 28 more tables in QUOTED PASCALCASE │
└─────────────────────────────────────────────────────────────────────────┘
↕ MISMATCH!
APPLICATION LAYER:
┌─────────────────────────────────────────────────────────────────────────┐
│ C# / EF Core │
│ │
│ SnakeCaseNamingConvention ENABLED │
│ ├── Entity: BaseItemEntity │
│ │ ├── Property: Id → Column: id (lowercase) │
│ │ ├── Property: Type → Column: type (lowercase) │
│ │ ├── Property: IsMovie → Column: is_movie (snake_case) │
│ │ ├── Property: DateCreated → Column: date_created (snake_case) │
│ │ └── ... 70 more properties → SNAKE_CASE COLUMNS │
│ │ │
│ ├── Entity: ActivityLog → Table: activity_logs (lowercase) │
│ ├── Entity: ApiKey → Table: api_keys (snake_case) │
│ ├── Entity: Device → Table: devices (lowercase) │
│ └── ... 28 more entities → SNAKE_CASE TABLE NAMES │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## PostgreSQL Identifier Resolution
```
How PostgreSQL resolves identifiers:
WITHOUT quotes: my_table
├─ Converted to: my_table (lowercase by default)
├─ Searches for: my_table
└─ Result: ✓ Found
WITH quotes: "MyTable"
├─ NOT converted
├─ Searches for: MyTable (case-sensitive!)
└─ Result: ✓ Found (only if exact case matches)
CURRENT SITUATION:
┌────────────────────────────────────────────────────────────┐
│ SQL Definition: library."BaseItems" │
│ ↑ │
│ EF Core Query: SELECT * FROM library.base_items │
│ ↑ │
│ PostgreSQL Logic: │
│ - Sees: "base_items" (unquoted, lowercase by default) │
│ - Searches for: base_items │
│ - Available table: "BaseItems" (quoted PascalCase) │
│ - Match? NO ❌ │
│ │
│ ERROR 42P01: relation "base_items" does not exist │
└────────────────────────────────────────────────────────────┘
```
---
## Resolution Decision Tree
```
┌─── DECISION: How to Fix the Mismatch? ───────┐
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Option 1: Update SQL to Lowercase │ │
│ ├─────────────────────────────────────────┤ │
│ │ Action: │ │
│ │ ✓ Regenerate create_database_schema.sql│ │
│ │ ✓ Convert all "TableName" → table_name │ │
│ │ ✓ Convert all "ColumnName" → column_name│ │
│ │ ✓ Remove double quotes │ │
│ │ │ │
│ │ Pros: │ │
│ │ ✓ Matches PostgreSQL best practices │ │
│ │ ✓ Matches C# configuration (recommended)│ │
│ │ ✓ Cleaner, more maintainable │ │
│ │ │ │
│ │ Cons: │ │
│ │ ✗ Requires full database migration │ │
│ │ ✗ Data loss risk if not done carefully│ │
│ │ ✗ Downtime required │ │
│ │ │ │
│ │ Effort: HIGH │ │
│ │ Risk: HIGH │ │
│ │ Recommendation: ⭐ BEST LONG-TERM │ │
│ └─────────────────────────────────────────┘ │
│ ↓ (Recommended) │
│ ┌─────────────────────────────────────────┐ │
│ │ Option 2: Update C# to PascalCase │ │
│ ├─────────────────────────────────────────┤ │
│ │ Action: │ │
│ │ ✓ Remove SnakeCaseNamingConvention │ │
│ │ ✓ Update all ToTable() mappings │ │
│ │ ✓ Update all HasColumnName() configs │ │
│ │ │ │
│ │ Pros: │ │
│ │ ✓ Quick fix (code changes only) │ │
│ │ ✓ No database changes needed │ │
│ │ ✓ No data loss │ │
│ │ │ │
│ │ Cons: │ │
│ │ ✗ Violates PostgreSQL conventions │ │
│ │ ✗ Harder to maintain │ │
│ │ ✗ Non-standard naming scheme │ │
│ │ │ │
│ │ Effort: MEDIUM │ │
│ │ Risk: LOW │ │
│ │ Recommendation: ⚠️ QUICK FIX │ │
│ └─────────────────────────────────────────┘ │
│ ↓ (Not Recommended) │
│ ┌─────────────────────────────────────────┐ │
│ │ Option 3: Custom Naming Convention │ │
│ ├─────────────────────────────────────────┤ │
│ │ Action: │ │
│ │ ✓ Create hybrid convention │ │
│ │ ✓ Recognize existing table patterns │ │
│ │ │ │
│ │ Pros: │ │
│ │ ✓ Works with existing database │ │
│ │ ✓ Minimal code changes │ │
│ │ │ │
│ │ Cons: │ │
│ │ ✗ Very complex to implement │ │
│ │ ✗ Difficult to maintain │ │
│ │ ✗ Fragile (pattern-dependent) │ │
│ │ ✗ Non-standard solution │ │
│ │ │ │
│ │ Effort: VERY HIGH │ │
│ │ Risk: VERY HIGH │ │
│ │ Recommendation: ❌ NOT RECOMMENDED │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
```
---
## Column Naming Rule Examples
```
SnakeCaseNamingConvention applies these rules:
(From SnakeCaseNamingConvention.cs - Lines 35-40)
Input (C# Property) → Output (SQL Column) → SQL Actually Has
──────────────────────────────────────────────────────────────────────────
Id → id → "Id" ❌
UserId → user_id → "UserId" ❌
DateCreated → date_created → "DateCreated" ❌
IsMovie → is_movie → "IsMovie" ❌
DvVersionMajor → dv_version_major → "DvVersionMajor"❌
SeriesPresentationUniqueKey → series_presentation_unique_key → "SeriesPresentationUniqueKey" ❌
The Regex Rules Applied:
┌─────────────────────────────────────────────────────────────────┐
│ Rule 1: ([A-Z]+)([A-Z][a-z]) → "$1_$2" │
│ Example: "HTTPServer" → "HTTP_Server" │
│ │
│ Rule 2: ([a-z\d])([A-Z]) → "$1_$2" │
│ Example: "DateCreated" → "Date_Created" │
│ │
│ Rule 3: Convert to lowercase (.ToLowerInvariant()) │
│ Example: "Date_Created" → "date_created" │
└─────────────────────────────────────────────────────────────────┘
```
---
## Query Execution Failure Timeline
```
STARTUP SEQUENCE:
1. ✓ 0ms - Connection opened
2. ✓ 50ms - Migration history table checked (__EFMigrationsHistory exists)
3. ✓ 150ms - Migrations applied
4. ✓ 500ms - Application configuration loaded
5. ✓ 1000ms - First request received
6. ❌ 1050ms - Query executes: SELECT * FROM activitylog.activity_logs
ERROR: 42P01 - relation "activity_logs" does not exist
7. 💥 1051ms - Application crashes
STACK TRACE:
at Npgsql.NpgsqlDataReader.NextResult()
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader()
at Microsoft.EntityFrameworkCore.Query.QueryingEnumerable`1.Enumerator.MoveNext()
at System.Linq.Enumerable.ToList() // User called .ToList() or .ToListAsync()
at YourRepositoryClass.GetActivityLogs() in YourRepository.cs:line X
```
---
## File Locations Summary
```
AFFECTED FILES:
SQL Schema:
└─ Jellyfin.Server/sql/schema_init/create_database_schema.sql
Lines: 92-886 (31 tables with PascalCase names)
C# Configuration:
├─ src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/
│ └─ PostgresDatabaseProvider.cs
│ Lines 763-823: OnModelCreating() - Table mappings
│ Lines 872-875: ConfigureConventions() - SnakeCaseNamingConvention
├─ src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/
│ └─ SnakeCaseNamingConvention.cs
│ Lines 16-30: Column name conversion logic
└─ src/Jellyfin.Database/Jellyfin.Database.Implementations/
└─ ModelConfiguration/
├─ ActivityLogConfiguration.cs
├─ ApiKeyConfiguration.cs
├─ DeviceConfiguration.cs
└─ ... (31 configuration files total)
```
---
**Visual Reference Generated:** 2025-05-01
**Status:** Ready for review and decision
+493
View File
@@ -0,0 +1,493 @@
# PostgreSQL Database Schema Mismatch Report
**Generated:** 2025-05-01
**Status:** ⚠️ CRITICAL MISMATCH FOUND
**Severity:** HIGH - Code cannot query database tables correctly
---
## Executive Summary
There is a **critical mismatch** between:
1. **SQL Schema File** (`create_database_schema.sql`) - Uses **PascalCase** table and column names with double quotes
2. **C# Code Configuration** (`PostgresDatabaseProvider.cs`) - Expects **snake_case** table names with SnakeCaseNamingConvention for columns
This will cause **runtime failures** when EF Core tries to query tables that don't exist at the expected snake_case names.
---
## Detailed Mismatch Analysis
### ACTIVITYLOG Schema
#### SQL Definition:
```sql
CREATE TABLE activitylog."ActivityLogs" (
"Id" integer NOT NULL,
"Name" character varying(512) NOT NULL,
"Overview" character varying(512),
"ShortOverview" character varying(512),
"Type" character varying(256) NOT NULL,
"UserId" uuid NOT NULL,
"ItemId" character varying(256),
"DateCreated" timestamp with time zone NOT NULL,
"LogSeverity" integer NOT NULL,
"RowVersion" bigint NOT NULL
);
```
#### C# Code Mapping:
```csharp
modelBuilder.Entity<ActivityLog>().ToTable("activity_logs", Schemas.ActivityLog);
```
#### Mismatch Details:
| Aspect | SQL Schema | C# Code | Status |
|--------|-----------|---------|--------|
| Table Name | `"ActivityLogs"` | `activity_logs` | ❌ MISMATCH |
| Column: Id | `"Id"` | `id` (via convention) | ❌ MISMATCH |
| Column: Name | `"Name"` | `name` (via convention) | ❌ MISMATCH |
| Column: Overview | `"Overview"` | `overview` (via convention) | ❌ MISMATCH |
| Column: ShortOverview | `"ShortOverview"` | `short_overview` (via convention) | ❌ MISMATCH |
| Column: Type | `"Type"` | `type` (via convention) | ❌ MISMATCH |
| Column: UserId | `"UserId"` | `user_id` (via convention) | ❌ MISMATCH |
| Column: ItemId | `"ItemId"` | `item_id` (via convention) | ❌ MISMATCH |
| Column: DateCreated | `"DateCreated"` | `date_created` (via convention) | ❌ MISMATCH |
| Column: LogSeverity | `"LogSeverity"` | `log_severity` (via convention) | ❌ MISMATCH |
| Column: RowVersion | `"RowVersion"` | `row_version` (via convention) | ❌ MISMATCH |
**Root Cause:** Table name is PascalCase; columns are PascalCase but SnakeCaseNamingConvention converts them to snake_case.
---
### AUTHENTICATION Schema
#### APIKEYS Table
**SQL Definition:**
```sql
CREATE TABLE authentication."ApiKeys" (
"Id" integer NOT NULL,
"DateCreated" timestamp with time zone NOT NULL,
"DateLastActivity" timestamp with time zone NOT NULL,
"Name" character varying(64) NOT NULL,
"AccessToken" text NOT NULL
);
```
**C# Mapping:**
```csharp
modelBuilder.Entity<ApiKey>().ToTable("api_keys", Schemas.Authentication);
```
**Mismatch:** Table name `"ApiKeys"` vs `api_keys`
---
#### DEVICEOPTIONS Table
**SQL Definition:**
```sql
CREATE TABLE authentication."DeviceOptions" (
"Id" integer NOT NULL,
"DeviceId" text NOT NULL,
"CustomName" text
);
```
**C# Mapping:**
```csharp
modelBuilder.Entity<DeviceOptions>().ToTable("device_options", Schemas.Authentication);
```
**Mismatch:** Table name `"DeviceOptions"` vs `device_options`
---
#### DEVICES Table
**SQL Definition:**
```sql
CREATE TABLE authentication."Devices" (
"Id" integer NOT NULL,
"UserId" uuid NOT NULL,
"AccessToken" text NOT NULL,
"AppName" character varying(64) NOT NULL,
"AppVersion" character varying(32) NOT NULL,
"DeviceName" character varying(64) NOT NULL,
"DeviceId" character varying(256) NOT NULL,
"IsActive" boolean NOT NULL,
"DateCreated" timestamp with time zone NOT NULL,
"DateModified" timestamp with time zone NOT NULL,
"DateLastActivity" timestamp with time zone NOT NULL
);
```
**C# Mapping:**
```csharp
modelBuilder.Entity<Device>().ToTable("devices", Schemas.Authentication);
```
**Mismatch:** Table name `"Devices"` vs `devices`
---
### DISPLAYPREFERENCES Schema
#### CUSTOMITEMDISPLAYPREFERENCES Table
**SQL:** `"CustomItemDisplayPreferences"`
**C# Code:** `custom_item_display_preferences`
**Mismatch:**
#### DISPLAYPREFERENCES Table
**SQL:** `"DisplayPreferences"`
**C# Code:** `display_preferences`
**Mismatch:**
#### HOMESECTION Table
**SQL:** `"HomeSection"`
**C# Code:** `home_sections`
**Mismatch:** ❌ (Plural mismatch too!)
#### ITEMDISPLAYPREFERENCES Table
**SQL:** `"ItemDisplayPreferences"`
**C# Code:** `item_display_preferences`
**Mismatch:**
---
### LIBRARY Schema (Most Critical - 15+ Tables)
#### ANCESTORIDS Table
**SQL:** `"AncestorIds"`
**C# Code:** `ancestor_ids`
**Mismatch:**
#### ATTACHMENTSTREAMINFOS Table
**SQL:** `"AttachmentStreamInfos"`
**C# Code:** `attachment_stream_infos`
**Mismatch:**
#### BASEITEMIMAGINFOS Table
**SQL:** `"BaseItemImageInfos"`
**C# Code:** `base_item_image_infos`
**Mismatch:**
#### BASEITEMMETADATAFIELDS Table
**SQL:** `"BaseItemMetadataFields"`
**C# Code:** `base_item_metadata_fields`
**Mismatch:**
#### BASEITEMPROVIDERS Table
**SQL:** `"BaseItemProviders"`
**C# Code:** `base_item_providers`
**Mismatch:**
#### BASEITEMTRAILERTYPES Table
**SQL:** `"BaseItemTrailerTypes"`
**C# Code:** `base_item_trailer_types`
**Mismatch:**
#### BASEITEMS Table
**SQL:** `"BaseItems"`
**C# Code:** `base_items`
**Mismatch:**
#### CHAPTERS Table
**SQL:** `"Chapters"`
**C# Code:** `chapters`
**Mismatch:**
#### IMAGEINFOS Table
**SQL:** `"ImageInfos"`
**C# Code:** `image_infos`
**Mismatch:**
#### ITEMVALUES Table
**SQL:** `"ItemValues"`
**C# Code:** `item_values`
**Mismatch:**
#### ITEMVALUESMAP Table
**SQL:** `"ItemValuesMap"`
**C# Code:** `item_values_map`
**Mismatch:**
#### KEYFRAMEDATA Table
**SQL:** `"KeyframeData"`
**C# Code:** `keyframe_data`
**Mismatch:**
#### MEDIASEGMENTS Table
**SQL:** `"MediaSegments"`
**C# Code:** `media_segments`
**Mismatch:**
#### MEDIASTREAMINFOS Table
**SQL:** `"MediaStreamInfos"`
**C# Code:** `media_stream_infos`
**Mismatch:**
#### PEOPLEBASEITEMMAP Table
**SQL:** `"PeopleBaseItemMap"`
**C# Code:** `people_base_item_map`
**Mismatch:**
#### PEOPLES Table
**SQL:** `"Peoples"`
**C# Code:** `peoples`
**Mismatch:**
#### TRICKPLAYINFOS Table
**SQL:** `"TrickplayInfos"`
**C# Code:** `trickplay_infos`
**Mismatch:**
#### USERDATA Table
**SQL:** `"UserData"`
**C# Code:** `user_data`
**Mismatch:**
---
### USERS Schema
#### ACCESSSCHEDULES Table
**SQL:** `"AccessSchedules"`
**C# Code:** `access_schedules`
**Mismatch:**
#### PERMISSIONS Table
**SQL:** `"Permissions"`
**C# Code:** `permissions`
**Mismatch:**
#### PREFERENCES Table
**SQL:** `"Preferences"`
**C# Code:** `preferences`
**Mismatch:**
#### USERS Table
**SQL:** `"Users"`
**C# Code:** `users`
**Mismatch:**
---
### SPECIAL TABLES
#### __EFMigrationsHistory Table
**SQL Definition (Line 738):**
```sql
CREATE TABLE public."__EFMigrationsHistory" (
"MigrationId" character varying(150) NOT NULL,
"ProductVersion" character varying(32) NOT NULL
);
```
**Status:** ✓ Correct (EF Core will query `public.__EFMigrationsHistory`)
**Note:** Columns are PascalCase in SQL but EF Core's built-in history repository handles this correctly.
---
## Complete Mismatch Summary Table
| Schema | Entity | SQL Table Name | C# Expected Name | Match? |
|--------|--------|----------------|------------------|--------|
| activitylog | ActivityLog | `"ActivityLogs"` | `activity_logs` | ❌ |
| authentication | ApiKey | `"ApiKeys"` | `api_keys` | ❌ |
| authentication | Device | `"Devices"` | `devices` | ❌ |
| authentication | DeviceOptions | `"DeviceOptions"` | `device_options` | ❌ |
| displaypreferences | CustomItemDisplayPreferences | `"CustomItemDisplayPreferences"` | `custom_item_display_preferences` | ❌ |
| displaypreferences | DisplayPreferences | `"DisplayPreferences"` | `display_preferences` | ❌ |
| displaypreferences | HomeSection | `"HomeSection"` | `home_sections` | ❌ |
| displaypreferences | ItemDisplayPreferences | `"ItemDisplayPreferences"` | `item_display_preferences` | ❌ |
| library | AncestorId | `"AncestorIds"` | `ancestor_ids` | ❌ |
| library | AttachmentStreamInfo | `"AttachmentStreamInfos"` | `attachment_stream_infos` | ❌ |
| library | BaseItemImageInfo | `"BaseItemImageInfos"` | `base_item_image_infos` | ❌ |
| library | BaseItemMetadataField | `"BaseItemMetadataFields"` | `base_item_metadata_fields` | ❌ |
| library | BaseItemProvider | `"BaseItemProviders"` | `base_item_providers` | ❌ |
| library | BaseItemTrailerType | `"BaseItemTrailerTypes"` | `base_item_trailer_types` | ❌ |
| library | BaseItemEntity | `"BaseItems"` | `base_items` | ❌ |
| library | Chapter | `"Chapters"` | `chapters` | ✓ |
| library | ImageInfo | `"ImageInfos"` | `image_infos` | ❌ |
| library | ItemValue | `"ItemValues"` | `item_values` | ❌ |
| library | ItemValueMap | `"ItemValuesMap"` | `item_values_map` | ❌ |
| library | KeyframeData | `"KeyframeData"` | `keyframe_data` | ❌ |
| library | MediaSegment | `"MediaSegments"` | `media_segments` | ❌ |
| library | MediaStreamInfo | `"MediaStreamInfos"` | `media_stream_infos` | ❌ |
| library | PeopleBaseItemMap | `"PeopleBaseItemMap"` | `people_base_item_map` | ❌ |
| library | People | `"Peoples"` | `peoples` | ✓ |
| library | TrickplayInfo | `"TrickplayInfos"` | `trickplay_infos` | ❌ |
| library | UserData | `"UserData"` | `user_data` | ❌ |
| users | AccessSchedule | `"AccessSchedules"` | `access_schedules` | ❌ |
| users | Permission | `"Permissions"` | `permissions` | ✓ |
| users | Preference | `"Preferences"` | `preferences` | ✓ |
| users | User | `"Users"` | `users` | ✓ |
| public | __EFMigrationsHistory | `"__EFMigrationsHistory"` | `__EFMigrationsHistory` | ✓ |
**Summary:**
-**31 table mismatches found**
-**5 tables with correct lowercase names**
- **96% tables have naming conflicts**
---
## Impact Analysis
### When EF Core Runs
1. **Query Execution:** EF Core generates queries using snake_case table names
```sql
SELECT * FROM library.base_items -- EF Core generates this
```
2. **Database Response:** PostgreSQL looks for table named `base_items` in lowercase
3. **Result:** Table not found because actual table is `"BaseItems"` (quoted PascalCase)
```
ERROR: relation "base_items" does not exist
```
### Error Pattern You Will See
```
Npgsql.PostgresException (0x80004005): 42P01: relation "activity_logs" does not exist
Npgsql.PostgresException (0x80004005): 42P01: relation "api_keys" does not exist
Npgsql.PostgresException (0x80004005): 42P01: relation "base_items" does not exist
```
---
## Root Cause Analysis
### The Problem
The SQL schema file was generated from a `pg_dump` of an existing database that uses **PascalCase with double quotes** for case preservation. However, the C# code was configured to map to **lowercase snake_case** table names.
### Configuration Details
**PostgresDatabaseProvider.cs Line 763-819:**
```csharp
public void OnModelCreating(ModelBuilder modelBuilder)
{
// ... example mappings ...
modelBuilder.Entity<ActivityLog>().ToTable("activity_logs", Schemas.ActivityLog);
modelBuilder.Entity<ApiKey>().ToTable("api_keys", Schemas.Authentication);
modelBuilder.Entity<Device>().ToTable("devices", Schemas.Authentication);
// ... etc ...
}
```
**PostgresDatabaseProvider.cs Line 872-875:**
```csharp
public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new SnakeCaseNamingConvention());
}
```
**SnakeCaseNamingConvention.cs:**
- Converts all PascalCase property names to snake_case column names
- Example: `DateCreated` property → `date_created` column
---
## Recommended Resolution
### Option 1: Update SQL Schema to Use Lowercase (RECOMMENDED)
- **Pros:** Matches PostgreSQL best practices, cleaner, matches C# configuration
- **Cons:** Requires recreating all tables
- **Effort:** High (full database migration required)
### Option 2: Update C# Code to Match PascalCase SQL
- **Pros:** Quick fix, no database changes needed
- **Cons:** Goes against PostgreSQL conventions, harder to maintain
- **Effort:** Low (code changes only)
### Option 3: Custom Naming Convention
- Create a hybrid convention that recognizes existing PascalCase tables
- **Pros:** Can work with existing database
- **Cons:** Complex, non-standard, harder to maintain
- **Effort:** Very High (custom infrastructure needed)
---
## Files Affected
### C# Code Files
1. `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs`
- Lines 763-823 (OnModelCreating table mappings)
2. `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/SnakeCaseNamingConvention.cs`
- Controls column name conversion
3. All Entity Configuration files in:
- `src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/`
### Database Schema Files
1. `Jellyfin.Server/sql/schema_init/create_database_schema.sql`
- Lines 92-886 (all table definitions)
---
## Next Steps
1. **Decision Required:** Which resolution option do you prefer?
- Option 1: Update SQL to lowercase (Recommended)
- Option 2: Update C# to PascalCase
- Option 3: Create custom convention
2. **If Option 1:** Generate a comprehensive SQL migration script
3. **If Option 2:** Generate C# code changes
4. **If Option 3:** Design custom naming convention
---
## Appendix: Column Name Conversion Examples
The SnakeCaseNamingConvention converts column names as follows:
| Original (C#) | Converted (SQL) | Status |
|---------------|-----------------|--------|
| Id | id | ✓ |
| UserId | user_id | ✓ |
| DateCreated | date_created | ✓ |
| LogSeverity | log_severity | ✓ |
| AccessToken | access_token | ✓ |
| APIKey | api_key | ✓ |
| DVVersionMajor | dv_version_major | ✓ |
**Note:** All column names in SQL schema are PascalCase (e.g., `"UserId"`, `"DateCreated"`), but C# code expects them in snake_case (e.g., `user_id`, `date_created`).
---
**Report Generated:** 2025-05-01
**Status:** Awaiting decision on resolution approach
+478
View File
@@ -0,0 +1,478 @@
# SQL Column Conversion Reference - All Tables
## Activity Log Schema
### activity_logs (activitylog."ActivityLogs")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"Name"` | `name` |
| `"Overview"` | `overview` |
| `"ShortOverview"` | `short_overview` |
| `"Type"` | `type` |
| `"UserId"` | `user_id` |
| `"ItemId"` | `item_id` |
| `"DateCreated"` | `date_created` |
| `"LogSeverity"` | `log_severity` |
| `"RowVersion"` | `row_version` |
---
## Authentication Schema
### api_keys (authentication."ApiKeys")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"DateCreated"` | `date_created` |
| `"DateLastActivity"` | `date_last_activity` |
| `"Name"` | `name` |
| `"AccessToken"` | `access_token` |
### devices (authentication."Devices")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"AccessToken"` | `access_token` |
| `"AppName"` | `app_name` |
| `"AppVersion"` | `app_version` |
| `"DeviceName"` | `device_name` |
| `"DeviceId"` | `device_id` |
| `"IsActive"` | `is_active` |
| `"DateCreated"` | `date_created` |
| `"DateModified"` | `date_modified` |
| `"DateLastActivity"` | `date_last_activity` |
### device_options (authentication."DeviceOptions")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"DeviceId"` | `device_id` |
| `"CustomName"` | `custom_name` |
---
## Display Preferences Schema
### display_preferences (displaypreferences."DisplayPreferences")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"ItemId"` | `item_id` |
| `"Client"` | `client` |
| `"ShowSidebar"` | `show_sidebar` |
| `"ShowBackdrop"` | `show_backdrop` |
| `"ScrollDirection"` | `scroll_direction` |
| `"IndexBy"` | `index_by` |
| `"SkipForwardLength"` | `skip_forward_length` |
| `"SkipBackwardLength"` | `skip_backward_length` |
| `"ChromecastVersion"` | `chromecast_version` |
| `"EnableNextVideoInfoOverlay"` | `enable_next_video_info_overlay` |
| `"DashboardTheme"` | `dashboard_theme` |
| `"TvHome"` | `tv_home` |
### item_display_preferences (displaypreferences."ItemDisplayPreferences")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"ItemId"` | `item_id` |
| `"Client"` | `client` |
| `"ViewType"` | `view_type` |
| `"RememberIndexing"` | `remember_indexing` |
| `"IndexBy"` | `index_by` |
| `"RememberSorting"` | `remember_sorting` |
| `"SortBy"` | `sort_by` |
| `"SortOrder"` | `sort_order` |
### custom_item_display_preferences (displaypreferences."CustomItemDisplayPreferences")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"ItemId"` | `item_id` |
| `"Client"` | `client` |
| `"Key"` | `key` |
| `"Value"` | `value` |
### home_sections (displaypreferences."HomeSection")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"DisplayPreferencesId"` | `display_preferences_id` |
| `"Order"` | `order` |
| `"Type"` | `type` |
---
## Library Schema
### base_items (library."BaseItems") - 73 columns
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"Type"` | `type` |
| `"Data"` | `data` |
| `"Path"` | `path` |
| `"StartDate"` | `start_date` |
| `"EndDate"` | `end_date` |
| `"ChannelId"` | `channel_id` |
| `"IsMovie"` | `is_movie` |
| `"CommunityRating"` | `community_rating` |
| `"CustomRating"` | `custom_rating` |
| `"IndexNumber"` | `index_number` |
| `"IsLocked"` | `is_locked` |
| `"Name"` | `name` |
| `"OfficialRating"` | `official_rating` |
| `"MediaType"` | `media_type` |
| `"Overview"` | `overview` |
| `"ParentIndexNumber"` | `parent_index_number` |
| `"PremiereDate"` | `premiere_date` |
| `"ProductionYear"` | `production_year` |
| `"Genres"` | `genres` |
| `"SortName"` | `sort_name` |
| `"ForcedSortName"` | `forced_sort_name` |
| `"RunTimeTicks"` | `run_time_ticks` |
| `"DateCreated"` | `date_created` |
| `"DateModified"` | `date_modified` |
| `"IsSeries"` | `is_series` |
| `"EpisodeTitle"` | `episode_title` |
| `"IsRepeat"` | `is_repeat` |
| `"PreferredMetadataLanguage"` | `preferred_metadata_language` |
| `"PreferredMetadataCountryCode"` | `preferred_metadata_country_code` |
| `"DateLastRefreshed"` | `date_last_refreshed` |
| `"DateLastSaved"` | `date_last_saved` |
| `"IsInMixedFolder"` | `is_in_mixed_folder` |
| `"Studios"` | `studios` |
| `"ExternalServiceId"` | `external_service_id` |
| `"Tags"` | `tags` |
| `"IsFolder"` | `is_folder` |
| `"InheritedParentalRatingValue"` | `inherited_parental_rating_value` |
| `"InheritedParentalRatingSubValue"` | `inherited_parental_rating_sub_value` |
| `"UnratedType"` | `unrated_type` |
| `"CriticRating"` | `critic_rating` |
| `"CleanName"` | `clean_name` |
| `"PresentationUniqueKey"` | `presentation_unique_key` |
| `"OriginalTitle"` | `original_title` |
| `"PrimaryVersionId"` | `primary_version_id` |
| `"DateLastMediaAdded"` | `date_last_media_added` |
| `"Album"` | `album` |
| `"LUFS"` | `lufs` |
| `"NormalizationGain"` | `normalization_gain` |
| `"IsVirtualItem"` | `is_virtual_item` |
| `"SeriesName"` | `series_name` |
| `"SeasonName"` | `season_name` |
| `"ExternalSeriesId"` | `external_series_id` |
| `"Tagline"` | `tagline` |
| `"ProductionLocations"` | `production_locations` |
| `"ExtraIds"` | `extra_ids` |
| `"TotalBitrate"` | `total_bitrate` |
| `"ExtraType"` | `extra_type` |
| `"Artists"` | `artists` |
| `"AlbumArtists"` | `album_artists` |
| `"ExternalId"` | `external_id` |
| `"SeriesPresentationUniqueKey"` | `series_presentation_unique_key` |
| `"ShowId"` | `show_id` |
| `"OwnerId"` | `owner_id` |
| `"Width"` | `width` |
| `"Height"` | `height` |
| `"Size"` | `size` |
| `"Audio"` | `audio` |
| `"ParentId"` | `parent_id` |
| `"TopParentId"` | `top_parent_id` |
| `"SeasonId"` | `season_id` |
| `"SeriesId"` | `series_id` |
### chapters (library."Chapters")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemId"` | `item_id` |
| `"ChapterIndex"` | `chapter_index` |
| `"StartPositionTicks"` | `start_position_ticks` |
| `"Name"` | `name` |
| `"ImagePath"` | `image_path` |
| `"ImageDateModified"` | `image_date_modified` |
### media_stream_infos (library."MediaStreamInfos") - 51 columns
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemId"` | `item_id` |
| `"StreamIndex"` | `stream_index` |
| `"StreamType"` | `stream_type` |
| `"Codec"` | `codec` |
| `"Language"` | `language` |
| `"ChannelLayout"` | `channel_layout` |
| `"Profile"` | `profile` |
| `"AspectRatio"` | `aspect_ratio` |
| `"Path"` | `path` |
| `"IsInterlaced"` | `is_interlaced` |
| `"BitRate"` | `bit_rate` |
| `"Channels"` | `channels` |
| `"SampleRate"` | `sample_rate` |
| `"IsDefault"` | `is_default` |
| `"IsForced"` | `is_forced` |
| `"IsExternal"` | `is_external` |
| `"Height"` | `height` |
| `"Width"` | `width` |
| `"AverageFrameRate"` | `average_frame_rate` |
| `"RealFrameRate"` | `real_frame_rate` |
| `"Level"` | `level` |
| `"PixelFormat"` | `pixel_format` |
| `"BitDepth"` | `bit_depth` |
| `"IsAnamorphic"` | `is_anamorphic` |
| `"RefFrames"` | `ref_frames` |
| `"CodecTag"` | `codec_tag` |
| `"Comment"` | `comment` |
| `"NalLengthSize"` | `nal_length_size` |
| `"IsAvc"` | `is_avc` |
| `"Title"` | `title` |
| `"TimeBase"` | `time_base` |
| `"CodecTimeBase"` | `codec_time_base` |
| `"ColorPrimaries"` | `color_primaries` |
| `"ColorSpace"` | `color_space` |
| `"ColorTransfer"` | `color_transfer` |
| `"DvVersionMajor"` | `dv_version_major` |
| `"DvVersionMinor"` | `dv_version_minor` |
| `"DvProfile"` | `dv_profile` |
| `"DvLevel"` | `dv_level` |
| `"RpuPresentFlag"` | `rpu_present_flag` |
| `"ElPresentFlag"` | `el_present_flag` |
| `"BlPresentFlag"` | `bl_present_flag` |
| `"DvBlSignalCompatibilityId"` | `dv_bl_signal_compatibility_id` |
| `"IsHearingImpaired"` | `is_hearing_impaired` |
| `"Rotation"` | `rotation` |
| `"KeyFrames"` | `key_frames` |
| `"Hdr10PlusPresentFlag"` | `hdr10_plus_present_flag` |
### attachment_stream_infos (library."AttachmentStreamInfos")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemId"` | `item_id` |
| `"Index"` | `index` |
| `"Codec"` | `codec` |
| `"CodecTag"` | `codec_tag` |
| `"Comment"` | `comment` |
| `"Filename"` | `filename` |
| `"MimeType"` | `mime_type` |
### image_infos (library."ImageInfos")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"Path"` | `path` |
| `"LastModified"` | `last_modified` |
### base_item_image_infos (library."BaseItemImageInfos")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"Path"` | `path` |
| `"DateModified"` | `date_modified` |
| `"ImageType"` | `image_type` |
| `"Width"` | `width` |
| `"Height"` | `height` |
| `"Blurhash"` | `blurhash` |
| `"ItemId"` | `item_id` |
### base_item_metadata_fields (library."BaseItemMetadataFields")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"ItemId"` | `item_id` |
### base_item_providers (library."BaseItemProviders")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemId"` | `item_id` |
| `"ProviderId"` | `provider_id` |
| `"ProviderValue"` | `provider_value` |
### base_item_trailer_types (library."BaseItemTrailerTypes")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"ItemId"` | `item_id` |
### item_values (library."ItemValues")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemValueId"` | `item_value_id` |
| `"Type"` | `type` |
| `"Value"` | `value` |
| `"CleanValue"` | `clean_value` |
### item_values_map (library."ItemValuesMap")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemId"` | `item_id` |
| `"ItemValueId"` | `item_value_id` |
### peoples (library."Peoples")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"Name"` | `name` |
| `"PersonType"` | `person_type` |
### people_base_item_map (library."PeopleBaseItemMap")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Role"` | `role` |
| `"ItemId"` | `item_id` |
| `"PeopleId"` | `people_id` |
| `"SortOrder"` | `sort_order` |
| `"ListOrder"` | `list_order` |
### user_data (library."UserData")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"CustomDataKey"` | `custom_data_key` |
| `"ItemId"` | `item_id` |
| `"UserId"` | `user_id` |
| `"Rating"` | `rating` |
| `"PlaybackPositionTicks"` | `playback_position_ticks` |
| `"PlayCount"` | `play_count` |
| `"IsFavorite"` | `is_favorite` |
| `"LastPlayedDate"` | `last_played_date` |
| `"Played"` | `played` |
| `"AudioStreamIndex"` | `audio_stream_index` |
| `"SubtitleStreamIndex"` | `subtitle_stream_index` |
| `"Likes"` | `likes` |
| `"RetentionDate"` | `retention_date` |
### ancestor_ids (library."AncestorIds")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ParentItemId"` | `parent_item_id` |
| `"ItemId"` | `item_id` |
### trickplay_infos (library."TrickplayInfos")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemId"` | `item_id` |
| `"Width"` | `width` |
| `"Height"` | `height` |
| `"TileWidth"` | `tile_width` |
| `"TileHeight"` | `tile_height` |
| `"ThumbnailCount"` | `thumbnail_count` |
| `"Interval"` | `interval` |
| `"Bandwidth"` | `bandwidth` |
### media_segments (library."MediaSegments")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"ItemId"` | `item_id` |
| `"Type"` | `type` |
| `"EndTicks"` | `end_ticks` |
| `"StartTicks"` | `start_ticks` |
| `"SegmentProviderId"` | `segment_provider_id` |
### keyframe_data (library."KeyframeData")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"ItemId"` | `item_id` |
| `"TotalDuration"` | `total_duration` |
| `"KeyframeTicks"` | `keyframe_ticks` |
### library_options (library."LibraryOptions") - Note: Not yet found in schema, may need verification
Expected columns (from LibraryOptionsEntity EF mapping):
- LibraryPath (primary key, text/string)
- DateModified (timestamp with time zone)
---
## Users Schema
### users (users."Users") - 36+ columns
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"Username"` | `username` |
| `"Password"` | `password` |
| `"MustUpdatePassword"` | `must_update_password` |
| `"AudioLanguagePreference"` | `audio_language_preference` |
| `"AuthenticationProviderId"` | `authentication_provider_id` |
| `"PasswordResetProviderId"` | `password_reset_provider_id` |
| `"InvalidLoginAttemptCount"` | `invalid_login_attempt_count` |
| `"LastActivityDate"` | `last_activity_date` |
| `"LastLoginDate"` | `last_login_date` |
| `"LoginAttemptsBeforeLockout"` | `login_attempts_before_lockout` |
| `"MaxActiveSessions"` | `max_active_sessions` |
| `"SubtitleMode"` | `subtitle_mode` |
| `"PlayDefaultAudioTrack"` | `play_default_audio_track` |
| `"SubtitleLanguagePreference"` | `subtitle_language_preference` |
| `"DisplayMissingEpisodes"` | `display_missing_episodes` |
| `"DisplayCollectionsView"` | `display_collections_view` |
| `"EnableLocalPassword"` | `enable_local_password` |
| `"HidePlayedInLatest"` | `hide_played_in_latest` |
| `"RememberAudioSelections"` | `remember_audio_selections` |
| `"RememberSubtitleSelections"` | `remember_subtitle_selections` |
| `"EnableNextEpisodeAutoPlay"` | `enable_next_episode_auto_play` |
| `"EnableAutoLogin"` | `enable_auto_login` |
| `"EnableUserPreferenceAccess"` | `enable_user_preference_access` |
| `"MaxParentalRatingScore"` | `max_parental_rating_score` |
| `"MaxParentalRatingSubScore"` | `max_parental_rating_sub_score` |
| `"RemoteClientBitrateLimit"` | `remote_client_bitrate_limit` |
| `"InternalId"` | `internal_id` |
| `"SyncPlayAccess"` | `sync_play_access` |
| `"CastReceiverId"` | `cast_receiver_id` |
| `"RowVersion"` | `row_version` |
### permissions (users."Permissions")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"Kind"` | `kind` |
| `"Value"` | `value` |
| `"RowVersion"` | `row_version` |
| `"Permission_Permissions_Guid"` | `permission_permissions_guid` |
### preferences (users."Preferences")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"Kind"` | `kind` |
| `"Value"` | `value` |
| `"RowVersion"` | `row_version` |
| `"Preference_Preferences_Guid"` | `preference_preferences_guid` |
### access_schedules (users."AccessSchedules")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"Id"` | `id` |
| `"UserId"` | `user_id` |
| `"DayOfWeek"` | `day_of_week` |
| `"StartHour"` | `start_hour` |
| `"EndHour"` | `end_hour` |
---
## Special System Tables
### __ef_migrations_history (public."__EFMigrationsHistory")
| Current (SQL) | Target (snake_case) |
|--------------|-------------------|
| `"MigrationId"` | `migration_id` |
| `"ProductVersion"` | `product_version` |
---
## Summary Statistics
- **Total Application Tables**: 19
- **Total Library Schema Tables**: 19 (with BaseItems being the largest at 73 columns)
- **Total Media Stream Columns**: 51 (MediaStreamInfos is the second-largest table)
- **Total Columns Across All Tables**: ~250+
- **Sequences to Rename**: 17 (one per auto-increment table)
- **Special System Table**: 1 (`__EFMigrationsHistory`)
---
Generated: 2025-05-01
For use in create_database_schema.sql conversion from quoted PascalCase to lowercase/snake_case identifiers
+182
View File
@@ -0,0 +1,182 @@
# SQL Schema Conversion Guide - PascalCase to snake_case
## Authoritative Table Mappings (from PostgresDatabaseProvider.OnModelCreating)
### Activity Log Schema
| Current (SQL) | Target (C#/PostgreSQL) | Entity Class |
|--------------|------------------------|--------------|
| `"ActivityLogs"` | `activity_logs` | `ActivityLog` |
### Authentication Schema
| Current (SQL) | Target (C#/PostgreSQL) | Entity Class |
|--------------|------------------------|--------------|
| `"ApiKeys"` | `api_keys` | `ApiKey` |
| `"Devices"` | `devices` | `Device` |
| `"DeviceOptions"` | `device_options` | `DeviceOptions` |
### Display Preferences Schema
| Current (SQL) | Target (C#/PostgreSQL) | Entity Class |
|--------------|------------------------|--------------|
| `"DisplayPreferences"` | `display_preferences` | `DisplayPreferences` |
| `"ItemDisplayPreferences"` | `item_display_preferences` | `ItemDisplayPreferences` |
| `"CustomItemDisplayPreferences"` | `custom_item_display_preferences` | `CustomItemDisplayPreferences` |
| `"HomeSections"` | `home_sections` | `HomeSection` |
### Users Schema
| Current (SQL) | Target (C#/PostgreSQL) | Entity Class |
|--------------|------------------------|--------------|
| `"Users"` | `users` | `User` |
| `"Permissions"` | `permissions` | `Permission` |
| `"Preferences"` | `preferences` | `Preference` |
| `"AccessSchedules"` | `access_schedules` | `AccessSchedule` |
### Library Schema
| Current (SQL) | Target (C#/PostgreSQL) | Entity Class |
|--------------|------------------------|--------------|
| `"BaseItems"` | `base_items` | `BaseItemEntity` |
| `"Chapters"` | `chapters` | `Chapter` |
| `"MediaStreamInfos"` | `media_stream_infos` | `MediaStreamInfo` |
| `"AttachmentStreamInfos"` | `attachment_stream_infos` | `AttachmentStreamInfo` |
| `"ImageInfos"` | `image_infos` | `ImageInfo` |
| `"BaseItemImageInfos"` | `base_item_image_infos` | `BaseItemImageInfo` |
| `"BaseItemProviders"` | `base_item_providers` | `BaseItemProvider` |
| `"BaseItemMetadataFields"` | `base_item_metadata_fields` | `BaseItemMetadataField` |
| `"BaseItemTrailerTypes"` | `base_item_trailer_types` | `BaseItemTrailerType` |
| `"ItemValues"` | `item_values` | `ItemValue` |
| `"ItemValuesMaps"` | `item_values_map` | `ItemValueMap` |
| `"Peoples"` | `peoples` | `People` |
| `"PeopleBaseItemMaps"` | `people_base_item_map` | `PeopleBaseItemMap` |
| `"UserData"` | `user_data` | `UserData` |
| `"AncestorIds"` | `ancestor_ids` | `AncestorId` |
| `"TrickplayInfos"` | `trickplay_infos` | `TrickplayInfo` |
| `"MediaSegments"` | `media_segments` | `MediaSegment` |
| `"KeyframeData"` | `keyframe_data` | `KeyframeData` |
| `"LibraryOptions"` | `library_options` | `LibraryOptionsEntity` |
### Special Tables
| Current (SQL) | Target (C#/PostgreSQL) | Notes |
|--------------|------------------------|-------|
| `"__EFMigrationsHistory"` | `__ef_migrations_history` | System table, special handling |
### Views (Library Schema)
| Current (SQL) | Target (C#/PostgreSQL) | Entity Class |
|--------------|------------------------|--------------|
| `"media_stream_infos_v"` | `media_stream_infos_v` | `MediaStreamInfoView` (already lowercase) |
| `"video_items_v"` | `video_items_v` | `VideoItemView` (already lowercase) |
| `"user_playback_history_v"` | `user_playback_history_v` | `UserPlaybackHistoryView` (already lowercase) |
| `"item_providers_v"` | `item_providers_v` | `ItemProviderView` (already lowercase) |
| `"item_people_v"` | `item_people_v` | `ItemPersonView` (already lowercase) |
| `"library_summary_v"` | `library_summary_v` | `LibrarySummaryView` (already lowercase) |
---
## Column Naming Rules
### SnakeCaseNamingConvention Conversion Rules
```csharp
// From SnakeCaseNamingConvention.cs - regex patterns applied:
// Rule 1: ([A-Z]+)([A-Z][a-z]) → "$1_$2" // Consecutive capitals: HTTPServer → HTTP_Server
// Rule 2: ([a-z\d])([A-Z]) → "$1_$2" // Lowercase/digit to capital: dateCreated → date_Created
// Rule 3: .ToLowerInvariant() // Lowercase: date_Created → date_created
```
### Common Column Conversions
Single-Word Columns:
- `"Id"``id`
- `"Name"``name`
- `"Type"``type`
- `"UserId"``user_id`
- `"ItemId"``item_id`
Multi-Word Columns:
- `"DateCreated"``date_created`
- `"DateModified"``date_modified`
- `"DateLastActivity"``date_last_activity`
- `"AccessToken"``access_token`
- `"IsMovie"``is_movie`
- `"IsSeries"``is_series`
- `"ShortOverview"``short_overview`
- `"LogSeverity"``log_severity`
- `"RowVersion"``row_version`
- `"DisplayOrder"``display_order`
- `"BaseItemId"``base_item_id`
- `"DeviceId"``device_id`
- `"ProviderId"``provider_id`
- `"SortName"``sort_name`
- `"ProviderIds"``provider_ids`
- `"DvVersionMajor"``dv_version_major`
- `"DvVersionMinor"``dv_version_minor`
- `"DvProfile"``dv_profile`
- `"DvLevel"``dv_level`
- `"RpuVersion"``rpu_version`
- `"CodecTag"``codec_tag`
- `"IsHDR"``is_hdr`
- `"IsHDR10"``is_hdr10`
- `"SeriesPresentationUniqueKey"``series_presentation_unique_key`
---
## Conversion Strategy in create_database_schema.sql
### Phase 1: Table Definition Lines
Find and replace quoted PascalCase table names immediately after `CREATE TABLE`:
- `CREATE TABLE activitylog."ActivityLogs"``CREATE TABLE activitylog.activity_logs`
- `CREATE TABLE authentication."ApiKeys"``CREATE TABLE authentication.api_keys`
- etc. (all 19 tables in schema sections)
### Phase 2: Column Definitions
Within each `CREATE TABLE` block, replace all quoted column names:
- `"Id"``id`
- `"Name"``name`
- `"DateCreated"``date_created`
- etc. (200+ column replacements across all tables)
### Phase 3: Sequence Names
Update sequence definitions to match renamed tables:
- `SEQUENCE NAME activitylog."ActivityLogs_Id_seq"``SEQUENCE NAME activitylog.activity_logs_id_seq`
- etc.
### Phase 4: Constraint References
Update all foreign key and constraint definitions:
- `CONSTRAINT "fk_base_items_users_user_id" FOREIGN KEY ("BaseItemId") REFERENCES users."Users" ("Id")`
`CONSTRAINT fk_base_items_users_user_id FOREIGN KEY (base_item_id) REFERENCES users.users (id)`
### Phase 5: Index References
Update index definitions:
- `CREATE INDEX "idx_activity_logs_user_id" ON activitylog."ActivityLogs" ("UserId")`
`CREATE INDEX idx_activity_logs_user_id ON activitylog.activity_logs (user_id)`
### Phase 6: Special Table (__EFMigrationsHistory)
Update system table for consistency:
- `"__EFMigrationsHistory"``__ef_migrations_history`
- All its columns follow the same snake_case rule
---
## File Location
- **Source**: `Jellyfin.Server/sql/schema_init/create_database_schema.sql` (1879 lines)
- **Output**: Same file (in-place conversion)
---
## Validation Checklist
- [ ] All 19 application tables converted from PascalCase to snake_case
- [ ] All 200+ columns converted to lowercase/snake_case
- [ ] All sequence names updated
- [ ] All foreign key references updated
- [ ] All indexes updated
- [ ] Special table `__ef_migrations_history` created with lowercase columns
- [ ] Views remain unchanged (already lowercase)
- [ ] SQL syntax is valid
- [ ] No orphaned quotes remain in identifier positions
- [ ] Schema still respects owner = jellyfin
---
Generated: 2025-05-01
Reference Documents:
- `PostgresDatabaseProvider.cs` lines 763-875
- `SnakeCaseNamingConvention.cs` lines 16-40
- `create_database_schema.sql` full schema dump