learning_ai_invt_trdg/backend/schema/015_reconciliation_lock.sql

74 lines
2.3 KiB
PL/PgSQL

-- ============================================================
-- 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;