d3ro-voice/server/supabase/tests/server-owned-sync-timestamps.integration.sql

197 lines
7.2 KiB
PL/PgSQL

\set ON_ERROR_STOP on
-- Regression for 20260927000037_server_owned_sync_timestamps.sql.
-- Every synced table is pulled with a (updated_at, id) keyset cursor, so an
-- INSERT must not keep a client-chosen updated_at: a stale value (offline
-- recording, archive restore) lands behind other desktops' cursors, and a
-- future value (fast clock) pushes their cursors ahead.
BEGIN;
CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF condition IS NOT TRUE THEN
RAISE EXCEPTION 'assertion_failed: %', message;
END IF;
END;
$$;
-- Catalog: every cursor-pulled table stamps updated_at on INSERT.
DO $$
DECLARE
t text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'history',
'dictionary',
'meetings',
'meeting_memos',
'meeting_documents',
'custom_instructions',
'user_templates',
'knowledge_documents'
]
LOOP
PERFORM pg_temp.assert_true(
EXISTS (
SELECT 1
FROM pg_trigger AS trg
WHERE trg.tgrelid = format('public.%I', t)::regclass
AND trg.tgname = 'stamp_sync_insert_v1'
AND NOT trg.tgisinternal
AND trg.tgenabled <> 'D'
-- ROW (1) | BEFORE (2) | INSERT (4)
AND (trg.tgtype & 7) = 7
),
format('%s stamps updated_at before insert', t)
);
END LOOP;
END $$;
SELECT pg_temp.assert_true(
EXISTS (
SELECT 1
FROM pg_trigger AS trg
WHERE trg.tgrelid = 'public.user_templates'::regclass
AND trg.tgname = 'stamp_sync_update_v1'
-- ROW (1) | BEFORE (2) | UPDATE (16)
AND (trg.tgtype & 19) = 19
),
'user_templates stamps updated_at before update'
);
SELECT pg_temp.assert_true(
NOT has_function_privilege('authenticated', 'public.stamp_sync_timestamp_v1()', 'EXECUTE'),
'clients cannot call the stamp function directly'
);
INSERT INTO auth.users (
id, aud, role, email, encrypted_password, email_confirmed_at,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES (
'37000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
'sync-timestamps@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
);
-- Behaviour: client-supplied past and future timestamps are replaced by the
-- transaction clock on every table.
INSERT INTO public.history (id, user_id, original_text, duration, updated_at)
VALUES
('37000000-0000-4000-8000-0000000000a1', '37000000-0000-4000-8000-000000000001',
'recorded offline yesterday', 1.5, now() - interval '1 day'),
('37000000-0000-4000-8000-0000000000a2', '37000000-0000-4000-8000-000000000001',
'recorded on a fast clock', 1.5, now() + interval '2 hours');
INSERT INTO public.dictionary (id, user_id, word, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000b1', '37000000-0000-4000-8000-000000000001',
'cursor', now() - interval '3 days');
INSERT INTO public.meetings (id, user_id, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000c1', '37000000-0000-4000-8000-000000000001',
now() + interval '5 hours');
INSERT INTO public.meeting_memos (id, meeting_id, user_id, content, timestamp_ms, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000d1', '37000000-0000-4000-8000-0000000000c1',
'37000000-0000-4000-8000-000000000001', 'memo', 1000, now() - interval '10 days');
INSERT INTO public.meeting_documents (id, meeting_id, user_id, template_type, title, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000e1', '37000000-0000-4000-8000-0000000000c1',
'37000000-0000-4000-8000-000000000001', 'minutes', 'minutes', now() - interval '1 year');
INSERT INTO public.custom_instructions (id, user_id, name, prompt, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000f1', '37000000-0000-4000-8000-000000000001',
'sync timestamp fixture', 'prompt', now() + interval '1 day');
INSERT INTO public.user_templates (
id, user_id, template_kind, name, template_type, system_prompt, updated_at
) VALUES (
'37000000-0000-4000-8000-000000000101', '37000000-0000-4000-8000-000000000001',
'meeting_document', 'sync timestamp fixture', 'custom', 'prompt', now() - interval '30 days'
);
INSERT INTO public.knowledge_documents (id, user_id, title, updated_at)
VALUES ('37000000-0000-4000-8000-000000000111', '37000000-0000-4000-8000-000000000001',
'knowledge', now() - interval '2 days');
DO $$
DECLARE
owner uuid := '37000000-0000-4000-8000-000000000001';
t text;
stale integer;
BEGIN
FOREACH t IN ARRAY ARRAY[
'history',
'dictionary',
'meetings',
'meeting_memos',
'meeting_documents',
'custom_instructions',
'user_templates',
'knowledge_documents'
]
LOOP
EXECUTE format(
'SELECT count(*) FROM public.%I WHERE user_id = $1 AND id::text LIKE ''37000000-%%'' '
'AND updated_at IS DISTINCT FROM now()',
t
)
INTO stale
USING owner;
PERFORM pg_temp.assert_true(stale = 0, format('%s insert keeps the server clock, not the client value', t));
END LOOP;
END $$;
-- The desktop pushes with PostgREST upsert (INSERT ... ON CONFLICT DO UPDATE).
-- A fresh row goes through the INSERT stamp, an existing row through the
-- UPDATE stamp; neither keeps the client timestamp.
INSERT INTO public.history (id, user_id, original_text, duration, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000a3', '37000000-0000-4000-8000-000000000001',
'upserted while offline', 2.0, now() - interval '6 hours')
ON CONFLICT (id) DO UPDATE SET original_text = EXCLUDED.original_text, updated_at = EXCLUDED.updated_at;
INSERT INTO public.history (id, user_id, original_text, duration, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000a1', '37000000-0000-4000-8000-000000000001',
'edited offline', 1.5, now() - interval '6 hours')
ON CONFLICT (id) DO UPDATE SET original_text = EXCLUDED.original_text, updated_at = EXCLUDED.updated_at;
SELECT pg_temp.assert_true(
(SELECT count(*) FROM public.history
WHERE id IN ('37000000-0000-4000-8000-0000000000a1', '37000000-0000-4000-8000-0000000000a3')
AND updated_at = now()) = 2,
'upsert insert and upsert update both carry the server clock'
);
-- Same stamp for a client write through RLS, as PostgREST performs it.
SET LOCAL ROLE authenticated;
SELECT set_config(
'request.jwt.claims',
'{"sub":"37000000-0000-4000-8000-000000000001","role":"authenticated"}',
true
);
INSERT INTO public.dictionary (id, user_id, word, updated_at)
VALUES ('37000000-0000-4000-8000-0000000000b2', '37000000-0000-4000-8000-000000000001',
'skewed', now() + interval '2 hours');
SELECT pg_temp.assert_true(
(SELECT updated_at FROM public.dictionary WHERE id = '37000000-0000-4000-8000-0000000000b2') = now(),
'an authenticated client cannot choose updated_at on insert'
);
RESET ROLE;
-- user_templates: a direct UPDATE (no RPC) also moves the cursor column.
UPDATE public.user_templates
SET name = 'renamed fixture', updated_at = now() - interval '9 days'
WHERE id = '37000000-0000-4000-8000-000000000101';
SELECT pg_temp.assert_true(
(SELECT updated_at FROM public.user_templates WHERE id = '37000000-0000-4000-8000-000000000101') = now(),
'user_templates update carries the server clock'
);
ROLLBACK;