joshua_kelly
06/12/2025, 12:09 AM// In order to make progress, the lastUpdated timestamp must change from the first entry of the current page
// to the first entry of the next page; otherwise we'd make the exact same request in a loop
But the issue here is that if a large number of resources are inserted quickly, and they have the same lastUpdated timestamp - and that their number exceeds the page size, well, we're kind of SOL on an export no?cody
06/12/2025, 5:10 AMjoshua_kelly
06/12/2025, 2:56 PMjoshua_kelly
07/09/2025, 7:26 PMjoshua_kelly
07/09/2025, 7:52 PMjoshua_kelly
07/10/2025, 2:54 PMjoshua_kelly
07/10/2025, 2:55 PMjoshua_kelly
07/10/2025, 9:37 PMjoshua_kelly
07/10/2025, 9:37 PMrahul1
07/10/2025, 10:00 PMjoshua_kelly
07/14/2025, 4:18 PMjoshua_kelly
07/14/2025, 4:18 PMjoshua_kelly
07/14/2025, 4:19 PM-- Count how many records would be affected
SELECT
COUNT(*) as total_records_to_jitter,
COUNT(DISTINCT date_trunc('milliseconds', "lastUpdated")) as unique_timestamp_groups
FROM "Observation"
WHERE date_trunc('milliseconds', "lastUpdated") IN (
SELECT date_trunc('milliseconds', "lastUpdated")
FROM "Observation"
GROUP BY date_trunc('milliseconds', "lastUpdated")
HAVING COUNT(*) > 250
);joshua_kelly
07/14/2025, 4:20 PMjoshua_kelly
07/14/2025, 4:21 PMWITH jittered_updates AS (
SELECT
pr.id,
pr."lastUpdated" as original_timestamp,
-- Generate MD5 hash from ID and timestamp, then convert to numeric offset
-- Use the first 8 characters of MD5 hash as hex, convert to decimal
-- Then modulo by 1000 to get milliseconds offset (0-999ms)
(('x' || substr(md5(pr.id || pr."lastUpdated"::text), 1, 8))::bit(32)::int % 1000) as jitter_ms,
-- Calculate the new jittered timestamp
pr."lastUpdated" + INTERVAL '1 millisecond' * (('x' || substr(md5(pr.id || pr."lastUpdated"::text), 1, 8))::bit(32)::int % 1000) as jittered_timestamp
FROM "Observation" pr
WHERE date_trunc('milliseconds', pr."lastUpdated") IN (
SELECT date_trunc('milliseconds', "lastUpdated")
FROM "Observation"
GROUP BY date_trunc('milliseconds', "lastUpdated")
HAVING COUNT(*) > 250
)
ORDER BY pr.id -- Consistent ordering for chunking
LIMIT 5000 -- Process first 5000 records
),joshua_kelly
07/14/2025, 4:21 PM-- Update both the lastUpdated column and the JSON content
updated_records AS (
UPDATE "Observation"
SET
"lastUpdated" = ju.jittered_timestamp,
"content" = CASE
WHEN ("content"::json -> 'meta' ->> 'lastUpdated') IS NOT NULL THEN
-- Replace the existing lastUpdated value in JSON with the jittered timestamp
replace(
"content",
'"lastUpdated":"' || ("content"::json -> 'meta' ->> 'lastUpdated') || '"',
'"lastUpdated":"' || to_char(ju.jittered_timestamp AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') || '"'
)
ELSE
"content"
END
FROM jittered_updates ju
WHERE "Observation".id = ju.id
RETURNING "Observation".id, ju.original_timestamp, ju.jitter_ms, "Observation"."lastUpdated" as new_timestamp
)
SELECT
COUNT(*) as records_updated_chunk_1,
MIN(jitter_ms) as min_jitter_ms,
MAX(jitter_ms) as max_jitter_ms,
AVG(jitter_ms) as avg_jitter_ms
FROM updated_records;joshua_kelly
07/14/2025, 4:21 PMjoshua_kelly
07/14/2025, 4:22 PMjoshua_kelly
07/14/2025, 4:23 PMjoshua_kelly
07/16/2025, 1:53 PM