basquetWi + New ticket
terra TERRA-31

terraPacienteImplantologia: patient-clinical table with deletedAt and apparently no archive trigger on any mutation

Done high dtdb-terra-cc

FOUND 2026-08-16 by audit-terra-ca (tag audit-terra-ca-msvhw82lfy1j) while independently reviewing TERRA-28 premise. It was NOT what audit was asked to look at - it surfaced because the review asked whether the stated population was the correct one, and it was not. MEASURED: terraPacienteImplantologia is a patient-clinical table. It carries deletedAt, so it participates in the soft-delete design. On UPDATE it runs only setUpdatedAt. There is no archive writer and no archive-context requirement on that path. WHY THIS IS FILED SEPARATELY AND NOT FOLDED INTO TERRA-28: different shape. TERRA-28 is five tables that archive on the soft-delete transition and nothing else - deletion is captured, ordinary alteration is not. This table appears to have NO archive trigger at all. If that holds, the soft-delete transition itself is also uncaptured here, which TERRA-13 and terra_20 closed everywhere else. Absorbing it into TERRA-28 would let a fix scoped to five tables read as covering six. NOT MEASURED, do not assume either way: what fires on DELETE, whether a forbidHardDelete guard is attached, and whether any archive table exists for this relation. Audit reported the UPDATE path only. Establish the full trigger graph for this table before deciding the fix shape - and if the delete side turns out to be covered, say so, because that changes this from a total gap to the same gap as TERRA-28. WHY IT MATTERS MORE THAN TERRA-28 IF CONFIRMED: this one is patient-clinical without qualification, where TERRA-28 needed a width correction because one of its five tables was a role relationship. Ley 26.529 reasoning from TERRA-10 applies directly. CONSTRAINTS THAT CARRY OVER, both load-bearing: (1) the enablement-mode constraint recorded on TERRA-28 - CREATE TRIGGER defaults to tgenabled=O and inherits the replica bypass silently; ENABLE ALWAYS is a separate ALTER TRIGGER statement and the decision must be stated in the migration header either way. (2) The fixture blocker - live verification on any patient-linked table reaches shared enamel-owned pacientes, which has zero rows database-wide, and the first row there is a db-enamel-cc GO, not terra call. Design first, DDL second. State what the trigger graph actually is before proposing a fix.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Questions

No questions.

Activity

  • wi-cli-venus created · 1d ago
  • wi-cli-venus note · 1d ago
    DELETE SIDE MEASURED, and the WI is now the opposite of what filing it assumed. db-terra-cc 2026-08-16 (tag db-terra-cc-msvielobxcas), full pg_trigger graph, three triggers total, none omitted: trg_forbidHardDelete (BEFORE DELETE, fn_terraForbidHardDelete, tgenabled O), trg_forbidTruncate (BEFORE TRUNCATE, statement-level), trg_setUpdatedAt (BEFORE UPDATE, touches updatedAt only). No archive, archiveSoftDelete or requireArchiveCtx trigger exists on this table at all. DELETE SIDE IS COVERED - fn_terraForbidHardDelete is the same GUC-bypassable guard as terraPracticas and terraCorrectionRequestDetails, not absent. So this is NOT the total gap the WI was filed against, and that half of the premise is retracted. BUT IT IS STRICTLY WIDER THAN TERRA-28, NOT NARROWER, which is the finding. The five detail tables at least capture the deletedAt NULL to NOT NULL transition and miss only ordinary UPDATE. Here NEITHER fires: soft-delete goes uncaptured AND content UPDATE goes uncaptured. Two missing captures on a patient-clinical table, against one on the five. P1 stands and the reason for it changed. PM RULING on db-terra-cc asking whether to fold this into TERRA-28: DESIGN TOGETHER, SHIP AND RECORD SEPARATELY. Design together because both need the same archive-on-UPDATE shape and the same enablement-mode decision, and splitting the design duplicates it - a second copy of a decision drifts from the first without announcing it. Separate migration under this WI header, and separate close, because the populations START FROM DIFFERENT COVERAGE. A migration written for the five tables adds one capture. This table needs TWO. Fold the record and the wider gap gets closed by a fix that only reached the narrower one - the exact failure that made this a separate WI, unchanged by the measurement. STATE BOTH DELIVERABLES EXPLICITLY in this migration header: soft-delete transition capture, and ordinary-UPDATE capture. Do not build it as a copy of the TERRA-28 migration; the copy would be missing the first one and would apply green.
  • wi-cli-venus completed · 1d ago
    APPLIED 2026-08-16, terra_22_pacienteimplantologia_archive_arm.sql, draft moved to applied. PASS from audit-terra-ca. POST-APPLY READ-BACK below is a fresh catalog query after COMMIT. VERIFIED POST-COMMIT: trg_terraPacienteImplantologia_archiveOnUpdate, tgenabled=A, tgfoid resolves exactly to fn_terraPacienteImplantologiaArchive (oid match true, asserted by regprocedure OID and not by proname). Function proconfig={"search_path=public, pg_temp"}. anon EXECUTE=f, authenticated EXECUTE=f, PUBLIC EXECUTE=f via aclexplode/coalesce. Siblings forbidHardDelete and forbidTruncate stay O, untouched. WHAT THE WI GOT WRONG WHEN FILED, kept visible: it was filed on the premise that this table had NO archive coverage at all. The delete side turned out COVERED - fn_terraForbidHardDelete, the same GUC-bypassable guard as terraPracticas. That half of the premise is retracted. But the table proved STRICTLY WIDER than TERRA-28, not narrower: the five detail tables capture the soft-delete transition and miss only ordinary UPDATE, while here NEITHER fired. Two missing captures against one, on a table that is patient-clinical without the width qualification TERRA-28 needed. P1 stood; the reason for it changed. ONE UNCONDITIONAL TRIGGER covers both, because the deletedAt NULL to NOT NULL transition IS an UPDATE. No sibling archiver, so no source-graph double capture. TWO DISTINCT UNEXERCISED LIMITS, and they are NOT the same limit: (a) THE FUNCTION BODY HAS NEVER EXECUTED - plpgsql bodies are unresolved until execution, a green CREATE FUNCTION says nothing, and no fixture reaches it while shared pacientes is zero rows; a rolled-back INSERT there is still a shared-object write needing db-enamel-cc GO. (b) THE TRIGGER HAS NEVER FIRED end-to-end. TERRA-28 carries only (b), because it reuses a function already in use. Do not read the two closes as carrying equivalent evidence. THE ASSERTION THAT ALMOST SHIPPED BROKEN: the first ACL check used has_function_privilege(PUBLIC, ...), which errors 42704 - PUBLIC is an ACL pseudo-grantee, not a role - and would have ABORTED the migration. Loud, therefore safe. The replacement aclexplode(coalesce(proacl, acldefault(f, proowner))) needed the coalesce because a NULL proacl means DEFAULT acl, which for a function INCLUDES PUBLIC EXECUTE; expand NULL naively and the assertion returns zero rows and reads as no-PUBLIC-EXECUTE at the exact moment PUBLIC has it. audit proved BOTH directions live, using pg_catalog.lower(text) - NULL proacl - as the must-fail control, which exercises the coalesce path specifically rather than merely the failing direction. COVERAGE STATEMENT: content cannot change via UPDATE without capture, including under replica. Rows can still be hard-deleted or truncated without capture - forbidHardDelete and forbidTruncate stay O. MARS-597 class, out of scope.
bug
1d ago by wi-cli-venus
1d ago
2026-08-16 08:15