complete category-scoped values, rules, and report parity
- Add category-bound date and numeric values - Complete rule execution and conflict handling - Expand Markdown and HTML report output
This commit is contained in:
168
src/db/schema.rs
168
src/db/schema.rs
@@ -98,6 +98,114 @@ pub(super) fn migrate(connection: &Connection) -> Result<()> {
|
||||
if version < 4 {
|
||||
connection.pragma_update(None, "user_version", 4)?;
|
||||
}
|
||||
if version < 5 {
|
||||
add_lotus_values_and_rules(connection)?;
|
||||
connection.pragma_update(None, "user_version", 5)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_lotus_values_and_rules(connection: &Connection) -> Result<()> {
|
||||
connection.execute_batch(
|
||||
"BEGIN;
|
||||
ALTER TABLE item_categories RENAME TO old_item_categories;
|
||||
CREATE TABLE 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 CHECK(assignment IN ('explicit','automatic','conditional','excluded')),
|
||||
PRIMARY KEY(item_id, category_id, assignment)
|
||||
);
|
||||
INSERT INTO item_categories(item_id,category_id,assignment)
|
||||
SELECT item_id,category_id,assignment FROM old_item_categories;
|
||||
DROP TABLE old_item_categories;
|
||||
CREATE INDEX idx_item_categories_category ON item_categories(category_id);
|
||||
CREATE TABLE item_values (
|
||||
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
||||
category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
|
||||
numeric_value REAL,
|
||||
date_value TEXT,
|
||||
assignment TEXT NOT NULL DEFAULT 'explicit'
|
||||
CHECK(assignment IN ('explicit','automatic','conditional')),
|
||||
PRIMARY KEY(item_id, category_id),
|
||||
CHECK((numeric_value IS NULL) OR (date_value IS NULL))
|
||||
);
|
||||
CREATE TABLE rule_action_runs (
|
||||
rule_id INTEGER NOT NULL REFERENCES category_rules(id) ON DELETE CASCADE,
|
||||
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY(rule_id,item_id)
|
||||
);
|
||||
CREATE TABLE rule_conflicts (
|
||||
item_id INTEGER PRIMARY KEY REFERENCES items(id) ON DELETE CASCADE,
|
||||
message TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
COMMIT;",
|
||||
)?;
|
||||
for (column, definition) in [
|
||||
(
|
||||
"category_id",
|
||||
"INTEGER REFERENCES categories(id) ON DELETE SET NULL",
|
||||
),
|
||||
("number_label", "TEXT NOT NULL DEFAULT ''"),
|
||||
("decimals", "INTEGER NOT NULL DEFAULT 2"),
|
||||
("decimal_separator", "TEXT NOT NULL DEFAULT '.'"),
|
||||
("thousands_separator", "TEXT NOT NULL DEFAULT 'comma'"),
|
||||
("negative_style", "TEXT NOT NULL DEFAULT 'minus'"),
|
||||
("percent_total", "INTEGER NOT NULL DEFAULT 0"),
|
||||
("alignment", "TEXT NOT NULL DEFAULT 'left'"),
|
||||
] {
|
||||
if !column_exists(connection, "view_columns", column)? {
|
||||
connection.execute_batch(&format!(
|
||||
"ALTER TABLE view_columns ADD COLUMN {column} {definition};"
|
||||
))?;
|
||||
}
|
||||
}
|
||||
connection.execute_batch(
|
||||
"UPDATE view_columns
|
||||
SET category_id=(SELECT id FROM categories WHERE kind='numeric' ORDER BY id LIMIT 1),
|
||||
field='numeric'
|
||||
WHERE field='value';
|
||||
INSERT INTO categories(name,kind,sort_order)
|
||||
SELECT 'Value','numeric',(SELECT COALESCE(MAX(sort_order),0)+1 FROM categories)
|
||||
WHERE EXISTS(SELECT 1 FROM items WHERE numeric_value IS NOT NULL)
|
||||
AND NOT EXISTS(SELECT 1 FROM categories WHERE kind='numeric')
|
||||
AND NOT EXISTS(SELECT 1 FROM categories WHERE name='Value' COLLATE NOCASE);
|
||||
INSERT INTO categories(name,kind,sort_order)
|
||||
SELECT 'Numeric Value','numeric',(SELECT COALESCE(MAX(sort_order),0)+1 FROM categories)
|
||||
WHERE EXISTS(SELECT 1 FROM items WHERE numeric_value IS NOT NULL)
|
||||
AND NOT EXISTS(SELECT 1 FROM categories WHERE kind='numeric');
|
||||
UPDATE view_columns
|
||||
SET category_id=(SELECT id FROM categories WHERE kind='numeric' ORDER BY id LIMIT 1)
|
||||
WHERE field='numeric' AND category_id IS NULL;
|
||||
INSERT OR IGNORE INTO item_values(item_id,category_id,numeric_value,assignment)
|
||||
SELECT i.id,
|
||||
COALESCE(
|
||||
(SELECT ic.category_id FROM item_categories ic JOIN categories c ON c.id=ic.category_id
|
||||
WHERE ic.item_id=i.id AND ic.assignment<>'excluded' AND c.kind='numeric' LIMIT 1),
|
||||
(SELECT id FROM categories WHERE kind='numeric' ORDER BY id LIMIT 1)
|
||||
),
|
||||
i.numeric_value,'explicit'
|
||||
FROM items i WHERE i.numeric_value IS NOT NULL
|
||||
AND EXISTS(SELECT 1 FROM categories WHERE kind='numeric');
|
||||
INSERT OR IGNORE INTO item_categories(item_id,category_id,assignment)
|
||||
SELECT item_id,category_id,'explicit' FROM item_values;
|
||||
UPDATE items SET numeric_value=NULL
|
||||
WHERE numeric_value IS NOT NULL AND EXISTS(
|
||||
SELECT 1 FROM item_values WHERE item_values.item_id=items.id
|
||||
);
|
||||
UPDATE category_rules SET action_kind='none'
|
||||
WHERE action_kind='assign' AND TRIM(action_value)='';
|
||||
INSERT OR IGNORE INTO rule_action_runs(rule_id,item_id)
|
||||
SELECT r.id,positive.item_id FROM category_rules r
|
||||
JOIN item_categories positive ON positive.category_id=r.category_id
|
||||
AND positive.assignment<>'excluded'
|
||||
WHERE r.action_kind<>'none' AND NOT EXISTS(
|
||||
SELECT 1 FROM item_categories excluded
|
||||
WHERE excluded.item_id=positive.item_id
|
||||
AND excluded.category_id=positive.category_id
|
||||
AND excluded.assignment='excluded'
|
||||
);",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -124,3 +232,63 @@ fn column_exists(connection: &Connection, table: &str, column: &str) -> Result<b
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok(names.iter().any(|name| name == column))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn version_four_numbers_migrate_without_losing_values_or_provenance() {
|
||||
let connection = Connection::open_in_memory().unwrap();
|
||||
connection
|
||||
.execute_batch(
|
||||
"PRAGMA foreign_keys=ON;
|
||||
CREATE TABLE items(id INTEGER PRIMARY KEY,text TEXT NOT NULL,note TEXT NOT NULL DEFAULT '',priority INTEGER NOT NULL DEFAULT 3,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,recurrence TEXT NOT NULL DEFAULT '');
|
||||
CREATE TABLE categories(id INTEGER PRIMARY KEY,name TEXT NOT NULL UNIQUE,parent_id INTEGER,kind TEXT NOT NULL DEFAULT 'standard',match_text TEXT NOT NULL DEFAULT '',exclusive INTEGER NOT NULL DEFAULT 0,sort_order INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE item_categories(item_id INTEGER NOT NULL REFERENCES items(id),category_id INTEGER NOT NULL REFERENCES categories(id),assignment TEXT NOT NULL DEFAULT 'explicit',PRIMARY KEY(item_id,category_id));
|
||||
CREATE TABLE views(id INTEGER PRIMARY KEY,name TEXT NOT NULL UNIQUE,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,filter_expr TEXT NOT NULL DEFAULT '');
|
||||
CREATE TABLE view_columns(id INTEGER PRIMARY KEY,view_id INTEGER NOT NULL REFERENCES views(id),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);
|
||||
INSERT INTO categories(id,name,kind) VALUES(1,'Hours','numeric');
|
||||
INSERT INTO items(id,text,numeric_value,created_at,updated_at) VALUES(1,'Legacy',42.25,'now','now');
|
||||
INSERT INTO item_categories VALUES(1,1,'explicit');
|
||||
INSERT INTO views(id,name) VALUES(1,'Legacy');
|
||||
INSERT INTO view_columns(view_id,field,heading) VALUES(1,'value','Hours');
|
||||
PRAGMA user_version=4;",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
migrate(&connection).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
connection
|
||||
.query_row(
|
||||
"SELECT numeric_value FROM item_values WHERE item_id=1 AND category_id=1",
|
||||
[],
|
||||
|row| row.get::<_, f64>(0),
|
||||
)
|
||||
.unwrap(),
|
||||
42.25
|
||||
);
|
||||
assert!(
|
||||
connection
|
||||
.query_row("SELECT numeric_value FROM items WHERE id=1", [], |row| {
|
||||
row.get::<_, Option<f64>>(0)
|
||||
})
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
connection
|
||||
.execute("INSERT INTO item_categories VALUES(1,1,'conditional')", [])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
connection
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM item_categories WHERE item_id=1 AND category_id=1",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user