modularize app and database internals

- Extract editor, form, export, and schema modules
- Add database workflow integration tests
This commit is contained in:
Chili Palmer
2026-08-19 22:20:15 +02:00
parent c8ad487ca1
commit b5b8ae0d55
11 changed files with 778 additions and 659 deletions

126
src/db/schema.rs Normal file
View 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))
}