Skip to content

Commit ad72e96

Browse files
committed
feat(store): add identity_facts table and CRUD methods
1 parent 1b595f4 commit ad72e96

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

src/store.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ pub struct PlacementCorrection {
7979
pub corrected_at: String,
8080
}
8181

82+
/// A fact about the user's identity, inferred or stated (v1.6).
83+
#[derive(Debug, Clone, serde::Serialize)]
84+
pub struct IdentityFact {
85+
pub id: i64,
86+
pub tier: i64,
87+
pub key: String,
88+
pub value: String,
89+
pub source: Option<String>,
90+
pub updated_at: String,
91+
}
92+
8293
/// Summary statistics for the store.
8394
#[derive(Debug)]
8495
pub struct StoreStats {
@@ -357,6 +368,19 @@ impl Store {
357368
CREATE INDEX IF NOT EXISTS idx_migration_id ON migration_log(migration_id);",
358369
)?;
359370

371+
// Identity facts table (v1.6)
372+
self.conn.execute_batch(
373+
"CREATE TABLE IF NOT EXISTS identity_facts (
374+
id INTEGER PRIMARY KEY,
375+
tier INTEGER NOT NULL,
376+
key TEXT NOT NULL,
377+
value TEXT NOT NULL,
378+
source TEXT,
379+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
380+
UNIQUE(tier, key, value)
381+
);",
382+
)?;
383+
360384
Ok(())
361385
}
362386

@@ -1614,6 +1638,50 @@ impl Store {
16141638
Ok(())
16151639
}
16161640

1641+
// ── Identity Facts ───────────────────────────────────────────
1642+
1643+
pub fn upsert_identity_fact(
1644+
&self,
1645+
tier: i64,
1646+
key: &str,
1647+
value: &str,
1648+
source: Option<&str>,
1649+
) -> Result<()> {
1650+
self.conn.execute(
1651+
"INSERT INTO identity_facts (tier, key, value, source, updated_at)
1652+
VALUES (?1, ?2, ?3, ?4, datetime('now'))
1653+
ON CONFLICT(tier, key, value) DO UPDATE SET
1654+
source = excluded.source,
1655+
updated_at = datetime('now')",
1656+
rusqlite::params![tier, key, value, source],
1657+
)?;
1658+
Ok(())
1659+
}
1660+
1661+
pub fn get_identity_facts(&self, tier: i64) -> Result<Vec<IdentityFact>> {
1662+
let mut stmt = self.conn.prepare(
1663+
"SELECT id, tier, key, value, source, updated_at
1664+
FROM identity_facts WHERE tier = ?1 ORDER BY key, value",
1665+
)?;
1666+
let rows = stmt.query_map(rusqlite::params![tier], |row| {
1667+
Ok(IdentityFact {
1668+
id: row.get(0)?,
1669+
tier: row.get(1)?,
1670+
key: row.get(2)?,
1671+
value: row.get(3)?,
1672+
source: row.get(4)?,
1673+
updated_at: row.get(5)?,
1674+
})
1675+
})?;
1676+
Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
1677+
}
1678+
1679+
pub fn clear_identity_facts(&self, tier: i64) -> Result<()> {
1680+
self.conn
1681+
.execute("DELETE FROM identity_facts WHERE tier = ?1", rusqlite::params![tier])?;
1682+
Ok(())
1683+
}
1684+
16171685
// ── Helpers ─────────────────────────────────────────────────
16181686

16191687
pub fn next_vector_id(&self) -> Result<u64> {
@@ -3525,4 +3593,41 @@ mod tests {
35253593
assert!(record.is_some());
35263594
assert_eq!(record.unwrap().content_hash, "hash1");
35273595
}
3596+
3597+
#[test]
3598+
fn test_insert_and_get_identity_facts() {
3599+
let store = Store::open_memory().unwrap();
3600+
store.upsert_identity_fact(0, "name", "Test User", None).unwrap();
3601+
store.upsert_identity_fact(1, "active_project", "Project A", Some("01-Projects/a.md")).unwrap();
3602+
store.upsert_identity_fact(1, "active_project", "Project B", Some("01-Projects/b.md")).unwrap();
3603+
3604+
let l0 = store.get_identity_facts(0).unwrap();
3605+
assert_eq!(l0.len(), 1);
3606+
assert_eq!(l0[0].key, "name");
3607+
assert_eq!(l0[0].value, "Test User");
3608+
3609+
let l1 = store.get_identity_facts(1).unwrap();
3610+
assert_eq!(l1.len(), 2);
3611+
}
3612+
3613+
#[test]
3614+
fn test_upsert_identity_fact_replaces() {
3615+
let store = Store::open_memory().unwrap();
3616+
store.upsert_identity_fact(0, "name", "Old Name", None).unwrap();
3617+
store.upsert_identity_fact(0, "name", "New Name", None).unwrap();
3618+
3619+
let facts = store.get_identity_facts(0).unwrap();
3620+
assert_eq!(facts.len(), 2); // Different values = different rows
3621+
}
3622+
3623+
#[test]
3624+
fn test_clear_identity_facts_by_tier() {
3625+
let store = Store::open_memory().unwrap();
3626+
store.upsert_identity_fact(0, "name", "User", None).unwrap();
3627+
store.upsert_identity_fact(1, "active_project", "P1", None).unwrap();
3628+
store.clear_identity_facts(1).unwrap();
3629+
3630+
assert_eq!(store.get_identity_facts(0).unwrap().len(), 1);
3631+
assert_eq!(store.get_identity_facts(1).unwrap().len(), 0);
3632+
}
35283633
}

0 commit comments

Comments
 (0)