fix(sync): let the server own updated_at on insert for cursor-pulled tables
This commit is contained in:
parent
2251fe2da1
commit
f517eedffc
2 changed files with 265 additions and 0 deletions
|
|
@ -0,0 +1,197 @@
|
|||
\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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue