Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/uteke-core/src/edges.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1276,7 +1276,7 @@ mod tests {
fn migration_dispatcher_reaches_v8() {
let store = Store::open(":memory:").unwrap();
let v = store.schema_version().unwrap();
assert_eq!(v, 13, "fresh store must reach CURRENT_SCHEMA_VERSION=13");
assert_eq!(v, 14, "fresh store must reach CURRENT_SCHEMA_VERSION=14");

// memory_edges table must exist and be queryable after migration.
let n = store.count_memory_edges().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/uteke-core/src/memory/crud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,6 @@ mod content_type_tests {
let store = super::super::store::Store::open(":memory:").unwrap();
assert!(store.column_exists("content_type"));
let version = store.schema_version().unwrap();
assert_eq!(version, 13); // v13 = global docs no namespace (#614); v12 = hierarchical docs (#438); v11 = document engine (#406); v10 = source columns (#348); v9 = timeline (#347); v8 = edges + slug; v7 = graph
assert_eq!(version, 14); // v14 = FTS5 memory_type column (#662); v13 = global docs no namespace (#614); v12 = hierarchical docs (#438); v11 = document engine (#406); v10 = source columns (#348); v9 = timeline (#347); v8 = edges + slug; v7 = graph
}
}
69 changes: 61 additions & 8 deletions crates/uteke-core/src/memory/fts5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl super::Store {
content,
tags,
namespace,
memory_type,
content='memories',
content_rowid='rowid'
);
Expand All @@ -45,20 +46,20 @@ impl super::Store {
.execute_batch(
r#"
CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
INSERT INTO memories_fts(rowid, content, tags, namespace)
VALUES (new.rowid, new.content, new.tags, new.namespace);
INSERT INTO memories_fts(rowid, content, tags, namespace, memory_type)
VALUES (new.rowid, new.content, new.tags, new.namespace, new.memory_type);
END;

CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, content, tags, namespace)
VALUES ('delete', old.rowid, old.content, old.tags, old.namespace);
INSERT INTO memories_fts(memories_fts, rowid, content, tags, namespace, memory_type)
VALUES ('delete', old.rowid, old.content, old.tags, old.namespace, old.memory_type);
END;

CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, content, tags, namespace)
VALUES ('delete', old.rowid, old.content, old.tags, old.namespace);
INSERT INTO memories_fts(rowid, content, tags, namespace)
VALUES (new.rowid, new.content, new.tags, new.namespace);
INSERT INTO memories_fts(memories_fts, rowid, content, tags, namespace, memory_type)
VALUES ('delete', old.rowid, old.content, old.tags, old.namespace, old.memory_type);
INSERT INTO memories_fts(rowid, content, tags, namespace, memory_type)
VALUES (new.rowid, new.content, new.tags, new.namespace, new.memory_type);
END;
"#,
)
Expand Down Expand Up @@ -382,4 +383,56 @@ mod tests {
// FTS5 is now auto-initialized on open (#544).
assert!(store.fts5_exists().unwrap());
}

#[test]
fn test_fts5_memory_type_column_search() {
let store = Store::open(":memory:").unwrap();
store.init_fts5().unwrap();

let mut decision = make_test_memory("1", "chose Rust for performance", &[]);
decision.memory_type = "decision".to_string();
store.insert(&decision).unwrap();

let mut fact = make_test_memory("2", "Rust has zero-cost abstractions", &[]);
fact.memory_type = "fact".to_string();
store.insert(&fact).unwrap();

let mut procedure = make_test_memory("3", "compile with cargo build", &[]);
procedure.memory_type = "procedure".to_string();
store.insert(&procedure).unwrap();

// Search by memory_type:decision
let results = store.search_fts5("decision", None, 10).unwrap();
assert!(
!results.is_empty(),
"Should find at least 1 result for 'decision'"
);
// memory_type "decision" should match
assert!(results.iter().any(|(m, _)| m.memory_type == "decision"));

// Search by memory_type:fact
let results = store.search_fts5("fact", None, 10).unwrap();
assert!(
!results.is_empty(),
"Should find at least 1 result for 'fact'"
);
assert!(results.iter().any(|(m, _)| m.memory_type == "fact"));
}

