-- Social foundation hardening. Apply after 0001 and 0002. -- This migration is additive: it adds database-side timestamp maintenance and -- tenant relationship checks without changing or deleting customer data. begin; create or replace function mediarouter_social_touch_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; do $$ declare table_name text; begin foreach table_name in array array[ 'social_accounts', 'social_account_tokens', 'social_account_capabilities', 'social_campaigns', 'social_posts', 'social_post_targets', 'social_schedules', 'social_jobs' ] loop execute format( 'drop trigger if exists mediarouter_social_touch_updated_at on %I', table_name ); execute format( 'create trigger mediarouter_social_touch_updated_at before update on %I for each row execute function mediarouter_social_touch_updated_at()', table_name ); end loop; end; $$; -- SQLAlchemy enums are represented as text for compatibility with the -- existing schema. These constraints keep direct SQL writes within the same -- state vocabulary enforced by the application state machine. do $$ begin if not exists (select 1 from pg_constraint where conname = 'ck_social_accounts_status') then alter table social_accounts add constraint ck_social_accounts_status check (status in ('pending', 'connected', 'reauth_required', 'disconnected', 'error')); end if; if not exists (select 1 from pg_constraint where conname = 'ck_social_posts_status') then alter table social_posts add constraint ck_social_posts_status check (status in ('draft', 'scheduled', 'queued', 'preparing', 'processing', 'uploading', 'publishing', 'published', 'partial_success', 'retrying', 'failed', 'cancelled')); end if; if not exists (select 1 from pg_constraint where conname = 'ck_social_post_targets_status') then alter table social_post_targets add constraint ck_social_post_targets_status check (status in ('draft', 'scheduled', 'queued', 'preparing', 'processing', 'uploading', 'publishing', 'published', 'partial_success', 'retrying', 'failed', 'cancelled')); end if; if not exists (select 1 from pg_constraint where conname = 'ck_social_schedules_status') then alter table social_schedules add constraint ck_social_schedules_status check (status in ('scheduled', 'queued', 'cancelled')); end if; if not exists (select 1 from pg_constraint where conname = 'ck_social_jobs_status') then alter table social_jobs add constraint ck_social_jobs_status check (status in ('draft', 'scheduled', 'queued', 'preparing', 'processing', 'uploading', 'publishing', 'published', 'retrying', 'failed', 'cancelled')); end if; if not exists (select 1 from pg_constraint where conname = 'ck_social_jobs_attempt_bounds') then alter table social_jobs add constraint ck_social_jobs_attempt_bounds check (attempt_count >= 0 and max_attempts >= 0); end if; end; $$; create or replace function mediarouter_social_assert_workspace_integrity() returns trigger language plpgsql as $$ declare post_workspace text; related_workspace text; related_provider text; related_post_id text; begin if tg_op = 'UPDATE' and tg_table_name in ('social_posts', 'social_jobs') and new.workspace_id is distinct from old.workspace_id then raise exception 'social workspace cannot be reassigned' using errcode = '23514'; end if; if tg_table_name = 'social_posts' then if new.campaign_id is not null then select workspace_id into related_workspace from social_campaigns where id = new.campaign_id; if related_workspace is distinct from new.workspace_id then raise exception 'social post campaign must belong to the same workspace' using errcode = '23503'; end if; end if; if new.source_variant_id is not null then select workspace_id into related_workspace from media_variants where id = new.source_variant_id; if related_workspace is distinct from new.workspace_id then raise exception 'social post variant must belong to the same workspace' using errcode = '23503'; end if; end if; elsif tg_table_name = 'social_post_targets' then select workspace_id into post_workspace from social_posts where id = new.social_post_id; select workspace_id, provider into related_workspace, related_provider from social_accounts where id = new.social_account_id; if post_workspace is null or related_workspace is distinct from post_workspace then raise exception 'social post target account must belong to the post workspace' using errcode = '23503'; end if; if new.provider is distinct from related_provider then raise exception 'social post target provider must match its account' using errcode = '23514'; end if; elsif tg_table_name = 'social_post_media' and new.media_variant_id is not null then select workspace_id into post_workspace from social_posts where id = new.social_post_id; select workspace_id into related_workspace from media_variants where id = new.media_variant_id; if post_workspace is null or related_workspace is distinct from post_workspace then raise exception 'social post media variant must belong to the post workspace' using errcode = '23503'; end if; elsif tg_table_name = 'social_jobs' then select workspace_id into post_workspace from social_posts where id = new.social_post_id; if post_workspace is distinct from new.workspace_id then raise exception 'social job must belong to the post workspace' using errcode = '23503'; end if; if new.social_post_target_id is not null then select social_post_id, provider into related_post_id, related_provider from social_post_targets where id = new.social_post_target_id; if related_post_id is distinct from new.social_post_id then raise exception 'social job target must belong to the social post' using errcode = '23503'; end if; if new.provider is not null and new.provider is distinct from related_provider then raise exception 'social job provider must match its target' using errcode = '23514'; end if; end if; elsif tg_table_name = 'social_post_metrics' then if new.social_post_target_id is not null then select social_post_id, provider into related_post_id, related_provider from social_post_targets where id = new.social_post_target_id; if related_post_id is distinct from new.social_post_id then raise exception 'social metric target must belong to the social post' using errcode = '23503'; end if; if new.provider is distinct from related_provider then raise exception 'social metric provider must match its target' using errcode = '23514'; end if; end if; end if; return new; end; $$; drop trigger if exists mediarouter_social_post_workspace_integrity on social_posts; create trigger mediarouter_social_post_workspace_integrity before insert or update of workspace_id, campaign_id, source_variant_id on social_posts for each row execute function mediarouter_social_assert_workspace_integrity(); drop trigger if exists mediarouter_social_target_workspace_integrity on social_post_targets; create trigger mediarouter_social_target_workspace_integrity before insert or update of social_post_id, social_account_id, provider on social_post_targets for each row execute function mediarouter_social_assert_workspace_integrity(); drop trigger if exists mediarouter_social_post_media_workspace_integrity on social_post_media; create trigger mediarouter_social_post_media_workspace_integrity before insert or update of social_post_id, media_variant_id on social_post_media for each row execute function mediarouter_social_assert_workspace_integrity(); drop trigger if exists mediarouter_social_job_workspace_integrity on social_jobs; create trigger mediarouter_social_job_workspace_integrity before insert or update of workspace_id, social_post_id, social_post_target_id, provider on social_jobs for each row execute function mediarouter_social_assert_workspace_integrity(); drop trigger if exists mediarouter_social_metric_workspace_integrity on social_post_metrics; create trigger mediarouter_social_metric_workspace_integrity before insert or update of social_post_id, social_post_target_id, provider on social_post_metrics for each row execute function mediarouter_social_assert_workspace_integrity(); commit;