modularize app and database internals
- Extract editor, form, export, and schema modules - Add database workflow integration tests
This commit is contained in:
126
src/db/schema.rs
Normal file
126
src/db/schema.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
|
||||
pub(super) fn migrate(connection: &Connection) -> Result<()> {
|
||||
connection.execute_batch(
|
||||
"BEGIN;
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY, value TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
priority INTEGER NOT NULL DEFAULT 3 CHECK(priority BETWEEN 1 AND 5),
|
||||
when_at TEXT,
|
||||
done_at TEXT,
|
||||
alarm_at TEXT,
|
||||
numeric_value REAL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
discarded INTEGER NOT NULL DEFAULT 0 CHECK(discarded IN (0,1))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
parent_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
||||
kind TEXT NOT NULL DEFAULT 'standard',
|
||||
match_text TEXT NOT NULL DEFAULT '',
|
||||
exclusive INTEGER NOT NULL DEFAULT 0 CHECK(exclusive IN (0,1)),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS item_categories (
|
||||
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
||||
category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
|
||||
assignment TEXT NOT NULL DEFAULT 'explicit',
|
||||
PRIMARY KEY(item_id, category_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS dependencies (
|
||||
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
||||
prerequisite_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(item_id, prerequisite_id),
|
||||
CHECK(item_id <> prerequisite_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS views (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
kind TEXT NOT NULL DEFAULT 'list',
|
||||
filter_value TEXT NOT NULL DEFAULT '',
|
||||
sort_key TEXT NOT NULL DEFAULT 'manual',
|
||||
show_done INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_items_when ON items(when_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_items_done ON items(done_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_item_categories_category ON item_categories(category_id);
|
||||
COMMIT;",
|
||||
)?;
|
||||
|
||||
let version: i64 = connection.query_row("PRAGMA user_version", [], |row| row.get(0))?;
|
||||
if version < 2 {
|
||||
add_filter_expression(connection)?;
|
||||
}
|
||||
connection.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS view_columns (
|
||||
id INTEGER PRIMARY KEY, view_id INTEGER NOT NULL REFERENCES views(id) ON DELETE CASCADE,
|
||||
field TEXT NOT NULL, heading TEXT NOT NULL, width INTEGER NOT NULL DEFAULT 20,
|
||||
aggregate TEXT NOT NULL DEFAULT 'none', sort_order INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS view_sections (
|
||||
id INTEGER PRIMARY KEY, view_id INTEGER NOT NULL REFERENCES views(id) ON DELETE CASCADE,
|
||||
heading TEXT NOT NULL, filter_expr TEXT NOT NULL DEFAULT '', collapsed INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS category_rules (
|
||||
id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
|
||||
condition_expr TEXT NOT NULL, action_kind TEXT NOT NULL, action_value TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 0);",
|
||||
)?;
|
||||
if version < 2 {
|
||||
connection.pragma_update(None, "user_version", 2)?;
|
||||
}
|
||||
if version < 3 {
|
||||
add_recurrence(connection)?;
|
||||
connection.pragma_update(None, "user_version", 3)?;
|
||||
}
|
||||
connection.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS macros (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
source TEXT NOT NULL,
|
||||
key_binding TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS macro_variables (
|
||||
name TEXT PRIMARY KEY COLLATE NOCASE,
|
||||
value TEXT NOT NULL
|
||||
);",
|
||||
)?;
|
||||
if version < 4 {
|
||||
connection.pragma_update(None, "user_version", 4)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_filter_expression(connection: &Connection) -> Result<()> {
|
||||
if !column_exists(connection, "views", "filter_expr")? {
|
||||
connection
|
||||
.execute_batch("ALTER TABLE views ADD COLUMN filter_expr TEXT NOT NULL DEFAULT '';")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_recurrence(connection: &Connection) -> Result<()> {
|
||||
if !column_exists(connection, "items", "recurrence")? {
|
||||
connection
|
||||
.execute_batch("ALTER TABLE items ADD COLUMN recurrence TEXT NOT NULL DEFAULT '';")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn column_exists(connection: &Connection, table: &str, column: &str) -> Result<bool> {
|
||||
let mut statement = connection.prepare(&format!("PRAGMA table_info({table})"))?;
|
||||
let names = statement
|
||||
.query_map([], |row| row.get::<_, String>(1))?
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok(names.iter().any(|name| name == column))
|
||||
}
|
||||
Reference in New Issue
Block a user