#[test]
fn test_fts5_memory_type_rebuild() {
let store = Store::open(":memory:").unwrap();
store.init_fts5().unwrap();

let mut decision = make_test_memory("1", "deploy strategy", &[]);
decision.memory_type = "decision".to_string();
store.insert(&decision).unwrap();

// Rebuild FTS5 index — memory_type should still be searchable
store.rebuild_fts5().unwrap();

let results = store.search_fts5("decision", None, 10).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].0.memory_type, "decision");
}
}
32 changes: 32 additions & 0 deletions crates/uteke-core/src/memory/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ impl super::Store {
12 => self.migrate_v11_to_v12()?,
// v13: Global documents — remove namespace isolation (#614)
13 => self.migrate_v12_to_v13()?,
// v14: Add memory_type to FTS5 index (#662)
14 => self.migrate_v13_to_v14()?,
_ => {
// No-op for future versions.
}
Expand Down Expand Up @@ -941,4 +943,34 @@ impl super::Store {
tracing::info!("Migration v12 to v13 complete: documents are now global");
Ok(())
}

/// v14: Add memory_type column to FTS5 index (#662).
///
/// FTS5 virtual tables cannot be ALTERed, so we must:
/// 1. Drop existing triggers (they reference old column set)
/// 2. Drop existing FTS5 table
/// 3. Recreate with memory_type column
/// 4. Rebuild index from existing memories
fn migrate_v13_to_v14(&self) -> Result<(), Error> {
tracing::info!("Applying schema migration v13 to v14: add memory_type to FTS5 index");

// Drop old triggers first (they reference the old column set).
for trigger_name in &["memories_fts_ai", "memories_fts_ad", "memories_fts_au"] {
let _ = self
.conn
.execute(&format!("DROP TRIGGER IF EXISTS {}", trigger_name), []);
}

// Drop old FTS5 table.
let _ = self.conn.execute("DROP TABLE IF EXISTS memories_fts", []);

// Recreate FTS5 with memory_type column.
self.init_fts5()?;

// Rebuild FTS5 index from existing memories.
self.rebuild_fts5()?;

tracing::info!("Migration v13 to v14 complete: memory_type now in FTS5 index");
Ok(())
}
}
4 changes: 2 additions & 2 deletions crates/uteke-core/src/memory/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ pub(super) const SCHEMA_INDEXES: &[&str] = &[
];

/// Current schema version. Increment when adding migrations.
pub(super) const CURRENT_SCHEMA_VERSION: i32 = 13;
pub(super) const CURRENT_SCHEMA_VERSION: i32 = 14;

/// Persistent SQLite store for memories.
pub struct Store {
Expand Down Expand Up @@ -1778,7 +1778,7 @@ mod tests {
|r| r.get(0),
)
.unwrap();
assert_eq!(version, 13, "schema_version should be 13 after migration");
assert_eq!(version, 14, "schema_version should be 14 after migration");

// 7. Verify hierarchy columns now exist (in documents table).
let cols = ["parent_id", "path", "depth", "sort_order", "has_children"];
Expand Down
2 changes: 1 addition & 1 deletion crates/uteke-core/src/timeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ mod tests {
fn migration_dispatcher_reaches_v9() {
let store = Store::open(":memory:").unwrap();
let v = store.schema_version().unwrap();
assert_eq!(v, 13, "fresh store must reach CURRENT_SCHEMA_VERSION=13");
assert_eq!(v, 14, "fresh store must reach CURRENT_SCHEMA_VERSION=14");
// timeline_events table must exist.
let m = mem();
store.insert(&m).unwrap();
Expand Down