mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
refactor(schema): fan desired-state schema out to one file per table (#8131)
This commit is contained in:
78
schema/functions/assert_community_write_allowed.sql
Normal file
78
schema/functions/assert_community_write_allowed.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
CREATE FUNCTION assert_community_write_allowed(target UUID) RETURNS VOID
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
lifecycle TEXT;
|
||||
generation BIGINT;
|
||||
executor_community TEXT;
|
||||
executor_generation TEXT;
|
||||
serving_community TEXT;
|
||||
serving_lease_id TEXT;
|
||||
serving_owner TEXT;
|
||||
serving_generation TEXT;
|
||||
serving_fence_generation TEXT;
|
||||
serving_lease_valid BOOLEAN := false;
|
||||
BEGIN
|
||||
-- The fence proof requires a fresh statement snapshot after lock grant;
|
||||
-- pinned RR/Serializable snapshots can retain pre-fence authorization.
|
||||
IF current_setting('transaction_isolation') <> 'read committed' THEN
|
||||
RAISE EXCEPTION 'community writes require READ COMMITTED isolation'
|
||||
USING ERRCODE = 'invalid_transaction_state';
|
||||
END IF;
|
||||
|
||||
-- Nullable operator-attribution rows without a tenant are unrelated.
|
||||
IF target IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock_shared(community_deletion_lock_key(target));
|
||||
SELECT deletion_state, deletion_fence_generation
|
||||
INTO lifecycle, generation
|
||||
FROM communities
|
||||
WHERE id = target;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'community write rejected: community % is missing', target
|
||||
USING ERRCODE = 'object_not_in_prerequisite_state';
|
||||
END IF;
|
||||
|
||||
-- Authorization is evaluated independently for every community checked.
|
||||
executor_community := current_setting('buzz.deletion_executor_community', true);
|
||||
executor_generation := current_setting('buzz.deletion_fence_generation', true);
|
||||
IF executor_community = target::TEXT
|
||||
AND executor_generation ~ '^[0-9]+$'
|
||||
AND executor_generation::BIGINT = generation THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- A serving mutation admitted before quiescing may finish only while its
|
||||
-- exact durable lease remains current and bound to this fence generation.
|
||||
serving_community := current_setting('buzz.serving_write_community', true);
|
||||
serving_lease_id := current_setting('buzz.serving_write_lease_id', true);
|
||||
serving_owner := current_setting('buzz.serving_write_owner', true);
|
||||
serving_generation := current_setting('buzz.serving_write_generation', true);
|
||||
serving_fence_generation := current_setting('buzz.serving_write_fence_generation', true);
|
||||
IF lifecycle IN ('active', 'quiescing')
|
||||
AND serving_community = target::TEXT
|
||||
AND serving_lease_id ~ '^[0-9a-fA-F-]{36}$'
|
||||
AND serving_generation ~ '^[0-9]+$'
|
||||
AND serving_fence_generation ~ '^[0-9]+$'
|
||||
AND serving_fence_generation::BIGINT = generation THEN
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM community_serving_write_leases lease
|
||||
WHERE lease.id = serving_lease_id::UUID
|
||||
AND lease.community_id = target
|
||||
AND lease.owner = serving_owner
|
||||
AND lease.generation = serving_generation::BIGINT
|
||||
AND lease.fence_generation = serving_fence_generation::BIGINT
|
||||
AND lease.lease_until >= now()
|
||||
) INTO serving_lease_valid;
|
||||
IF serving_lease_valid THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF lifecycle <> 'active' THEN
|
||||
RAISE EXCEPTION 'community write fenced: community % generation %', target, generation
|
||||
USING ERRCODE = 'object_not_in_prerequisite_state';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
45
schema/functions/attach_community_write_fence.sql
Normal file
45
schema/functions/attach_community_write_fence.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- Attach the universal fence to one community-scoped relation. Future
|
||||
-- migrations must invoke this helper explicitly after CREATE/ALTER introduces
|
||||
-- community_id; the migration lint enforces that contract.
|
||||
CREATE FUNCTION attach_community_write_fence(target REGCLASS) RETURNS VOID
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
relation_name NAME;
|
||||
BEGIN
|
||||
SELECT c.relname
|
||||
INTO relation_name
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.oid = target
|
||||
AND n.nspname = current_schema()
|
||||
AND c.relkind IN ('r', 'p')
|
||||
AND NOT c.relispartition;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'community write fence target % is not a table in the current schema', target
|
||||
USING ERRCODE = 'wrong_object_type';
|
||||
END IF;
|
||||
IF community_write_fence_excluded_table(relation_name) THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_attribute
|
||||
WHERE attrelid = target AND attname = 'community_id' AND NOT attisdropped
|
||||
) THEN
|
||||
RAISE EXCEPTION 'community write fence target % has no community_id', target
|
||||
USING ERRCODE = 'undefined_column';
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_trigger
|
||||
WHERE tgrelid = target
|
||||
AND tgname = 'community_write_fence_' || relation_name
|
||||
AND NOT tgisinternal
|
||||
) THEN
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I BEFORE INSERT OR UPDATE OR DELETE ON %s '
|
||||
'FOR EACH ROW EXECUTE FUNCTION enforce_community_write_fence()',
|
||||
'community_write_fence_' || relation_name,
|
||||
target
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
12
schema/functions/channels_community_id_immutable.sql
Normal file
12
schema/functions/channels_community_id_immutable.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- channels.community_id is immutable: a channel can never be re-tenanted.
|
||||
-- (Conformance: "Migration lint forbids channel re-tenanting except through an
|
||||
-- explicitly modeled admission path." We have no such path, so: hard block.)
|
||||
CREATE FUNCTION channels_community_id_immutable() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.community_id IS DISTINCT FROM OLD.community_id THEN
|
||||
RAISE EXCEPTION 'channels.community_id is immutable (channel % cannot be re-tenanted)', OLD.id
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
4
schema/functions/community_deletion_lock_key.sql
Normal file
4
schema/functions/community_deletion_lock_key.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
CREATE FUNCTION community_deletion_lock_key(target UUID) RETURNS BIGINT
|
||||
LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS $$
|
||||
SELECT hashtextextended('buzz-community-deletion:' || target::text, 0)
|
||||
$$;
|
||||
24
schema/functions/community_write_allowed.sql
Normal file
24
schema/functions/community_write_allowed.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- Fleet-wide writers filter candidates through this VOLATILE predicate in
|
||||
-- the mutating statement so fenced tenants are skipped before row triggers run.
|
||||
CREATE FUNCTION community_write_allowed(target UUID) RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql VOLATILE AS $$
|
||||
DECLARE
|
||||
lifecycle TEXT;
|
||||
BEGIN
|
||||
IF current_setting('transaction_isolation') <> 'read committed' THEN
|
||||
RAISE EXCEPTION 'community writes require READ COMMITTED isolation'
|
||||
USING ERRCODE = 'invalid_transaction_state';
|
||||
END IF;
|
||||
|
||||
IF target IS NULL THEN
|
||||
RETURN true;
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock_shared(community_deletion_lock_key(target));
|
||||
SELECT deletion_state
|
||||
INTO lifecycle
|
||||
FROM communities
|
||||
WHERE id = target;
|
||||
RETURN FOUND AND lifecycle = 'active';
|
||||
END
|
||||
$$;
|
||||
12
schema/functions/community_write_fence_excluded_table.sql
Normal file
12
schema/functions/community_write_fence_excluded_table.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- Keep the deletion control plane writable while its target tenant is fenced.
|
||||
-- This predicate is the single SQL source of truth used by attachment and live
|
||||
-- catalog validation.
|
||||
CREATE FUNCTION community_write_fence_excluded_table(target NAME) RETURNS BOOLEAN
|
||||
LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS $$
|
||||
SELECT target::TEXT = ANY (ARRAY[
|
||||
'community_deletion_requests', 'community_deletion_approvals',
|
||||
'community_deletion_checkpoints', 'community_serving_write_leases',
|
||||
'community_deletion_executor_heartbeats', 'product_feedback',
|
||||
'rate_limit_violations', 'operator_listener_outbox'
|
||||
]::TEXT[])
|
||||
$$;
|
||||
28
schema/functions/enforce_community_tombstone.sql
Normal file
28
schema/functions/enforce_community_tombstone.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE FUNCTION enforce_community_tombstone() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
executor_community TEXT := current_setting('buzz.deletion_executor_community', true);
|
||||
executor_generation TEXT := current_setting('buzz.deletion_fence_generation', true);
|
||||
expected_generation BIGINT;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
IF OLD.deletion_state <> 'active' OR OLD.deleted_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'community tombstones are permanent'
|
||||
USING ERRCODE = 'object_not_in_prerequisite_state';
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
expected_generation := CASE WHEN NEW.deletion_fence_generation > OLD.deletion_fence_generation
|
||||
THEN NEW.deletion_fence_generation ELSE OLD.deletion_fence_generation END;
|
||||
IF executor_community = OLD.id::text AND executor_generation ~ '^[0-9]+$'
|
||||
AND executor_generation::BIGINT = expected_generation THEN RETURN NEW; END IF;
|
||||
IF OLD.deletion_state <> 'active' OR NEW.deletion_state <> OLD.deletion_state
|
||||
OR NEW.deletion_fence_generation <> OLD.deletion_fence_generation
|
||||
OR NEW.deleted_at IS DISTINCT FROM OLD.deleted_at THEN
|
||||
RAISE EXCEPTION 'community tombstone mutation rejected: community % generation %',
|
||||
OLD.id, OLD.deletion_fence_generation
|
||||
USING ERRCODE = 'object_not_in_prerequisite_state';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
24
schema/functions/enforce_community_write_fence.sql
Normal file
24
schema/functions/enforce_community_write_fence.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
CREATE FUNCTION enforce_community_write_fence() RETURNS TRIGGER
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
PERFORM assert_community_write_allowed(NEW.community_id);
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
PERFORM assert_community_write_allowed(OLD.community_id);
|
||||
ELSIF OLD.community_id IS NOT DISTINCT FROM NEW.community_id THEN
|
||||
PERFORM assert_community_write_allowed(OLD.community_id);
|
||||
ELSIF OLD.community_id IS NULL THEN
|
||||
PERFORM assert_community_write_allowed(NEW.community_id);
|
||||
ELSIF NEW.community_id IS NULL THEN
|
||||
PERFORM assert_community_write_allowed(OLD.community_id);
|
||||
ELSIF OLD.community_id < NEW.community_id THEN
|
||||
PERFORM assert_community_write_allowed(OLD.community_id);
|
||||
PERFORM assert_community_write_allowed(NEW.community_id);
|
||||
ELSE
|
||||
PERFORM assert_community_write_allowed(NEW.community_id);
|
||||
PERFORM assert_community_write_allowed(OLD.community_id);
|
||||
END IF;
|
||||
|
||||
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
||||
END
|
||||
$$;
|
||||
28
schema/functions/enqueue_push_match_job.sql
Normal file
28
schema/functions/enqueue_push_match_job.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- T1b push gate (keep in sync with migrations/0023). Enqueue only when the
|
||||
-- community has an active, endpoint-enabled, unexpired lease; the shared
|
||||
-- advisory lock pairs with the exclusive lock taken by lease activations
|
||||
-- (crates/buzz-db/src/push.rs) to close the lost-wake race.
|
||||
CREATE FUNCTION enqueue_push_match_job() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
-- Keep this allowlist identical to the relay's validated NIP-PL descriptor.
|
||||
-- Centralizing it on the events table covers every durable producer,
|
||||
-- including internal paths that bypass live dispatch.
|
||||
IF NEW.kind IN (9, 40002, 45001, 45003) THEN
|
||||
PERFORM pg_advisory_xact_lock_shared(
|
||||
hashtextextended('buzz_push_gate:' || NEW.community_id::text, 0));
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM push_leases
|
||||
WHERE community_id = NEW.community_id
|
||||
AND active
|
||||
AND endpoint_enabled
|
||||
AND expires_at > EXTRACT(EPOCH FROM now())::bigint
|
||||
) THEN
|
||||
INSERT INTO push_match_queue (community_id, event_id)
|
||||
VALUES (NEW.community_id, NEW.id)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
29
schema/functions/events_created_at_floor_guard.sql
Normal file
29
schema/functions/events_created_at_floor_guard.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- Replica-fence floor guard (keep in sync with migrations/0021). A deferred
|
||||
-- constraint trigger re-checks, inside COMMIT processing, that channel-bearing
|
||||
-- event rows are no older than `buzz.created_at_floor` seconds before commit
|
||||
-- time (clock_timestamp(), NOT the transaction-frozen now()). This turns the
|
||||
-- relay's ingest-time created_at envelope into a commit-time storage
|
||||
-- invariant, which is what lets keyset-cursor pages below the replica fence
|
||||
-- be served by a read replica without holes. Enforcement is armed per session
|
||||
-- via the GUC (set by the relay's writer pool on connect); sessions without
|
||||
-- the GUC (pg_restore, manual backfills) bypass it and must hold the replica
|
||||
-- fence closed for their duration. The only structural exemption is
|
||||
-- channel_id IS NULL: those rows never appear in keyset-paged windows.
|
||||
CREATE FUNCTION events_created_at_floor_guard() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
floor_secs numeric := nullif(current_setting('buzz.created_at_floor', true), '')::numeric;
|
||||
BEGIN
|
||||
IF floor_secs IS NOT NULL
|
||||
AND floor_secs > 0
|
||||
AND NEW.channel_id IS NOT NULL
|
||||
AND NEW.created_at < clock_timestamp() - make_interval(secs => floor_secs)
|
||||
THEN
|
||||
RAISE EXCEPTION
|
||||
'events.created_at % is more than % s before commit time %; below the replica-fence floor',
|
||||
NEW.created_at, floor_secs, clock_timestamp()
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;
|
||||
72
schema/functions/guard_channel_roster_snapshot.sql
Normal file
72
schema/functions/guard_channel_roster_snapshot.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
-- Channel roster snapshot fence (keep in sync with migrations/0032).
|
||||
-- Prevent mixed-version relay pods from publishing a stale NIP-29 member
|
||||
-- snapshot after a newer canonical roster has been committed.
|
||||
--
|
||||
-- Old binaries already serialize kind 39002 replacement on the replacement
|
||||
-- advisory key. This trigger adds the channel-membership key at INSERT time,
|
||||
-- after that canonical key, and validates every p tag against the current
|
||||
-- active membership set and roles. New binaries take both keys in the same
|
||||
-- order before capture and replacement. Thus old and new writers remain
|
||||
-- compatible during a rolling deploy.
|
||||
CREATE OR REPLACE FUNCTION guard_channel_roster_snapshot()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
canonical_members TEXT[];
|
||||
snapshot_members TEXT[];
|
||||
BEGIN
|
||||
IF NEW.kind <> 39002 OR NEW.channel_id IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(
|
||||
'buzz_channel_membership:' || NEW.community_id::text || ':' || NEW.channel_id::text,
|
||||
0
|
||||
));
|
||||
|
||||
SELECT COALESCE(
|
||||
array_agg(encode(cm.pubkey, 'hex') || ':' || cm.role::text ORDER BY cm.pubkey),
|
||||
ARRAY[]::TEXT[]
|
||||
)
|
||||
INTO canonical_members
|
||||
FROM channel_members cm
|
||||
WHERE cm.community_id = NEW.community_id
|
||||
AND cm.channel_id = NEW.channel_id
|
||||
AND cm.removed_at IS NULL;
|
||||
|
||||
-- A roster is canonical only when every p tag uses the emitted four-field
|
||||
-- shape, contains a 32-byte hex pubkey and valid authoritative role, has no
|
||||
-- duplicate members, and exactly matches the active membership rows.
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(NEW.tags) AS roster_tag(tag_json)
|
||||
WHERE roster_tag.tag_json->>0 = 'p'
|
||||
AND (
|
||||
jsonb_array_length(roster_tag.tag_json) <> 4
|
||||
OR COALESCE(roster_tag.tag_json->>1, '') !~ '^[0-9a-fA-F]{64}$'
|
||||
OR roster_tag.tag_json->>2 <> ''
|
||||
OR COALESCE(roster_tag.tag_json->>3, '') NOT IN ('owner', 'admin', 'bot', 'member', 'guest')
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'kind 39002 roster contains an invalid p tag'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
SELECT COALESCE(
|
||||
array_agg(
|
||||
lower((roster_tag.tag_json->>1)) || ':' || (roster_tag.tag_json->>3)
|
||||
ORDER BY decode((roster_tag.tag_json->>1), 'hex')
|
||||
),
|
||||
ARRAY[]::TEXT[]
|
||||
)
|
||||
INTO snapshot_members
|
||||
FROM jsonb_array_elements(NEW.tags) AS roster_tag(tag_json)
|
||||
WHERE roster_tag.tag_json->>0 = 'p';
|
||||
|
||||
IF snapshot_members IS DISTINCT FROM canonical_members THEN
|
||||
RAISE EXCEPTION 'kind 39002 roster does not match canonical channel membership'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE FUNCTION prevent_community_deletion_approval_removal()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'community deletion approval evidence is immutable'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,39 @@
|
||||
CREATE OR REPLACE FUNCTION prevent_community_deletion_request_retargeting()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NEW.community_id IS DISTINCT FROM OLD.community_id
|
||||
OR NEW.community_host IS DISTINCT FROM OLD.community_host
|
||||
THEN
|
||||
RAISE EXCEPTION 'community deletion target identity is immutable'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END IF;
|
||||
IF NEW.request_origin IS DISTINCT FROM OLD.request_origin
|
||||
OR NEW.owner_pubkey IS DISTINCT FROM OLD.owner_pubkey
|
||||
OR NEW.mediating_operator_pubkey IS DISTINCT FROM OLD.mediating_operator_pubkey
|
||||
OR NEW.acknowledgement_version IS DISTINCT FROM OLD.acknowledgement_version
|
||||
THEN
|
||||
RAISE EXCEPTION 'community deletion request provenance is immutable'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END IF;
|
||||
IF OLD.inventory_frozen_at IS NOT NULL AND (
|
||||
NEW.schema_manifest IS DISTINCT FROM OLD.schema_manifest
|
||||
OR NEW.storage_manifest IS DISTINCT FROM OLD.storage_manifest
|
||||
OR NEW.inventory_manifest IS DISTINCT FROM OLD.inventory_manifest
|
||||
OR NEW.inventory_digest IS DISTINCT FROM OLD.inventory_digest
|
||||
OR NEW.inventory_frozen_at IS DISTINCT FROM OLD.inventory_frozen_at
|
||||
) THEN
|
||||
RAISE EXCEPTION 'frozen community deletion inventory is immutable'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END IF;
|
||||
IF OLD.destructive_storage_frozen_at IS NOT NULL AND (
|
||||
NEW.destructive_storage_manifest IS DISTINCT FROM OLD.destructive_storage_manifest
|
||||
OR NEW.destructive_storage_frozen_at IS DISTINCT FROM OLD.destructive_storage_frozen_at
|
||||
) THEN
|
||||
RAISE EXCEPTION 'frozen destructive storage manifest is immutable'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Chunk content is immutable once written; the only permitted update is the
|
||||
-- one-way deleted_at stamp. New chunks are permitted only while the request is
|
||||
-- fenced and its destructive manifest remains unfrozen. Removal is permitted
|
||||
-- only while the destructive manifest has not yet frozen (a retried partial
|
||||
-- freeze rewrites its chunks) or once the request has passed logical
|
||||
-- verification (terminal cleanup).
|
||||
CREATE FUNCTION protect_community_deletion_manifest_keys()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
frozen_at TIMESTAMPTZ;
|
||||
request_stage TEXT;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
IF NEW.request_id IS DISTINCT FROM OLD.request_id
|
||||
OR NEW.chunk_no IS DISTINCT FROM OLD.chunk_no
|
||||
OR NEW.prefix IS DISTINCT FROM OLD.prefix
|
||||
OR NEW.keys IS DISTINCT FROM OLD.keys
|
||||
OR OLD.deleted_at IS NOT NULL
|
||||
THEN
|
||||
RAISE EXCEPTION 'community deletion manifest key chunks are immutable'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
SELECT destructive_storage_frozen_at, stage
|
||||
INTO frozen_at, request_stage
|
||||
FROM community_deletion_requests
|
||||
WHERE id = CASE WHEN TG_OP = 'INSERT' THEN NEW.request_id ELSE OLD.request_id END
|
||||
FOR UPDATE;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
IF FOUND AND frozen_at IS NULL AND request_stage = 'fenced' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
RAISE EXCEPTION 'community deletion manifest key chunks require an unfrozen fenced request'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END IF;
|
||||
IF NOT FOUND
|
||||
OR frozen_at IS NULL
|
||||
OR request_stage IN ('logically_verified', 'retention_pending')
|
||||
THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RAISE EXCEPTION 'community deletion manifest key chunks cannot be removed mid-execution'
|
||||
USING ERRCODE = 'integrity_constraint_violation';
|
||||
END;
|
||||
$$;
|
||||
41
schema/functions/refresh_channel_ttl_after_event_insert.sql
Normal file
41
schema/functions/refresh_channel_ttl_after_event_insert.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- Channel TTL refresh (keep in sync with migrations/0024). Runs deferred, in
|
||||
-- the transaction that makes a channel-scoped event durable, so a TTL
|
||||
-- transition committed while ingest was in flight is never missed. The
|
||||
-- per-channel advisory lock is SHARED here — permanent-channel commits admit
|
||||
-- each other — and taken EXCLUSIVE by TTL transitions (update_channel in
|
||||
-- crates/buzz-db/src/channel.rs), which forces the same total order the
|
||||
-- 0022 row lock provided without serializing the hot path.
|
||||
CREATE FUNCTION refresh_channel_ttl_after_event_insert() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
channel_ttl INTEGER;
|
||||
BEGIN
|
||||
-- Kind 9007 creates the channel and initializes its deadline itself.
|
||||
IF NEW.channel_id IS NOT NULL AND NEW.kind <> 9007 THEN
|
||||
BEGIN
|
||||
PERFORM pg_advisory_xact_lock_shared(hashtextextended(
|
||||
'buzz_channel_ttl:' || NEW.community_id::text || ':' || NEW.channel_id::text, 0));
|
||||
|
||||
SELECT ttl_seconds INTO channel_ttl
|
||||
FROM channels
|
||||
WHERE community_id = NEW.community_id AND id = NEW.channel_id;
|
||||
|
||||
IF channel_ttl IS NOT NULL THEN
|
||||
UPDATE channels
|
||||
SET ttl_deadline = clock_timestamp() + make_interval(secs => ttl_seconds)
|
||||
WHERE community_id = NEW.community_id
|
||||
AND id = NEW.channel_id
|
||||
AND ttl_seconds IS NOT NULL
|
||||
AND archived_at IS NULL
|
||||
AND deleted_at IS NULL;
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
-- Preserve the existing best-effort contract: a TTL refresh failure
|
||||
-- must not reject an otherwise valid durable event.
|
||||
RAISE WARNING 'channel TTL refresh failed for community %, channel %: %',
|
||||
NEW.community_id, NEW.channel_id, SQLERRM;
|
||||
END;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;
|
||||
Reference in New Issue
Block a user