diff --git a/lib/model/migrations/20251025-01-auditlog-eventcounters-01.down.sql b/lib/model/migrations/20251025-01-auditlog-eventcounters-01.down.sql new file mode 100644 index 000000000..8a35b2652 --- /dev/null +++ b/lib/model/migrations/20251025-01-auditlog-eventcounters-01.down.sql @@ -0,0 +1,9 @@ + +--- drop: eventcounter_bump_triggerfunction --- +DROP FUNCTION IF EXISTS "public"."eventcounter_bump_triggerfunction"() CASCADE; + +--- drop: eventcounter_squash(actee_id character varying) --- +DROP FUNCTION IF EXISTS "public"."eventcounter_squash"(actee_id character varying) CASCADE; + +--- drop: public.audits.eventcounter_bump_trigger --- +DROP TRIGGER IF EXISTS eventcounter_bump_trigger ON "public"."audits" CASCADE; diff --git a/lib/model/migrations/20251025-01-auditlog-eventcounters-01.up.sql b/lib/model/migrations/20251025-01-auditlog-eventcounters-01.up.sql new file mode 100644 index 000000000..0985fec7b --- /dev/null +++ b/lib/model/migrations/20251025-01-auditlog-eventcounters-01.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE eventcounters ( + "acteeId" varchar(36) REFERENCES actees (id) ON DELETE CASCADE NOT NULL, + evt_count integer NOT NULL DEFAULT 1 +); + +-- Embedding the evt_count into the index makes the aggregate sum(evt_count) for a given actor +-- possible with an index-only scan. +CREATE INDEX idx_eventcounters ON eventcounters USING btree ("acteeId") INCLUDE (evt_count); diff --git a/lib/model/migrations/20251025-01-auditlog-eventcounters-02.down.sql b/lib/model/migrations/20251025-01-auditlog-eventcounters-02.down.sql new file mode 100644 index 000000000..ef99ae02b --- /dev/null +++ b/lib/model/migrations/20251025-01-auditlog-eventcounters-02.down.sql @@ -0,0 +1 @@ +DROP TABLE eventcounters; diff --git a/lib/model/migrations/20251025-01-auditlog-eventcounters-02.up.sql b/lib/model/migrations/20251025-01-auditlog-eventcounters-02.up.sql new file mode 100644 index 000000000..6bc5e5f3d --- /dev/null +++ b/lib/model/migrations/20251025-01-auditlog-eventcounters-02.up.sql @@ -0,0 +1,59 @@ + +--- create: eventcounter_squash(actee_id varchar(36)) --- +CREATE FUNCTION "public"."eventcounter_squash"(actee_id varchar(36)) +RETURNS integer +AS + $BODY$ + WITH deleted AS ( + DELETE FROM eventcounters WHERE "acteeId" = actee_id RETURNING evt_count + ) + INSERT INTO eventcounters ("acteeId", evt_count) VALUES (actee_id, (SELECT COALESCE(SUM(evt_count), 0) FROM deleted)) RETURNING evt_count + $BODY$ +LANGUAGE sql +VOLATILE +STRICT +PARALLEL UNSAFE +; + +--- sign: eventcounter_squash(actee_id varchar(36)) --- +COMMENT ON FUNCTION "public"."eventcounter_squash"(actee_id varchar(36)) IS '{"dbsamizdat": {"version": 1, "definition_hash": "b264d1502e124c331ad7d11b20b8fca2"}}'; + +--- create: eventcounter_bump_triggerfunction --- +CREATE FUNCTION "public"."eventcounter_bump_triggerfunction"() +RETURNS trigger +AS + $BODY$ + BEGIN + INSERT INTO eventcounters ("acteeId") VALUES (NEW."acteeId"); + IF + (random() < 0.01) + THEN + PERFORM eventcounter_squash(NEW."acteeId"); + END IF; + RETURN NULL; + END; + $BODY$ +LANGUAGE plpgsql +VOLATILE +STRICT +PARALLEL UNSAFE +; + +--- sign: eventcounter_bump_triggerfunction --- +COMMENT ON FUNCTION "public"."eventcounter_bump_triggerfunction"() IS '{"dbsamizdat": {"version": 1, "definition_hash": "2ddd8bde4e781812dd87338f21b024e8"}}'; + +--- create: public.audits.eventcounter_bump_trigger --- + CREATE TRIGGER "eventcounter_bump_trigger" + AFTER INSERT OR UPDATE OF processed +ON "public"."audits" + FOR EACH ROW + WHEN ( + (NEW."acteeId" IS NOT NULL) + AND + (NEW.action = ANY(ARRAY['form.update.publish', 'submission.create', 'submission.update', 'submission.update.version', 'submission.attachment.update', 'submission.delete', 'submission.restore', 'dataset.update', 'dataset.update.publish', 'entity.create', 'entity.update.version', 'entity.update.resolve', 'entity.delete', 'entity.restore', 'entity.bulk.delete', 'entity.bulk.restore'])) + ) + EXECUTE PROCEDURE eventcounter_bump_triggerfunction() + ; + +--- sign: public.audits.eventcounter_bump_trigger --- +COMMENT ON TRIGGER "eventcounter_bump_trigger" ON "public"."audits" IS '{"dbsamizdat": {"version": 1, "definition_hash": "45fddf42c072bc6d04104bbaed5ac120"}}'; diff --git a/lib/model/migrations/20251025-01-auditlog-eventcounters.js b/lib/model/migrations/20251025-01-auditlog-eventcounters.js new file mode 100644 index 000000000..1a47cf703 --- /dev/null +++ b/lib/model/migrations/20251025-01-auditlog-eventcounters.js @@ -0,0 +1,10 @@ +// Copyright 2025 ODK Central Developers +// See the NOTICE file at the top-level directory of this distribution and at +// https://github.com/getodk/central-backend/blob/master/NOTICE. +// This file is part of ODK Central. It is subject to the license terms in +// the LICENSE file found in the top-level directory of this distribution and at +// https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, +// including this file, may be copied, modified, propagated, or distributed +// except according to the terms contained in the LICENSE file. + +module.exports = require('../pure-sql-migration')(__filename);