1use super::*;
4
5fn is_knowledge_key(key: &str) -> bool {
11 key.starts_with("file:")
12 || key.starts_with("gotcha:")
13 || key.starts_with("decision:")
14 || key.starts_with("dep:")
15 || key.starts_with("dev_note:")
16 || key.starts_with("stage:")
17}
18
19fn read_record(txn: &Transaction, key: &str) -> Result<Option<Record>> {
21 match txn.get(key.as_bytes())? {
22 None => Ok(None),
23 Some(bytes) => {
24 let record = rmps::from_slice::<Record>(&bytes)
25 .with_context(|| format!("corrupt record at key '{key}'"))?;
26 Ok(Some(record))
27 }
28 }
29}
30
31fn skv_durability(d: Durability) -> SkvDurability {
33 match d {
34 Durability::Immediate => SkvDurability::Immediate,
35 Durability::Eventual => SkvDurability::Eventual,
36 }
37}
38
39pub(super) fn prefix_end(prefix: &str) -> String {
43 let mut bytes = prefix.as_bytes().to_vec();
44 for b in bytes.iter_mut().rev() {
46 if *b < 0xff {
47 *b += 1;
48 return String::from_utf8(bytes).unwrap_or_else(|_| "\u{ffff}".to_owned());
49 }
50 *b = 0x00;
51 }
52 "\u{ffff}".to_owned()
54}
55
56impl Store {
57 pub async fn get(&self, key: &str) -> Result<Option<Record>> {
63 let txn = self.tree_for(key).begin_with_mode(Mode::ReadOnly)?;
64 read_record(&txn, key)
65 }
66
67 pub async fn put(&self, key: &str, record: &Record) -> Result<()> {
71 debug_assert_eq!(
72 Encoding::for_key(key),
73 Encoding::Record,
74 "put() writes a Record into {key}, which is a Raw namespace; see store::durability::Encoding"
75 );
76 let durability = Durability::for_key(key);
77 let tree = self.tree_for(key);
78 let mut txn = tree.begin_with_mode(Mode::WriteOnly)?;
79 txn.set_durability(skv_durability(durability));
80
81 let bytes = rmps::to_vec_named(record)
82 .with_context(|| format!("failed to serialize record for key '{key}'"))?;
83 txn.set(key.as_bytes(), bytes)?;
84 txn.commit().await?;
85
86 if is_knowledge_key(key) {
90 let _ = std::fs::write(self.root.join(SEARCH_SYNC_PENDING), b"");
91 }
92
93 let mut search_synced = false;
102 match self.ensure_search() {
103 Ok(search) => {
104 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
105 search.add_record(record)
106 })) {
107 Ok(Ok(())) => {
108 search_synced = true;
109 }
110 Ok(Err(e)) => {
111 tracing::warn!("search index update failed for '{key}': {e}");
112 }
113 Err(_panic) => {
114 tracing::error!(
115 "search index panicked during put for '{key}' — \
116 index will be rebuilt on next startup"
117 );
118 }
119 }
120 }
121 Err(e) => {
122 tracing::warn!("search index unavailable during put: {e}");
123 }
124 }
125 if is_knowledge_key(key) {
126 self.bump_write_seq();
127 if search_synced {
128 let _ = std::fs::remove_file(self.root.join(SEARCH_SYNC_PENDING));
129 }
130 }
131 Ok(())
132 }
133
134 pub async fn put_batch_kv_only(&self, records: &[(&str, &Record)]) -> Result<()> {
142 if records.is_empty() {
143 return Ok(());
144 }
145 let mut immediate: Vec<(&str, &Record)> = Vec::new();
146 let mut eventual: Vec<(&str, &Record)> = Vec::new();
147 for &(key, record) in records {
148 match Durability::for_key(key) {
149 Durability::Immediate => immediate.push((key, record)),
150 Durability::Eventual => eventual.push((key, record)),
151 }
152 }
153 if !immediate.is_empty() {
154 let mut txn = self.knowledge.begin_with_mode(Mode::WriteOnly)?;
155 txn.set_durability(SkvDurability::Immediate);
156 for (key, record) in &immediate {
157 let bytes = rmps::to_vec_named(record)
158 .with_context(|| format!("failed to serialize record for key '{key}'"))?;
159 txn.set(key.as_bytes(), bytes)?;
160 }
161 txn.commit().await?;
162 }
163 if !eventual.is_empty() {
164 let mut txn = self.sessions.begin_with_mode(Mode::WriteOnly)?;
165 txn.set_durability(SkvDurability::Eventual);
166 for (key, record) in &eventual {
167 let bytes = rmps::to_vec_named(record)
168 .with_context(|| format!("failed to serialize record for key '{key}'"))?;
169 txn.set(key.as_bytes(), bytes)?;
170 }
171 txn.commit().await?;
172 }
173 if records.iter().any(|(k, _)| is_knowledge_key(k)) {
174 self.bump_write_seq();
175 }
176 Ok(())
177 }
178
179 pub fn mark_search_stale(&self) {
187 let _ = std::fs::write(self.root.join(SEARCH_STALE_MARKER), b"");
188 }
189
190 pub async fn put_batch(&self, records: &[(&str, &Record)]) -> Result<()> {
199 if records.is_empty() {
200 return Ok(());
201 }
202
203 let mut immediate: Vec<(&str, &Record)> = Vec::new();
205 let mut eventual: Vec<(&str, &Record)> = Vec::new();
206 for &(key, record) in records {
207 match Durability::for_key(key) {
208 Durability::Immediate => immediate.push((key, record)),
209 Durability::Eventual => eventual.push((key, record)),
210 }
211 }
212
213 if !immediate.is_empty() {
214 let mut txn = self.knowledge.begin_with_mode(Mode::WriteOnly)?;
215 txn.set_durability(SkvDurability::Immediate);
216 for (key, record) in &immediate {
217 let bytes = rmps::to_vec_named(record)
218 .with_context(|| format!("failed to serialize record for key '{key}'"))?;
219 txn.set(key.as_bytes(), bytes)?;
220 }
221 txn.commit().await?;
222 }
223
224 if !eventual.is_empty() {
225 let mut txn = self.sessions.begin_with_mode(Mode::WriteOnly)?;
226 txn.set_durability(SkvDurability::Eventual);
227 for (key, record) in &eventual {
228 let bytes = rmps::to_vec_named(record)
229 .with_context(|| format!("failed to serialize record for key '{key}'"))?;
230 txn.set(key.as_bytes(), bytes)?;
231 }
232 txn.commit().await?;
233 }
234
235 let has_knowledge = records.iter().any(|(k, _)| is_knowledge_key(k));
236
237 if has_knowledge {
239 let _ = std::fs::write(self.root.join(SEARCH_SYNC_PENDING), b"");
240 }
241
242 let mut search_synced = false;
246 match self.ensure_search() {
247 Ok(search) => {
248 let search_records: Vec<&Record> = records.iter().map(|(_, r)| *r).collect();
249 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
250 search.add_records(&search_records)
251 })) {
252 Ok(Ok(_)) => {
253 search_synced = true;
254 }
255 Ok(Err(e)) => {
256 tracing::warn!("search index update failed in put_batch: {e}");
257 }
258 Err(_panic) => {
259 tracing::error!(
260 "search index panicked during put_batch — \
261 index will be rebuilt on next startup"
262 );
263 }
264 }
265 }
266 Err(e) => {
267 tracing::warn!("search index unavailable during put_batch: {e}");
268 }
269 }
270 if has_knowledge {
271 self.bump_write_seq();
272 if search_synced {
273 let _ = std::fs::remove_file(self.root.join(SEARCH_SYNC_PENDING));
274 }
275 }
276 Ok(())
277 }
278
279 pub async fn delete(&self, key: &str) -> Result<()> {
281 let tree = self.tree_for(key);
282 let mut txn = tree.begin_with_mode(Mode::WriteOnly)?;
283 txn.set_durability(skv_durability(Durability::for_key(key)));
284 txn.delete(key.as_bytes())?;
285 txn.commit().await?;
286
287 if is_knowledge_key(key) {
288 let _ = std::fs::write(self.root.join(SEARCH_SYNC_PENDING), b"");
289 }
290
291 let mut search_synced = false;
292 match self.ensure_search() {
293 Ok(search) => {
294 search.delete_key(key)?;
295 search_synced = true;
296 }
297 Err(e) => {
298 tracing::warn!("search index unavailable during delete: {e}");
299 }
300 }
301
302 if is_knowledge_key(key) {
303 self.bump_write_seq();
304 if search_synced {
305 let _ = std::fs::remove_file(self.root.join(SEARCH_SYNC_PENDING));
306 }
307 }
308 Ok(())
309 }
310
311 pub async fn scan_prefix(&self, prefix: &str) -> Result<Vec<Record>> {
318 debug_assert_eq!(
319 Encoding::for_key(prefix),
320 Encoding::Record,
321 "scan_prefix() deserializes Records from {prefix}, which is a Raw namespace; use scan_keys instead"
322 );
323 let tree = self.tree_for(prefix);
324 let txn = tree.begin_with_mode(Mode::ReadOnly)?;
325
326 let end = prefix_end(prefix);
328 let iter = txn.range(prefix.as_bytes(), end.as_bytes())?;
329
330 let mut records = Vec::new();
331 let mut cursor = iter;
332 while cursor.next()? {
333 let bytes = cursor.value()?;
334 match rmps::from_slice::<Record>(&bytes) {
335 Ok(record) => records.push(record),
336 Err(e) => {
337 tracing::warn!("skipping malformed record during scan: {e}");
338 }
339 }
340 }
341 Ok(records)
342 }
343
344 pub async fn scan_prefix_each<F>(&self, prefix: &str, mut callback: F) -> Result<()>
356 where
357 F: FnMut(Record),
358 {
359 debug_assert_eq!(
360 Encoding::for_key(prefix),
361 Encoding::Record,
362 "scan_prefix_each() deserializes Records from {prefix}, which is a Raw namespace; use scan_keys instead"
363 );
364 let tree = self.tree_for(prefix);
365 let txn = tree.begin_with_mode(Mode::ReadOnly)?;
366 let end = prefix_end(prefix);
367 let mut cursor = txn.range(prefix.as_bytes(), end.as_bytes())?;
368 while cursor.next()? {
369 let bytes = cursor.value()?;
370 match rmps::from_slice::<Record>(&bytes) {
371 Ok(record) => callback(record),
372 Err(e) => {
373 tracing::warn!("skipping malformed record during scan: {e}");
374 }
375 }
376 }
377 Ok(())
378 }
379
380 pub async fn search(&self, text: &str, limit: usize) -> Result<Vec<Record>> {
389 let search = self.ensure_search()?;
390 let keys = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
391 search.query_keys(text, limit)
392 })) {
393 Ok(result) => result?,
394 Err(_panic) => {
395 tracing::error!("search index panicked during query — returning empty results");
396 return Ok(vec![]);
397 }
398 };
399 let mut records = Vec::with_capacity(keys.len());
400 for key in &keys {
401 if let Some(record) = self.get(key).await? {
402 records.push(record);
403 }
404 }
405 Ok(records)
406 }
407
408 pub async fn search_scored(&self, text: &str, limit: usize) -> Result<Vec<(f32, Record)>> {
414 let search = self.ensure_search()?;
415 let scored_keys = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
416 search.query_keys_scored(text, limit)
417 })) {
418 Ok(result) => result?,
419 Err(_panic) => {
420 tracing::error!(
421 "search index panicked during scored query — returning empty results"
422 );
423 return Ok(vec![]);
424 }
425 };
426 let mut results = Vec::with_capacity(scored_keys.len());
427 for (score, key) in &scored_keys {
428 if let Some(record) = self.get(key).await? {
429 results.push((*score, record));
430 }
431 }
432 Ok(results)
433 }
434
435 pub async fn get_raw_bytes(&self, key: &str) -> Result<Option<Vec<u8>>> {
440 let tree = self.tree_for(key);
441 let txn = tree.begin_with_mode(Mode::ReadOnly)?;
442 match txn.get(key.as_bytes())? {
443 None => Ok(None),
444 Some(bytes) => Ok(Some(bytes.to_vec())),
445 }
446 }
447
448 pub async fn put_raw(&self, key: &str, value: &[u8]) -> Result<()> {
455 debug_assert_eq!(
456 Encoding::for_key(key),
457 Encoding::Raw,
458 "put_raw() writes bare bytes into {key}, which is a Record namespace; a prefix scan would skip it. See store::durability::Encoding"
459 );
460 let durability = Durability::for_key(key);
461 let tree = self.tree_for(key);
462 let mut txn = tree.begin_with_mode(Mode::WriteOnly)?;
463 txn.set_durability(skv_durability(durability));
464 txn.set(key.as_bytes(), value.to_vec())?;
465 txn.commit().await?;
466 Ok(())
467 }
468
469 pub async fn put_batch_raw(&self, records: &[(&str, &[u8])]) -> Result<()> {
474 if records.is_empty() {
475 return Ok(());
476 }
477
478 let mut immediate: Vec<(&str, &[u8])> = Vec::new();
479 let mut eventual: Vec<(&str, &[u8])> = Vec::new();
480 for &(key, value) in records {
481 match Durability::for_key(key) {
482 Durability::Immediate => immediate.push((key, value)),
483 Durability::Eventual => eventual.push((key, value)),
484 }
485 }
486
487 if !immediate.is_empty() {
488 let mut txn = self.knowledge.begin_with_mode(Mode::WriteOnly)?;
489 txn.set_durability(SkvDurability::Immediate);
490 for (key, value) in &immediate {
491 txn.set(key.as_bytes(), value.to_vec())?;
492 }
493 txn.commit().await?;
494 }
495
496 if !eventual.is_empty() {
497 let mut txn = self.sessions.begin_with_mode(Mode::WriteOnly)?;
498 txn.set_durability(SkvDurability::Eventual);
499 for (key, value) in &eventual {
500 txn.set(key.as_bytes(), value.to_vec())?;
501 }
502 txn.commit().await?;
503 }
504
505 Ok(())
506 }
507
508 pub async fn transact_knowledge(&self, ops: &[KnowledgeWriteOp<'_>]) -> Result<()> {
526 if ops.is_empty() {
527 return Ok(());
528 }
529 for op in ops {
530 let k = match op {
531 KnowledgeWriteOp::PutRecord { key, .. } => *key,
532 KnowledgeWriteOp::PutRaw { key, .. } => *key,
533 };
534 if Durability::for_key(k) != Durability::Immediate {
535 anyhow::bail!(
536 "transact_knowledge: key '{k}' routes to sessions tree, not knowledge"
537 );
538 }
539 }
540
541 let mut record_refs: Vec<&Record> = Vec::new();
543
544 let mut txn = self.knowledge.begin_with_mode(Mode::WriteOnly)?;
545 txn.set_durability(SkvDurability::Immediate);
546 for op in ops {
547 match op {
548 KnowledgeWriteOp::PutRecord { key, record } => {
549 let bytes = rmps::to_vec_named(record)
550 .with_context(|| format!("failed to serialize record for key '{key}'"))?;
551 txn.set(key.as_bytes(), bytes)?;
552 record_refs.push(record);
553 }
554 KnowledgeWriteOp::PutRaw { key, value } => {
555 txn.set(key.as_bytes(), value.to_vec())?;
556 }
557 }
558 }
559 txn.commit().await?;
560
561 let has_knowledge = ops.iter().any(|op| {
563 let k = match op {
564 KnowledgeWriteOp::PutRecord { key, .. } => key,
565 KnowledgeWriteOp::PutRaw { key, .. } => key,
566 };
567 is_knowledge_key(k)
568 });
569 if has_knowledge {
570 let _ = std::fs::write(self.root.join(SEARCH_SYNC_PENDING), b"");
571 }
572 let mut search_synced = false;
573 if !record_refs.is_empty() {
574 if let Ok(search) = self.ensure_search() {
575 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
576 search.add_records(&record_refs)
577 })) {
578 Ok(Ok(_)) => search_synced = true,
579 Ok(Err(e)) => tracing::warn!("transact_knowledge: tantivy sync failed: {e}"),
580 Err(_) => tracing::error!("transact_knowledge: tantivy panicked"),
581 }
582 }
583 }
584 if has_knowledge {
585 self.bump_write_seq();
586 if search_synced {
587 let _ = std::fs::remove_file(self.root.join(SEARCH_SYNC_PENDING));
588 }
589 }
590 Ok(())
591 }
592
593 pub async fn transact_sessions_raw(&self, entries: &[(&str, &[u8])]) -> Result<()> {
601 if entries.is_empty() {
602 return Ok(());
603 }
604 for (k, _) in entries {
605 if Durability::for_key(k) != Durability::Eventual {
606 anyhow::bail!(
607 "transact_sessions_raw: key '{k}' routes to knowledge tree, not sessions"
608 );
609 }
610 }
611
612 let mut txn = self.sessions.begin_with_mode(Mode::WriteOnly)?;
613 txn.set_durability(SkvDurability::Eventual);
614 for (key, value) in entries {
615 txn.set(key.as_bytes(), value.to_vec())?;
616 }
617 txn.commit().await?;
618 Ok(())
619 }
620
621 pub async fn scan_keys(&self, prefix: &str) -> Result<Vec<String>> {
627 let tree = self.tree_for(prefix);
628 let txn = tree.begin_with_mode(Mode::ReadOnly)?;
629 let end = prefix_end(prefix);
630 let mut cursor = txn.range(prefix.as_bytes(), end.as_bytes())?;
631
632 let mut keys = Vec::new();
633 while cursor.next()? {
634 let user_key = cursor.key().user_key();
635 match std::str::from_utf8(user_key) {
636 Ok(s) => keys.push(s.to_string()),
637 Err(e) => tracing::warn!("skipping non-UTF8 key in scan_keys: {e}"),
638 }
639 }
640 Ok(keys)
641 }
642}