1use crate::error::{ErrorData, Result};
24use crate::providers::local_store::{
25 as_blob, as_i64, as_opt_i64, as_text, opt_i64_value, query_all, LocalStore, StoreSpec,
26};
27use crate::traits::{Binding, Kv, PutOptions, ScanResult};
28use alien_error::{AlienError, Context as _, IntoAlienError as _};
29use async_trait::async_trait;
30use chrono::Utc;
31use std::path::PathBuf;
32use turso::Connection;
33
34static KV_SPEC: StoreSpec = StoreSpec {
35 db_filename: "localkv.sqlite",
36 format_version: "localkv.v1",
37 binding_type: "local KV",
38 schema_ddl: "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value BLOB NOT NULL, expires_at INTEGER);",
39};
40
41#[derive(Debug)]
42pub struct LocalKv {
43 store: LocalStore,
44}
45
46fn kv_error(operation: &str, key: &str, reason: &str) -> ErrorData {
48 ErrorData::KvOperationFailed {
49 operation: operation.to_string(),
50 key: key.to_string(),
51 reason: reason.to_string(),
52 }
53}
54
55async fn delete_expired(conn: &Connection, operation: &str, key: &str, now: i64) -> Result<()> {
58 conn.execute(
59 "DELETE FROM kv WHERE key = ?1 AND expires_at IS NOT NULL AND expires_at <= ?2",
60 (key, now),
61 )
62 .await
63 .into_alien_error()
64 .context(kv_error(operation, key, "failed to delete expired row"))?;
65 Ok(())
66}
67
68impl LocalKv {
69 pub async fn new(data_dir: PathBuf) -> Result<Self> {
70 Ok(Self {
71 store: LocalStore::open(data_dir, &KV_SPEC).await?,
72 })
73 }
74
75 pub fn data_dir(&self) -> &PathBuf {
77 self.store.data_dir()
78 }
79
80 pub async fn len(&self) -> Result<usize> {
83 self.store
84 .with_conn(|conn| async move {
85 let rows = query_all(&conn, "SELECT COUNT(*) FROM kv", ())
86 .await
87 .into_alien_error()
88 .context(kv_error("len", "*", "failed to count rows"))?;
89 let count = rows
90 .first()
91 .and_then(|row| row.first())
92 .and_then(as_i64)
93 .ok_or_else(|| {
94 AlienError::new(kv_error("len", "*", "count query returned no value"))
95 })?;
96 Ok(count as usize)
97 })
98 .await
99 }
100
101 pub async fn is_empty(&self) -> Result<bool> {
104 Ok(self.len().await? == 0)
105 }
106
107 pub async fn clear(&self) -> Result<()> {
110 self.store
111 .with_conn(|conn| async move {
112 conn.execute("DELETE FROM kv", ())
113 .await
114 .into_alien_error()
115 .context(kv_error("clear", "*", "failed to clear local KV store"))?;
116 Ok(())
117 })
118 .await
119 }
120
121 pub async fn keys(&self) -> Result<Vec<String>> {
124 self.store
125 .with_conn(|conn| async move {
126 let rows = query_all(&conn, "SELECT key FROM kv", ())
127 .await
128 .into_alien_error()
129 .context(kv_error("keys", "*", "failed to scan keys"))?;
130 let mut keys = Vec::with_capacity(rows.len());
131 for row in &rows {
132 keys.push(row.first().and_then(as_text).ok_or_else(|| {
133 AlienError::new(kv_error("keys", "*", "failed to read key row"))
134 })?);
135 }
136 Ok(keys)
137 })
138 .await
139 }
140
141 fn validate_key(key: &str) -> Result<()> {
143 crate::providers::kv::validate_key(key)
144 }
145
146 fn validate_value(value: &[u8]) -> Result<()> {
148 crate::providers::kv::validate_value(value)
149 }
150}
151
152impl Binding for LocalKv {}
153
154#[async_trait]
155impl Kv for LocalKv {
156 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
157 Self::validate_key(key)?;
158
159 self.store
160 .with_conn(|conn| async move {
161 let now = Utc::now().timestamp_millis();
162 let rows = query_all(
163 &conn,
164 "SELECT value, expires_at FROM kv WHERE key = ?1",
165 (key,),
166 )
167 .await
168 .into_alien_error()
169 .context(kv_error("get", key, "failed to read value"))?;
170
171 let Some(row) = rows.first() else {
172 return Ok(None);
173 };
174 let value = row.first().and_then(as_blob).ok_or_else(|| {
175 AlienError::new(kv_error("get", key, "stored value is not a blob"))
176 })?;
177 let expires_at = row.get(1).and_then(as_opt_i64).ok_or_else(|| {
178 AlienError::new(kv_error("get", key, "stored expires_at is not an integer"))
179 })?;
180
181 if matches!(expires_at, Some(exp) if exp <= now) {
182 delete_expired(&conn, "get", key, now).await?;
184 Ok(None)
185 } else {
186 Ok(Some(value))
187 }
188 })
189 .await
190 }
191
192 async fn put(&self, key: &str, value: Vec<u8>, options: Option<PutOptions>) -> Result<bool> {
193 Self::validate_key(key)?;
194 Self::validate_value(&value)?;
195 let options = options.unwrap_or_default();
196
197 self.store
198 .with_conn(|conn| async move {
199 let now = Utc::now().timestamp_millis();
200 let expires_at: Option<i64> = options
201 .ttl
202 .map(|d| now.saturating_add(i64::try_from(d.as_millis()).unwrap_or(i64::MAX)));
203
204 if options.if_not_exists {
205 let changed = conn
210 .execute(
211 "INSERT INTO kv (key, value, expires_at) VALUES (?1, ?2, ?3) \
212 ON CONFLICT(key) DO UPDATE SET value = ?2, expires_at = ?3 \
213 WHERE kv.expires_at IS NOT NULL AND kv.expires_at <= ?4",
214 (key, value, opt_i64_value(expires_at), now),
215 )
216 .await
217 .into_alien_error()
218 .context(kv_error("put", key, "failed conditional put"))?;
219 Ok(changed == 1)
220 } else {
221 conn.execute(
222 "INSERT INTO kv (key, value, expires_at) VALUES (?1, ?2, ?3) \
223 ON CONFLICT(key) DO UPDATE SET value = ?2, expires_at = ?3",
224 (key, value, opt_i64_value(expires_at)),
225 )
226 .await
227 .into_alien_error()
228 .context(kv_error("put", key, "failed to upsert value"))?;
229 Ok(true)
230 }
231 })
232 .await
233 }
234
235 async fn delete(&self, key: &str) -> Result<()> {
236 Self::validate_key(key)?;
237
238 self.store
239 .with_conn(|conn| async move {
240 conn.execute("DELETE FROM kv WHERE key = ?1", (key,))
241 .await
242 .into_alien_error()
243 .context(kv_error("delete", key, "failed to delete key"))?;
244 Ok(())
245 })
246 .await
247 }
248
249 async fn exists(&self, key: &str) -> Result<bool> {
250 Self::validate_key(key)?;
251
252 self.store
253 .with_conn(|conn| async move {
254 let now = Utc::now().timestamp_millis();
255 let rows = query_all(&conn, "SELECT expires_at FROM kv WHERE key = ?1", (key,))
256 .await
257 .into_alien_error()
258 .context(kv_error("exists", key, "failed to check existence"))?;
259
260 let Some(row) = rows.first() else {
261 return Ok(false);
262 };
263 let expires_at = row.first().and_then(as_opt_i64).ok_or_else(|| {
264 AlienError::new(kv_error(
265 "exists",
266 key,
267 "stored expires_at is not an integer",
268 ))
269 })?;
270
271 if matches!(expires_at, Some(exp) if exp <= now) {
272 delete_expired(&conn, "exists", key, now).await?;
274 Ok(false)
275 } else {
276 Ok(true)
277 }
278 })
279 .await
280 }
281
282 async fn scan_prefix(
283 &self,
284 prefix: &str,
285 limit: Option<usize>,
286 cursor: Option<String>,
287 ) -> Result<ScanResult> {
288 Self::validate_key(prefix)?;
289
290 let start_offset = if let Some(cursor_str) = cursor {
292 cursor_str.parse::<usize>().map_err(|_| {
293 AlienError::new(ErrorData::InvalidInput {
294 operation_context: "KV scan cursor parsing".to_string(),
295 details: format!("Invalid cursor format: {}", cursor_str),
296 field_name: Some("cursor".to_string()),
297 })
298 })?
299 } else {
300 0
301 };
302
303 let matching: Vec<(String, Vec<u8>)> = self
305 .store
306 .with_conn(|conn| async move {
307 let now = Utc::now().timestamp_millis();
308 let rows = query_all(
309 &conn,
310 "SELECT key, value, expires_at FROM kv WHERE key >= ?1 ORDER BY key",
311 (prefix,),
312 )
313 .await
314 .into_alien_error()
315 .context(kv_error(
316 "scan_prefix",
317 prefix,
318 "failed to scan prefix",
319 ))?;
320
321 let mut matching = Vec::new();
322 for row in &rows {
323 let k = row.first().and_then(as_text).ok_or_else(|| {
324 AlienError::new(kv_error(
325 "scan_prefix",
326 prefix,
327 "failed to read scan row key",
328 ))
329 })?;
330 if !k.starts_with(prefix) {
333 break;
334 }
335 let v = row.get(1).and_then(as_blob).ok_or_else(|| {
336 AlienError::new(kv_error(
337 "scan_prefix",
338 prefix,
339 "stored value is not a blob",
340 ))
341 })?;
342 let exp = row.get(2).and_then(as_opt_i64).ok_or_else(|| {
343 AlienError::new(kv_error(
344 "scan_prefix",
345 prefix,
346 "stored expires_at is not an integer",
347 ))
348 })?;
349 if matches!(exp, Some(e) if e <= now) {
350 continue; }
352 matching.push((k, v));
353 }
354 Ok(matching)
355 })
356 .await?;
357
358 let total_items = matching.len();
360 let end_offset = start_offset + limit.unwrap_or(total_items);
361
362 let items = matching
363 .into_iter()
364 .skip(start_offset)
365 .take(limit.unwrap_or(usize::MAX))
366 .collect::<Vec<_>>();
367
368 let next_cursor = if end_offset < total_items {
369 Some(end_offset.to_string())
370 } else {
371 None
372 };
373
374 Ok(ScanResult { items, next_cursor })
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::providers::local_store::open_database;
382 use std::sync::Arc;
383 use std::time::Duration;
384 use tempfile::TempDir;
385 use tokio::time;
386
387 async fn create_test_kv() -> (LocalKv, TempDir) {
388 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
389 let kv = LocalKv::new(temp_dir.path().join("kv.db"))
390 .await
391 .expect("Failed to create LocalKv");
392 (kv, temp_dir)
393 }
394
395 #[tokio::test]
396 async fn test_basic_operations() {
397 let (kv, _temp_dir) = create_test_kv().await;
398
399 assert!(kv
400 .put("test_key", b"test_value".to_vec(), None)
401 .await
402 .unwrap());
403 let value = kv.get("test_key").await.unwrap();
404 assert_eq!(value, Some(b"test_value".to_vec()));
405
406 assert!(kv.exists("test_key").await.unwrap());
407 assert!(!kv.exists("nonexistent").await.unwrap());
408
409 kv.delete("test_key").await.unwrap();
410 assert!(!kv.exists("test_key").await.unwrap());
411 assert_eq!(kv.get("test_key").await.unwrap(), None);
412 }
413
414 #[tokio::test]
415 async fn test_conditional_put() {
416 let (kv, _temp_dir) = create_test_kv().await;
417
418 let options = Some(PutOptions {
419 ttl: None,
420 if_not_exists: true,
421 });
422 assert!(kv
423 .put("key", b"value1".to_vec(), options.clone())
424 .await
425 .unwrap());
426
427 assert!(!kv.put("key", b"value2".to_vec(), options).await.unwrap());
428
429 assert_eq!(kv.get("key").await.unwrap(), Some(b"value1".to_vec()));
430
431 assert!(kv.put("key", b"value3".to_vec(), None).await.unwrap());
432 assert_eq!(kv.get("key").await.unwrap(), Some(b"value3".to_vec()));
433 }
434
435 #[tokio::test]
436 async fn test_ttl_expiration() {
437 let (kv, _temp_dir) = create_test_kv().await;
438
439 let options = Some(PutOptions {
440 ttl: Some(Duration::from_millis(500)),
441 if_not_exists: false,
442 });
443
444 kv.put("expiring_key", b"value".to_vec(), options)
445 .await
446 .unwrap();
447
448 assert!(kv.exists("expiring_key").await.unwrap());
449 assert_eq!(
450 kv.get("expiring_key").await.unwrap(),
451 Some(b"value".to_vec())
452 );
453
454 time::sleep(Duration::from_millis(750)).await;
455
456 assert!(!kv.exists("expiring_key").await.unwrap());
457 assert_eq!(kv.get("expiring_key").await.unwrap(), None);
458 }
459
460 #[tokio::test]
461 async fn test_prefix_scanning() {
462 let (kv, _temp_dir) = create_test_kv().await;
463
464 kv.put("prefix:key1", b"value1".to_vec(), None)
465 .await
466 .unwrap();
467 kv.put("prefix:key2", b"value2".to_vec(), None)
468 .await
469 .unwrap();
470 kv.put("prefix:key3", b"value3".to_vec(), None)
471 .await
472 .unwrap();
473 kv.put("other:key", b"other".to_vec(), None).await.unwrap();
474
475 let result = kv.scan_prefix("prefix:", None, None).await.unwrap();
476 assert_eq!(result.items.len(), 3);
477 assert!(result.next_cursor.is_none());
478
479 assert_eq!(result.items[0].0, "prefix:key1");
480 assert_eq!(result.items[1].0, "prefix:key2");
481 assert_eq!(result.items[2].0, "prefix:key3");
482
483 let result = kv.scan_prefix("prefix:", Some(2), None).await.unwrap();
484 assert_eq!(result.items.len(), 2);
485 assert!(result.next_cursor.is_some());
486
487 let cursor = result.next_cursor.unwrap();
488 let result = kv
489 .scan_prefix("prefix:", Some(2), Some(cursor))
490 .await
491 .unwrap();
492 assert_eq!(result.items.len(), 1);
493 assert_eq!(result.items[0].0, "prefix:key3");
494 assert!(result.next_cursor.is_none());
495 }
496
497 #[tokio::test]
498 async fn test_persistence_across_reopens() {
499 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
500 let db_path = temp_dir.path().join("kv.db");
501
502 {
503 let kv = LocalKv::new(db_path.clone())
504 .await
505 .expect("Failed to create LocalKv");
506 kv.put("persistent_key", b"persistent_value".to_vec(), None)
507 .await
508 .unwrap();
509 }
510
511 {
512 let kv = LocalKv::new(db_path)
513 .await
514 .expect("Failed to reopen LocalKv");
515 let value = kv.get("persistent_key").await.unwrap();
516 assert_eq!(value, Some(b"persistent_value".to_vec()));
517 }
518 }
519
520 #[tokio::test]
521 async fn test_key_validation() {
522 let (kv, _temp_dir) = create_test_kv().await;
523
524 assert!(kv.put("", b"value".to_vec(), None).await.is_err());
525 assert!(kv.get("").await.is_err());
526
527 let long_key = "a".repeat(513);
528 assert!(kv.put(&long_key, b"value".to_vec(), None).await.is_err());
529
530 assert!(kv
531 .put("key with spaces", b"value".to_vec(), None)
532 .await
533 .is_err());
534 assert!(kv
535 .put("key\nwith\nnewlines", b"value".to_vec(), None)
536 .await
537 .is_err());
538 assert!(kv
539 .put("key/with/slashes", b"value".to_vec(), None)
540 .await
541 .is_err());
542
543 assert!(kv
544 .put("valid_key-123", b"value".to_vec(), None)
545 .await
546 .is_ok());
547 assert!(kv
548 .put("domain.com:8080", b"value".to_vec(), None)
549 .await
550 .is_ok());
551 }
552
553 #[tokio::test]
554 async fn test_value_validation() {
555 let (kv, _temp_dir) = create_test_kv().await;
556
557 let large_value = vec![0u8; 24_577];
558 assert!(kv.put("key", large_value, None).await.is_err());
559
560 let max_value = vec![0u8; 24_576];
561 assert!(kv.put("key", max_value, None).await.is_ok());
562 }
563
564 #[tokio::test]
565 async fn test_utility_methods() {
566 let (kv, _temp_dir) = create_test_kv().await;
567
568 assert!(kv.is_empty().await.unwrap());
569 assert_eq!(kv.len().await.unwrap(), 0);
570 assert_eq!(kv.keys().await.unwrap(), Vec::<String>::new());
571
572 kv.put("key1", b"value1".to_vec(), None).await.unwrap();
573 kv.put("key2", b"value2".to_vec(), None).await.unwrap();
574
575 assert!(!kv.is_empty().await.unwrap());
576 assert_eq!(kv.len().await.unwrap(), 2);
577
578 let mut keys = kv.keys().await.unwrap();
579 keys.sort();
580 assert_eq!(keys, vec!["key1", "key2"]);
581
582 kv.clear().await.unwrap();
583 assert!(kv.is_empty().await.unwrap());
584 assert_eq!(kv.len().await.unwrap(), 0);
585 }
586
587 #[tokio::test]
588 async fn test_unknown_format_rejected_on_open() {
589 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
590 let dir = temp_dir.path().join("kv");
591
592 {
594 let kv = LocalKv::new(dir.clone()).await.expect("initial open");
595 kv.put("k", b"v".to_vec(), None).await.unwrap();
596 }
597 {
598 let db = open_database(&dir.join("localkv.sqlite"), "test")
599 .await
600 .expect("raw open");
601 let conn = db.connect().expect("raw connect");
602 conn.execute(
603 "UPDATE meta SET value = 'localkv.v2' WHERE key = 'format'",
604 (),
605 )
606 .await
607 .expect("format overwrite");
608 }
609
610 let err = LocalKv::new(dir)
612 .await
613 .expect_err("unknown format must be rejected");
614 let msg = err.to_string();
615 assert!(
616 msg.contains("localkv.v2"),
617 "error must name the found format, got: {msg}"
618 );
619 assert!(
620 msg.contains("localkv.v1"),
621 "error must name the expected format, got: {msg}"
622 );
623 }
624
625 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
628 async fn test_conditional_put_atomicity_across_handles() {
629 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
630 let dir = temp_dir.path().join("kv");
631 let kv_a = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle a"));
633 let kv_b = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle b"));
634
635 let n = 16;
636 let mut handles = Vec::new();
637 for i in 0..n {
638 let kv = if i % 2 == 0 {
639 kv_a.clone()
640 } else {
641 kv_b.clone()
642 };
643 handles.push(tokio::spawn(async move {
644 let val = format!("val-{i}").into_bytes();
645 let opts = Some(PutOptions {
646 ttl: None,
647 if_not_exists: true,
648 });
649 let won = kv.put("race", val.clone(), opts).await.expect("put ok");
650 (won, val)
651 }));
652 }
653
654 let mut winners = Vec::new();
655 for h in handles {
656 let (won, val) = h.await.expect("task join");
657 if won {
658 winners.push(val);
659 }
660 }
661
662 assert_eq!(
663 winners.len(),
664 1,
665 "exactly one conditional put must win across both handles"
666 );
667 let stored = kv_a.get("race").await.unwrap().expect("key present");
668 assert_eq!(stored, winners[0], "stored value must equal the winner");
669 assert_eq!(
670 kv_b.get("race").await.unwrap().expect("key present via b"),
671 winners[0]
672 );
673 }
674
675 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
676 async fn test_ttl_expiry_takeover_conditional_put() {
677 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
678 let dir = temp_dir.path().join("kv");
679 let kv_a = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle a"));
680 let kv_b = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle b"));
681
682 assert!(kv_a
684 .put(
685 "k",
686 b"initial".to_vec(),
687 Some(PutOptions {
688 ttl: Some(Duration::from_millis(300)),
689 if_not_exists: true,
690 }),
691 )
692 .await
693 .unwrap());
694
695 assert!(!kv_b
697 .put(
698 "k",
699 b"early".to_vec(),
700 Some(PutOptions {
701 ttl: None,
702 if_not_exists: true,
703 }),
704 )
705 .await
706 .unwrap());
707
708 time::sleep(Duration::from_millis(450)).await;
710
711 let n = 12;
713 let mut handles = Vec::new();
714 for i in 0..n {
715 let kv = if i % 2 == 0 {
716 kv_a.clone()
717 } else {
718 kv_b.clone()
719 };
720 handles.push(tokio::spawn(async move {
721 let val = format!("takeover-{i}").into_bytes();
722 let won = kv
723 .put(
724 "k",
725 val.clone(),
726 Some(PutOptions {
727 ttl: None,
728 if_not_exists: true,
729 }),
730 )
731 .await
732 .expect("put ok");
733 (won, val)
734 }));
735 }
736
737 let mut winners = Vec::new();
738 for h in handles {
739 let (won, val) = h.await.expect("task join");
740 if won {
741 winners.push(val);
742 }
743 }
744
745 assert_eq!(
746 winners.len(),
747 1,
748 "exactly one takeover conditional put must win after expiry"
749 );
750 let stored = kv_b.get("k").await.unwrap().expect("key present");
751 assert_eq!(
752 stored, winners[0],
753 "stored value must equal the takeover winner"
754 );
755 }
756
757 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
758 async fn test_multi_handle_concurrent_smoke() {
759 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
760 let dir = temp_dir.path().join("kv");
761 let kv_a = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle a"));
762 let kv_b = Arc::new(LocalKv::new(dir.clone()).await.expect("open handle b"));
763
764 let mut handles = Vec::new();
765 for i in 0..50 {
766 let kv = if i % 2 == 0 {
767 kv_a.clone()
768 } else {
769 kv_b.clone()
770 };
771 handles.push(tokio::spawn(async move {
772 let key = format!("key_{i}");
773 let val = format!("v{i}").into_bytes();
774 kv.put(&key, val.clone(), None).await.expect("put ok");
776 let got = kv.get(&key).await.expect("get ok");
777 assert_eq!(got, Some(val));
778 }));
779 }
780 for h in handles {
781 h.await.expect("task join");
782 }
783
784 assert_eq!(kv_a.len().await.unwrap(), 50, "handle a sees all keys");
785 assert_eq!(kv_b.len().await.unwrap(), 50, "handle b sees all keys");
786 }
787}