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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user