-- ============================================================ -- Migration 013: Distributed Entry Locking -- Date: 2026-02-18 -- Purpose: Add deterministic distributed locking + lifecycle guard for ENTRY flows. -- ============================================================ -- Determine a stable 64-bit key per profile+symbol pair. CREATE OR REPLACE FUNCTION fn_entry_lock_key(p_profile uuid, p_symbol text) RETURNS bigint AS $$ DECLARE profile_hash bigint; symbol_hash bigint; BEGIN profile_hash := abs(hashtext(coalesce(p_profile::text, ''))::bigint) % 2147483647; symbol_hash := abs(hashtext(coalesce(lower(p_symbol), ''))::bigint) % 4294967295; RETURN ((profile_hash << 32) + symbol_hash) % 9223372036854775807; END; $$ LANGUAGE plpgsql IMMUTABLE; -- Try to obtain the advisory lock without blocking. CREATE OR REPLACE FUNCTION fn_try_acquire_entry_lock(p_profile uuid, p_symbol text) RETURNS boolean AS $$ BEGIN RETURN pg_try_advisory_lock(fn_entry_lock_key(p_profile, p_symbol)); END; $$ LANGUAGE plpgsql; -- Release the advisory lock. CREATE OR REPLACE FUNCTION fn_release_entry_lock(p_profile uuid, p_symbol text) RETURNS boolean AS $$ BEGIN RETURN pg_advisory_unlock(fn_entry_lock_key(p_profile, p_symbol)); END; $$ LANGUAGE plpgsql; -- Check for an existing ENTRY/OPEN lifecycle for the same profile+symbol. CREATE OR REPLACE FUNCTION fn_is_entry_in_progress(p_profile uuid, p_symbol text) RETURNS boolean AS $$ BEGIN RETURN EXISTS ( SELECT 1 FROM trade_lifecycle tl JOIN positions p ON p.profile_id = tl.profile_id AND p.trade_id = tl.trade_id WHERE tl.profile_id = p_profile AND lower(p.symbol) = lower(coalesce(p_symbol, '')) AND tl.current_stage IN ('ENTRY', 'OPEN') ); END; $$ LANGUAGE plpgsql STABLE;