read-revisions.sql 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. BEGIN;
  2. -- Append one event per writer transaction/domain. No shared counter-row locks.
  3. CREATE TABLE IF NOT EXISTS xs_read_revision_event (
  4. company text NOT NULL,domain text NOT NULL,revision bigint NOT NULL,
  5. PRIMARY KEY(company,domain,revision)
  6. );
  7. CREATE TABLE IF NOT EXISTS xs_history_summary (company text NOT NULL,actor text NOT NULL,key text NOT NULL,revision text NOT NULL,value jsonb NOT NULL,expires_at timestamptz NOT NULL,PRIMARY KEY(company,actor,key));
  8. CREATE OR REPLACE FUNCTION xs_read_version(co text,scope text) RETURNS text LANGUAGE sql STABLE AS $$
  9. WITH frontier AS (SELECT GREATEST(
  10. COALESCE((SELECT max(revision) FROM xs_read_revision_event WHERE company=co AND domain=scope),0),
  11. COALESCE((SELECT max(revision) FROM xs_read_revision_event WHERE company='*' AND domain=scope),0)) revision)
  12. SELECT revision::text||':'||COALESCE((SELECT string_agg(x::text,',' ORDER BY x)
  13. FROM txid_snapshot_xip(txid_current_snapshot()) x WHERE x<=frontier.revision),'') FROM frontier
  14. $$;
  15. -- Include older in-flight writer IDs in the version. If an older writer commits
  16. -- after the frontier, its disappearance invalidates the cache (MAX alone cannot).
  17. CREATE OR REPLACE FUNCTION xs_prune_read_revisions() RETURNS void LANGUAGE sql AS $$
  18. DELETE FROM xs_read_revision_event e USING (
  19. SELECT company,domain,max(revision) keep FROM xs_read_revision_event
  20. WHERE revision<txid_snapshot_xmin(txid_current_snapshot()) GROUP BY company,domain
  21. ) k WHERE e.company=k.company AND e.domain=k.domain AND e.revision<k.keep;
  22. DELETE FROM xs_history_summary WHERE expires_at<=now();
  23. $$;
  24. CREATE OR REPLACE FUNCTION xs_bump_read_revision() RETURNS trigger LANGUAGE plpgsql AS $$
  25. DECLARE companies text[];co text;scope text;
  26. BEGIN
  27. IF TG_OP='TRUNCATE' THEN
  28. INSERT INTO xs_read_revision_event(company,domain,revision) SELECT '*',domain,txid_current() FROM unnest(string_to_array(TG_ARGV[0],',')) domain ON CONFLICT DO NOTHING;
  29. RETURN NULL;
  30. ELSIF TG_OP='INSERT' THEN
  31. SELECT array_agg(DISTINCT COALESCE(to_jsonb(n)->>'company','*')) INTO companies FROM new_rows n;
  32. ELSIF TG_OP='DELETE' THEN
  33. SELECT array_agg(DISTINCT COALESCE(to_jsonb(o)->>'company','*')) INTO companies FROM old_rows o;
  34. ELSE
  35. SELECT array_agg(DISTINCT company ORDER BY company) INTO companies FROM (
  36. SELECT COALESCE(to_jsonb(n)->>'company','*') company FROM new_rows n
  37. UNION SELECT COALESCE(to_jsonb(o)->>'company','*') FROM old_rows o
  38. ) x;
  39. END IF;
  40. -- Keys are transaction-specific; different writers never contend on a counter.
  41. FOR co IN SELECT unnest(companies) ORDER BY 1 LOOP
  42. FOR scope IN SELECT unnest(string_to_array(TG_ARGV[0],',')) ORDER BY 1 LOOP
  43. INSERT INTO xs_read_revision_event(company,domain,revision) VALUES(co,scope,txid_current()) ON CONFLICT DO NOTHING;
  44. END LOOP;
  45. END LOOP;
  46. RETURN NULL;
  47. END $$;
  48. DO $$
  49. DECLARE source record;
  50. BEGIN
  51. FOR source IN SELECT * FROM (VALUES
  52. ('LessonRecord','account,lessons'),('CommonModel','learning,lessons'),
  53. ('LessonCompletionReport','lessons'),('CourseAppointment','appointments,lessons'),
  54. ('_User','account,lessons'),('Node','courses,lessons'),('CourseBinding','courses'),
  55. ('DailyStudyRecord','learning,lessons'),('MemoryPracticeRecord','lessons,review'),
  56. ('CourseCreditReservation','account'),('SurveyItem','reading'),('SurveyLog','reading')
  57. ) s(tbl,domains) LOOP
  58. IF to_regclass(format('%I',source.tbl)) IS NULL THEN CONTINUE;END IF;
  59. EXECUTE format('DROP TRIGGER IF EXISTS xs_revision_insert ON %I',source.tbl);
  60. EXECUTE format('DROP TRIGGER IF EXISTS xs_revision_update ON %I',source.tbl);
  61. EXECUTE format('DROP TRIGGER IF EXISTS xs_revision_delete ON %I',source.tbl);
  62. EXECUTE format('DROP TRIGGER IF EXISTS xs_revision_truncate ON %I',source.tbl);
  63. EXECUTE format('CREATE TRIGGER xs_revision_insert AFTER INSERT ON %I REFERENCING NEW TABLE AS new_rows FOR EACH STATEMENT EXECUTE FUNCTION xs_bump_read_revision(%L)',source.tbl,source.domains);
  64. EXECUTE format('CREATE TRIGGER xs_revision_update AFTER UPDATE ON %I REFERENCING OLD TABLE AS old_rows NEW TABLE AS new_rows FOR EACH STATEMENT EXECUTE FUNCTION xs_bump_read_revision(%L)',source.tbl,source.domains);
  65. EXECUTE format('CREATE TRIGGER xs_revision_delete AFTER DELETE ON %I REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION xs_bump_read_revision(%L)',source.tbl,source.domains);
  66. EXECUTE format('CREATE TRIGGER xs_revision_truncate AFTER TRUNCATE ON %I FOR EACH STATEMENT EXECUTE FUNCTION xs_bump_read_revision(%L)',source.tbl,source.domains);
  67. END LOOP;
  68. END $$;
  69. COMMIT;