trainlog/tui/src/web_session_deletions.c

702 lines
28 KiB
C

#include "trainlog/web_session_deletions.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <openssl/evp.h>
#include <sqlite3.h>
#include <yyjson.h>
#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;
}