Add session type persistence and TUI support
This commit is contained in:
parent
d555ddeffd
commit
23c5fcdf06
9 changed files with 810 additions and 42 deletions
|
|
@ -84,3 +84,10 @@ Next:
|
||||||
- added terminal performance graph;
|
- added terminal performance graph;
|
||||||
- assistance explicitly treats lower assistance as better;
|
- assistance explicitly treats lower assistance as better;
|
||||||
- best recorded set remains distinct from measured max.
|
- 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.
|
||||||
|
|
|
||||||
28
docs/reviews/session_type_schema_v2.md
Normal file
28
docs/reviews/session_type_schema_v2.md
Normal 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.
|
||||||
|
|
@ -124,3 +124,17 @@ TUI_EXERCISE_PERFORMANCE=IMPLEMENTED
|
||||||
MEASURED_MAX_TRACKING=NEXT
|
MEASURED_MAX_TRACKING=NEXT
|
||||||
PREVIOUS_SESSION_DEFAULTS=AFTER
|
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.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
#include "trainlog/model.h"
|
#include "trainlog/model.h"
|
||||||
#include "trainlog/status.h"
|
#include "trainlog/status.h"
|
||||||
|
|
||||||
#define TRAINLOG_DATABASE_SCHEMA_VERSION 1
|
#define TRAINLOG_DATABASE_SCHEMA_VERSION 2
|
||||||
|
|
||||||
typedef struct TrainlogDatabase TrainlogDatabase;
|
typedef struct TrainlogDatabase TrainlogDatabase;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ typedef enum TrainlogLoadMode {
|
||||||
TRAINLOG_LOAD_ASSISTANCE
|
TRAINLOG_LOAD_ASSISTANCE
|
||||||
} TrainlogLoadMode;
|
} TrainlogLoadMode;
|
||||||
|
|
||||||
|
typedef enum TrainlogSessionType {
|
||||||
|
TRAINLOG_SESSION_TRAINING = 0,
|
||||||
|
TRAINLOG_SESSION_MAX_TEST
|
||||||
|
} TrainlogSessionType;
|
||||||
|
|
||||||
typedef struct TrainlogExercise {
|
typedef struct TrainlogExercise {
|
||||||
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
char exercise_id[TRAINLOG_ID_MAX + 1U];
|
||||||
char name[TRAINLOG_NAME_MAX + 1U];
|
char name[TRAINLOG_NAME_MAX + 1U];
|
||||||
|
|
@ -56,6 +61,7 @@ typedef struct TrainlogSessionInput {
|
||||||
char session_id[TRAINLOG_ID_MAX + 1U];
|
char session_id[TRAINLOG_ID_MAX + 1U];
|
||||||
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
|
TrainlogSessionType session_type;
|
||||||
const char *notes;
|
const char *notes;
|
||||||
const TrainlogSessionExerciseInput *exercises;
|
const TrainlogSessionExerciseInput *exercises;
|
||||||
size_t exercise_count;
|
size_t exercise_count;
|
||||||
|
|
@ -65,6 +71,7 @@ typedef struct TrainlogSessionSummary {
|
||||||
char session_id[TRAINLOG_ID_MAX + 1U];
|
char session_id[TRAINLOG_ID_MAX + 1U];
|
||||||
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
|
TrainlogSessionType session_type;
|
||||||
size_t exercise_count;
|
size_t exercise_count;
|
||||||
} TrainlogSessionSummary;
|
} TrainlogSessionSummary;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,3 +147,15 @@ test(
|
||||||
'exercise_performance',
|
'exercise_performance',
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ struct TrainlogDatabase {
|
||||||
sqlite3 *connection;
|
sqlite3 *connection;
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *const SCHEMA_V1_SQL =
|
static const char *const SCHEMA_V2_SQL =
|
||||||
"BEGIN IMMEDIATE;"
|
"BEGIN IMMEDIATE;"
|
||||||
|
|
||||||
"CREATE TABLE IF NOT EXISTS exercises ("
|
"CREATE TABLE IF NOT EXISTS exercises ("
|
||||||
|
|
@ -34,6 +34,8 @@ static const char *const SCHEMA_V1_SQL =
|
||||||
" session_id TEXT NOT NULL UNIQUE,"
|
" session_id TEXT NOT NULL UNIQUE,"
|
||||||
" started_at TEXT NOT NULL,"
|
" started_at TEXT NOT NULL,"
|
||||||
" ended_at TEXT,"
|
" ended_at TEXT,"
|
||||||
|
" session_type TEXT NOT NULL DEFAULT 'training'"
|
||||||
|
" CHECK (session_type IN ('training', 'max_test')),"
|
||||||
" notes TEXT"
|
" 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;";
|
"COMMIT;";
|
||||||
|
|
||||||
static TrainlogStatus execute_sql(
|
static TrainlogStatus execute_sql(
|
||||||
|
|
@ -177,7 +187,10 @@ static TrainlogStatus initialize_or_validate_schema(
|
||||||
int version = 0;
|
int version = 0;
|
||||||
TrainlogStatus status;
|
TrainlogStatus status;
|
||||||
|
|
||||||
status = trainlog_database_schema_version(database, &version);
|
status = trainlog_database_schema_version(
|
||||||
|
database,
|
||||||
|
&version
|
||||||
|
);
|
||||||
if (status != TRAINLOG_STATUS_OK) {
|
if (status != TRAINLOG_STATUS_OK) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
@ -190,13 +203,28 @@ static TrainlogStatus initialize_or_validate_schema(
|
||||||
return TRAINLOG_STATUS_OK;
|
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;
|
return TRAINLOG_STATUS_SCHEMA_UNSUPPORTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
status = execute_sql(database, SCHEMA_V1_SQL);
|
|
||||||
if (status != TRAINLOG_STATUS_OK) {
|
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;
|
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(
|
TrainlogStatus trainlog_database_insert_exercise(
|
||||||
TrainlogDatabase *database,
|
TrainlogDatabase *database,
|
||||||
const char *exercise_id,
|
const char *exercise_id,
|
||||||
|
|
@ -464,6 +508,29 @@ static TrainlogTrackingMode tracking_mode_from_sql(const char *text)
|
||||||
: TRAINLOG_TRACKING_REPS;
|
: 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(
|
TrainlogStatus trainlog_database_list_exercises(
|
||||||
TrainlogDatabase *database,
|
TrainlogDatabase *database,
|
||||||
TrainlogExercise *output,
|
TrainlogExercise *output,
|
||||||
|
|
@ -586,13 +653,27 @@ static TrainlogStatus insert_session_header(
|
||||||
sqlite3_int64 *output_row_id
|
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;
|
sqlite3_stmt *statement = NULL;
|
||||||
|
const char *session_type;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
|
session_type =
|
||||||
|
session_type_to_sql(
|
||||||
|
session->session_type
|
||||||
|
);
|
||||||
|
|
||||||
|
if (session_type == NULL) {
|
||||||
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
rc = sqlite3_prepare_v2(
|
rc = sqlite3_prepare_v2(
|
||||||
database->connection,
|
database->connection,
|
||||||
"INSERT INTO sessions(session_id, started_at, ended_at, notes) "
|
SQL,
|
||||||
"VALUES(?1, ?2, ?3, ?4);",
|
|
||||||
-1,
|
-1,
|
||||||
&statement,
|
&statement,
|
||||||
NULL
|
NULL
|
||||||
|
|
@ -601,43 +682,79 @@ static TrainlogStatus insert_session_header(
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sqlite3_bind_text(statement, 1, session->session_id, -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
rc = sqlite3_bind_text(
|
||||||
sqlite3_bind_text(statement, 2, session->started_at, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
|
statement,
|
||||||
(void)sqlite3_finalize(statement);
|
1,
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
session->session_id,
|
||||||
|
-1,
|
||||||
|
SQLITE_TRANSIENT
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
rc = sqlite3_bind_text(
|
||||||
|
statement,
|
||||||
|
2,
|
||||||
|
session->started_at,
|
||||||
|
-1,
|
||||||
|
SQLITE_TRANSIENT
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session->ended_at[0] != '\0') {
|
if (rc == SQLITE_OK) {
|
||||||
rc = sqlite3_bind_text(
|
rc =
|
||||||
|
session->ended_at[0] != '\0'
|
||||||
|
? sqlite3_bind_text(
|
||||||
statement,
|
statement,
|
||||||
3,
|
3,
|
||||||
session->ended_at,
|
session->ended_at,
|
||||||
-1,
|
-1,
|
||||||
SQLITE_TRANSIENT
|
SQLITE_TRANSIENT
|
||||||
|
)
|
||||||
|
: sqlite3_bind_null(
|
||||||
|
statement,
|
||||||
|
3
|
||||||
);
|
);
|
||||||
} 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') {
|
if (rc == SQLITE_OK) {
|
||||||
rc = sqlite3_bind_text(statement, 4, session->notes, -1, SQLITE_TRANSIENT);
|
rc = sqlite3_bind_text(
|
||||||
} else {
|
statement,
|
||||||
rc = sqlite3_bind_null(statement, 4);
|
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) {
|
if (rc != SQLITE_OK) {
|
||||||
(void)sqlite3_finalize(statement);
|
(void)sqlite3_finalize(statement);
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
rc = sqlite3_step(statement);
|
rc = sqlite3_step(statement);
|
||||||
|
|
||||||
if (rc == SQLITE_CONSTRAINT) {
|
if (rc == SQLITE_CONSTRAINT) {
|
||||||
(void)sqlite3_finalize(statement);
|
(void)sqlite3_finalize(statement);
|
||||||
return TRAINLOG_STATUS_CONFLICT;
|
return TRAINLOG_STATUS_CONFLICT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rc != SQLITE_DONE) {
|
if (rc != SQLITE_DONE) {
|
||||||
(void)sqlite3_finalize(statement);
|
(void)sqlite3_finalize(statement);
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
|
@ -647,7 +764,11 @@ static TrainlogStatus insert_session_header(
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
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;
|
return TRAINLOG_STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -906,12 +1027,18 @@ TrainlogStatus trainlog_database_list_sessions(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
static const char *const SQL =
|
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) "
|
"COUNT(se.id) "
|
||||||
"FROM sessions AS s "
|
"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 "
|
"GROUP BY s.id "
|
||||||
"ORDER BY s.started_at DESC, s.id DESC;";
|
"ORDER BY s.started_at DESC, s.id DESC;";
|
||||||
|
|
||||||
sqlite3_stmt *statement = NULL;
|
sqlite3_stmt *statement = NULL;
|
||||||
size_t count = 0U;
|
size_t count = 0U;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
@ -923,19 +1050,38 @@ TrainlogStatus trainlog_database_list_sessions(
|
||||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
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) {
|
if (rc != SQLITE_OK) {
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
|
||||||
if (count < capacity) {
|
if (count < capacity) {
|
||||||
const unsigned char *id = sqlite3_column_text(statement, 0);
|
const unsigned char *id =
|
||||||
const unsigned char *started = sqlite3_column_text(statement, 1);
|
sqlite3_column_text(statement, 0);
|
||||||
const unsigned char *ended = sqlite3_column_text(statement, 2);
|
|
||||||
sqlite3_int64 exercise_count = sqlite3_column_int64(statement, 3);
|
|
||||||
|
|
||||||
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) {
|
exercise_count < 0) {
|
||||||
(void)sqlite3_finalize(statement);
|
(void)sqlite3_finalize(statement);
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
|
|
@ -947,20 +1093,33 @@ TrainlogStatus trainlog_database_list_sessions(
|
||||||
"%s",
|
"%s",
|
||||||
(const char *)id
|
(const char *)id
|
||||||
);
|
);
|
||||||
|
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
output[count].started_at,
|
output[count].started_at,
|
||||||
sizeof(output[count].started_at),
|
sizeof(output[count].started_at),
|
||||||
"%s",
|
"%s",
|
||||||
(const char *)started
|
(const char *)started
|
||||||
);
|
);
|
||||||
|
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
output[count].ended_at,
|
output[count].ended_at,
|
||||||
sizeof(output[count].ended_at),
|
sizeof(output[count].ended_at),
|
||||||
"%s",
|
"%s",
|
||||||
(const char *)ended
|
(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;
|
++count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -973,7 +1132,11 @@ TrainlogStatus trainlog_database_list_sessions(
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
*output_count = count < capacity ? count : capacity;
|
*output_count =
|
||||||
|
count < capacity
|
||||||
|
? count
|
||||||
|
: capacity;
|
||||||
|
|
||||||
return TRAINLOG_STATUS_OK;
|
return TRAINLOG_STATUS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1440,7 +1603,7 @@ TrainlogStatus trainlog_database_get_session_details(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
static const char *const HEADER_SQL =
|
static const char *const HEADER_SQL =
|
||||||
"SELECT started_at, COALESCE(ended_at, '') "
|
"SELECT started_at, COALESCE(ended_at, ''), session_type "
|
||||||
"FROM sessions "
|
"FROM sessions "
|
||||||
"WHERE session_id = ?1;";
|
"WHERE session_id = ?1;";
|
||||||
|
|
||||||
|
|
@ -1514,7 +1677,12 @@ TrainlogStatus trainlog_database_get_session_details(
|
||||||
const unsigned char *ended =
|
const unsigned char *ended =
|
||||||
sqlite3_column_text(header, 1);
|
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);
|
(void)sqlite3_finalize(header);
|
||||||
return TRAINLOG_STATUS_DATABASE_ERROR;
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
@ -1539,6 +1707,14 @@ TrainlogStatus trainlog_database_get_session_details(
|
||||||
"%s",
|
"%s",
|
||||||
(const char *)ended
|
(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) {
|
if (sqlite3_finalize(header) != SQLITE_OK) {
|
||||||
|
|
|
||||||
251
tui/src/tui.c
251
tui/src/tui.c
|
|
@ -3708,6 +3708,224 @@ static bool build_session_exercise(
|
||||||
return true;
|
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)
|
static void screen_new_session(TrainlogDatabase *database)
|
||||||
{
|
{
|
||||||
TrainlogSessionExerciseInput exercise_inputs[MAX_SESSION_EXERCISES];
|
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 session_id[TRAINLOG_GENERATED_ID_CAPACITY];
|
||||||
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
char started_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
char ended_at[TRAINLOG_TIMESTAMP_MAX + 1U];
|
||||||
|
TrainlogSessionType session_type =
|
||||||
|
TRAINLOG_SESSION_TRAINING;
|
||||||
size_t exercise_count = 0U;
|
size_t exercise_count = 0U;
|
||||||
int another = 1;
|
int another = 1;
|
||||||
TrainlogStatus status;
|
TrainlogStatus status;
|
||||||
|
|
@ -3740,6 +3960,12 @@ static void screen_new_session(TrainlogDatabase *database)
|
||||||
|
|
||||||
exercise_count = 0U;
|
exercise_count = 0U;
|
||||||
|
|
||||||
|
if (!choose_session_type(
|
||||||
|
&session_type
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (trainlog_id_generate(
|
if (trainlog_id_generate(
|
||||||
"se",
|
"se",
|
||||||
session_id,
|
session_id,
|
||||||
|
|
@ -3788,6 +4014,9 @@ static void screen_new_session(TrainlogDatabase *database)
|
||||||
}
|
}
|
||||||
|
|
||||||
(void)memset(&session, 0, sizeof(session));
|
(void)memset(&session, 0, sizeof(session));
|
||||||
|
session.session_type =
|
||||||
|
session_type;
|
||||||
|
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
session.session_id,
|
session.session_id,
|
||||||
sizeof(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));
|
attroff(trainlog_theme_attribute(TRAINLOG_COLOR_SUCCESS));
|
||||||
mvprintw(6, 2, "Début : %s", started_at);
|
mvprintw(6, 2, "Début : %s", started_at);
|
||||||
mvprintw(7, 2, "Fin : %s", ended_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 {
|
} else {
|
||||||
status_line(
|
status_line(
|
||||||
"Échec lors de l'enregistrement de la séance.",
|
"Échec lors de l'enregistrement de la séance.",
|
||||||
|
|
@ -3894,6 +4129,15 @@ static void screen_session_detail(
|
||||||
: "séance ouverte"
|
: "séance ouverte"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mvprintw(
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
"Type : %s",
|
||||||
|
session_type_label(
|
||||||
|
session.session_type
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (count == 0U) {
|
if (count == 0U) {
|
||||||
mvprintw(7, 4, "Aucun exercice dans cette séance.");
|
mvprintw(7, 4, "Aucun exercice dans cette séance.");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -4120,8 +4364,11 @@ static void screen_history(TrainlogDatabase *database)
|
||||||
mvprintw(
|
mvprintw(
|
||||||
item_row,
|
item_row,
|
||||||
4,
|
4,
|
||||||
" %-25s %2zu exercice(s) ",
|
" %-25s %-14s %2zu exercice(s) ",
|
||||||
sessions[absolute].started_at,
|
sessions[absolute].started_at,
|
||||||
|
session_type_history_label(
|
||||||
|
sessions[absolute].session_type
|
||||||
|
),
|
||||||
sessions[absolute].exercise_count
|
sessions[absolute].exercise_count
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
277
tui/tests/test_session_type_schema.c
Normal file
277
tui/tests/test_session_type_schema.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue