BEGIN; CREATE TABLE storage_endpoints ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, internal_endpoint TEXT NOT NULL, public_endpoint TEXT NOT NULL, bucket VARCHAR(100) NOT NULL, region VARCHAR(100) NOT NULL DEFAULT 'garage', access_key_encrypted TEXT NOT NULL, secret_key_encrypted TEXT NOT NULL, access_key_hint VARCHAR(32) NOT NULL, force_path_style BOOLEAN NOT NULL DEFAULT true, presign_ttl_seconds INTEGER NOT NULL DEFAULT 300, is_active BOOLEAN NOT NULL DEFAULT false, last_test_at TIMESTAMPTZ, last_test_ok BOOLEAN, last_test_error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID REFERENCES users(id) ON DELETE SET NULL, deleted_at TIMESTAMPTZ, CONSTRAINT storage_endpoint_presign_ttl CHECK (presign_ttl_seconds BETWEEN 60 AND 3600) ); CREATE UNIQUE INDEX storage_endpoints_one_active ON storage_endpoints (is_active) WHERE is_active = true; CREATE UNIQUE INDEX storage_endpoints_name_active ON storage_endpoints (lower(name)) WHERE deleted_at IS NULL; CREATE INDEX storage_endpoints_updated_at ON storage_endpoints (updated_at DESC); ALTER TABLE task_files ADD COLUMN input_path TEXT, ADD COLUMN storage_backend VARCHAR(16) NOT NULL DEFAULT 'local', ADD COLUMN storage_endpoint_id UUID REFERENCES storage_endpoints(id) ON DELETE RESTRICT, ADD COLUMN storage_key TEXT, ADD COLUMN storage_etag TEXT; UPDATE task_files SET input_path = storage_path WHERE status IN ('pending', 'processing') AND storage_path IS NOT NULL; UPDATE task_files SET storage_key = storage_path WHERE status = 'completed' AND storage_path IS NOT NULL; CREATE INDEX task_files_storage_endpoint ON task_files (storage_endpoint_id) WHERE storage_endpoint_id IS NOT NULL; ALTER TABLE tasks ADD COLUMN retention_hours INTEGER NOT NULL DEFAULT 24, ADD COLUMN zip_storage_backend VARCHAR(16), ADD COLUMN zip_storage_endpoint_id UUID REFERENCES storage_endpoints(id) ON DELETE RESTRICT, ADD COLUMN zip_storage_key TEXT, ADD COLUMN zip_storage_etag TEXT, ADD COLUMN zip_size BIGINT; UPDATE tasks SET retention_hours = GREATEST( 1, CEIL(EXTRACT(EPOCH FROM (expires_at - created_at)) / 3600.0)::INTEGER ); CREATE INDEX tasks_zip_storage_endpoint ON tasks (zip_storage_endpoint_id) WHERE zip_storage_endpoint_id IS NOT NULL; UPDATE plans SET retention_days = 15, updated_at = NOW() WHERE code = 'business_monthly'; COMMIT;