Initial commit
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
//! Database schema definition and migration
|
||||
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
/// Current schema version
|
||||
pub const SCHEMA_VERSION: i32 = 1;
|
||||
|
||||
/// Create the database schema
|
||||
pub fn create_schema(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(SCHEMA_SQL)?;
|
||||
|
||||
// Set schema version
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO schema_version (id, version) VALUES (1, ?1)",
|
||||
[SCHEMA_VERSION],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if schema needs migration
|
||||
pub fn check_schema_version(conn: &Connection) -> Result<Option<i32>> {
|
||||
// Check if schema_version table exists
|
||||
let exists: bool = conn.query_row(
|
||||
"SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='schema_version'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
if !exists {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let version: i32 = conn.query_row(
|
||||
"SELECT version FROM schema_version WHERE id = 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
Ok(Some(version))
|
||||
}
|
||||
|
||||
/// Schema SQL for all tables
|
||||
const SCHEMA_SQL: &str = r"
|
||||
-- Schema version tracking
|
||||
CREATE TABLE IF NOT EXISTS schema_version (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Protocols (top-level container, like a Brainstorm protocol)
|
||||
CREATE TABLE IF NOT EXISTS protocols (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
modified_at TEXT NOT NULL,
|
||||
settings_json TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
|
||||
-- Subjects within a protocol
|
||||
CREATE TABLE IF NOT EXISTS subjects (
|
||||
id TEXT PRIMARY KEY,
|
||||
protocol_id TEXT NOT NULL REFERENCES protocols(id) ON DELETE CASCADE,
|
||||
label TEXT NOT NULL,
|
||||
anatomy_path TEXT,
|
||||
demographics_json TEXT DEFAULT '{}',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_subjects_protocol ON subjects(protocol_id);
|
||||
|
||||
-- Conditions (experimental conditions for grouping)
|
||||
CREATE TABLE IF NOT EXISTS conditions (
|
||||
id TEXT PRIMARY KEY,
|
||||
protocol_id TEXT NOT NULL REFERENCES protocols(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT DEFAULT '#4A90D9',
|
||||
description TEXT DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_conditions_protocol ON conditions(protocol_id);
|
||||
|
||||
-- Recordings (loaded data files)
|
||||
CREATE TABLE IF NOT EXISTS recordings (
|
||||
id TEXT PRIMARY KEY,
|
||||
subject_id TEXT REFERENCES subjects(id) ON DELETE SET NULL,
|
||||
condition_id TEXT REFERENCES conditions(id) ON DELETE SET NULL,
|
||||
handle_id TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
format TEXT NOT NULL,
|
||||
n_channels INTEGER NOT NULL,
|
||||
n_samples INTEGER NOT NULL,
|
||||
sfreq REAL NOT NULL,
|
||||
duration REAL NOT NULL,
|
||||
bad_channels_json TEXT DEFAULT '[]',
|
||||
annotations_json TEXT DEFAULT '[]',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recordings_subject ON recordings(subject_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_recordings_condition ON recordings(condition_id);
|
||||
|
||||
-- Epochs sets
|
||||
CREATE TABLE IF NOT EXISTS epochs (
|
||||
id TEXT PRIMARY KEY,
|
||||
recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
handle_id TEXT NOT NULL,
|
||||
event_ids_json TEXT NOT NULL,
|
||||
tmin REAL NOT NULL,
|
||||
tmax REAL NOT NULL,
|
||||
baseline_json TEXT,
|
||||
n_epochs INTEGER NOT NULL,
|
||||
n_channels INTEGER NOT NULL,
|
||||
n_times INTEGER NOT NULL,
|
||||
reject_criteria_json TEXT DEFAULT '{}',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_epochs_recording ON epochs(recording_id);
|
||||
|
||||
-- Forward models
|
||||
CREATE TABLE IF NOT EXISTS forward_models (
|
||||
id TEXT PRIMARY KEY,
|
||||
recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
handle_id TEXT NOT NULL,
|
||||
model_type TEXT NOT NULL,
|
||||
n_sensors INTEGER NOT NULL,
|
||||
n_sources INTEGER NOT NULL,
|
||||
config_json TEXT NOT NULL,
|
||||
gain_matrix_path TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_forward_recording ON forward_models(recording_id);
|
||||
|
||||
-- Inverse operators
|
||||
CREATE TABLE IF NOT EXISTS inverse_operators (
|
||||
id TEXT PRIMARY KEY,
|
||||
forward_id TEXT NOT NULL REFERENCES forward_models(id) ON DELETE CASCADE,
|
||||
handle_id TEXT NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
lambda2 REAL NOT NULL,
|
||||
n_sources INTEGER NOT NULL,
|
||||
n_channels INTEGER NOT NULL,
|
||||
kernel_path TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inverse_forward ON inverse_operators(forward_id);
|
||||
|
||||
-- SSP projectors
|
||||
CREATE TABLE IF NOT EXISTS ssp_projectors (
|
||||
id TEXT PRIMARY KEY,
|
||||
recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
handle_id TEXT NOT NULL,
|
||||
n_components INTEGER NOT NULL,
|
||||
artifact_type TEXT,
|
||||
explained_var_json TEXT DEFAULT '[]',
|
||||
projector_path TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ssp_recording ON ssp_projectors(recording_id);
|
||||
|
||||
-- ICA models
|
||||
CREATE TABLE IF NOT EXISTS ica_models (
|
||||
id TEXT PRIMARY KEY,
|
||||
recording_id TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
handle_id TEXT NOT NULL,
|
||||
n_components INTEGER NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
excluded_json TEXT DEFAULT '[]',
|
||||
mixing_matrix_path TEXT,
|
||||
unmixing_matrix_path TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ica_recording ON ica_models(recording_id);
|
||||
";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rusqlite::Connection;
|
||||
|
||||
#[test]
|
||||
fn test_create_schema() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
create_schema(&conn).unwrap();
|
||||
|
||||
// Verify tables exist
|
||||
let count: i32 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(count >= 10); // At least 10 tables
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_version() {
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
|
||||
// Before schema creation
|
||||
assert_eq!(check_schema_version(&conn).unwrap(), None);
|
||||
|
||||
// After schema creation
|
||||
create_schema(&conn).unwrap();
|
||||
assert_eq!(check_schema_version(&conn).unwrap(), Some(SCHEMA_VERSION));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user