Add session type persistence and TUI support

This commit is contained in:
fy59 2026-09-06 08:42:10 +02:00
parent d555ddeffd
commit 23c5fcdf06
9 changed files with 810 additions and 42 deletions

View file

@ -84,3 +84,10 @@ Next:
- added terminal performance graph;
- assistance explicitly treats lower assistance as better;
- best recorded set remains distinct from measured max.
### Session type schema v2
- added local `training` / `max_test` session classification;
- added transactional SQLite schema migration v1 -> v2;
- preserved migrated sessions as `training`;
- kept Trainlog JSON v1 frozen and unchanged.

View file

@ -0,0 +1,28 @@
# Session type schema v2
## Status
```text
DATABASE_SCHEMA_V2=IMPLEMENTED
SESSION_TYPE_PERSISTENCE=IMPLEMENTED
TRAINLOG_FORMAT_V1=FROZEN
SESSION_TYPE_TUI=NEXT
```
Schema v2 adds one local SQLite field to `sessions`:
```text
session_type = training | max_test
```
Rules:
- `training` is the default;
- all rows migrated from schema v1 become `training`;
- zero-initialized `TrainlogSessionInput` values remain `training`;
- invalid database values are rejected by a SQLite `CHECK`;
- `max_test` is explicit and is never inferred from performance.
Migration is transactional. Fresh databases are created directly as schema v2.
The frozen Trainlog JSON v1 exchange format is unchanged.

View file

