Repository navigation
buzz-db: enforce five single-table guards in app writers (phase 1) - #8320
TheSentinel454 wants to merge 8 commits into
Conversation
Phase 1 of BUZZ-318. The five single-table guard triggers stay for mixed-version pods; their invariants now also hold in the application writers, so a later PR can drop them. - communities lifecycle: begin_quiescing, fence, purge_postgres and abort go through one transition_community state machine over a locked snapshot (deleted_at one-way, generation monotonic, deleted rows rejected). set_community_icon only updates an active, undeleted row. - community_deletion_requests: both initial inventory freezes require an unfrozen, unblocked submitted row in the UPDATE predicate; the destructive-manifest freeze refuses a frozen row with missing JSON. - community_deletion_manifest_keys: append, pre-freeze clear and verified cleanup check the request row via guard_manifest_chunk_write. - channels.community_id and community_deletion_approvals hold by construction; a Rust source policy in observability_source.rs forbids retenanting, lifecycle writes outside the transition helper, community DELETE, request retargeting, approval UPDATE/DELETE and unguarded manifest writers. Five PG tests each disable only their own trigger and exercise the app path's rejected and permitted mutations. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Address review on the single-table guard writers: - Require deletion_state = 'active' on every non-lifecycle communities UPDATE and conflict upsert; add it to archive/unarchive UPDATEs, which already lock an active row. - Require the inventory and destructive-storage one-way freeze predicates on every request UPDATE that assigns those columns. - Pin abort's generation increment with a trigger-disabled PG test covering quiescing/fenced and archived/unarchived communities. - Parse ON CONFLICT DO UPDATE SET, tuple SET, and column-less community inserts; require AND deleted_at IS NULL on the manifest stamp. - Assert exact production rule-hit sets so a parser that stops matching fails. - Correct the lifecycle lock doc comment and format the fence body. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
ba7adbf to
d736917
Compare
A guard now admits a statement only as a plain conjunct of its WHERE (no OR outside the frozen-or-equal groups, no subquery), and every destructive_storage_* assignment must preserve a frozen value with COALESCE. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
A guard must follow the WHERE start or an AND (optionally table-qualified), so AND NOT <guard> no longer satisfies it, and OR/SELECT are matched as whole words so )OR( is caught. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Automated review of 1e2c3f8f6966bdfbf5ba1ec257b1cdf0acdf5f24 (base d25384a4f3c3a940b5b999f26105091c58ab76d6): blocking — three gaps in the source policy.
Thanks for this. The production writers hold up: two independent source reviews found no new state-transition or frozen-data violation, and all five triggers still protect the database in this phase. The three blockers are all in every_production_single_table_guard_writer_preserves_its_invariants. That test is what keeps these invariants once phase 2 drops the triggers, and in each case it accepts a writer the matching trigger refuses, contradicting what the description says the policy enforces. Each one is a small fix plus a negative fixture.
Blocking
-
Approval upserts get past the append-only rule (
crates/buzz-db/tests/observability_source.rs:2203-2207). The approval branch only looks forUPDATE community_deletion_approvalsandDELETE FROM community_deletion_approvalsthroughsql_table_suffixes. It never callssql_update_sets(), the helper that understands conflict updates. If someone addedON CONFLICT (request_id) DO UPDATE SET note = EXCLUDED.noteto theapproveinsert, the policy would pass and the exact rule-hit sets wouldn't change, even though approval evidence would now be mutable. The description says the policy forbids "any production UPDATE" on approvals. Fix: also reject a conflict update oncommunity_deletion_approvals, and add a negative upsert fixture. -
Calling
guard_manifest_chunk_writelets content UPDATEs through (:2209-2228). Any function in the deletion store that writes a manifest chunk passes as long as it containsguard_manifest_chunk_write(, whatever the write does. SoUPDATE community_deletion_manifest_keys SET keys = $nadded toappend_manifest_key_chunkpasses, even though the trigger never allows chunk content to change. The accept fixture at:2516-2548actually treats a guardedSET keys = $1UPDATE as acceptable. Fix: reject manifest UPDATEs except thedeleted_atstamp, checked strictly as described in item 3. Keep insert, pre-freeze delete and verified delete as the only guarded operations, and turn the guarded content UPDATE into a negative fixture. The guard check also only shows the call is present. It doesn't show the result is awaited and propagated, or that the guard runs inside the writer's transaction. Add that to the "Known policy limit" paragraph rather than presenting presence as proof. -
The stamp exemption skips the plain-conjunct rule (
:2215-2219). It accepts the stamp when the WHEREcontains("and deleted_at is null"), not when it passesplain_guard(). SoWHERE request_id = $1 AND chunk_no = $2 AND deleted_at IS NULL OR truestill gets the exemption with every exact set and count unchanged. After the triggers are dropped, that would restamp already-deleted chunks across requests. A subquery containing the same text also passes. An operator-precedence slip in this predicate is an easy real mistake to make, and the description says every guard is a plain conjunct with noORand no subquery. Fix: run the exemption throughplain_guard(), or the same statement-local conjunct,ORand subquery check, and add negative fixtures for both forms.
Optional
- The six new PG tests run
ALTER TABLE … DISABLE TRIGGERand never turn the trigger back on. That's safe under thepostgres-ciwrapper, which gives each test its own database. A plaincargo test -- --ignoredagainst the default localbuzzdatabase leaves all five triggers off there. The roster tests usecreate_scratch_dbinstead. - More textual limits that belong in the "Known policy limit" paragraph alongside trailing negation: a SQL comment inside the literal can hold the guard text (
-- AND deletion_state = 'active'); the frozen-or-equalORtext is removed without checking its grouping (the accept fixture at:2387has an unparenthesizedOR); function discovery uses a fixed list of signature prefixes (:935-947); and dynamic SQL is only recognized in specific shapes such asset_parts.push(:2093-2096). None of these hides an unsafe writer today. The paragraph should just not read as general SQL or control-flow enforcement. - The shared source filter skips any file named
test_support.rsin every crate. That's right today, since both such files are compiled only undercfg(test), but the exemption is keyed on the filename alone. - The table rows that say the app "matches the trigger" are narrower than the triggers.
enforce_community_tombstone()accepts a matching executor GUC before it looks at lifecycle changes, so it doesn't encode the new transition graph itself. The chunk DELETE trigger allows missing-parent or any unfrozen-parent cleanup, whileguard_manifest_chunk_writeonly allows fenced-and-unfrozen or verified cleanup. Current callers already require those stages, so nothing breaks, but the wording should say the app is stricter. - The new chunk-freeze PG test stays green when
AND deleted_at IS NULLis removed from the stamp. The existingmanifest_key_chunks_bind_freeze_execution_and_cleanuptest and the source policy catch it, so a one-way stamp assertion in the new test is optional. - Already on main, not caused by this PR: purge can take the request-row foreign-key lock (checkpoint insert) before the community lock, while abort locks community then request (
deletion.rs:2646-2648vs3887-3895). PostgreSQL resolves that as a deadlock and cancels one side, which fails closed. Purge used the same order at the base. - The description doesn't carry
buzz-review-completedyet (AGENTS.md step 4).
What was checked
- Each of the five trigger functions at the base against the app code.
transition_communityreproduces the four old lifecycle UPDATEs: quiesce keeps the generation, fence and abort add 1, tombstone keeps the fenced generation and now requiresdeleted_at IS NULL(stricter than the oldCOALESCE), and abort restoresarchived_at. Both inventory freezes use atomic unfrozen predicates. The destructive freeze preserves frozen values withCOALESCEand refuses a frozen row with missing manifest JSON. - Concurrency. Purge keeps its deletes, the tombstone transition and the request advance in one transaction. If an abort wins first, the transition refuses and the deletes roll back. If purge wins, abort can't restore the tombstone. Lock order for purge versus abort is unchanged from the base. Icon updates are atomic and limited to active, undeleted rows. Archive and unarchive lock an active row before updating.
- Writer inventory across every crate's
src: outsidecfg(test), every production writer ofchannels.community_id,communities, deletion requests, approvals and manifest chunks is one the PR lists. That includes the rolled-back bootstrap community inserts instore/channel_members.rsandruntime/replica_fence.rs. - Behavior change:
set_community_iconnow returnsAccessDeniedfor a missing or non-active community. Its only production caller, the kind 9033 handler inrelay_admin.rs, propagates the error before returning success. - Local run against a private PostgreSQL 16 cluster: all six
single_table_guard_*tests, all 11buzz-deletionPG tests and all 30observability_sourcecases pass. Three mutations each turned a test red: removinginventory_frozen_at IS NULLfromfreeze_inventory, abort keeping the old generation, and removingAND deleted_at IS NULLfrom the stamp. Hosted CI at this head shows the six new tests as named PASS in the PostgreSQL job and the source-policy test passing in the unit job.
Reject ON CONFLICT DO UPDATE on approvals, reject every manifest chunk UPDATE other than the one-way deleted_at stamp even when the chunk guard is present, and hold that stamp to the plain-conjunct rule. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Add negatives for a second chunk write and a conflict-upsert stamp, and require chunk_no = $2 so one chunk's purge cannot stamp the whole request. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Automated review of 88c6a3231022c1de19fd6cbcca8becbbc16a3341 (base d25384a4f3c3a940b5b999f26105091c58ab76d6): blocking, one gap left in the upsert detection that two of the earlier fixes depend on.
Thanks for the quick turnaround. The stamp fix is complete, and the first two fixes work for the spelling in the new fixtures. Both of those fixes rely on sql_update_sets() to find conflict updates, though, and it only recognizes one spelling.
Blocking
-
ON CONFLICT(with no space isn't detected as an upsert (crates/buzz-db/tests/observability_source.rs:1978-1984).sql_update_sets()looks for the substring" on conflict ", andnormalize_sql_source()doesn't put a space before(. SoON CONFLICT(request_id) DO UPDATE SET …, which is valid PostgreSQL, returns no conflict update. Two consequences:- Adding
ON CONFLICT(request_id) DO UPDATE SET note = EXCLUDED.noteto theapproveinsert gets past the new append-only branch (:2203-2208). - Adding
ON CONFLICT(request_id, chunk_no) DO UPDATE SET keys = EXCLUDED.keysto theappend_manifest_key_chunkinsert leavessetsempty with zero direct UPDATEs. The existing guard call then admits it at:2231-2234, so chunk content becomes mutable.
Neither change moves the writer counts or the exact rule-hit sets. This isn't an unusual spelling:
buzz-push-gateway/src/postgres.rs:347,485already writesON CONFLICT(at the base. The triggers still protect phase 1, but this test is what holds these rules once phase 2 drops them, and the description says it rejects these upserts. Fix: matchon conflictat a token boundary (followed by a space or(), and add negative fixtures for the no-space spelling on both tables. Keep the plain INSERT andDO NOTHINGaccept fixtures. - Adding
Optional
UPDATE ONLY <table>andDELETE FROM ONLY <table>aren't recognized either, sincesql_table_suffixes()needs the table name right after the verb. That's worth a line in "Known policy limits" next toMERGE/TRUNCATE.- Still open from the first review: the six PG tests disable their trigger and never turn it back on (fine under the per-test
postgres-cidatabases, but it leaves the default localbuzzdatabase without triggers), and the description doesn't carrybuzz-review-completedyet (AGENTS.md step 4).
What was checked
- The delta
1e2c3f8f..88c6a323touches only the source-policy test. Two independent source reviews found nothing loosened: every change tightens admission, and the production writers, discovery floors and expected rule-hit sets are unchanged. - The spaced approval upsert and the guarded
SET keysUPDATE are now rejected. - The stamp exemption is closed. It requires the deletion-store path, one direct chunk UPDATE that is the function's only chunk write, exactly
SET deleted_at = now(), andrequest_id = $1,chunk_no = $2anddeleted_at IS NULLthroughplain_guard(). The fixtures rejectOR true, the subquery,NULLstamps, extra assignments, missing predicates, extra writes, renamed or out-of-file exemptions and conflict-upsert stamps. The productionmark_manifest_chunk_deletedmatches the exact form. - The updated "Known policy limits" list now covers the SQL-comment, trailing-negation,
OR-grouping, guard-presence/transaction, function-discovery, dynamic-SQL and filename-filter limits from the first review. cargo test -p buzz-db --test observability_sourcepasses 32/32 at this head locally. HostedRust / Unit Testsshows the source-policy tests as named PASS, and CI overall is 56 pass, 0 fail.
sql_update_sets() matched only the spaced ' on conflict ' spelling, so ON CONFLICT(request_id) DO UPDATE ... slipped past the approval append-only rule and the manifest content-UPDATE rule. Match on conflict and do update set at identifier boundaries and add no-space negative fixtures for both tables plus a no-space DO NOTHING accept fixture. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
|
Thanks @wpfleger96 — addressed at Latest review
First review (fixed at |
Match the SET keyword at identifier boundaries in plain UPDATEs and in ON CONFLICT ... DO UPDATE, so the multi-column SET(a, b) = ... spelling without a space is parsed like SET (a, b). Add no-space fixtures for a channels tuple UPDATE and an approval DO UPDATE SET(note) upsert. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Automated review of 11fb45ba1e171f3c215ac0ddf19b97d2914c0b29 (base d25384a4f3c3a940b5b999f26105091c58ab76d6): clear, nothing blocking.
Thanks for the fix. sql_token_suffixes() now matches on conflict, do update set and set at identifier boundaries. Since the source is already lowercased and whitespace-collapsed, ON CONFLICT(…) DO UPDATE reaches the approval append-only rejection and the manifest content-UPDATE rejection, the same as the spaced form. ON CONFLICT ON CONSTRAINT … DO UPDATE, INSERT … AS alias, CTE-wrapped and public.-prefixed upserts also reach the check. SET(a, b) = … is caught in plain UPDATEs and in DO UPDATE. Ordinary aliases still work (UPDATE t SET, UPDATE t alias SET, UPDATE t AS alias SET), and names like asset or offset don't take the set match.
Optional
- Worth one more line in "Known policy limits":
sql_update_sets()cuts each statement at the first"or;(this predates the PR), so quoted identifiers and some literals hide what comes after.ON CONFLICT ("request_id") DO UPDATE …,INSERT INTO community_deletion_approvals AS "a" … ON CONFLICT …, or a string value with a;beforeON CONFLICTwould leave no detected conflict UPDATE. For a guarded manifest INSERT, that means the upsert is admitted. A quoted table name (UPDATE "channels") also escapes table discovery. No current approval or chunk INSERT uses any of these forms. - The
DISABLE TRIGGERlines in the six PG tests (deletion.rs:4580,4632,4691,4780,4917,4978) still never re-enable. Your reasoning holds for the supported per-test-database runner, and phase 2 removes them. Running one of those ignored tests directly against a shared local database leaves that guard off, though. buzz-review-completedisn't in the description yet (AGENTS.md step 4).
Verified
cargo test -p buzz-db --test observability_sourcepasses 32/32 at this head. Swapping in the oldsql_update_sets()body fails exactly the three new no-space fixtures (single_table_policy_rejects_approval_mutations,single_table_policy_rejects_manifest_content_updates,single_table_policy_checks_tuple_assignments). Restoring it passes 32/32 again.cargo test -p buzz-dbpasses 203 with 0 failures (PG-dependent ignored tests not run).- Hosted
Rust / Unit Testsnames all three as PASS. CI is 56 pass, 0 fail. - The
UPDATE ONLY/DELETE FROM ONLYlimit from the last review is now in the description.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket (systems/lifecycle), Mantis (correctness/security/concurrency), and Groot (product/operator/retry) reviews.
No actionable findings in this phase-1 change. Reviewed head 11fb45ba1e171f3c215ac0ddf19b97d2914c0b29 against base/merge-base d25384a4f3c3a940b5b999f26105091c58ab76d6.
The shared lifecycle transition preserves generation increments, abort archive restoration, transactional tombstoning and rejection of deleted rows. Manifest writes serialize on the request row with freeze; verified cleanup follows the stage update in the same transaction. The added freeze predicates reject repeated inventory mutation and malformed frozen manifests without JSON. Icon/archive predicates preserve the supported callers’ lifecycle boundaries. I also reviewed the source-policy rules and fixtures, retained trigger definitions, and public description/discussion; no attached images or actionable disclosure found.
Limitations: source analysis only. No tests, builds, SQL, live deletion, concurrency or performance experiments were run. A read-only check-run snapshot for this head reports 56 successful and 28 skipped checks; that is not independent runtime validation. The textual policy does not prove arbitrary SQL or control flow safe. Its documented limits, the pre-existing purge/abort lock-order issue, and the disposable-database requirement for trigger-disabling tests remain relevant. This review covers retaining all five triggers, not the later trigger-removal phase.
No changes requested; submitted as a non-blocking COMMENT, not an approval.
What
This is phase 1 of 2 for retiring five single-table guard triggers (BUZZ-318). Their invariants now hold in the application writers. All five triggers and their functions stay for mixed-version pods. A later PR drops them.
trg_channels_community_id_immutableUPDATE channelsassignscommunity_id, andChannelUpdatehas no community field. A source policy pins this.communities_deletion_tombstone/enforce_community_tombstone()begin_quiescing,fence,purge_postgresandabortall go throughtransition_community, using aFOR UPDATErow snapshot in the caller's transaction (quiesce, fence and abort also hold the community deletion advisory lock; purge does not, and its tombstone transition fails if a concurrent abort won). Allowed transitions: active→quiescing (plus a quiescing retry); quiescing→fenced, which increments the generation; fenced→tombstone at the same generation, with a one-waydeleted_at; active/quiescing/fenced→active on abort, which also increments the generation. Every transition rejects an already-deleted row. The UPDATE is conditioned on the snapshot's state and generation.set_community_iconupdates only an active, undeleted row and returnsAccessDeniedotherwise. Archive and unarchive lock an active row, and their UPDATEs also carrydeletion_state = 'active', so every non-lifecyclecommunitiesUPDATE or upsert carries the active predicate (policy-pinned: a non-active row is immutable outside the state machine). No productionDELETE FROM communitiesexists, and the policy forbids adding one. Executor GUCs are unchanged.community_deletion_request_retargeting_guardfreeze_inventory,complete_owner_preparation) requirestage = 'submitted' AND blocked_at IS NULL AND inventory_frozen_at IS NULLin their UPDATE. Every request UPDATE that assigns an inventory column must carryinventory_frozen_at IS NULL, and one that assignsdestructive_storage_*must carry the frozen-or-equal guard (policy-pinned). The destructive-manifest freeze now also refuses a row that is marked frozen but is missing its manifest JSON.community_deletion_approval_removal_guardapprove,complete_owner_preparation). The policy forbids any production UPDATE,ON CONFLICT … DO UPDATEupsert or DELETE.community_deletion_manifest_keys_guardguard_manifest_chunk_write. Chunk content is never updated: the policy rejects every chunk UPDATE or conflict upsert exceptmark_manifest_chunk_deleted, the only writer that stampsdeleted_at. That stamp must be the function's only chunk write, a plain UPDATE of exactlySET deleted_at = now(), withrequest_id = $1,chunk_no = $2anddeleted_at IS NULLas plain conjuncts (noOR, no subquery), so it stays one-way.The app is stricter than the triggers, not equivalent to them.
enforce_community_tombstone()admits any change when the executor GUC matches, before it looks at lifecycle columns, so the transition graph above lives only intransition_community. The chunk DELETE trigger also allows cleanup when the parent request is missing or unfrozen, whileguard_manifest_chunk_writeallows only fenced-and-unfrozen or verified cleanup. Every current caller already runs at those stages.Writer inventory: I searched every production writer of these tables and columns across all crates, excluding
#[cfg(test)]. That covers buzz-db, buzz-deletion, buzz-relay handlers, side effects and operator/admin APIs, and buzz-audit. Relay, operator and executor entry points all call the store methods above. No other crate writes these tables directly.Tests
The six PG tests are in
store::deletion::postgres_tests. Each disables only its own trigger, then checks that the app path rejects the forbidden mutation and permits the allowed one:single_table_guard_channels_remain_tenant_bound_without_triggersingle_table_guard_community_lifecycle_rejects_deleted_row_without_trigger: icon refused while quiescing; fence refused on a deleted row; then the full fence→tombstone sequence; abort after tombstone fails.single_table_guard_abort_invalidates_generation_without_trigger: abort from quiescing and from fenced, for unarchived and pre-archived communities; asserts active state, community and request generation = previous + 1, restoredarchived_at, nulldeleted_at.single_table_guard_request_inventory_freeze_is_one_way_without_trigger: re-freeze is refused on both the operator and owner-preparation paths. The owner path leaves no approval evidence behind. Owner request-ID retargeting is refused, and a frozen manifest with missing JSON can't be filled in.single_table_guard_approval_evidence_is_append_only_without_trigger: asserts the exact error.single_table_guard_manifest_chunks_stop_at_freeze_without_triggerThe source policy lives in
crates/buzz-db/tests/observability_source.rs(every_production_single_table_guard_writer_preserves_its_invariants). It scans every crate'ssrcdirectory and reuses the file's existing cfg-test stripping, comment and literal masking, function slicing and SQL normalization. Each rule is checked per function. A minimum count of discovered writers per table makes the test fail if discovery stops matching, and exact sets of production functions hit by each rule (lifecycle exemption, stamp exemption, chunk-guard callers, active-community writers, request-freeze writers) make it fail if a parser stops matching. The parser handlesON CONFLICT … DO UPDATE SET(with or without a space before(), tupleSET (a, b) =, and rejects column-less community inserts. A guard counts only as a plain conjunct of the statement's own WHERE: it must follow the WHERE start or anAND(optionally table-qualified, neverNOT), with no whole-wordORoutside the frozen-or-equal groups and no subquery, and everydestructive_storage_*assignment must preserve a frozen value withCOALESCE. Negative and accept fixtures cover every rule. The shared source filter now also skipstest_support.rs, which is compiled only undercfg(test).Each check was confirmed to fail when its fix is removed. Seven mutants each failed on an assertion: the
inventory_frozen_atpredicate removed from each of the two freeze paths, and each of the five policy rules disabled. Review rework also falsified the injectedSET icon = NULLwriter, an unguarded inventory/destructive-storage writer, abort keeping the old generation, and the stamp withoutdeleted_at IS NULL; every new fixture test fails against the previous policy. Round 1 also falsified the lifecycledeleted_atcheck, the manifest append guard, the icon predicate and the destructive-manifest predicate.What got simpler
The four lifecycle UPDATE statements, each with its own generation arithmetic and its own GUC setup, are now one transition function with one SQL writer. Both manifest removal modes share one admission check. For inventory freeze, the check lives in the existing atomic UPDATE, with no new helper or transaction. Channel immutability and approval append-only behavior stay construction rules, not duplicated runtime checks. The source-policy scans now share one Rust crate walker, which this PR extracted into
collect_production_rs_files.Known policy limits: the policy is a textual check of Rust source, not a SQL parser or a control-flow analysis. None of these gaps hides an unsafe writer today, but phase-2 reviewers should not read the policy as more than that:
INSERT,UPDATEandDELETEstatements are recognized, including CTE andON CONFLICTforms.MERGEandTRUNCATEare not.UPDATE ONLY <table>andDELETE FROM ONLY <table>are not recognized either, because the table name must follow the verb directly.deletion_state = 'active' IS FALSE,… = false) is not caught.-- AND deletion_state = 'active') can supply the guard text.ORtext is removed before theORcheck without checking how it is grouped.guard_manifest_chunk_write(is checked for presence in the writing function only. The policy does not show the result is awaited and propagated, or that the guard runs in the same transaction as the write. With the trigger disabled,single_table_guard_manifest_chunks_stop_at_freeze_without_triggershows append and pre-freeze clear refusing after freeze; verified cleanup has no trigger-disabled test.set_parts.push.test_support.rsby name. Both such files are compiled only undercfg(test)today.Phase 2 (not here): a forward migration plus a desired-state change will drop only these five triggers and functions and update
validate_serving_catalog_onand the schema assertions. It ships after every serving and executor pod runs these writers.Identified debt (pre-existing, outside this diff): purge can take the request-row FK lock (checkpoint insert) before the community lock, while abort locks community then request (
deletion.rspurge vs abort). PostgreSQL detects that as a deadlock and cancels one side, which fails closed; the order is unchanged from the base.runtime::postgres_tests::writer_pool_rejects_non_read_committed_database_defaulthangs until its 120 s bound on clean177551ddaas well. Some test-only fallbacks still hardcodelocalhost:5432, and one relay test helper hardcodes Redis 6379.Verification (Blox, byte-identical tree to this commit)
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo nextest run -p buzz-db --libcargo nextest run -p buzz-db --test observability_sourcescripts/test-postgres-test-discovery.shscripts/postgres-test-run.sh -pbuzz-admin / audit / deletion / push-gateway / relay / search / workflowscripts/postgres-test-run.sh -p buzz-dbThe new PG tests are selected by the
postgres-cifilter(test(/postgres_tests::/) or binary(/^postgres_/)) and not test(/(^|::)external_infra[^:]*::/). The policy tests run injust test-unit(--test observability_source).