-
Notifications
You must be signed in to change notification settings - Fork 0
fix(db): backfill 4 orphan migration versions to repair prod integration #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| -- ================================================================ | ||
| -- SMOKE TESTS MENSAIS — v2 (SECURITY INVOKER porque fn_run_smoke_tests | ||
| -- interna usa RESET role que não funciona em SECURITY DEFINER) | ||
| -- ================================================================ | ||
|
|
||
| CREATE TABLE IF NOT EXISTS public.smoke_tests_runs ( | ||
| id bigserial PRIMARY KEY, | ||
| ran_at timestamptz NOT NULL DEFAULT now(), | ||
| test_name text NOT NULL, | ||
| test_category text, | ||
| result text NOT NULL, | ||
| details text, | ||
| duration_ms numeric | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_smoke_tests_runs_ran_at | ||
| ON public.smoke_tests_runs (ran_at DESC); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_smoke_tests_runs_test_result | ||
| ON public.smoke_tests_runs (test_name, result, ran_at DESC); | ||
|
|
||
| ALTER TABLE public.smoke_tests_runs ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| DROP POLICY IF EXISTS smoke_tests_runs_read_admin ON public.smoke_tests_runs; | ||
| CREATE POLICY smoke_tests_runs_read_admin | ||
| ON public.smoke_tests_runs FOR SELECT | ||
| TO authenticated | ||
| USING (is_admin_or_above((SELECT auth.uid()))); | ||
|
|
||
| GRANT SELECT ON public.smoke_tests_runs TO authenticated; | ||
|
Comment on lines
+22
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The migration grants only Useful? React with 👍 / 👎. |
||
| REVOKE ALL ON public.smoke_tests_runs FROM anon; | ||
| GRANT USAGE, SELECT ON SEQUENCE public.smoke_tests_runs_id_seq TO authenticated; | ||
|
|
||
| -- Wrapper SECURITY INVOKER (default) | ||
| CREATE OR REPLACE FUNCTION public.fn_run_and_persist_smoke_tests() | ||
|
|
||
| RETURNS TABLE( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This migration introduces Useful? React with 👍 / 👎. |
||
| ran_at timestamptz, | ||
| total_tests int, | ||
| passed int, | ||
| failed int, | ||
| warned int, | ||
| failed_tests text[] | ||
| ) | ||
| LANGUAGE plpgsql | ||
| SECURITY INVOKER | ||
| SET search_path = public, pg_catalog | ||
| AS $$ | ||
| DECLARE | ||
| v_ran_at timestamptz := now(); | ||
| v_total int := 0; | ||
| v_passed int := 0; | ||
| v_failed int := 0; | ||
| v_warned int := 0; | ||
| v_failed_tests text[] := ARRAY[]::text[]; | ||
| r RECORD; | ||
| BEGIN | ||
| FOR r IN SELECT * FROM public.fn_run_smoke_tests() LOOP | ||
| v_total := v_total + 1; | ||
|
|
||
| INSERT INTO public.smoke_tests_runs ( | ||
| ran_at, test_name, test_category, result, details, duration_ms | ||
| ) VALUES ( | ||
| v_ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms | ||
| ); | ||
|
Comment on lines
+60
to
+64
|
||
|
|
||
| IF upper(r.result) = 'PASS' THEN v_passed := v_passed + 1; | ||
| ELSIF upper(r.result) = 'FAIL' THEN | ||
| v_failed := v_failed + 1; | ||
| v_failed_tests := array_append(v_failed_tests, r.test_name); | ||
| ELSIF upper(r.result) IN ('WARN','WARNING') THEN v_warned := v_warned + 1; | ||
| END IF; | ||
|
Comment on lines
+66
to
+71
|
||
| END LOOP; | ||
|
|
||
| RETURN QUERY SELECT v_ran_at, v_total, v_passed, v_failed, v_warned, v_failed_tests; | ||
| END; | ||
| $$; | ||
|
|
||
| COMMENT ON FUNCTION public.fn_run_and_persist_smoke_tests() IS | ||
| 'Wrapper SECURITY INVOKER. Roda fn_run_smoke_tests(), persiste em smoke_tests_runs e retorna sumário. | ||
| Cron mensal + execução manual via dashboard admin.'; | ||
|
|
||
| -- Grant para admin/postgres executar | ||
| GRANT EXECUTE ON FUNCTION public.fn_run_and_persist_smoke_tests() TO postgres; | ||
| GRANT EXECUTE ON FUNCTION public.fn_run_and_persist_smoke_tests() TO authenticated; | ||
|
|
||
|
|
||
| -- Views | ||
| CREATE OR REPLACE VIEW public.v_smoke_tests_latest_run | ||
| WITH (security_invoker = on) AS | ||
| WITH latest AS ( | ||
| SELECT max(ran_at) AS ran_at FROM public.smoke_tests_runs | ||
| ) | ||
| SELECT | ||
| r.ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms | ||
| FROM public.smoke_tests_runs r | ||
| JOIN latest l ON r.ran_at = l.ran_at | ||
| ORDER BY | ||
| CASE upper(r.result) | ||
| WHEN 'FAIL' THEN 1 WHEN 'WARN' THEN 2 WHEN 'PASS' THEN 3 ELSE 4 | ||
| END, r.test_name; | ||
|
Comment on lines
+97
to
+99
|
||
|
|
||
| GRANT SELECT ON public.v_smoke_tests_latest_run TO authenticated; | ||
| REVOKE SELECT ON public.v_smoke_tests_latest_run FROM anon; | ||
|
|
||
| CREATE OR REPLACE VIEW public.v_smoke_tests_trend | ||
| WITH (security_invoker = on) AS | ||
| SELECT | ||
| ran_at, | ||
| count(*) AS total, | ||
| count(*) FILTER (WHERE upper(result) = 'PASS') AS passed, | ||
| count(*) FILTER (WHERE upper(result) = 'FAIL') AS failed, | ||
| count(*) FILTER (WHERE upper(result) IN ('WARN','WARNING')) AS warned, | ||
|
Comment on lines
+109
to
+111
|
||
| round(avg(duration_ms)::numeric, 1) AS avg_duration_ms | ||
| FROM public.smoke_tests_runs | ||
| GROUP BY ran_at | ||
| ORDER BY ran_at DESC | ||
| LIMIT 12; | ||
|
|
||
| GRANT SELECT ON public.v_smoke_tests_trend TO authenticated; | ||
| REVOKE SELECT ON public.v_smoke_tests_trend FROM anon; | ||
|
|
||
| -- Cron mensal (dia 1, 03h UTC — domingo madrugada se cair, sem impacto) | ||
| DO $$ | ||
| BEGIN | ||
| IF NOT EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'smoke_tests_monthly') THEN | ||
| PERFORM cron.schedule( | ||
| 'smoke_tests_monthly', | ||
| '0 3 1 * *', | ||
| $cron$ SELECT public.fn_run_and_persist_smoke_tests(); $cron$ | ||
| ); | ||
| END IF; | ||
| END $$; | ||
|
|
||
| -- Rotação: manter últimas 24 execuções (~2 anos) | ||
| DO $$ | ||
| BEGIN | ||
| IF NOT EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'smoke_tests_runs_purge') THEN | ||
| PERFORM cron.schedule( | ||
| 'smoke_tests_runs_purge', | ||
| '0 4 1 * *', | ||
| $cron$ | ||
| DELETE FROM public.smoke_tests_runs | ||
| WHERE ran_at < ( | ||
| SELECT min(ran_at) FROM ( | ||
| SELECT DISTINCT ran_at FROM public.smoke_tests_runs | ||
| ORDER BY ran_at DESC LIMIT 24 | ||
| ) keep | ||
| ); | ||
| $cron$ | ||
| ); | ||
| END IF; | ||
| END $$; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| -- ================================================================ | ||
| -- FIX: fn_run_smoke_tests retorna result com emoji prefix | ||
| -- ("✅ PASS", "❌ FAIL", "⚠️ WARN"). Ajustar parser para usar LIKE. | ||
| -- ================================================================ | ||
|
|
||
| CREATE OR REPLACE FUNCTION public.fn_run_and_persist_smoke_tests() | ||
| RETURNS TABLE( | ||
| ran_at timestamptz, | ||
| total_tests int, | ||
| passed int, | ||
| failed int, | ||
| warned int, | ||
| failed_tests text[] | ||
| ) | ||
| LANGUAGE plpgsql | ||
| SECURITY INVOKER | ||
| SET search_path = public, pg_catalog | ||
| AS $$ | ||
| DECLARE | ||
| v_ran_at timestamptz := now(); | ||
| v_total int := 0; | ||
| v_passed int := 0; | ||
| v_failed int := 0; | ||
| v_warned int := 0; | ||
| v_failed_tests text[] := ARRAY[]::text[]; | ||
| r RECORD; | ||
| BEGIN | ||
| FOR r IN SELECT * FROM public.fn_run_smoke_tests() LOOP | ||
| v_total := v_total + 1; | ||
|
|
||
| INSERT INTO public.smoke_tests_runs ( | ||
| ran_at, test_name, test_category, result, details, duration_ms | ||
| ) VALUES ( | ||
| v_ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms | ||
| ); | ||
|
|
||
| -- Match com emoji prefix (✅ PASS, ❌ FAIL, ⚠️ WARN) | ||
| IF r.result ILIKE '%PASS%' THEN | ||
| v_passed := v_passed + 1; | ||
| ELSIF r.result ILIKE '%FAIL%' THEN | ||
| v_failed := v_failed + 1; | ||
| v_failed_tests := array_append(v_failed_tests, r.test_name); | ||
| ELSIF r.result ILIKE '%WARN%' THEN | ||
| v_warned := v_warned + 1; | ||
| END IF; | ||
| END LOOP; | ||
|
|
||
| RETURN QUERY SELECT v_ran_at, v_total, v_passed, v_failed, v_warned, v_failed_tests; | ||
| END; | ||
| $$; | ||
|
|
||
| -- View latest_run ajustar ordenação para emoji prefix | ||
| CREATE OR REPLACE VIEW public.v_smoke_tests_latest_run | ||
| WITH (security_invoker = on) AS | ||
| WITH latest AS ( | ||
| SELECT max(ran_at) AS ran_at FROM public.smoke_tests_runs | ||
| ) | ||
| SELECT | ||
| r.ran_at, r.test_name, r.test_category, r.result, r.details, r.duration_ms | ||
| FROM public.smoke_tests_runs r | ||
| JOIN latest l ON r.ran_at = l.ran_at | ||
| ORDER BY | ||
| CASE | ||
| WHEN r.result ILIKE '%FAIL%' THEN 1 | ||
| WHEN r.result ILIKE '%WARN%' THEN 2 | ||
| WHEN r.result ILIKE '%PASS%' THEN 3 | ||
| ELSE 4 | ||
| END, | ||
| r.test_name; | ||
|
|
||
| -- View trend ajustar | ||
| CREATE OR REPLACE VIEW public.v_smoke_tests_trend | ||
| WITH (security_invoker = on) AS | ||
| SELECT | ||
| ran_at, | ||
| count(*) AS total, | ||
| count(*) FILTER (WHERE result ILIKE '%PASS%') AS passed, | ||
| count(*) FILTER (WHERE result ILIKE '%FAIL%') AS failed, | ||
| count(*) FILTER (WHERE result ILIKE '%WARN%') AS warned, | ||
| round(avg(duration_ms)::numeric, 1) AS avg_duration_ms | ||
| FROM public.smoke_tests_runs | ||
| GROUP BY ran_at | ||
| ORDER BY ran_at DESC | ||
| LIMIT 12; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| -- ================================================================ | ||
| -- ROLLOUT GRADUAL DO KILL-SWITCH (A/B controlado) | ||
| -- | ||
| -- Permite desligar o switch progressivamente: 5% → 25% → 50% → 100% | ||
| -- usando hash determinístico do user_id/session_id como bucket. | ||
| -- | ||
| -- Estratégia: | ||
| -- 1. Coluna rollout_percentage em system_kill_switches (0-100) | ||
| -- 2. Função fn_should_apply_kill_switch(switch_name, bucket_key) → boolean | ||
| -- Retorna TRUE se a chave cai dentro do bucket de rollout (kill aplicado) | ||
| -- 3. View v_kill_switch_with_rollout para o front consultar | ||
| -- =============================================================== | ||
|
|
||
| -- 1) Adicionar coluna de rollout | ||
| ALTER TABLE public.system_kill_switches | ||
| ADD COLUMN IF NOT EXISTS rollout_percentage smallint NOT NULL DEFAULT 100 | ||
| CHECK (rollout_percentage BETWEEN 0 AND 100); | ||
|
|
||
| COMMENT ON COLUMN public.system_kill_switches.rollout_percentage IS | ||
| 'Porcentagem de tráfego (0-100) que recebe o kill-switch quando enabled=false. | ||
| 100 = todos os clientes (default). 5 = apenas 5% (canary). | ||
| Útil para A/B testing antes de desligar definitivamente.'; | ||
|
|
||
| -- 2) Função pura de roteamento — determinística por hash do bucket_key | ||
| CREATE OR REPLACE FUNCTION public.fn_should_apply_kill_switch( | ||
| p_switch_name text, | ||
| p_bucket_key text | ||
| ) RETURNS boolean | ||
| LANGUAGE plpgsql | ||
| STABLE | ||
| SECURITY INVOKER | ||
| SET search_path = public, pg_catalog | ||
| AS $$ | ||
| DECLARE | ||
| v_enabled boolean; | ||
| v_rollout smallint; | ||
| v_bucket int; | ||
| BEGIN | ||
| -- Lê estado do switch (RLS aplicável: anon tem SELECT) | ||
| SELECT enabled, rollout_percentage | ||
| INTO v_enabled, v_rollout | ||
| FROM public.system_kill_switches | ||
| WHERE switch_name = p_switch_name; | ||
|
|
||
| -- Switch não cadastrado = não aplicar (fail-open) | ||
| IF NOT FOUND THEN RETURN false; END IF; | ||
|
|
||
| -- Switch ATIVO (enabled=true) = nunca aplicar kill, independente de rollout | ||
| IF v_enabled THEN RETURN false; END IF; | ||
|
|
||
| -- Switch OFF (enabled=false): aplicar kill conforme rollout % | ||
| IF v_rollout >= 100 THEN RETURN true; END IF; | ||
| IF v_rollout <= 0 THEN RETURN false; END IF; | ||
|
|
||
| -- Hash determinístico → bucket 0-99 | ||
| -- Mesmo bucket_key sempre cai no mesmo bucket (estabilidade entre reloads) | ||
| v_bucket := abs(hashtext(coalesce(p_bucket_key, 'anonymous')) % 100); | ||
|
|
||
| RETURN v_bucket < v_rollout; | ||
| END; | ||
| $$; | ||
|
|
||
| COMMENT ON FUNCTION public.fn_should_apply_kill_switch IS | ||
| 'Decide se o kill-switch deve ser APLICADO (= bloquear chamada) para um bucket. | ||
| Determinístico por bucket_key (mesma chave sempre cai no mesmo grupo). | ||
| Lógica: | ||
| enabled=true → false (nunca bloqueia) | ||
| enabled=false, rollout=100 → true (sempre bloqueia) | ||
| enabled=false, rollout=0 → false (nunca bloqueia — pré-rollout) | ||
| enabled=false, rollout=X% → true se hashtext(bucket_key) % 100 < X | ||
| Bucket_key sugerido: auth.uid() para logged-in; localStorage uuid para anon.'; | ||
|
|
||
| GRANT EXECUTE ON FUNCTION public.fn_should_apply_kill_switch TO anon; | ||
| GRANT EXECUTE ON FUNCTION public.fn_should_apply_kill_switch TO authenticated; | ||
|
|
||
| -- 3) Validar: switch atual está enabled=true, então sempre retorna false | ||
| SELECT | ||
| public.fn_should_apply_kill_switch('edge_external_db_bridge', 'test-user-1') AS rollout_test_1, | ||
| public.fn_should_apply_kill_switch('edge_external_db_bridge', 'test-user-2') AS rollout_test_2, | ||
| public.fn_should_apply_kill_switch('non_existent_switch', 'anyone') AS unknown_switch; | ||
|
Comment on lines
+75
to
+79
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| -- Restore the BEFORE INSERT trigger that auto-generates public.quotes.quote_number. | ||
| -- The trigger was lost during the migration-replay drift cleanup while | ||
| -- public.generate_quote_number() survived. Without it, quote_number (NOT NULL, | ||
| -- no default) is never populated and every quote INSERT from the app fails. | ||
| DROP TRIGGER IF EXISTS set_quote_number ON public.quotes; | ||
|
|
||
| CREATE TRIGGER set_quote_number | ||
| BEFORE INSERT ON public.quotes | ||
| FOR EACH ROW | ||
| WHEN (NEW.quote_number IS NULL OR NEW.quote_number = '') | ||
| EXECUTE FUNCTION public.generate_quote_number(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RLS is enabled on
public.smoke_tests_runs, but this migration only creates aFOR SELECTpolicy forauthenticated. The same migration definesfn_run_and_persist_smoke_tests()asSECURITY INVOKERand grants it toauthenticated, and that function performsINSERTintosmoke_tests_runs; without a matchingFOR INSERTpolicy, authenticated callers (including admin dashboard users) will hit an RLS violation and the RPC cannot persist results.Useful? React with 👍 / 👎.