Table: education.videos Features: - Multipart upload tracking (upload_id, parts, progress) - Storage integration (S3/R2/Cloudflare Stream) - Video processing status (uploading → processing → ready) - Transcoded versions JSONB (multiple resolutions) - CDN URLs (video + thumbnail) - Metadata JSONB (tags, language, difficulty, captions, transcript) - Soft delete (deleted_at) - GIN index for metadata search - Helper functions (soft_delete_video) - View for active videos Constraints: - Valid status enum - Valid storage provider enum - Positive file size & duration - Progress 0-100% Blocker: BLOCKER-003 (ST4.3 Video Upload Backend) Epic: OQI-002 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
151 lines
6.3 KiB
PL/PgSQL
151 lines
6.3 KiB
PL/PgSQL
-- =====================================================
|
|
-- 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 core.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)';
|