1use rusqlite::{params, Connection, OptionalExtension};
15use serde::{Deserialize, Serialize};
16
17use crate::error::{EngramError, Result};
18
19pub const CREATE_MEMORY_BLOCKS_TABLE: &str = r#"
25CREATE TABLE IF NOT EXISTS memory_blocks (
26 name TEXT PRIMARY KEY,
27 content TEXT NOT NULL DEFAULT '',
28 version INTEGER NOT NULL DEFAULT 1,
29 max_tokens INTEGER NOT NULL DEFAULT 4096,
30 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
31 updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
32);
33"#;
34
35pub const CREATE_BLOCK_EDIT_LOG_TABLE: &str = r#"
37CREATE TABLE IF NOT EXISTS block_edit_log (
38 id INTEGER PRIMARY KEY AUTOINCREMENT,
39 block_name TEXT NOT NULL,
40 old_content TEXT NOT NULL,
41 new_content TEXT NOT NULL,
42 edit_reason TEXT NOT NULL DEFAULT '',
43 timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
44 FOREIGN KEY (block_name) REFERENCES memory_blocks(name) ON DELETE CASCADE
45);
46"#;
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct MemoryBlock {
53 pub name: String,
54 pub content: String,
55 pub version: i64,
56 pub max_tokens: usize,
57 pub created_at: String,
58 pub updated_at: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct BlockEditLog {
64 pub id: i64,
65 pub block_name: String,
66 pub old_content: String,
67 pub new_content: String,
68 pub edit_reason: String,
69 pub timestamp: String,
70}
71
72fn row_to_block(row: &rusqlite::Row<'_>) -> rusqlite::Result<MemoryBlock> {
75 Ok(MemoryBlock {
76 name: row.get(0)?,
77 content: row.get(1)?,
78 version: row.get(2)?,
79 max_tokens: row.get::<_, i64>(3)? as usize,
80 created_at: row.get(4)?,
81 updated_at: row.get(5)?,
82 })
83}
84
85fn row_to_log(row: &rusqlite::Row<'_>) -> rusqlite::Result<BlockEditLog> {
86 Ok(BlockEditLog {
87 id: row.get(0)?,
88 block_name: row.get(1)?,
89 old_content: row.get(2)?,
90 new_content: row.get(3)?,
91 edit_reason: row.get(4)?,
92 timestamp: row.get(5)?,
93 })
94}
95
96pub fn create_block(
102 conn: &Connection,
103 name: &str,
104 content: &str,
105 max_tokens: usize,
106) -> Result<MemoryBlock> {
107 if name.is_empty() {
108 return Err(EngramError::InvalidInput(
109 "block name must not be empty".into(),
110 ));
111 }
112
113 conn.execute(
114 r#"
115 INSERT INTO memory_blocks (name, content, version, max_tokens, created_at, updated_at)
116 VALUES (?, ?, 1, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
117 "#,
118 params![name, content, max_tokens as i64],
119 )?;
120
121 get_block_required(conn, name)
122}
123
124pub fn get_block(conn: &Connection, name: &str) -> Result<Option<MemoryBlock>> {
126 let result = conn
127 .query_row(
128 r#"
129 SELECT name, content, version, max_tokens, created_at, updated_at
130 FROM memory_blocks WHERE name = ?
131 "#,
132 params![name],
133 row_to_block,
134 )
135 .optional()?;
136 Ok(result)
137}
138
139pub fn update_block(
143 conn: &Connection,
144 name: &str,
145 new_content: &str,
146 reason: &str,
147) -> Result<MemoryBlock> {
148 let old = get_block(conn, name)?
149 .ok_or_else(|| EngramError::Storage(format!("memory block '{}' not found", name)))?;
150
151 conn.execute(
152 r#"
153 UPDATE memory_blocks
154 SET content = ?,
155 version = version + 1,
156 updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
157 WHERE name = ?
158 "#,
159 params![new_content, name],
160 )?;
161
162 conn.execute(
163 r#"
164 INSERT INTO block_edit_log (block_name, old_content, new_content, edit_reason, timestamp)
165 VALUES (?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
166 "#,
167 params![name, old.content, new_content, reason],
168 )?;
169
170 get_block_required(conn, name)
171}
172
173pub fn list_blocks(conn: &Connection) -> Result<Vec<MemoryBlock>> {
175 let mut stmt = conn.prepare(
176 r#"
177 SELECT name, content, version, max_tokens, created_at, updated_at
178 FROM memory_blocks ORDER BY name
179 "#,
180 )?;
181 let blocks: rusqlite::Result<Vec<MemoryBlock>> = stmt.query_map([], row_to_block)?.collect();
182 Ok(blocks?)
183}
184
185pub fn delete_block(conn: &Connection, name: &str) -> Result<()> {
187 let rows = conn.execute("DELETE FROM memory_blocks WHERE name = ?", params![name])?;
188 if rows == 0 {
189 return Err(EngramError::Storage(format!(
190 "memory block '{}' not found",
191 name
192 )));
193 }
194 Ok(())
195}
196
197pub fn get_block_history(conn: &Connection, name: &str, limit: usize) -> Result<Vec<BlockEditLog>> {
201 let sql = if limit > 0 {
202 format!(
203 r#"
204 SELECT id, block_name, old_content, new_content, edit_reason, timestamp
205 FROM block_edit_log WHERE block_name = ?
206 ORDER BY id DESC LIMIT {}
207 "#,
208 limit
209 )
210 } else {
211 r#"
212 SELECT id, block_name, old_content, new_content, edit_reason, timestamp
213 FROM block_edit_log WHERE block_name = ?
214 ORDER BY id DESC
215 "#
216 .to_string()
217 };
218
219 let mut stmt = conn.prepare(&sql)?;
220 let entries: rusqlite::Result<Vec<BlockEditLog>> =
221 stmt.query_map(params![name], row_to_log)?.collect();
222 Ok(entries?)
223}
224
225pub fn archive_overflow(conn: &Connection, name: &str) -> Result<Option<String>> {
231 let block = get_block(conn, name)?
232 .ok_or_else(|| EngramError::Storage(format!("memory block '{}' not found", name)))?;
233
234 let max_chars = block.max_tokens * 4;
235 if block.content.len() <= max_chars {
236 return Ok(None);
237 }
238
239 let keep = &block.content[..max_chars];
241 let overflow = block.content[max_chars..].to_string();
242
243 update_block(conn, name, keep, "overflow archived")?;
244
245 Ok(Some(overflow))
246}
247
248fn get_block_required(conn: &Connection, name: &str) -> Result<MemoryBlock> {
251 get_block(conn, name)?.ok_or_else(|| {
252 EngramError::Storage(format!("memory block '{}' unexpectedly missing", name))
253 })
254}
255
256#[cfg(test)]
259mod tests {
260 use super::*;
261 use rusqlite::Connection;
262
263 fn setup() -> Connection {
264 let conn = Connection::open_in_memory().unwrap();
265 conn.execute_batch(CREATE_MEMORY_BLOCKS_TABLE).unwrap();
266 conn.execute_batch(CREATE_BLOCK_EDIT_LOG_TABLE).unwrap();
267 conn
268 }
269
270 #[test]
272 fn test_create_and_get_block() {
273 let conn = setup();
274 let block = create_block(&conn, "persona", "I am a helpful assistant.", 512).unwrap();
275
276 assert_eq!(block.name, "persona");
277 assert_eq!(block.content, "I am a helpful assistant.");
278 assert_eq!(block.version, 1);
279 assert_eq!(block.max_tokens, 512);
280
281 let fetched = get_block(&conn, "persona").unwrap().unwrap();
282 assert_eq!(fetched.name, block.name);
283 assert_eq!(fetched.content, block.content);
284 }
285
286 #[test]
288 fn test_update_increments_version() {
289 let conn = setup();
290 create_block(&conn, "notes", "initial", 256).unwrap();
291
292 let v2 = update_block(&conn, "notes", "updated once", "first edit").unwrap();
293 assert_eq!(v2.version, 2);
294
295 let v3 = update_block(&conn, "notes", "updated twice", "second edit").unwrap();
296 assert_eq!(v3.version, 3);
297 }
298
299 #[test]
301 fn test_update_logs_edit_history() {
302 let conn = setup();
303 create_block(&conn, "context", "old text", 256).unwrap();
304 update_block(&conn, "context", "new text", "test reason").unwrap();
305
306 let history = get_block_history(&conn, "context", 0).unwrap();
307 assert_eq!(history.len(), 1);
308 assert_eq!(history[0].old_content, "old text");
309 assert_eq!(history[0].new_content, "new text");
310 assert_eq!(history[0].edit_reason, "test reason");
311 }
312
313 #[test]
315 fn test_list_blocks_returns_all() {
316 let conn = setup();
317 create_block(&conn, "alpha", "a", 128).unwrap();
318 create_block(&conn, "beta", "b", 128).unwrap();
319 create_block(&conn, "gamma", "c", 128).unwrap();
320
321 let blocks = list_blocks(&conn).unwrap();
322 assert_eq!(blocks.len(), 3);
323 assert_eq!(blocks[0].name, "alpha");
325 assert_eq!(blocks[1].name, "beta");
326 assert_eq!(blocks[2].name, "gamma");
327 }
328
329 #[test]
331 fn test_delete_block() {
332 let conn = setup();
333 create_block(&conn, "temp", "to be deleted", 64).unwrap();
334 delete_block(&conn, "temp").unwrap();
335
336 let result = get_block(&conn, "temp").unwrap();
337 assert!(result.is_none());
338 }
339
340 #[test]
342 fn test_get_history_with_limit() {
343 let conn = setup();
344 create_block(&conn, "log", "v1", 256).unwrap();
345 update_block(&conn, "log", "v2", "edit 1").unwrap();
346 update_block(&conn, "log", "v3", "edit 2").unwrap();
347 update_block(&conn, "log", "v4", "edit 3").unwrap();
348
349 let limited = get_block_history(&conn, "log", 2).unwrap();
350 assert_eq!(limited.len(), 2);
351 assert_eq!(limited[0].new_content, "v4");
353 assert_eq!(limited[1].new_content, "v3");
354 }
355
356 #[test]
358 fn test_archive_overflow_truncates_and_returns_excess() {
359 let conn = setup();
360 let long_content = "12345678overflow_part";
362 create_block(&conn, "small", long_content, 2).unwrap();
363
364 let overflow = archive_overflow(&conn, "small").unwrap();
365 assert!(overflow.is_some());
366 assert_eq!(overflow.unwrap(), "overflow_part");
367
368 let block = get_block(&conn, "small").unwrap().unwrap();
369 assert_eq!(block.content, "12345678");
370 }
371
372 #[test]
374 fn test_archive_non_overflowing_returns_none() {
375 let conn = setup();
376 create_block(&conn, "roomy", "short", 1024).unwrap();
377
378 let result = archive_overflow(&conn, "roomy").unwrap();
379 assert!(result.is_none());
380
381 let block = get_block(&conn, "roomy").unwrap().unwrap();
383 assert_eq!(block.content, "short");
384 assert_eq!(block.version, 1);
385 }
386
387 #[test]
389 fn test_get_nonexistent_returns_none() {
390 let conn = setup();
391 let result = get_block(&conn, "does_not_exist").unwrap();
392 assert!(result.is_none());
393 }
394
395 #[test]
397 fn test_delete_nonexistent_returns_error() {
398 let conn = setup();
399 let result = delete_block(&conn, "ghost");
400 assert!(result.is_err());
401 }
402
403 #[test]
405 fn test_update_nonexistent_returns_error() {
406 let conn = setup();
407 let result = update_block(&conn, "ghost", "content", "reason");
408 assert!(result.is_err());
409 }
410
411 #[test]
413 fn test_create_empty_name_returns_error() {
414 let conn = setup();
415 let result = create_block(&conn, "", "content", 256);
416 assert!(result.is_err());
417 }
418}