feat: implement fact deletion Tier-1/2/3 (retract, retract-by-batch-id, forget)#324
Merged
Conversation
- Implement wyl_fact_store_retract_batch() as thin wrapper that
forces op=WYL_FACT_STORE_OP_RETRACT and delegates to
wyl_fact_store_append_batch (reuses validation, mutex,
transaction, idempotency, scope-check, event-log paths).
- Soft retract appends __wyl_valid=FALSE rows; the event log,
fact_batches row, and replay determinism are preserved.
- Add check_fact_store_retracts_idempotently() with four cases:
* normal retract (inserted=TRUE, __wyl_valid=FALSE on retracted
row, sibling row stays valid, batch op recorded as 'retract')
* idempotent retry (inserted=FALSE, content_hash matches)
* non-existent row retract (WYRELOG_E_OK, ghost retract row
recorded with __wyl_valid=FALSE)
* wrong-scope retract (WYRELOG_E_POLICY)
Implement POST /facts/{tenant}/{graph}/{relation}:retract via single-handler
verb dispatch. Generalizes parse_fact_append_path to parse_fact_op_path with
:append/:retract suffix recognition. Extends emit_fact_append_audit to
emit_fact_op_audit with operation-aware action string (fact_retract vs
fact_append). Adds 7 test cases covering normal retract, idempotent replay,
content_hash conflict, op/path mismatch, RBAC deny, sealed graph, and
missing schema.
Error codes branch to fact_retract_failed (retract ops) vs fact_append_failed
(append ops) for accurate client diagnostics.
Implement wyl_fact_store_retract_by_batch_id() to retract all valid rows from a specified batch_id in a single atomic SELECT+INSERT operation within a single mutex and transaction. Adds 10 test cases covering normal retract, idempotency, not-found, policy violations, and row limits. Also fixes pre-existing bug in validate_projection_shape_unlocked: replace list_count() with len() (list_count requires core_functions extension not loaded by default; len is built-in alias).
Add fact_forget_audit table to wyl_fact_store_create_schema() to record GDPR/right-to-forget purge audit trails. Uses IF NOT EXISTS for safe migration of existing databases.
Implements wyl_fact_store_forget() which physically purges all rows for a given batch_id from the projection table, fact_event_log, and fact_batches (in FK-safe order under a single mutex), then records the operation in fact_forget_audit. Adds wyl_fact_store_forget_options_t struct to store-private.h. Includes two test cases: basic forget (assert + forget, verify rows=0 and audit record present) and NOT_FOUND guard for missing batch_id.
- Add DELETE /facts/{tenant}/{graph}/{relation}:forget endpoint
- Parse :forget suffix in fact_op_path dispatch
- Extend facts_route_handler to accept DELETE method for :forget
- Call wyl_fact_store_forget with batch_id, operator, reason from JSON body
- Returns {ok: true, rows_purged: N} on success
- Requires wr.fact.write authorization
- Add 2 HTTP integration tests: normal forget (200) and no permission (403)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #323
Summary
Complete implementation of all 3 tiers for fact store deletion as specified in issue #323:
Tier-1: Single-batch soft retraction
wyl_fact_store_retract_batch()library APIPOST /facts/{tenant}/{graph}/{relation}:retractHTTP endpoint__wyl_valid=FALSE, maintains event sourcing semanticsTier-2: Batch ID-based bulk retraction
wyl_fact_store_retract_by_batch_id()atomically retracts all rows from a batchexisting_batch_matches_unlockedTier-3: Hard deletion with immutable audit trail
wyl_fact_store_forget()physically removes facts and logs tofact_forget_audittableDELETE /facts/{tenant}/{graph}/{relation}:forgetHTTP endpoint with operator & reasonArchitecture
__wyl_valid=FALSE, original asserts remainwyl_fact_store_append_batchwrapper patternwr.fact.writepermission required for all operationsTest Coverage
Files Changed
wyrelog/fact/store.c: Three new APIs (retract_batch,retract_by_batch_id,forget) + helperswyrelog/fact/store-private.h: Public declarations, macros, opaque typeswyrelog/daemon/http.c: Generalized op dispatch (:append/:retract/:forget), new DELETE handlertests/test-fact-store.c: 10 unit tests covering all tierstests/test-daemon-http-facts.c: 9 integration tests with error casesCommits
Each commit is individually compilable and tested per CLAUDE.md TDD requirements:
Design Decisions
Implementation Notes