-- ===================================================== -- TABLE: education.videos -- ===================================================== -- Proyecto: OrbiQuant IA (Trading Platform) -- Módulo: OQI-002 - Education -- Especificación: ET-EDU-008-video-upload-architecture.md -- Blocker: BLOCKER-003 (ST4.3) -- ===================================================== CREATE TABLE education.videos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Relaciones course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE, lesson_id UUID REFERENCES education.lessons(id) ON DELETE SET NULL, uploaded_by UUID NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT, -- Información básica title VARCHAR(200) NOT NULL, description TEXT, original_filename VARCHAR(500) NOT NULL, -- Storage (S3/R2) storage_provider VARCHAR(50) NOT NULL DEFAULT 's3', -- 's3', 'r2', 'cloudflare_stream' storage_bucket VARCHAR(200) NOT NULL, storage_key VARCHAR(500) NOT NULL, -- S3/R2 key (path) storage_region VARCHAR(50), -- File info file_size_bytes BIGINT NOT NULL, mime_type VARCHAR(100) NOT NULL DEFAULT 'video/mp4', duration_seconds INTEGER, -- Detectado después de upload -- Status & Processing status VARCHAR(50) NOT NULL DEFAULT 'uploading', -- 'uploading', 'uploaded', 'processing', 'ready', 'error', 'deleted' processing_started_at TIMESTAMPTZ, processing_completed_at TIMESTAMPTZ, processing_error TEXT, -- CDN & URLs cdn_url VARCHAR(1000), -- URL pública del video (CDN) thumbnail_url VARCHAR(1000), -- Transcoded versions (múltiples resoluciones) transcoded_versions JSONB, -- Ejemplo: [ -- {resolution: "1080p", storage_key: "...", cdn_url: "...", file_size_bytes: 123456}, -- {resolution: "720p", storage_key: "...", cdn_url: "...", file_size_bytes: 67890}, -- {resolution: "480p", storage_key: "...", cdn_url: "...", file_size_bytes: 34567} -- ] -- Metadata educativo metadata JSONB NOT NULL DEFAULT '{}'::jsonb, -- Ejemplo: { -- "tags": ["trading", "stocks", "technical-analysis"], -- "language": "en", -- "difficulty": "intermediate", -- "captions": [{language: "en", url: "...srt"}, {language: "es", url: "...srt"}], -- "transcript": "Full text transcript...", -- "video_codec": "h264", -- "audio_codec": "aac", -- "bitrate_kbps": 5000, -- "fps": 30, -- "resolution": "1920x1080" -- } -- Multipart upload tracking upload_id VARCHAR(500), -- AWS/R2 multipart upload ID upload_parts_completed INTEGER DEFAULT 0, upload_parts_total INTEGER, upload_progress_percent INTEGER DEFAULT 0, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), uploaded_at TIMESTAMPTZ, -- Cuando se completó el upload deleted_at TIMESTAMPTZ, -- Soft delete -- Constraints CONSTRAINT valid_status CHECK (status IN ('uploading', 'uploaded', 'processing', 'ready', 'error', 'deleted')), CONSTRAINT valid_storage_provider CHECK (storage_provider IN ('s3', 'r2', 'cloudflare_stream')), CONSTRAINT valid_progress CHECK (upload_progress_percent >= 0 AND upload_progress_percent <= 100), CONSTRAINT positive_duration CHECK (duration_seconds IS NULL OR duration_seconds > 0), CONSTRAINT positive_file_size CHECK (file_size_bytes > 0) ); -- Índices CREATE INDEX idx_videos_course ON education.videos(course_id); CREATE INDEX idx_videos_lesson ON education.videos(lesson_id); CREATE INDEX idx_videos_uploader ON education.videos(uploaded_by); CREATE INDEX idx_videos_status ON education.videos(status); CREATE INDEX idx_videos_created ON education.videos(created_at DESC); CREATE INDEX idx_videos_storage_key ON education.videos(storage_key); -- Índice GIN para búsqueda en metadata (tags, language, etc.) CREATE INDEX idx_videos_metadata ON education.videos USING GIN (metadata jsonb_path_ops); -- Índice para soft delete (excluir deleted) CREATE INDEX idx_videos_active ON education.videos(id) WHERE deleted_at IS NULL; -- Índice compuesto para queries frecuentes CREATE INDEX idx_videos_course_status ON education.videos(course_id, status) WHERE deleted_at IS NULL; -- Comentarios COMMENT ON TABLE education.videos IS 'Videos educativos con soporte de multipart upload, transcoding y CDN'; COMMENT ON COLUMN education.videos.storage_key IS 'S3/R2 object key (ruta completa del archivo)'; COMMENT ON COLUMN education.videos.status IS 'uploading: En proceso de upload | uploaded: Upload completo | processing: Transcoding en progreso | ready: Listo para uso | error: Falló processing | deleted: Soft deleted'; COMMENT ON COLUMN education.videos.transcoded_versions IS 'Array de versiones transcodificadas en diferentes resoluciones (1080p, 720p, 480p, etc.)'; COMMENT ON COLUMN education.videos.metadata IS 'Metadata educativo: tags, language, difficulty, captions, transcript, codecs, etc.'; COMMENT ON COLUMN education.videos.upload_id IS 'AWS/R2 multipart upload ID para tracking de upload en progreso'; COMMENT ON COLUMN education.videos.cdn_url IS 'URL pública del video servido desde CDN (CloudFront/Cloudflare)'; COMMENT ON COLUMN education.videos.thumbnail_url IS 'URL del thumbnail generado automáticamente del video'; COMMENT ON COLUMN education.videos.deleted_at IS 'Soft delete: NULL = activo, NOT NULL = eliminado'; -- Función para actualizar updated_at automáticamente CREATE OR REPLACE FUNCTION education.update_videos_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_videos_updated_at BEFORE UPDATE ON education.videos FOR EACH ROW EXECUTE FUNCTION education.update_videos_updated_at(); -- Función helper para soft delete CREATE OR REPLACE FUNCTION education.soft_delete_video(video_uuid UUID) RETURNS VOID AS $$ BEGIN UPDATE education.videos SET deleted_at = NOW(), status = 'deleted', updated_at = NOW() WHERE id = video_uuid AND deleted_at IS NULL; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION education.soft_delete_video IS 'Soft delete de un video (marca deleted_at en lugar de eliminar el registro)'; -- View para videos activos (excluye soft deleted) CREATE OR REPLACE VIEW education.active_videos AS SELECT * FROM education.videos WHERE deleted_at IS NULL; COMMENT ON VIEW education.active_videos IS 'Vista de videos activos (excluye soft deleted)';