-- ===================================================================== -- RENDER SCHEMA — Part 2: Jobs, Frames, Snapshots, Exports -- ===================================================================== SET search_path TO render, public; CREATE TYPE render_step AS ENUM ( 'Queued','Preparing','TemplateCache','JsxGen','Music', 'Rendering','Validating','Repairing','Optimisation','Video', 'Mixing','Final','Uploading','Done','Failed','Cancelled' ); CREATE TYPE price_kind AS ENUM ('Free','Preview','Cash','Plan','Snapshot','Reseller'); CREATE TYPE render_quality AS ENUM ('Low','Medium','High','Full','Lossless'); CREATE TYPE frame_job_status AS ENUM ( 'Pending','Rendering','Validated','Repairing','Converting','Done','Failed' ); CREATE TYPE render_priority_queue AS ENUM ( 'snapshot','vip','paid','preview','mockup','voiceover' ); -- --------------------------------------------------------------------- -- render_jobs — top-level render task -- --------------------------------------------------------------------- CREATE TABLE render_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, -- References (loose cross-schema) saved_project_id UUID NOT NULL, original_project_id UUID NOT NULL, project_name TEXT, -- Job identity title TEXT, name TEXT, external_job_id TEXT, -- RabbitMQ message ID priority_queue render_priority_queue NOT NULL, priority_score INT NOT NULL DEFAULT 50, -- 0-100, higher = sooner -- Pipeline state step render_step NOT NULL DEFAULT 'Queued', render_progress INT NOT NULL DEFAULT 0 CHECK (render_progress BETWEEN 0 AND 100), convert_progress INT NOT NULL DEFAULT 0, image_preview_b64 TEXT, -- last frame thumbnail -- Pricing price_type price_kind NOT NULL, paid_price_minor BIGINT NOT NULL DEFAULT 0, discount_code TEXT, support_flatrender BOOLEAN NOT NULL DEFAULT FALSE, -- Output config mode TEXT NOT NULL, -- FIX/FLEXIBLE/MockUp/MusicVisualizer quality render_quality NOT NULL DEFAULT 'High', resolution TEXT NOT NULL, -- FullHD/FourK r_height INT NOT NULL, frame_rate INT NOT NULL DEFAULT 30, is_60_fps BOOLEAN NOT NULL DEFAULT FALSE, duration_sec NUMERIC(8,2) NOT NULL, export_duration_sec NUMERIC(8,2), -- Audio (NEW) has_music BOOLEAN NOT NULL DEFAULT FALSE, has_sfx BOOLEAN NOT NULL DEFAULT FALSE, has_voiceover BOOLEAN NOT NULL DEFAULT FALSE, music_volume NUMERIC(4,3), sfx_volume NUMERIC(4,3), voiceover_volume NUMERIC(4,3), -- Resource allocation render_node_count INT NOT NULL DEFAULT 1, current_active_nodes INT NOT NULL DEFAULT 0, region TEXT, -- preferred region tell_me_when_done BOOLEAN NOT NULL DEFAULT TRUE, -- Retry / recovery retry_count INT NOT NULL DEFAULT 0, max_retries INT NOT NULL DEFAULT 3, repair_attempts INT NOT NULL DEFAULT 0, failed_message TEXT, failed_at_step render_step, -- File outputs (paths in MinIO; final URL goes in exports.path) render_folder TEXT, -- temp working dir output_folder TEXT, -- frames dir physical_render_folder TEXT, -- absolute path for nodes physical_output_folder TEXT, target_replica_name TEXT, -- Reference to result export_id UUID, -- Timing task_start_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), start_render_date TIMESTAMPTZ, done_task_date TIMESTAMPTZ, queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_render_jobs_user ON render_jobs(user_id, created_at DESC); CREATE INDEX idx_render_jobs_tenant ON render_jobs(tenant_id, created_at DESC); CREATE INDEX idx_render_jobs_step ON render_jobs(step) WHERE step NOT IN ('Done','Failed','Cancelled'); CREATE INDEX idx_render_jobs_queue ON render_jobs(priority_queue, priority_score DESC, queued_at) WHERE step = 'Queued'; CREATE INDEX idx_render_jobs_in_flight ON render_jobs(started_at) WHERE step NOT IN ('Done','Failed','Cancelled','Queued'); CREATE TRIGGER tg_render_jobs_updated_at BEFORE UPDATE ON render_jobs FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- frame_jobs — per-node frame-range assignments -- --------------------------------------------------------------------- CREATE TABLE frame_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE, node_id UUID NOT NULL REFERENCES render_nodes(id), -- Range start_frame INT NOT NULL, end_frame INT NOT NULL, collect_frame_count INT NOT NULL, order_value INT NOT NULL DEFAULT 0, folder_name TEXT NOT NULL, -- "O0", "O1"... convert_url TEXT, -- Status status frame_job_status NOT NULL DEFAULT 'Pending', frames_rendered INT NOT NULL DEFAULT 0, frames_validated INT NOT NULL DEFAULT 0, -- Errors attempt INT NOT NULL DEFAULT 1, last_error TEXT, -- Outputs output_mp4_url TEXT, -- chunk MP4 after ffmpeg -- Timing assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, last_progress_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_frame_jobs_render ON frame_jobs(render_job_id, order_value); CREATE INDEX idx_frame_jobs_node ON frame_jobs(node_id, status); CREATE INDEX idx_frame_jobs_stalled ON frame_jobs(last_progress_at) WHERE status = 'Rendering'; CREATE TRIGGER tg_frame_jobs_updated_at BEFORE UPDATE ON frame_jobs FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- frame_repair_jobs — missing/corrupt frame repair tracking -- --------------------------------------------------------------------- CREATE TABLE frame_repair_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE, node_id UUID REFERENCES render_nodes(id), -- Range to repair start_frame INT NOT NULL, end_frame INT NOT NULL, missing_frames INT[] NOT NULL DEFAULT '{}', corrupt_frames INT[] NOT NULL DEFAULT '{}', attempt INT NOT NULL DEFAULT 1, status frame_job_status NOT NULL DEFAULT 'Pending', error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); CREATE INDEX idx_frame_repair_job ON frame_repair_jobs(render_job_id, attempt); -- --------------------------------------------------------------------- -- snapshots — single-frame scene previews (new feature) -- --------------------------------------------------------------------- CREATE TABLE snapshots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, saved_project_id UUID NOT NULL, scene_key TEXT NOT NULL, frame_number INT NOT NULL, -- Cache key — same inputs = same output inputs_hash TEXT NOT NULL, -- Status status TEXT NOT NULL DEFAULT 'Pending', -- Pending/Rendering/Done/Failed render_node_id UUID REFERENCES render_nodes(id), -- Output image_url TEXT, thumbnail_url TEXT, width INT, height INT, size_bytes BIGINT, -- Timing requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ, duration_ms INT, expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days', error_message TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX uq_snapshots_cache ON snapshots(saved_project_id, scene_key, frame_number, inputs_hash) WHERE status = 'Done'; CREATE INDEX idx_snapshots_user ON snapshots(user_id, requested_at DESC); CREATE INDEX idx_snapshots_expire ON snapshots(expires_at) WHERE status = 'Done'; -- --------------------------------------------------------------------- -- exports — final rendered output records -- --------------------------------------------------------------------- CREATE TYPE export_create_type AS ENUM ('Render','Upload','Snapshot','Reupload'); CREATE TYPE export_file_type AS ENUM ('Video','Image','Audio','GIF','PDF'); CREATE TABLE exports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, saved_project_id UUID NOT NULL, project_id UUID NOT NULL, render_job_id UUID REFERENCES render_jobs(id) ON DELETE SET NULL, -- Output image TEXT, -- thumbnail path TEXT NOT NULL, -- main file URL file_extension TEXT NOT NULL DEFAULT 'mp4', file_type export_file_type NOT NULL DEFAULT 'Video', render_quality render_quality NOT NULL, create_type export_create_type NOT NULL DEFAULT 'Render', size_bytes BIGINT NOT NULL, duration_sec NUMERIC(8,2), width INT, height INT, -- Lifecycle produce_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), auto_delete_date TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 days', delete_notified BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_exports_user ON exports(user_id, produce_date DESC) WHERE deleted_at IS NULL; CREATE INDEX idx_exports_tenant ON exports(tenant_id) WHERE deleted_at IS NULL; CREATE INDEX idx_exports_saved_project ON exports(saved_project_id); CREATE INDEX idx_exports_auto_delete ON exports(auto_delete_date) WHERE deleted_at IS NULL; -- Now wire FK back to render_jobs ALTER TABLE render_jobs ADD CONSTRAINT fk_render_jobs_export FOREIGN KEY (export_id) REFERENCES exports(id) ON DELETE SET NULL; -- --------------------------------------------------------------------- -- export_files — mockup mode produces multiple images per export -- --------------------------------------------------------------------- CREATE TABLE export_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), export_id UUID NOT NULL REFERENCES exports(id) ON DELETE CASCADE, user_id UUID NOT NULL, name TEXT, thumbnail TEXT, path TEXT NOT NULL, size_bytes BIGINT NOT NULL, file_type export_file_type NOT NULL DEFAULT 'Image', width INT, height INT, sort INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_export_files_export ON export_files(export_id, sort); -- --------------------------------------------------------------------- -- render_progress_events — WebSocket fan-out source (short-lived) -- --------------------------------------------------------------------- CREATE TABLE render_progress_events ( id BIGSERIAL PRIMARY KEY, render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE, step render_step NOT NULL, progress INT NOT NULL, current_frame INT, total_frames INT, eta_seconds INT, preview_b64 TEXT, message TEXT, emitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_progress_events_job ON render_progress_events(render_job_id, emitted_at DESC); -- Cleanup: keep last N per job via cron, drop > 7 days