learning_ai_invt_trdg/backend/schema/014_entry_row_lock.sql

80 lines
2.4 KiB
PL/PgSQL

-- ============================================================
-- Migration 014: Row-Based Entry Lock Table
-- Date: 2026-02-18
-- Purpose: Replace session-based advisory locks with durable row locks.
-- ============================================================
CREATE TABLE IF NOT EXISTS entry_locks (
profile_id uuid NOT NULL,
symbol text NOT NULL,
symbol_normalized text GENERATED ALWAYS AS (lower(symbol)) STORED NOT NULL,
owner text NOT NULL,
acquired_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
PRIMARY KEY (profile_id, symbol_normalized)
);
CREATE INDEX IF NOT EXISTS entry_locks_expires_idx ON entry_locks (expires_at);
ALTER TABLE entry_locks ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Allow entry lock owner" ON entry_locks;
CREATE POLICY "Allow entry lock owner" ON entry_locks
FOR ALL
USING (auth.uid() = profile_id)
WITH CHECK (auth.uid() = profile_id);
DROP POLICY IF EXISTS "Allow service role entry lock access" ON entry_locks;
CREATE POLICY "Allow service role entry lock access" ON entry_locks
FOR ALL
USING (auth.role() = 'service_role')
WITH CHECK (auth.role() = 'service_role');
CREATE OR REPLACE FUNCTION fn_try_acquire_entry_lock_row(
p_profile_id uuid,
p_symbol text,
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 entry_locks (profile_id, symbol, owner, acquired_at, expires_at)
VALUES (p_profile_id, p_symbol, p_owner, now_ts, expires)
ON CONFLICT (profile_id, symbol_normalized)
WHERE entry_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 VOLATILE;
CREATE OR REPLACE FUNCTION fn_release_entry_lock_row(
p_profile_id uuid,
p_symbol text,
p_owner text
)
RETURNS boolean AS
$$
DECLARE
deleted_count integer;
BEGIN
DELETE FROM entry_locks
WHERE profile_id = p_profile_id
AND symbol_normalized = lower(p_symbol)
AND owner = p_owner;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count > 0;
END;
$$
LANGUAGE plpgsql VOLATILE;