@ -124,3 +124,17 @@ TUI_EXERCISE_PERFORMANCE=IMPLEMENTED
MEASURED_MAX_TRACKING=NEXT
PREVIOUS_SESSION_DEFAULTS=AFTER
```
## Session type / measured max foundation
```text
DATABASE_SCHEMA_V2=IMPLEMENTED
SESSION_TYPE_PERSISTENCE=IMPLEMENTED
SESSION_TYPE_TUI=NEXT
MEASURED_MAX_TRACKING=AFTER
TRAINLOG_FORMAT_V1=FROZEN
```
SQLite schema v2 adds `sessions.session_type` with `training` and `max_test`.
Existing v1 rows migrate to `training`. No existing session is retroactively
classified as a max test.

View file

@ -11,7 +11,7 @@
#include "trainlog/model.h"
#include "trainlog/status.h"
#define TRAINLOG_DATABASE_SCHEMA_VERSION 1
#define TRAINLOG_DATABASE_SCHEMA_VERSION 2
typedef struct TrainlogDatabase TrainlogDatabase;

View file

@ -25,6 +25,11 @@ typedef enum TrainlogLoadMode {
TRAINLOG_LOAD_ASSISTANCE
} TrainlogLoadMode;
typedef enum TrainlogSessionType {
TRAINLOG_SESSION_TRAINING = 0,
TRAINLOG_SESSION_MAX_TEST
} TrainlogSessionType;
typedef struct TrainlogExercise {
char exercise_id[TRAINLOG_ID_MAX + 1U];
char name[TRAINLOG_NAME_MAX + 1U];
@ -56,6 +61,7 @@ typedef struct TrainlogSessionInput {
char session_id[TRAINLOG_ID_MAX + 1U];
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
TrainlogSessionType session_type;
const char *notes;
const TrainlogSessionExerciseInput *exercises;
size_t exercise_count;
@ -65,6 +71,7 @@ typedef struct TrainlogSessionSummary {
char session_id[TRAINLOG_ID_MAX + 1U];
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
TrainlogSessionType session_type;
size_t exercise_count;
} TrainlogSessionSummary;

View file

@ -147,3 +147,15 @@ test(
'exercise_performance',
test_exercise_performance,
)
test_session_type_schema = executable(
'test_session_type_schema',
'tests/test_session_type_schema.c',
dependencies: trainlog_core_dep,
c_args: strict_c_args,
)
test(
'session_type_schema',
test_session_type_schema,
)

View file

@ -17,7 +17,7 @@ struct TrainlogDatabase {
sqlite3 *connection;
};
static const char *const SCHEMA_V1_SQL =
static const char *const SCHEMA_V2_SQL =
"BEGIN IMMEDIATE;"
"CREATE TABLE IF NOT EXISTS exercises ("
@ -34,6 +34,8 @@ static const char *const SCHEMA_V1_SQL =
" session_id TEXT NOT NULL UNIQUE,"
" started_at TEXT NOT NULL,"
" ended_at TEXT,"
" session_type TEXT NOT NULL DEFAULT 'training'"
" CHECK (session_type IN ('training', 'max_test')),"
" notes TEXT"
");"
@ -116,7 +118,15 @@ static const char *const SCHEMA_V1_SQL =
" )"
");"
"PRAGMA user_version = 1;"
"PRAGMA user_version = 2;"
"COMMIT;";
static const char *const MIGRATE_V1_TO_V2_SQL =
"BEGIN IMMEDIATE;"
"ALTER TABLE sessions "
"ADD COLUMN session_type TEXT NOT NULL DEFAULT 'training' "
"CHECK (session_type IN ('training', 'max_test'));"
"PRAGMA user_version = 2;"
"COMMIT;";
static TrainlogStatus execute_sql(
@ -177,7 +187,10 @@ static TrainlogStatus initialize_or_validate_schema(
int version = 0;
TrainlogStatus status;
status = trainlog_database_schema_version(database, &version);
status = trainlog_database_schema_version(
database,
&version
);
if (status != TRAINLOG_STATUS_OK) {
return status;
}
@ -190,13 +203,28 @@ static TrainlogStatus initialize_or_validate_schema(
return TRAINLOG_STATUS_OK;
}
if (version != 0) {
if (version == 0) {
status = execute_sql(
database,
SCHEMA_V2_SQL
);
} else if (version == 1) {
status = execute_sql(
database,
MIGRATE_V1_TO_V2_SQL
);
} else {
return TRAINLOG_STATUS_SCHEMA_UNSUPPORTED;
}
status = execute_sql(database, SCHEMA_V1_SQL);
if (status != TRAINLOG_STATUS_OK) {
(void)sqlite3_exec(database->connection, "ROLLBACK;", NULL, NULL, NULL);
(void)sqlite3_exec(
database->connection,
"ROLLBACK;",
NULL,
NULL,
NULL
);
}
return status;
@ -332,6 +360,22 @@ static const char *load_mode_to_sql(TrainlogLoadMode mode)
}
}
static const char *session_type_to_sql(
TrainlogSessionType type
)
{
switch (type) {
case TRAINLOG_SESSION_TRAINING:
return "training";
case TRAINLOG_SESSION_MAX_TEST:
return "max_test";
default:
return NULL;
}
}
TrainlogStatus trainlog_database_insert_exercise(
TrainlogDatabase *database,
const char *exercise_id,
@ -464,6 +508,29 @@ static TrainlogTrackingMode tracking_mode_from_sql(const char *text)
: TRAINLOG_TRACKING_REPS;
}
static bool session_type_from_sql(
const char *text,
TrainlogSessionType *output
)
{
if (text == NULL ||
output == NULL) {
return false;
}
if (strcmp(text, "training") == 0) {
*output = TRAINLOG_SESSION_TRAINING;
return true;
}
if (strcmp(text, "max_test") == 0) {
*output = TRAINLOG_SESSION_MAX_TEST;
return true;
}
return false;
}
TrainlogStatus trainlog_database_list_exercises(
TrainlogDatabase *database,
TrainlogExercise *output,
@ -586,13 +653,27 @@ static TrainlogStatus insert_session_header(
sqlite3_int64 *output_row_id
)
{
static const char *const SQL =
"INSERT INTO sessions("
"session_id, started_at, ended_at, session_type, notes"
") VALUES(?1, ?2, ?3, ?4, ?5);";
sqlite3_stmt *statement = NULL;
const char *session_type;
int rc;
session_type =
session_type_to_sql(
session->session_type
);
if (session_type == NULL) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
rc = sqlite3_prepare_v2(
database->connection,
"INSERT INTO sessions(session_id, started_at, ended_at, notes) "
"VALUES(?1, ?2, ?3, ?4);",
SQL,
-1,
&statement,
NULL
@ -601,43 +682,79 @@ static TrainlogStatus insert_session_header(
return TRAINLOG_STATUS_DATABASE_ERROR;
}
if (sqlite3_bind_text(statement, 1, session->session_id, -1, SQLITE_TRANSIENT) != SQLITE_OK ||
sqlite3_bind_text(statement, 2, session->started_at, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
(void)sqlite3_finalize(statement);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
rc = sqlite3_bind_text(
statement,
1,
session->session_id,
-1,
SQLITE_TRANSIENT
);
if (session->ended_at[0] != '\0') {
if (rc == SQLITE_OK) {
rc = sqlite3_bind_text(
statement,
3,
session->ended_at,
2,
session->started_at,
-1,
SQLITE_TRANSIENT
);
} else {
rc = sqlite3_bind_null(statement, 3);
}
if (rc != SQLITE_OK) {
(void)sqlite3_finalize(statement);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
if (session->notes != NULL && session->notes[0] != '\0') {
rc = sqlite3_bind_text(statement, 4, session->notes, -1, SQLITE_TRANSIENT);
} else {
rc = sqlite3_bind_null(statement, 4);
if (rc == SQLITE_OK) {
rc =
session->ended_at[0] != '\0'
? sqlite3_bind_text(
statement,
3,
session->ended_at,
-1,
SQLITE_TRANSIENT
)
: sqlite3_bind_null(
statement,
3
);
}
if (rc == SQLITE_OK) {
rc = sqlite3_bind_text(
statement,
4,
session_type,
-1,
SQLITE_STATIC
);
}
if (rc == SQLITE_OK) {
rc =
session->notes != NULL &&
session->notes[0] != '\0'
? sqlite3_bind_text(
statement,
5,
session->notes,
-1,
SQLITE_TRANSIENT
)
: sqlite3_bind_null(
statement,
5
);
}
if (rc != SQLITE_OK) {
(void)sqlite3_finalize(statement);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
rc = sqlite3_step(statement);
if (rc == SQLITE_CONSTRAINT) {
(void)sqlite3_finalize(statement);
return TRAINLOG_STATUS_CONFLICT;
}
if (rc != SQLITE_DONE) {
(void)sqlite3_finalize(statement);
return TRAINLOG_STATUS_DATABASE_ERROR;
@ -647,7 +764,11 @@ static TrainlogStatus insert_session_header(
return TRAINLOG_STATUS_DATABASE_ERROR;
}
*output_row_id = sqlite3_last_insert_rowid(database->connection);
*output_row_id =
sqlite3_last_insert_rowid(
database->connection
);
return TRAINLOG_STATUS_OK;
}
@ -906,12 +1027,18 @@ TrainlogStatus trainlog_database_list_sessions(
)
{
static const char *const SQL =
"SELECT s.session_id, s.started_at, COALESCE(s.ended_at, ''), "
"SELECT "
"s.session_id, "
"s.started_at, "
"COALESCE(s.ended_at, ''), "
"s.session_type, "
"COUNT(se.id) "
"FROM sessions AS s "
"LEFT JOIN session_exercises AS se ON se.session_row_id = s.id "
"LEFT JOIN session_exercises AS se "
"ON se.session_row_id = s.id "
"GROUP BY s.id "
"ORDER BY s.started_at DESC, s.id DESC;";
sqlite3_stmt *statement = NULL;
size_t count = 0U;
int rc;
@ -923,19 +1050,38 @@ TrainlogStatus trainlog_database_list_sessions(
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
rc = sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL);
rc = sqlite3_prepare_v2(
database->connection,
SQL,
-1,
&statement,
NULL
);
if (rc != SQLITE_OK) {
return TRAINLOG_STATUS_DATABASE_ERROR;
}
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
if (count < capacity) {
const unsigned char *id = sqlite3_column_text(statement, 0);
const unsigned char *started = sqlite3_column_text(statement, 1);
const unsigned char *ended = sqlite3_column_text(statement, 2);
sqlite3_int64 exercise_count = sqlite3_column_int64(statement, 3);
const unsigned char *id =
sqlite3_column_text(statement, 0);
if (id == NULL || started == NULL || ended == NULL ||
const unsigned char *started =
sqlite3_column_text(statement, 1);
const unsigned char *ended =
sqlite3_column_text(statement, 2);
const unsigned char *session_type =
sqlite3_column_text(statement, 3);
sqlite3_int64 exercise_count =
sqlite3_column_int64(statement, 4);
if (id == NULL ||
started == NULL ||
ended == NULL ||
session_type == NULL ||
exercise_count < 0) {
(void)sqlite3_finalize(statement);
return TRAINLOG_STATUS_DATABASE_ERROR;
@ -947,20 +1093,33 @@ TrainlogStatus trainlog_database_list_sessions(
"%s",
(const char *)id
);
(void)snprintf(
output[count].started_at,
sizeof(output[count].started_at),
"%s",
(const char *)started
);
(void)snprintf(
output[count].ended_at,
sizeof(output[count].ended_at),
"%s",
(const char *)ended
);
output[count].exercise_count = (size_t)exercise_count;
if (!session_type_from_sql(
(const char *)session_type,
&output[count].session_type
)) {
(void)sqlite3_finalize(statement);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
output[count].exercise_count =
(size_t)exercise_count;
}
++count;
}
@ -973,7 +1132,11 @@ TrainlogStatus trainlog_database_list_sessions(
return TRAINLOG_STATUS_DATABASE_ERROR;
}
*output_count = count < capacity ? count : capacity;
*output_count =
count < capacity
? count
: capacity;
return TRAINLOG_STATUS_OK;
}
@ -1440,7 +1603,7 @@ TrainlogStatus trainlog_database_get_session_details(
)
{
static const char *const HEADER_SQL =
"SELECT started_at, COALESCE(ended_at, '') "
"SELECT started_at, COALESCE(ended_at, ''), session_type "
"FROM sessions "
"WHERE session_id = ?1;";
@ -1514,7 +1677,12 @@ TrainlogStatus trainlog_database_get_session_details(
const unsigned char *ended =
sqlite3_column_text(header, 1);
if (started == NULL || ended == NULL) {
const unsigned char *session_type =
sqlite3_column_text(header, 2);
if (started == NULL ||
ended == NULL ||
session_type == NULL) {
(void)sqlite3_finalize(header);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
@ -1539,6 +1707,14 @@ TrainlogStatus trainlog_database_get_session_details(
"%s",
(const char *)ended
);
if (!session_type_from_sql(
(const char *)session_type,
&output_session->session_type
)) {
(void)sqlite3_finalize(header);
return TRAINLOG_STATUS_DATABASE_ERROR;
}
}
if (sqlite3_finalize(header) != SQLITE_OK) {

View file

@ -3708,6 +3708,224 @@ static bool build_session_exercise(
return true;
}
/* TRAINLOG_SESSION_TYPE_TUI */
static const char *session_type_label(
TrainlogSessionType type
)
{
switch (type) {
case TRAINLOG_SESSION_MAX_TEST:
return "Test de max";
case TRAINLOG_SESSION_TRAINING:
default:
return "Entraînement";
}
}
static const char *session_type_history_label(
TrainlogSessionType type
)
{
return type == TRAINLOG_SESSION_MAX_TEST
? "[MAX]"
: "[ENTRAINEMENT]";
}
static bool choose_session_type(
TrainlogSessionType *output
)
{
int selected = 0;
if (output == NULL) {
return false;
}
for (;;) {
WINDOW *panel;
int key;
int panel_width =
COLS - 8;
if (panel_width < 40) {
panel_width = 40;
}
draw_shell(
"TRAINLOG — Nouvelle séance",
"↑↓ choisir Entrée valider Échap annuler"
);
panel = derwin(
stdscr,
9,
panel_width,
3,
4
);
if (panel == NULL) {
return false;
}
box(panel, 0, 0);
wattron(
panel,
A_BOLD |
trainlog_theme_attribute(
TRAINLOG_COLOR_ACCENT
)
);
mvwprintw(
panel,
0,
2,
" TYPE DE SEANCE "
);
wattroff(
panel,
A_BOLD |
trainlog_theme_attribute(
TRAINLOG_COLOR_ACCENT
)
);
if (selected == 0) {
wattron(
panel,
A_REVERSE |
trainlog_theme_attribute(
TRAINLOG_COLOR_ACCENT
)
);
}
mvwprintw(
panel,
2,
3,
" Entraînement "
);
if (selected == 0) {
wattroff(
panel,
A_REVERSE |
trainlog_theme_attribute(
TRAINLOG_COLOR_ACCENT
)
);
}
wattron(
panel,
trainlog_theme_attribute(
TRAINLOG_COLOR_MUTED
)
);
mvwprintw(
panel,
3,
5,
"Séance normale : progression, volume, travail courant."
);
wattroff(
panel,
trainlog_theme_attribute(
TRAINLOG_COLOR_MUTED
)
);
if (selected == 1) {
wattron(
panel,
A_REVERSE |
trainlog_theme_attribute(
TRAINLOG_COLOR_ACCENT
)
);
}
mvwprintw(
panel,
5,
3,
" Test de max "
);
if (selected == 1) {
wattroff(
panel,
A_REVERSE |
trainlog_theme_attribute(
TRAINLOG_COLOR_ACCENT
)
);
}
wattron(
panel,
trainlog_theme_attribute(
TRAINLOG_COLOR_MUTED
)
);
mvwprintw(
panel,
6,
5,
"Séance explicitement dédiée aux mesures de max."
);
wattroff(
panel,
trainlog_theme_attribute(
TRAINLOG_COLOR_MUTED
)
);
syncok(panel, TRUE);
wsyncup(panel);
delwin(panel);
refresh();
key = getch();
switch (key) {
case KEY_UP:
case KEY_LEFT:
case KEY_DOWN:
case KEY_RIGHT:
selected =
selected == 0
? 1
: 0;
break;
case '\n':
case KEY_ENTER:
*output =
selected == 0
? TRAINLOG_SESSION_TRAINING
: TRAINLOG_SESSION_MAX_TEST;
return true;
case 27:
return false;
default:
break;
}
}
}
static void screen_new_session(TrainlogDatabase *database)
{
TrainlogSessionExerciseInput exercise_inputs[MAX_SESSION_EXERCISES];
@ -3716,6 +3934,8 @@ static void screen_new_session(TrainlogDatabase *database)
char session_id[TRAINLOG_GENERATED_ID_CAPACITY];
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
TrainlogSessionType session_type =
TRAINLOG_SESSION_TRAINING;
size_t exercise_count = 0U;
int another = 1;
TrainlogStatus status;
@ -3740,6 +3960,12 @@ static void screen_new_session(TrainlogDatabase *database)
exercise_count = 0U;
if (!choose_session_type(
&session_type
)) {
return;
}
if (trainlog_id_generate(
"se",
session_id,
@ -3788,6 +4014,9 @@ static void screen_new_session(TrainlogDatabase *database)
}
(void)memset(&session, 0, sizeof(session));
session.session_type =
session_type;
(void)snprintf(
session.session_id,
sizeof(session.session_id),
@ -3819,7 +4048,13 @@ static void screen_new_session(TrainlogDatabase *database)
attroff(trainlog_theme_attribute(TRAINLOG_COLOR_SUCCESS));
mvprintw(6, 2, "Début : %s", started_at);
mvprintw(7, 2, "Fin : %s", ended_at);
mvprintw(8, 2, "Exercices : %zu", exercise_count);
mvprintw(
8,
2,
"Type : %s",
session_type_label(session_type)
);
mvprintw(9, 2, "Exercices : %zu", exercise_count);
} else {
status_line(
"Échec lors de l'enregistrement de la séance.",
@ -3894,6 +4129,15 @@ static void screen_session_detail(
: "séance ouverte"
);
mvprintw(
5,
4,
"Type : %s",
session_type_label(
session.session_type
)
);
if (count == 0U) {
mvprintw(7, 4, "Aucun exercice dans cette séance.");
} else {
@ -4120,8 +4364,11 @@ static void screen_history(TrainlogDatabase *database)
mvprintw(
item_row,
4,
" %-25s %2zu exercice(s) ",
" %-25s %-14s %2zu exercice(s) ",
sessions[absolute].started_at,
session_type_history_label(
sessions[absolute].session_type
),
sessions[absolute].exercise_count
);

View file

@ -0,0 +1,277 @@
/**
* @file test_session_type_schema.c
* @brief Schema v2 and session type persistence tests.
*/
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sqlite3.h>
#include "trainlog/database.h"
#define CHECK(condition) \
do { \
if (!(condition)) { \
(void)fprintf( \
stderr, \
"CHECK failed at %s:%d: %s\n", \
__FILE__, \
__LINE__, \
#condition \
); \
return false; \
} \
} while (0)
static bool test_session_type_roundtrip(void)
{
TrainlogDatabase *database = NULL;
TrainlogSessionInput training;
TrainlogSessionInput max_test;
TrainlogSessionSummary sessions[4];
size_t count = 0U;
CHECK(
trainlog_database_open(
":memory:",
&database
) == TRAINLOG_STATUS_OK
);
(void)memset(&training, 0, sizeof(training));
(void)snprintf(
training.session_id,
sizeof(training.session_id),
"%s",
"se_training"
);
(void)snprintf(
training.started_at,
sizeof(training.started_at),
"%s",
"2026-09-05T18:00:00+02:00"
);
CHECK(
training.session_type ==
TRAINLOG_SESSION_TRAINING
);
CHECK(
trainlog_database_insert_session(
database,
&training
) == TRAINLOG_STATUS_OK
);
(void)memset(&max_test, 0, sizeof(max_test));
(void)snprintf(
max_test.session_id,
sizeof(max_test.session_id),
"%s",
"se_max_test"
);
(void)snprintf(
max_test.started_at,
sizeof(max_test.started_at),
"%s",
"2026-09-05T19:00:00+02:00"
);
max_test.session_type =
TRAINLOG_SESSION_MAX_TEST;
CHECK(
trainlog_database_insert_session(
database,
&max_test
) == TRAINLOG_STATUS_OK
);
CHECK(
trainlog_database_list_sessions(
database,
sessions,
4U,
&count
) == TRAINLOG_STATUS_OK
);
CHECK(count == 2U);
CHECK(
strcmp(
sessions[0].session_id,
"se_max_test"
) == 0
);
CHECK(
sessions[0].session_type ==
TRAINLOG_SESSION_MAX_TEST
);
CHECK(
strcmp(
sessions[1].session_id,
"se_training"
) == 0
);
CHECK(
sessions[1].session_type ==
TRAINLOG_SESSION_TRAINING
);
trainlog_database_close(database);
return true;
}
static bool test_v1_to_v2_migration(void)
{
char path[] =
"/tmp/trainlog-schema-v1-XXXXXX";
static const char *const V1_SQL =
"CREATE TABLE sessions ("
"id INTEGER PRIMARY KEY,"
"session_id TEXT NOT NULL UNIQUE,"
"started_at TEXT NOT NULL,"
"ended_at TEXT,"
"notes TEXT"
");"
"INSERT INTO sessions("
"session_id, started_at, ended_at, notes"
") VALUES("
"'se_old',"
"'2026-08-01T10:00:00+02:00',"
"NULL,"
"NULL"
");"
"PRAGMA user_version = 1;";
sqlite3 *raw = NULL;
sqlite3_stmt *statement = NULL;
TrainlogDatabase *database = NULL;
int fd;
int version = 0;
int rc;
fd = mkstemp(path);
CHECK(fd >= 0);
CHECK(close(fd) == 0);
rc = sqlite3_open(path, &raw);
CHECK(rc == SQLITE_OK);
CHECK(
sqlite3_exec(
raw,
V1_SQL,
NULL,
NULL,
NULL
) == SQLITE_OK
);
CHECK(sqlite3_close(raw) == SQLITE_OK);
raw = NULL;
CHECK(
trainlog_database_open(
path,
&database
) == TRAINLOG_STATUS_OK
);
CHECK(
trainlog_database_schema_version(
database,
&version
) == TRAINLOG_STATUS_OK
);
CHECK(
version ==
TRAINLOG_DATABASE_SCHEMA_VERSION
);
trainlog_database_close(database);
database = NULL;
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
CHECK(
sqlite3_prepare_v2(
raw,
"SELECT session_type "
"FROM sessions "
"WHERE session_id = 'se_old';",
-1,
&statement,
NULL
) == SQLITE_OK
);
CHECK(sqlite3_step(statement) == SQLITE_ROW);
{
const unsigned char *type =
sqlite3_column_text(statement, 0);
CHECK(type != NULL);
CHECK(
strcmp(
(const char *)type,
"training"
) == 0
);
}
CHECK(
sqlite3_finalize(statement) ==
SQLITE_OK
);
statement = NULL;
rc = sqlite3_exec(
raw,
"INSERT INTO sessions("
"session_id, started_at, session_type"
") VALUES("
"'se_invalid',"
"'2026-08-02T10:00:00+02:00',"
"'invalid'"
");",
NULL,
NULL,
NULL
);
CHECK(rc == SQLITE_CONSTRAINT);
CHECK(sqlite3_close(raw) == SQLITE_OK);
raw = NULL;
CHECK(unlink(path) == 0);
return true;
}
int main(void)
{
CHECK(test_session_type_roundtrip());
CHECK(test_v1_to_v2_migration());
(void)printf("PASS session_type_schema\n");
return 0;
}