use grounddb::Store;
use std::collections::HashMap;
use tempfile::TempDir;
const SCHEMA: &str = r#"collections:
messages:
path: "{recipient}/{id}.md"
fields:
recipient: { type: string, required: true }
status: { type: string, default: unread }
content: true
views:
inbox_by_recipient:
query: |
SELECT id, recipient, status, content
FROM messages
WHERE recipient = :recipient
ORDER BY id
all_messages:
query: |
SELECT id, recipient, status, content
FROM messages
ORDER BY id
"#;
fn store() -> (TempDir, Store) {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("schema.yaml"), SCHEMA).unwrap();
let store = Store::open(&dir.path().to_string_lossy()).unwrap();
(dir, store)
}
fn insert(store: &Store, id: &str, recipient: &str, body: &str) {
let data = serde_json::json!({
"id": id,
"recipient": recipient,
"status": "unread",
});
store.insert_dynamic("messages", data, Some(body)).unwrap();
}
fn params(recipient: &str) -> HashMap<String, String> {
let mut p = HashMap::new();
p.insert("recipient".into(), recipient.to_string());
p
}
#[test]
fn a_message_is_visible_to_the_view_immediately_after_insert() {
let (_dir, store) = store();
insert(&store, "m1", "justin", "first");
let rows = store.query_dynamic("inbox_by_recipient", ¶ms("justin")).unwrap();
assert_eq!(rows.as_array().unwrap().len(), 1, "insert must be visible at once");
insert(&store, "m2", "justin", "second");
let rows = store.query_dynamic("inbox_by_recipient", ¶ms("justin")).unwrap();
assert_eq!(
rows.as_array().unwrap().len(),
2,
"a second insert must also be visible with no rebuild in between"
);
}
#[test]
fn the_parameter_filters_rather_than_matching_nothing() {
let (_dir, store) = store();
insert(&store, "m1", "justin", "for justin");
insert(&store, "m2", "tom", "for tom");
let justin = store.query_dynamic("inbox_by_recipient", ¶ms("justin")).unwrap();
let tom = store.query_dynamic("inbox_by_recipient", ¶ms("tom")).unwrap();
assert_eq!(justin.as_array().unwrap().len(), 1);
assert_eq!(tom.as_array().unwrap().len(), 1);
assert_eq!(
justin.as_array().unwrap()[0]["recipient"], "justin",
"the bound parameter must select the row, not NULL-match everything"
);
}
#[test]
fn a_parameterised_view_is_not_materialised() {
let (dir, store) = store();
insert(&store, "m1", "justin", "body");
let db = rusqlite::Connection::open(dir.path().join("_system.db")).unwrap();
let stored: Option<String> = db
.query_row(
"SELECT data_json FROM view_data WHERE view_name = 'inbox_by_recipient'",
[],
|r| r.get(0),
)
.ok();
assert!(
stored.is_none(),
"a parameterised view should not be materialised; found {stored:?}"
);
}
#[test]
fn a_view_without_placeholders_is_still_materialised() {
let (dir, store) = store();
insert(&store, "m1", "justin", "body");
let db = rusqlite::Connection::open(dir.path().join("_system.db")).unwrap();
let stored: String = db
.query_row(
"SELECT data_json FROM view_data WHERE view_name = 'all_messages'",
[],
|r| r.get(0),
)
.expect("a placeholder-free view must still be materialised");
let rows: serde_json::Value = serde_json::from_str(&stored).unwrap();
assert_eq!(
rows.as_array().unwrap().len(),
1,
"and it must hold real rows, not an empty array"
);
}