-- ============================================================ -- Migration 015: Reconciliation Row Lock -- Date: 2026-02-18 -- Purpose: Prevent overlapping reconciliation cycles per profile. -- ============================================================ CREATE TABLE IF NOT EXISTS reconciliation_locks ( profile_id uuid PRIMARY KEY, owner text NOT NULL, acquired_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL ); CREATE INDEX IF NOT EXISTS reconciliation_locks_expires_idx ON reconciliation_locks (expires_at); ALTER TABLE reconciliation_locks ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Allow reconciliation lock owner" ON reconciliation_locks; CREATE POLICY "Allow reconciliation lock owner" ON reconciliation_locks FOR ALL USING (auth.uid() = profile_id) WITH CHECK (auth.uid() = profile_id); DROP POLICY IF EXISTS "Allow service role reconciliation lock access" ON reconciliation_locks; CREATE POLICY "Allow service role reconciliation lock access" ON reconciliation_locks FOR ALL USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role'); CREATE OR REPLACE FUNCTION fn_try_acquire_reconciliation_lock_row( p_profile_id uuid, p_owner text, p_ttl_seconds integer DEFAULT 30 ) RETURNS boolean AS $$ DECLARE now_ts timestamptz := now(); ttl integer := GREATEST(COALESCE(p_ttl_seconds, 30), 1); expires timestamptz := now_ts + make_interval(secs := ttl); BEGIN INSERT INTO reconciliation_locks (profile_id, owner, acquired_at, expires_at) VALUES (p_profile_id, p_owner, now_ts, expires) ON CONFLICT (profile_id) WHERE reconciliation_locks.expires_at <= now_ts DO UPDATE SET owner = EXCLUDED.owner, acquired_at = EXCLUDED.acquired_at, expires_at = EXCLUDED.expires_at; RETURN TRUE; EXCEPTION WHEN unique_violation THEN RETURN FALSE; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION fn_release_reconciliation_lock_row( p_profile_id uuid, p_owner text ) RETURNS boolean AS $$ DECLARE deleted_count integer; BEGIN DELETE FROM reconciliation_locks WHERE profile_id = p_profile_id AND owner = p_owner; GET DIAGNOSTICS deleted_count = ROW_COUNT; RETURN deleted_count > 0; END; $$ LANGUAGE plpgsql;