\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;