wacrm/supabase/migrations/007_automations_increment_counter.sql
Arnas Donauskas 635b5c1df6
fix(automations): atomic execution_count via SQL function (migration 007)
The engine did a client-side read-modify-write on
automations.execution_count, so two concurrent dispatches of the
same automation (e.g. fired for two different contacts within the
same second) could both read N and both write N+1, permanently
losing one count.

- Migration 007 adds increment_automation_execution_count(p_automation_id)
  as a SECURITY DEFINER sql function. Only service_role can EXECUTE
  it — an authenticated user can't juice someone else's counter by
  calling the RPC directly.
- Engine now calls this RPC instead of reading automation.execution_count
  and adding 1. Atomic in one statement; concurrent dispatches both
  land correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:45:27 +03:00

36 lines
1.4 KiB
PL/PgSQL

-- ============================================================
-- 007_automations_increment_counter.sql
--
-- Atomic increment of automations.execution_count + refresh of
-- last_executed_at. Called via PostgREST RPC from the engine.
--
-- Before this, the engine did a read-modify-write:
-- UPDATE automations SET execution_count = <cached + 1> WHERE id = ...
-- so two concurrent dispatches (e.g. the same automation firing for
-- two different contacts in the same second) could both read N and
-- both write N+1, permanently losing one count.
--
-- Idempotent — safe to re-run.
-- ============================================================
CREATE OR REPLACE FUNCTION increment_automation_execution_count(p_automation_id UUID)
RETURNS VOID
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
UPDATE automations
SET
execution_count = execution_count + 1,
last_executed_at = NOW()
WHERE id = p_automation_id;
$$;
-- Only the service role needs to call this (engine uses the
-- service-role client). Explicitly lock anon / authenticated out so
-- an authenticated user can't juice someone else's counter via RPC.
REVOKE ALL ON FUNCTION increment_automation_execution_count(UUID) FROM PUBLIC;
REVOKE ALL ON FUNCTION increment_automation_execution_count(UUID) FROM anon;
REVOKE ALL ON FUNCTION increment_automation_execution_count(UUID) FROM authenticated;
GRANT EXECUTE ON FUNCTION increment_automation_execution_count(UUID) TO service_role;