#include "trainlog/web_session_deletions.h" #include #include #include #include #include #include #include #include #include "database_internal.h" #include "trainlog/id.h" #include "trainlog/model.h" #define CAUSAL_OPERATION_MAX 4096 #define CAUSAL_TEXT_BUDGET (3 * 1024 * 1024) #define PROPOSAL_WITHDRAWAL_MAX 256 typedef struct DeleteContext { const char *kind; const char *target_id; const char *expected_revision; const char *request_id; const char *failure_stage; char created_at[TRAINLOG_TIMESTAMP_MAX + 1U]; } DeleteContext; static void log_database_failure(TrainlogDatabase *database, const DeleteContext *context) { const int primary = sqlite3_errcode(database->connection); const int extended = sqlite3_extended_errcode(database->connection); const char *message = sqlite3_errmsg(database->connection); (void)fprintf(stderr, "Trainlog Web session deletion failed: stage=%s sqlite_primary=%d " "sqlite_extended=%d diagnostic=%s\n", context->failure_stage, primary, extended, message == NULL ? "unavailable" : message); } static bool timestamp_now(char output[TRAINLOG_TIMESTAMP_MAX + 1U]) { time_t now = time(NULL); struct tm utc; return now != (time_t)-1 && gmtime_r(&now, &utc) != NULL && strftime(output, TRAINLOG_TIMESTAMP_MAX + 1U, "%Y-%m-%dT%H:%M:%SZ", &utc) > 0U; } static bool copy_column(sqlite3_stmt *statement, int column, char **output, size_t *output_size) { const char *value = (const char *)sqlite3_column_text(statement, column); int bytes = sqlite3_column_bytes(statement, column); if (value == NULL || bytes <= 0) { return false; } *output = malloc((size_t)bytes + 1U); if (*output == NULL) { return false; } (void)memcpy(*output, value, (size_t)bytes); (*output)[bytes] = '\0'; *output_size = (size_t)bytes; return true; } static TrainlogStatus replay_request(TrainlogDatabase *database, const DeleteContext *context, char **output_json, size_t *output_size, bool *replayed) { static const char SQL[] = "SELECT resource_kind,target_id,expected_revision,response_json " "FROM web_session_deletion_requests WHERE request_id=?1 OR " "(resource_kind=?2 AND target_id=?3) ORDER BY request_id=?1 DESC LIMIT 1"; sqlite3_stmt *statement = NULL; int result; *replayed = false; result = sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->request_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 2, context->kind, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 3, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (result == SQLITE_DONE) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_OK; } if (result != SQLITE_ROW) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; } if (strcmp((const char *)sqlite3_column_text(statement, 0), context->kind) != 0 || strcmp((const char *)sqlite3_column_text(statement, 1), context->target_id) != 0 || strcmp((const char *)sqlite3_column_text(statement, 2), context->expected_revision) != 0) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_CONFLICT; } if (!copy_column(statement, 3, output_json, output_size)) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_SYSTEM_ERROR; } if (sqlite3_finalize(statement) != SQLITE_OK) { free(*output_json); *output_json = NULL; *output_size = 0U; return TRAINLOG_STATUS_DATABASE_ERROR; } *replayed = true; return TRAINLOG_STATUS_OK; } static TrainlogStatus load_current_revision(TrainlogDatabase *database, const char *kind, const char *target_id, char revision[TRAINLOG_ID_MAX + 1U], bool *active_draft) { static const char DRAFT_SQL[] = "SELECT revision_id,state FROM execution_drafts WHERE session_id=?1 AND NOT EXISTS(" "SELECT 1 FROM sync_causal_state WHERE target_kind='execution_draft' AND target_id=?1 " "AND deleted=1)"; static const char HISTORY_SQL[] = "SELECT c.current_revision_id,'' FROM sessions s JOIN sync_causal_state c ON " "c.target_kind='session' AND c.target_id=s.session_id AND c.deleted=0 " "WHERE s.session_id=?1"; sqlite3_stmt *statement = NULL; const char *sql = strcmp(kind, "draft") == 0 ? DRAFT_SQL : HISTORY_SQL; const char *current; int result; *active_draft = false; result = sqlite3_prepare_v2(database->connection, sql, -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (result != SQLITE_ROW) { (void)sqlite3_finalize(statement); return result == SQLITE_DONE ? TRAINLOG_STATUS_NOT_FOUND : TRAINLOG_STATUS_DATABASE_ERROR; } current = (const char *)sqlite3_column_text(statement, 0); if (current == NULL || strlen(current) > TRAINLOG_ID_MAX) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; } (void)snprintf(revision, TRAINLOG_ID_MAX + 1U, "%s", current); *active_draft = strcmp((const char *)sqlite3_column_text(statement, 1), "active") == 0; return sqlite3_finalize(statement) == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR; } static bool sha256_hex(const char *input, char output[65]) { unsigned char digest[32]; unsigned int digest_size = 0U; EVP_MD_CTX *context = EVP_MD_CTX_new(); size_t index; bool success; if (context == NULL) { return false; } success = EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1 && EVP_DigestUpdate(context, input, strlen(input)) == 1 && EVP_DigestFinal_ex(context, digest, &digest_size) == 1 && digest_size == 32U; EVP_MD_CTX_free(context); if (!success) { return false; } for (index = 0U; index < 32U; ++index) { (void)snprintf(output + (index * 2U), 3U, "%02x", digest[index]); } output[64] = '\0'; return true; } static TrainlogStatus causal_capacity_available(TrainlogDatabase *database) { sqlite3_stmt *statement = NULL; sqlite3_int64 count; sqlite3_int64 bytes; int result; result = sqlite3_prepare_v2( database->connection, "SELECT COUNT(*),COALESCE(SUM(length(operation_id)+length(target_kind)+" "length(target_id)+length(creator_id)+length(predecessor_revision_id)+" "length(created_at)+length(payload_sha256)+128),0) FROM sync_causal_operations", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (result != SQLITE_ROW) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; } count = sqlite3_column_int64(statement, 0); bytes = sqlite3_column_int64(statement, 1); if (sqlite3_finalize(statement) != SQLITE_OK) { return TRAINLOG_STATUS_DATABASE_ERROR; } return count < CAUSAL_OPERATION_MAX && bytes < CAUSAL_TEXT_BUDGET ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_CONFLICT; } static TrainlogStatus insert_causal_operation(TrainlogDatabase *database, const DeleteContext *context, const char *causal_kind, const char *predecessor_revision, const char *operation_id) { static const char CREATOR[] = "peer_desktop_web"; sqlite3_stmt *statement = NULL; char payload[1024]; char digest[65]; int written; int result; written = snprintf(payload, sizeof(payload), "{\"created_at\":\"%s\",\"creator_id\":\"%s\"," "\"operation_id\":\"%s\",\"predecessor_revision_id\":\"%s\"," "\"publication_context\":null,\"target_id\":\"%s\"," "\"target_kind\":\"%s\"}", context->created_at, CREATOR, operation_id, predecessor_revision, context->target_id, causal_kind); if (written < 0 || (size_t)written >= sizeof(payload) || !sha256_hex(payload, digest)) { return TRAINLOG_STATUS_SYSTEM_ERROR; } result = sqlite3_prepare_v2(database->connection, "INSERT INTO sync_causal_operations VALUES(?1,?2,?3,?4,?5,?6,?7,NULL)", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, operation_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 2, causal_kind, -1, SQLITE_STATIC); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 3, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 4, CREATOR, -1, SQLITE_STATIC); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 5, predecessor_revision, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 6, context->created_at, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 7, digest, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } return result == SQLITE_DONE ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR; } /* WHY: Program provenance is durable evidence even after a user removes the * completed workout from active history. CONTRACT: only a completed execution * may enter the terminal deleted state. INVARIANT: the transition and session * deletion share the surrounding transaction, so neither can commit alone. */ static TrainlogStatus retire_program_execution(TrainlogDatabase *database, DeleteContext *context) { sqlite3_stmt *statement = NULL; const char *state; int result; context->failure_stage = "load_program_execution"; result = sqlite3_prepare_v2(database->connection, "SELECT state FROM program_session_executions " "WHERE session_id=?1", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (result == SQLITE_DONE) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_OK; } if (result != SQLITE_ROW) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; } state = (const char *)sqlite3_column_text(statement, 0); if (state == NULL || strcmp(state, "completed") != 0) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_CONFLICT; } if (sqlite3_finalize(statement) != SQLITE_OK) { return TRAINLOG_STATUS_DATABASE_ERROR; } statement = NULL; context->failure_stage = "retire_program_execution"; result = sqlite3_prepare_v2(database->connection, "UPDATE program_session_executions " "SET state='deleted' " "WHERE session_id=?1 AND state='completed'", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } if (result != SQLITE_DONE) { return TRAINLOG_STATUS_DATABASE_ERROR; } return sqlite3_changes(database->connection) == 1 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_CONFLICT; } static TrainlogStatus apply_causal_effect(TrainlogDatabase *database, DeleteContext *context, const char *causal_kind, const char *operation_id) { sqlite3_stmt *statement = NULL; int result; if (strcmp(context->kind, "history") == 0) { TrainlogStatus status = retire_program_execution(database, context); if (status != TRAINLOG_STATUS_OK) { return status; } context->failure_stage = "detach_body_observations"; result = sqlite3_prepare_v2( database->connection, "UPDATE body_observations SET session_row_id=NULL WHERE session_row_id=(SELECT id " "FROM sessions WHERE session_id=?1)", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } statement = NULL; if (result != SQLITE_DONE) { return TRAINLOG_STATUS_DATABASE_ERROR; } } context->failure_stage = strcmp(context->kind, "draft") == 0 ? "delete_draft" : "delete_session"; result = sqlite3_prepare_v2(database->connection, strcmp(context->kind, "draft") == 0 ? "DELETE FROM execution_drafts WHERE session_id=?1" : "DELETE FROM sessions WHERE session_id=?1", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } statement = NULL; if (result != SQLITE_DONE || sqlite3_changes(database->connection) != 1) { return result == SQLITE_DONE ? TRAINLOG_STATUS_CONFLICT : TRAINLOG_STATUS_DATABASE_ERROR; } if (strcmp(context->kind, "history") == 0) { char final_revision[TRAINLOG_GENERATED_ID_CAPACITY + sizeof("deleted:")]; int written = snprintf(final_revision, sizeof(final_revision), "deleted:%s", operation_id); if (written < 0 || (size_t)written >= sizeof(final_revision)) { return TRAINLOG_STATUS_SYSTEM_ERROR; } context->failure_stage = "store_session_finalization"; result = sqlite3_prepare_v2( database->connection, "INSERT OR IGNORE INTO execution_draft_finalizations VALUES(?1,?2,?3)", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 2, final_revision, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 3, context->created_at, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } statement = NULL; if (result != SQLITE_DONE) { return TRAINLOG_STATUS_DATABASE_ERROR; } } context->failure_stage = "store_causal_state"; result = sqlite3_prepare_v2(database->connection, "INSERT OR REPLACE INTO sync_causal_state VALUES(?1,?2,?3,1,?3)", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, causal_kind, -1, SQLITE_STATIC); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 2, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 3, operation_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } return result == SQLITE_DONE ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR; } static TrainlogStatus delete_causal_resource(TrainlogDatabase *database, DeleteContext *context) { const char *causal_kind = strcmp(context->kind, "draft") == 0 ? "execution_draft" : "session"; char current_revision[TRAINLOG_ID_MAX + 1U]; char operation_id[TRAINLOG_GENERATED_ID_CAPACITY]; bool active_draft; TrainlogStatus status; context->failure_stage = "load_current_revision"; status = load_current_revision( database, context->kind, context->target_id, current_revision, &active_draft); if (status != TRAINLOG_STATUS_OK) { return status; } if (active_draft || strcmp(current_revision, context->expected_revision) != 0) { return TRAINLOG_STATUS_CONFLICT; } context->failure_stage = "check_causal_capacity"; status = causal_capacity_available(database); if (status != TRAINLOG_STATUS_OK) { return status; } if (trainlog_id_generate("del", operation_id, sizeof(operation_id)) != TRAINLOG_STATUS_OK) { return TRAINLOG_STATUS_SYSTEM_ERROR; } context->failure_stage = "insert_causal_operation"; status = insert_causal_operation(database, context, causal_kind, current_revision, operation_id); if (status == TRAINLOG_STATUS_OK) { status = apply_causal_effect(database, context, causal_kind, operation_id); } return status; } static TrainlogStatus delete_proposal(TrainlogDatabase *database, const DeleteContext *context) { sqlite3_stmt *statement = NULL; const char *revision; int result; result = sqlite3_prepare_v2( database->connection, "SELECT COALESCE(i.payload_sha256,d.draft_id) FROM ai_session_drafts d LEFT JOIN " "ai_session_draft_imports i ON i.draft_row_id=d.id WHERE d.draft_id=?1 AND " "d.withdrawn_at IS NULL", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (result != SQLITE_ROW) { (void)sqlite3_finalize(statement); return result == SQLITE_DONE ? TRAINLOG_STATUS_NOT_FOUND : TRAINLOG_STATUS_DATABASE_ERROR; } revision = (const char *)sqlite3_column_text(statement, 0); if (revision == NULL || strcmp(revision, context->expected_revision) != 0) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_CONFLICT; } if (sqlite3_finalize(statement) != SQLITE_OK) { return TRAINLOG_STATUS_DATABASE_ERROR; } statement = NULL; result = sqlite3_prepare_v2(database->connection, "SELECT COUNT(*) FROM ai_session_drafts " "WHERE withdrawn_at IS NOT NULL", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (result != SQLITE_ROW) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; } if (sqlite3_column_int64(statement, 0) >= PROPOSAL_WITHDRAWAL_MAX) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_CONFLICT; } if (sqlite3_finalize(statement) != SQLITE_OK) { return TRAINLOG_STATUS_DATABASE_ERROR; } statement = NULL; result = sqlite3_prepare_v2(database->connection, "UPDATE ai_session_drafts SET withdrawn_at=?1 WHERE draft_id=?2 " "AND withdrawn_at IS NULL", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->created_at, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 2, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } if (result != SQLITE_DONE || sqlite3_changes(database->connection) != 1) { return result == SQLITE_DONE ? TRAINLOG_STATUS_CONFLICT : TRAINLOG_STATUS_DATABASE_ERROR; } return TRAINLOG_STATUS_OK; } static TrainlogStatus build_response(const DeleteContext *context, char **output_json, size_t *output_size) { yyjson_mut_doc *document = yyjson_mut_doc_new(NULL); yyjson_mut_val *root = document == NULL ? NULL : yyjson_mut_obj(document); if (document == NULL || root == NULL) { yyjson_mut_doc_free(document); return TRAINLOG_STATUS_SYSTEM_ERROR; } yyjson_mut_doc_set_root(document, root); if (!yyjson_mut_obj_add_uint(document, root, "api_version", 1U) || !yyjson_mut_obj_add_strcpy(document, root, "kind", context->kind) || !yyjson_mut_obj_add_strcpy(document, root, "identity", context->target_id) || !yyjson_mut_obj_add_strcpy(document, root, "state", "deleted") || !yyjson_mut_obj_add_strcpy(document, root, "deleted_at", context->created_at)) { yyjson_mut_doc_free(document); return TRAINLOG_STATUS_SYSTEM_ERROR; } *output_json = yyjson_mut_write(document, 0U, output_size); yyjson_mut_doc_free(document); return *output_json != NULL ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR; } static TrainlogStatus store_request(TrainlogDatabase *database, const DeleteContext *context, const char *response_json) { sqlite3_stmt *statement = NULL; int result; result = sqlite3_prepare_v2(database->connection, "INSERT INTO web_session_deletion_requests " "VALUES(?1,?2,?3,?4,?5,?6)", -1, &statement, NULL); if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 1, context->request_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 2, context->kind, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 3, context->target_id, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 4, context->expected_revision, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 5, response_json, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_bind_text(statement, 6, context->created_at, -1, SQLITE_TRANSIENT); } if (result == SQLITE_OK) { result = sqlite3_step(statement); } if (statement != NULL && sqlite3_finalize(statement) != SQLITE_OK && result == SQLITE_DONE) { result = SQLITE_ERROR; } return result == SQLITE_DONE ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR; } TrainlogStatus trainlog_web_session_delete_json(TrainlogDatabase *database, const char *kind, const char *target_id, const char *expected_revision, const char *request_id, char **output_json, size_t *output_size) { DeleteContext context; TrainlogStatus status; bool replayed; if (database == NULL || kind == NULL || target_id == NULL || expected_revision == NULL || request_id == NULL || output_json == NULL || output_size == NULL || target_id[0] == '\0' || expected_revision[0] == '\0' || request_id[0] == '\0' || strchr(target_id, '/') != NULL || strlen(target_id) > TRAINLOG_ID_MAX || strlen(expected_revision) > TRAINLOG_ID_MAX || strlen(request_id) > 128U || (strcmp(kind, "proposal") != 0 && strcmp(kind, "draft") != 0 && strcmp(kind, "history") != 0)) { return TRAINLOG_STATUS_INVALID_ARGUMENT; } *output_json = NULL; *output_size = 0U; context.kind = kind; context.target_id = target_id; context.expected_revision = expected_revision; context.request_id = request_id; context.failure_stage = "begin_transaction"; if (!timestamp_now(context.created_at)) { return TRAINLOG_STATUS_SYSTEM_ERROR; } if (sqlite3_exec(database->connection, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) { log_database_failure(database, &context); return TRAINLOG_STATUS_DATABASE_ERROR; } context.failure_stage = "replay_request"; status = replay_request(database, &context, output_json, output_size, &replayed); if (status == TRAINLOG_STATUS_OK && replayed) { context.failure_stage = "commit_replayed_request"; if (sqlite3_exec(database->connection, "COMMIT", NULL, NULL, NULL) == SQLITE_OK) { return TRAINLOG_STATUS_OK; } status = TRAINLOG_STATUS_DATABASE_ERROR; } if (status == TRAINLOG_STATUS_OK) { context.failure_stage = "apply_deletion"; status = strcmp(kind, "proposal") == 0 ? delete_proposal(database, &context) : delete_causal_resource(database, &context); } if (status == TRAINLOG_STATUS_OK) { status = build_response(&context, output_json, output_size); } if (status == TRAINLOG_STATUS_OK) { context.failure_stage = "store_deletion_request"; status = store_request(database, &context, *output_json); } if (status == TRAINLOG_STATUS_OK) { context.failure_stage = "commit_transaction"; if (sqlite3_exec(database->connection, "COMMIT", NULL, NULL, NULL) == SQLITE_OK) { return TRAINLOG_STATUS_OK; } } if (status == TRAINLOG_STATUS_DATABASE_ERROR || status == TRAINLOG_STATUS_OK) { log_database_failure(database, &context); } (void)sqlite3_exec(database->connection, "ROLLBACK", NULL, NULL, NULL); free(*output_json); *output_json = NULL; *output_size = 0U; return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_DATABASE_ERROR : status; }