diff --git a/docs/submission-pipeline/SCHEMA_REFERENCE.md b/docs/submission-pipeline/SCHEMA_REFERENCE.md new file mode 100644 index 00000000..1d93ae36 --- /dev/null +++ b/docs/submission-pipeline/SCHEMA_REFERENCE.md @@ -0,0 +1,636 @@ +# Submission Pipeline Schema Reference + +**Critical Document**: This reference maps all entity types to their exact database schema fields across the entire submission pipeline to prevent schema mismatches. + +**Last Updated**: 2025-11-08 +**Status**: ✅ All schemas audited and verified + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Parks](#parks) +3. [Rides](#rides) +4. [Companies](#companies) +5. [Ride Models](#ride-models) +6. [Photos](#photos) +7. [Timeline Events](#timeline-events) +8. [Critical Functions Reference](#critical-functions-reference) +9. [Common Pitfalls](#common-pitfalls) + +--- + +## Overview + +### Pipeline Flow + +``` +User Input → *_submissions table → submission_items → Moderation → +process_approval_transaction → create/update_entity_from_submission → +Main entity table → Version trigger → *_versions table +``` + +### Entity Types + +- `park` - Theme parks and amusement parks +- `ride` - Individual rides and attractions +- `company` - Used for: `manufacturer`, `operator`, `designer`, `property_owner` +- `ride_model` - Ride model templates +- `photo` - Entity photos +- `timeline_event` - Historical events + +--- + +## Parks + +### Main Table: `parks` + +**Required Fields:** +- `id` (uuid, PK) +- `name` (text, NOT NULL) +- `slug` (text, NOT NULL, UNIQUE) +- `park_type` (text, NOT NULL) - Values: `theme_park`, `amusement_park`, `water_park`, etc. +- `status` (text, NOT NULL) - Values: `operating`, `closed`, `under_construction`, etc. + +**Optional Fields:** +- `description` (text) +- `location_id` (uuid, FK → locations) +- `operator_id` (uuid, FK → companies) +- `property_owner_id` (uuid, FK → companies) +- `opening_date` (date) +- `closing_date` (date) +- `opening_date_precision` (text) - Values: `year`, `month`, `day` +- `closing_date_precision` (text) +- `website_url` (text) +- `phone` (text) +- `email` (text) +- `banner_image_url` (text) +- `banner_image_id` (text) +- `card_image_url` (text) +- `card_image_id` (text) + +**Metadata Fields:** +- `view_count_all` (integer, default: 0) +- `view_count_30d` (integer, default: 0) +- `view_count_7d` (integer, default: 0) +- `average_rating` (numeric, default: 0.00) +- `review_count` (integer, default: 0) +- `created_at` (timestamptz) +- `updated_at` (timestamptz) +- `is_test_data` (boolean, default: false) + +### Submission Table: `park_submissions` + +**Schema Identical to Main Table** (excluding auto-generated fields like `id`, timestamps) + +**Additional Field:** +- `submission_id` (uuid, NOT NULL, FK → content_submissions) +- `temp_location_data` (jsonb) - For pending location creation + +### Version Table: `park_versions` + +**All Main Table Fields PLUS:** +- `version_id` (uuid, PK) +- `park_id` (uuid, NOT NULL, FK → parks) +- `version_number` (integer, NOT NULL) +- `change_type` (version_change_type, NOT NULL) - Values: `created`, `updated`, `restored` +- `change_reason` (text) +- `is_current` (boolean, default: true) +- `created_by` (uuid, FK → auth.users) +- `created_at` (timestamptz) +- `submission_id` (uuid, FK → content_submissions) + +--- + +## Rides + +### Main Table: `rides` + +**Required Fields:** +- `id` (uuid, PK) +- `name` (text, NOT NULL) +- `slug` (text, NOT NULL, UNIQUE) +- `park_id` (uuid, NOT NULL, FK → parks) +- `category` (text, NOT NULL) ⚠️ **CRITICAL: This field is required** + - Values: `roller_coaster`, `water_ride`, `dark_ride`, `flat_ride`, `transport`, `kids_ride` +- `status` (text, NOT NULL) + - Values: `operating`, `closed`, `under_construction`, `sbno`, etc. + +**⚠️ IMPORTANT: `rides` table does NOT have `ride_type` column!** +- `ride_type` only exists in `ride_models` table +- Using `ride_type` in rides updates will cause "column does not exist" error + +**Optional Relationship Fields:** +- `manufacturer_id` (uuid, FK → companies) +- `designer_id` (uuid, FK → companies) +- `ride_model_id` (uuid, FK → ride_models) + +**Optional Descriptive Fields:** +- `description` (text) +- `opening_date` (date) +- `closing_date` (date) +- `opening_date_precision` (text) +- `closing_date_precision` (text) + +**Optional Technical Fields:** +- `height_requirement` (integer) - Height requirement in cm +- `age_requirement` (integer) +- `max_speed_kmh` (numeric) +- `duration_seconds` (integer) +- `capacity_per_hour` (integer) +- `max_g_force` (numeric) +- `inversions` (integer) - Number of inversions +- `length_meters` (numeric) +- `max_height_meters` (numeric) +- `drop_height_meters` (numeric) + +**Category-Specific Fields:** + +*Roller Coasters:* +- `ride_sub_type` (text) +- `coaster_type` (text) +- `seating_type` (text) +- `intensity_level` (text) +- `track_material` (text) +- `support_material` (text) +- `propulsion_method` (text) + +*Water Rides:* +- `water_depth_cm` (integer) +- `splash_height_meters` (numeric) +- `wetness_level` (text) +- `flume_type` (text) +- `boat_capacity` (integer) + +*Dark Rides:* +- `theme_name` (text) +- `story_description` (text) +- `show_duration_seconds` (integer) +- `animatronics_count` (integer) +- `projection_type` (text) +- `ride_system` (text) +- `scenes_count` (integer) + +*Flat Rides:* +- `rotation_type` (text) +- `motion_pattern` (text) +- `platform_count` (integer) +- `swing_angle_degrees` (numeric) +- `rotation_speed_rpm` (numeric) +- `arm_length_meters` (numeric) +- `max_height_reached_meters` (numeric) + +*Kids Rides:* +- `min_age` (integer) +- `max_age` (integer) +- `educational_theme` (text) +- `character_theme` (text) + +*Transport:* +- `transport_type` (text) +- `route_length_meters` (numeric) +- `stations_count` (integer) +- `vehicle_capacity` (integer) +- `vehicles_count` (integer) +- `round_trip_duration_seconds` (integer) + +**Image Fields:** +- `banner_image_url` (text) +- `banner_image_id` (text) +- `card_image_url` (text) +- `card_image_id` (text) +- `image_url` (text) - Legacy field + +**Metadata Fields:** +- `view_count_all` (integer, default: 0) +- `view_count_30d` (integer, default: 0) +- `view_count_7d` (integer, default: 0) +- `average_rating` (numeric, default: 0.00) +- `review_count` (integer, default: 0) +- `created_at` (timestamptz) +- `updated_at` (timestamptz) +- `is_test_data` (boolean, default: false) + +### Submission Table: `ride_submissions` + +**Schema Identical to Main Table** (excluding auto-generated fields) + +**Additional Fields:** +- `submission_id` (uuid, NOT NULL, FK → content_submissions) + +### Version Table: `ride_versions` + +**All Main Table Fields PLUS:** +- `version_id` (uuid, PK) +- `ride_id` (uuid, NOT NULL, FK → rides) +- `version_number` (integer, NOT NULL) +- `change_type` (version_change_type, NOT NULL) +- `change_reason` (text) +- `is_current` (boolean, default: true) +- `created_by` (uuid, FK → auth.users) +- `created_at` (timestamptz) +- `submission_id` (uuid, FK → content_submissions) + +**⚠️ Field Name Differences (Version Table vs Main Table):** +- `height_requirement_cm` in versions → `height_requirement` in rides +- `gforce_max` in versions → `max_g_force` in rides +- `inversions_count` in versions → `inversions` in rides +- `height_meters` in versions → `max_height_meters` in rides +- `drop_meters` in versions → `drop_height_meters` in rides + +--- + +## Companies + +**Used For**: `manufacturer`, `operator`, `designer`, `property_owner` + +### Main Table: `companies` + +**Required Fields:** +- `id` (uuid, PK) +- `name` (text, NOT NULL) +- `slug` (text, NOT NULL, UNIQUE) +- `company_type` (text, NOT NULL) + - Values: `manufacturer`, `operator`, `designer`, `property_owner` + +**Optional Fields:** +- `description` (text) +- `person_type` (text, default: 'company') + - Values: `company`, `individual` +- `founded_year` (integer) +- `founded_date` (date) +- `founded_date_precision` (text) +- `headquarters_location` (text) +- `website_url` (text) +- `logo_url` (text) +- `banner_image_url` (text) +- `banner_image_id` (text) +- `card_image_url` (text) +- `card_image_id` (text) + +**Metadata Fields:** +- `view_count_all` (integer, default: 0) +- `view_count_30d` (integer, default: 0) +- `view_count_7d` (integer, default: 0) +- `average_rating` (numeric, default: 0.00) +- `review_count` (integer, default: 0) +- `created_at` (timestamptz) +- `updated_at` (timestamptz) +- `is_test_data` (boolean, default: false) + +### Submission Table: `company_submissions` + +**Schema Identical to Main Table** (excluding auto-generated fields) + +**Additional Field:** +- `submission_id` (uuid, NOT NULL, FK → content_submissions) + +### Version Table: `company_versions` + +**All Main Table Fields PLUS:** +- `version_id` (uuid, PK) +- `company_id` (uuid, NOT NULL, FK → companies) +- `version_number` (integer, NOT NULL) +- `change_type` (version_change_type, NOT NULL) +- `change_reason` (text) +- `is_current` (boolean, default: true) +- `created_by` (uuid, FK → auth.users) +- `created_at` (timestamptz) +- `submission_id` (uuid, FK → content_submissions) + +--- + +## Ride Models + +### Main Table: `ride_models` + +**Required Fields:** +- `id` (uuid, PK) +- `name` (text, NOT NULL) +- `slug` (text, NOT NULL, UNIQUE) +- `manufacturer_id` (uuid, NOT NULL, FK → companies) +- `category` (text, NOT NULL) ⚠️ **CRITICAL: This field is required** + - Values: `roller_coaster`, `water_ride`, `dark_ride`, `flat_ride`, `transport`, `kids_ride` + +**Optional Fields:** +- `ride_type` (text) ⚠️ **This field exists in ride_models but NOT in rides** + - More specific classification than category + - Example: category = `roller_coaster`, ride_type = `inverted_coaster` +- `description` (text) +- `banner_image_url` (text) +- `banner_image_id` (text) +- `card_image_url` (text) +- `card_image_id` (text) + +**Metadata Fields:** +- `view_count_all` (integer, default: 0) +- `view_count_30d` (integer, default: 0) +- `view_count_7d` (integer, default: 0) +- `average_rating` (numeric, default: 0.00) +- `review_count` (integer, default: 0) +- `installations_count` (integer, default: 0) +- `created_at` (timestamptz) +- `updated_at` (timestamptz) +- `is_test_data` (boolean, default: false) + +### Submission Table: `ride_model_submissions` + +**Schema Identical to Main Table** (excluding auto-generated fields) + +**Additional Field:** +- `submission_id` (uuid, NOT NULL, FK → content_submissions) + +### Version Table: `ride_model_versions` + +**All Main Table Fields PLUS:** +- `version_id` (uuid, PK) +- `ride_model_id` (uuid, NOT NULL, FK → ride_models) +- `version_number` (integer, NOT NULL) +- `change_type` (version_change_type, NOT NULL) +- `change_reason` (text) +- `is_current` (boolean, default: true) +- `created_by` (uuid, FK → auth.users) +- `created_at` (timestamptz) +- `submission_id` (uuid, FK → content_submissions) + +--- + +## Photos + +### Main Table: `photos` + +**Required Fields:** +- `id` (uuid, PK) +- `cloudflare_id` (text, NOT NULL) +- `url` (text, NOT NULL) +- `entity_type` (text, NOT NULL) +- `entity_id` (uuid, NOT NULL) +- `uploader_id` (uuid, NOT NULL, FK → auth.users) + +**Optional Fields:** +- `title` (text) +- `caption` (text) +- `taken_date` (date) +- `taken_date_precision` (text) +- `photographer_name` (text) +- `order_index` (integer, default: 0) +- `is_primary` (boolean, default: false) +- `status` (text, default: 'active') + +**Metadata Fields:** +- `created_at` (timestamptz) +- `updated_at` (timestamptz) +- `is_test_data` (boolean, default: false) + +### Submission Table: `photo_submissions` + +**Required Fields:** +- `id` (uuid, PK) +- `submission_id` (uuid, NOT NULL, FK → content_submissions) +- `entity_type` (text, NOT NULL) +- `entity_id` (uuid, NOT NULL) +- `cloudflare_id` (text, NOT NULL) +- `url` (text, NOT NULL) + +**Optional Fields:** +- `title` (text) +- `caption` (text) +- `taken_date` (date) +- `taken_date_precision` (text) +- `photographer_name` (text) +- `order_index` (integer) + +**Note**: Photos do NOT have version tables - they are immutable after approval + +--- + +## Timeline Events + +### Main Table: `entity_timeline_events` + +**Required Fields:** +- `id` (uuid, PK) +- `entity_type` (text, NOT NULL) +- `entity_id` (uuid, NOT NULL) +- `event_type` (text, NOT NULL) + - Values: `opening`, `closing`, `relocation`, `renovation`, `name_change`, `ownership_change`, etc. +- `title` (text, NOT NULL) +- `event_date` (date, NOT NULL) + +**Optional Fields:** +- `description` (text) +- `event_date_precision` (text, default: 'day') +- `from_value` (text) +- `to_value` (text) +- `from_entity_id` (uuid) +- `to_entity_id` (uuid) +- `from_location_id` (uuid) +- `to_location_id` (uuid) +- `is_public` (boolean, default: true) +- `display_order` (integer, default: 0) + +**Approval Fields:** +- `created_by` (uuid, FK → auth.users) +- `approved_by` (uuid, FK → auth.users) +- `submission_id` (uuid, FK → content_submissions) + +**Metadata Fields:** +- `created_at` (timestamptz) +- `updated_at` (timestamptz) + +### Submission Table: `timeline_event_submissions` + +**Schema Identical to Main Table** (excluding auto-generated fields) + +**Additional Field:** +- `submission_id` (uuid, NOT NULL, FK → content_submissions) + +**Note**: Timeline events do NOT have version tables + +--- + +## Critical Functions Reference + +### 1. `create_entity_from_submission` + +**Purpose**: Creates new entities from approved submissions + +**Parameters**: +- `p_entity_type` (text) - Entity type identifier +- `p_data` (jsonb) - Entity data from submission +- `p_created_by` (uuid) - User who created it +- `p_submission_id` (uuid) - Source submission + +**Critical Requirements**: +- ✅ MUST extract `category` for rides and ride_models +- ✅ MUST NOT use `ride_type` for rides (doesn't exist) +- ✅ MUST use `ride_type` for ride_models (does exist) +- ✅ MUST handle all required NOT NULL fields + +**Returns**: `uuid` - New entity ID + +### 2. `update_entity_from_submission` + +**Purpose**: Updates existing entities from approved edits + +**Parameters**: +- `p_entity_type` (text) - Entity type identifier +- `p_data` (jsonb) - Updated entity data +- `p_entity_id` (uuid) - Existing entity ID +- `p_changed_by` (uuid) - User who changed it + +**Critical Requirements**: +- ✅ MUST use COALESCE to preserve existing values +- ✅ MUST include `category` for rides and ride_models +- ✅ MUST NOT use `ride_type` for rides +- ✅ MUST use `ride_type` for ride_models +- ✅ MUST update `updated_at` timestamp + +**Returns**: `uuid` - Updated entity ID + +### 3. `process_approval_transaction` + +**Purpose**: Atomic transaction for selective approval + +**Parameters**: +- `p_submission_id` (uuid) +- `p_item_ids` (uuid[]) - Specific items to approve +- `p_moderator_id` (uuid) +- `p_change_reason` (text) + +**Critical Requirements**: +- ✅ MUST validate all item dependencies first +- ✅ MUST extract correct fields from submission tables +- ✅ MUST set session variables for triggers +- ✅ MUST handle rollback on any error + +**Called By**: Edge function `process-selective-approval` + +### 4. `create_submission_with_items` + +**Purpose**: Creates multi-item submissions atomically + +**Parameters**: +- `p_submission_id` (uuid) +- `p_entity_type` (text) +- `p_action_type` (text) - `create` or `edit` +- `p_items` (jsonb) - Array of submission items +- `p_user_id` (uuid) + +**Critical Requirements**: +- ✅ MUST resolve dependencies in order +- ✅ MUST validate all required fields per entity type +- ✅ MUST link items to submission correctly + +--- + +## Common Pitfalls + +### 1. ❌ Using `ride_type` for rides +```sql +-- WRONG +UPDATE rides SET ride_type = 'inverted_coaster' WHERE id = $1; +-- ERROR: column "ride_type" does not exist + +-- CORRECT +UPDATE rides SET category = 'roller_coaster' WHERE id = $1; +``` + +### 2. ❌ Missing `category` field +```sql +-- WRONG - Missing required category +INSERT INTO rides (name, slug, park_id, status) VALUES (...); +-- ERROR: null value violates not-null constraint + +-- CORRECT +INSERT INTO rides (name, slug, park_id, category, status) VALUES (..., 'roller_coaster', ...); +``` + +### 3. ❌ Wrong column names in version tables +```sql +-- WRONG +SELECT height_requirement FROM ride_versions WHERE ride_id = $1; +-- Returns null + +-- CORRECT +SELECT height_requirement_cm FROM ride_versions WHERE ride_id = $1; +``` + +### 4. ❌ Forgetting COALESCE in updates +```sql +-- WRONG - Overwrites fields with NULL +UPDATE rides SET + name = (p_data->>'name'), + description = (p_data->>'description') +WHERE id = $1; + +-- CORRECT - Preserves existing values if not provided +UPDATE rides SET + name = COALESCE(p_data->>'name', name), + description = COALESCE(p_data->>'description', description) +WHERE id = $1; +``` + +### 5. ❌ Not handling submission_id in version triggers +```sql +-- WRONG - Version doesn't link back to submission +INSERT INTO ride_versions (ride_id, ...) VALUES (...); + +-- CORRECT - Trigger must read session variable +v_submission_id := current_setting('app.submission_id', true)::uuid; +INSERT INTO ride_versions (ride_id, submission_id, ...) VALUES (..., v_submission_id, ...); +``` + +--- + +## Validation Checklist + +Before deploying any submission pipeline changes: + +- [ ] All entity tables have matching submission tables +- [ ] All required NOT NULL fields are included in CREATE functions +- [ ] All required NOT NULL fields are included in UPDATE functions +- [ ] `category` is extracted for rides and ride_models +- [ ] `ride_type` is NOT used for rides +- [ ] `ride_type` IS used for ride_models +- [ ] COALESCE is used for all UPDATE statements +- [ ] Version table column name differences are handled +- [ ] Session variables are set for version triggers +- [ ] Foreign key relationships are validated +- [ ] Dependency resolution works correctly +- [ ] Error handling and rollback logic is present + +--- + +## Maintenance + +**When adding new entity types:** + +1. Create main table with all fields +2. Create matching submission table + `submission_id` FK +3. Create version table with all fields + version metadata +4. Add case to `create_entity_from_submission` +5. Add case to `update_entity_from_submission` +6. Add case to `process_approval_transaction` +7. Add case to `create_submission_with_items` +8. Create version trigger for main table +9. Update this documentation +10. Run full test suite + +**When modifying schemas:** + +1. Check if field exists in ALL three tables (main, submission, version) +2. Update ALL three tables in migration +3. Update ALL functions that reference the field +4. Update this documentation +5. Test create, update, and rollback flows + +--- + +## Related Documentation + +- [Submission Pipeline Overview](./README.md) +- [Versioning System](../versioning/README.md) +- [Moderation Workflow](../moderation/README.md) +- [Migration Guide](../versioning/MIGRATION.md)