payroll-incremental-reads.sql 8.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. -- Derived, transactionally maintained fingerprints. No financial source is changed.
  2. -- Install atomically after verifying original definitions; rollback restores them first.
  3. CREATE TABLE IF NOT EXISTS "PayrollReadDigest" (
  4. kind text NOT NULL, company text NOT NULL, "objectId" text NOT NULL,
  5. model numeric, hash text NOT NULL, PRIMARY KEY(kind,company,"objectId")
  6. );
  7. CREATE INDEX IF NOT EXISTS payroll_read_digest_order ON "PayrollReadDigest" (company,kind COLLATE "C",hash COLLATE "C");
  8. CREATE TABLE IF NOT EXISTS "PayrollReadAuthority" (name text PRIMARY KEY, hash text);
  9. INSERT INTO "PayrollReadAuthority" SELECT name,md5(pg_get_functiondef(to_regprocedure(name))) FROM (VALUES ('xs_payroll_sources(text)'),('xs_payroll_source_version(text)'),('xs_payroll_cutoff_snapshot(text,text)')) v(name) ON CONFLICT(name) DO NOTHING;
  10. CREATE TABLE IF NOT EXISTS "PayrollReadDigestMeta" (kind text PRIMARY KEY, signature text NOT NULL);
  11. CREATE TABLE IF NOT EXISTS "PayrollReadChangeScope" (company text NOT NULL, scope text NOT NULL, xid xid8 NOT NULL, PRIMARY KEY(company,scope,xid));
  12. CREATE TABLE IF NOT EXISTS "PayrollReadChange" (company text NOT NULL, xid xid8 NOT NULL, PRIMARY KEY(company,xid));
  13. CREATE TABLE IF NOT EXISTS "PayrollVerifiedReadCache" (
  14. company text NOT NULL, key text NOT NULL, stamp text NOT NULL, payload jsonb NOT NULL,
  15. "createdAt" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(company,key)
  16. );
  17. REVOKE ALL ON "PayrollReadDigest","PayrollReadDigestMeta","PayrollVerifiedReadCache","PayrollReadChange","PayrollReadChangeScope","PayrollReadAuthority" FROM PUBLIC;
  18. CREATE OR REPLACE FUNCTION xs_payroll_authority_ready() RETURNS boolean LANGUAGE sql STABLE AS $$
  19. SELECT count(*)=3 AND bool_and(hash IS NOT DISTINCT FROM md5(pg_get_functiondef(to_regprocedure(name)))) FROM "PayrollReadAuthority";
  20. $$;
  21. CREATE OR REPLACE FUNCTION xs_payroll_digest_signature(tab text) RETURNS text LANGUAGE sql STABLE AS $$
  22. SELECT md5(COALESCE((SELECT string_agg(a.attnum||':'||a.attname||':'||a.atttypid||':'||a.atttypmod,'|' ORDER BY a.attnum) FROM pg_attribute a WHERE a.attrelid=to_regclass(format('public.%I',tab)) AND a.attnum>0 AND NOT a.attisdropped),'')||'|'||
  23. COALESCE((SELECT string_agg(t.tgname||':'||t.xmin||':'||t.ctid||':'||t.tgenabled::text||':'||pg_get_triggerdef(t.oid),'|' ORDER BY t.tgname) FROM pg_trigger t WHERE t.tgrelid=to_regclass(format('public.%I',tab)) AND t.tgname IN ('xs_payroll_digest_row','xs_payroll_digest_truncate')),'')||'|'||
  24. COALESCE(pg_get_functiondef(to_regprocedure('xs_payroll_digest_row()')),'')||COALESCE(pg_get_functiondef(to_regprocedure('xs_payroll_digest_truncate()')),''));
  25. $$;
  26. CREATE OR REPLACE FUNCTION xs_payroll_digest_row() RETURNS trigger LANGUAGE plpgsql
  27. SET TimeZone='Etc/GMT-8' SET DateStyle='ISO, MDY' SET IntervalStyle='postgres' SET extra_float_digits=1 SET bytea_output='hex' AS $$
  28. DECLARE k text:=TG_ARGV[0];
  29. BEGIN
  30. IF TG_OP<>'INSERT' AND OLD.company IS NOT NULL AND (k<>'common' OR (to_jsonb(OLD)->>'modelId')::numeric IN (54,59,60)) THEN
  31. INSERT INTO "PayrollReadChange" VALUES(OLD.company,pg_current_xact_id()) ON CONFLICT DO NOTHING;
  32. INSERT INTO "PayrollReadChangeScope" VALUES(OLD.company,CASE WHEN k='common' THEN k||':'||((to_jsonb(OLD)->>'modelId')::numeric)::int::text ELSE k END,pg_current_xact_id()) ON CONFLICT DO NOTHING;
  33. END IF;
  34. IF TG_OP<>'DELETE' AND NEW.company IS NOT NULL AND (k<>'common' OR (to_jsonb(NEW)->>'modelId')::numeric IN (54,59,60)) THEN
  35. INSERT INTO "PayrollReadChange" VALUES(NEW.company,pg_current_xact_id()) ON CONFLICT DO NOTHING;
  36. INSERT INTO "PayrollReadChangeScope" VALUES(NEW.company,CASE WHEN k='common' THEN k||':'||((to_jsonb(NEW)->>'modelId')::numeric)::int::text ELSE k END,pg_current_xact_id()) ON CONFLICT DO NOTHING;
  37. END IF;
  38. IF TG_OP<>'INSERT' THEN DELETE FROM "PayrollReadDigest" WHERE kind=k AND company=OLD.company AND "objectId"=OLD."objectId"; END IF;
  39. IF TG_OP<>'DELETE' AND NEW.company IS NOT NULL AND (k<>'common' OR (to_jsonb(NEW)->>'modelId')::numeric IN (54,59,60)) THEN
  40. INSERT INTO "PayrollReadDigest"(kind,company,"objectId",model,hash) VALUES(k,NEW.company,NEW."objectId",CASE WHEN k='common' THEN (to_jsonb(NEW)->>'modelId')::numeric END,md5(NEW::text));
  41. END IF;
  42. RETURN NULL;
  43. END $$;
  44. CREATE OR REPLACE FUNCTION xs_payroll_digest_truncate() RETURNS trigger LANGUAGE plpgsql AS $$
  45. BEGIN INSERT INTO "PayrollReadChangeScope" SELECT DISTINCT company,CASE WHEN kind='common' THEN kind||':'||model::int::text ELSE kind END,pg_current_xact_id() FROM "PayrollReadDigest" WHERE kind=TG_ARGV[0] ON CONFLICT DO NOTHING;INSERT INTO "PayrollReadChange" SELECT DISTINCT company,pg_current_xact_id() FROM "PayrollReadDigest" WHERE kind=TG_ARGV[0] ON CONFLICT DO NOTHING;DELETE FROM "PayrollReadDigest" WHERE kind=TG_ARGV[0];RETURN NULL;END $$;
  46. -- Matching environment and unchanged, ALWAYS-enabled triggers are prerequisites.
  47. -- DDL (including disable/re-enable) changes catalog tuple versions and fails closed.
  48. CREATE OR REPLACE FUNCTION xs_payroll_digest_ready() RETURNS boolean LANGUAGE sql STABLE AS $$
  49. SELECT current_setting('TimeZone')='Etc/GMT-8' AND current_setting('DateStyle')='ISO, MDY'
  50. AND current_setting('IntervalStyle')='postgres' AND current_setting('extra_float_digits')='1' AND current_setting('bytea_output')='hex'
  51. AND NOT EXISTS(SELECT 1 FROM (VALUES ('common','CommonModel'),('appointment','CourseAppointment'),('lesson','LessonRecord'),('review','MemoryPracticeRecord')) v(kind,tab)
  52. LEFT JOIN "PayrollReadDigestMeta" m ON m.kind=v.kind
  53. WHERE m.signature IS DISTINCT FROM xs_payroll_digest_signature(v.tab)
  54. OR (SELECT count(*) FROM pg_trigger t WHERE t.tgrelid=to_regclass(format('public.%I',v.tab)) AND t.tgname IN ('xs_payroll_digest_row','xs_payroll_digest_truncate') AND t.tgenabled='A')<>2);
  55. $$;
  56. CREATE OR REPLACE FUNCTION xs_payroll_data_version(co text) RETURNS text LANGUAGE plpgsql SET jit=off SET work_mem='64MB' AS $$
  57. DECLARE v text;
  58. BEGIN
  59. IF xs_payroll_digest_ready() THEN
  60. SELECT md5(COALESCE(string_agg(hash,'' ORDER BY kind COLLATE "C",hash COLLATE "C"),'')) INTO v FROM "PayrollReadDigest" WHERE company=co;
  61. ELSE
  62. SELECT md5(COALESCE(string_agg(hash,'' ORDER BY kind COLLATE "C",hash COLLATE "C"),'')) INTO v FROM (
  63. SELECT 'common' kind,md5(c::text) hash FROM "CommonModel" c WHERE company=co AND "modelId" IN (54,59,60)
  64. UNION ALL SELECT 'appointment',md5(c::text) FROM "CourseAppointment" c WHERE company=co
  65. UNION ALL SELECT 'lesson',md5(c::text) FROM "LessonRecord" c WHERE company=co
  66. UNION ALL SELECT 'review',md5(c::text) FROM "MemoryPracticeRecord" c WHERE company=co) s;
  67. END IF;
  68. RETURN v;
  69. END $$;
  70. -- Installation locks source writers until triggers and the initial digest agree.
  71. DO $$ DECLARE r record;BEGIN
  72. FOR r IN SELECT * FROM (VALUES ('common','CommonModel'),('appointment','CourseAppointment'),('lesson','LessonRecord'),('review','MemoryPracticeRecord')) v(kind,tab) LOOP
  73. EXECUTE format('LOCK TABLE %I IN SHARE ROW EXCLUSIVE MODE',r.tab);
  74. EXECUTE format('DROP TRIGGER IF EXISTS xs_payroll_digest_row ON %I',r.tab);
  75. EXECUTE format('DROP TRIGGER IF EXISTS xs_payroll_digest_truncate ON %I',r.tab);
  76. EXECUTE format('CREATE TRIGGER xs_payroll_digest_row AFTER INSERT OR UPDATE OR DELETE ON %I FOR EACH ROW EXECUTE FUNCTION xs_payroll_digest_row(%L)',r.tab,r.kind);
  77. EXECUTE format('CREATE TRIGGER xs_payroll_digest_truncate AFTER TRUNCATE ON %I FOR EACH STATEMENT EXECUTE FUNCTION xs_payroll_digest_truncate(%L)',r.tab,r.kind);
  78. EXECUTE format('ALTER TABLE %I ENABLE ALWAYS TRIGGER xs_payroll_digest_row',r.tab);
  79. EXECUTE format('ALTER TABLE %I ENABLE ALWAYS TRIGGER xs_payroll_digest_truncate',r.tab);
  80. DELETE FROM "PayrollReadDigest" WHERE kind=r.kind;
  81. -- Cast the actual source row, exactly as in the previous cutoff hash.
  82. EXECUTE format('INSERT INTO "PayrollReadDigest"(kind,company,"objectId",model,hash) SELECT %L,c.company,c."objectId",%s,md5(c::text) FROM %I c WHERE company IS NOT NULL %s',r.kind,CASE WHEN r.kind='common' THEN 'c."modelId"' ELSE 'NULL::numeric' END,r.tab,CASE WHEN r.kind='common' THEN 'AND c."modelId" IN (54,59,60)' ELSE '' END);
  83. INSERT INTO "PayrollReadDigestMeta" VALUES(r.kind,xs_payroll_digest_signature(r.tab)) ON CONFLICT(kind) DO UPDATE SET signature=EXCLUDED.signature;
  84. END LOOP;
  85. END $$;