1use std::path::{Path, PathBuf};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use chrono::DateTime;
5use r2d2::Pool;
6use r2d2_sqlite::SqliteConnectionManager;
7use rusqlite::params;
8
9use crate::errors::{Result, VecDbError};
10use crate::types::{CollectionConfig, VectorRecord};
11
12const SCHEMA: &str = r#"
14CREATE TABLE IF NOT EXISTS vectors (
15 id TEXT PRIMARY KEY NOT NULL,
16 mmap_index INTEGER NOT NULL,
17 payload TEXT NOT NULL DEFAULT '{}',
18 text TEXT,
19 created_at INTEGER NOT NULL,
20 updated_at INTEGER NOT NULL,
21 deleted INTEGER NOT NULL DEFAULT 0
22);
23
24CREATE INDEX IF NOT EXISTS idx_vectors_deleted ON vectors(deleted);
25CREATE INDEX IF NOT EXISTS idx_vectors_created_at ON vectors(created_at);
26
27CREATE TABLE IF NOT EXISTS collections (
28 name TEXT PRIMARY KEY NOT NULL,
29 config TEXT NOT NULL,
30 created_at INTEGER NOT NULL
31);
32"#;
33
34fn now_unix() -> i64 {
35 SystemTime::now()
36 .duration_since(UNIX_EPOCH)
37 .unwrap_or_default()
38 .as_secs() as i64
39}
40
41fn ts_to_dt(secs: i64) -> DateTime<chrono::Utc> {
42 DateTime::from_timestamp(secs, 0).unwrap_or_else(chrono::Utc::now)
43}
44
45fn build_pool(path: &Path, max_size: u32) -> Result<Pool<SqliteConnectionManager>> {
46 let manager = SqliteConnectionManager::file(path).with_init(|conn| {
47 conn.execute_batch(
48 "PRAGMA journal_mode=WAL; \
49 PRAGMA busy_timeout=5000; \
50 PRAGMA synchronous=NORMAL; \
51 PRAGMA foreign_keys=ON;",
52 )
53 });
54 r2d2::Pool::builder()
55 .max_size(max_size)
56 .build(manager)
57 .map_err(|e| VecDbError::StorageError(format!("connection pool: {e}")))
58}
59
60#[derive(Clone)]
61pub struct MetadataStore {
62 pool: Pool<SqliteConnectionManager>,
63 pub path: PathBuf,
64}
65
66impl MetadataStore {
67 pub fn open(path: &Path) -> Result<Self> {
68 Self::open_with_pool_size(path, 8)
69 }
70
71 pub fn open_with_pool_size(path: &Path, max_size: u32) -> Result<Self> {
72 let pool = build_pool(path, max_size)?;
73 let conn = pool
74 .get()
75 .map_err(|e| VecDbError::StorageError(format!("pool init: {e}")))?;
76 conn.execute_batch(SCHEMA)?;
77 drop(conn);
78 Ok(Self {
79 pool,
80 path: path.to_path_buf(),
81 })
82 }
83
84 fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
85 self.pool
86 .get()
87 .map_err(|e| VecDbError::StorageError(format!("pool get: {e}")))
88 }
89
90 pub fn upsert(&self, id: &str, mmap_index: usize, record: &VectorRecord) -> Result<()> {
91 let payload = serde_json::to_string(&record.payload)
92 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
93 let now = now_unix();
94 self.conn()?.execute(
95 "INSERT OR REPLACE INTO vectors \
96 (id, mmap_index, payload, text, created_at, updated_at, deleted) \
97 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)",
98 params![id, mmap_index as i64, payload, record.text, now, now],
99 )?;
100 Ok(())
101 }
102
103 pub fn upsert_batch(&self, items: &[(String, usize, &VectorRecord)]) -> Result<()> {
105 let mut conn = self.conn()?;
106 let now = now_unix();
107 let tx = conn.transaction()?;
108 {
109 let mut stmt = tx.prepare(
110 "INSERT OR REPLACE INTO vectors \
111 (id, mmap_index, payload, text, created_at, updated_at, deleted) \
112 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)",
113 )?;
114 for (id, mmap_index, record) in items {
115 let payload = serde_json::to_string(&record.payload)
116 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
117 stmt.execute(params![
118 id,
119 *mmap_index as i64,
120 payload,
121 record.text,
122 now,
123 now
124 ])?;
125 }
126 }
127 tx.commit()?;
128 Ok(())
129 }
130
131 pub fn get(&self, id: &str) -> Result<(usize, VectorRecord)> {
132 let conn = self.conn()?;
133 let row = conn.query_row(
134 "SELECT id, mmap_index, payload, text, created_at, updated_at \
135 FROM vectors WHERE id = ?1 AND deleted = 0",
136 params![id],
137 |row| {
138 Ok((
139 row.get::<_, String>(0)?,
140 row.get::<_, i64>(1)?,
141 row.get::<_, String>(2)?,
142 row.get::<_, Option<String>>(3)?,
143 row.get::<_, i64>(4)?,
144 row.get::<_, i64>(5)?,
145 ))
146 },
147 );
148
149 match row {
150 Ok((rid, mmap_idx, payload_str, text, created_ts, updated_ts)) => {
151 let payload = serde_json::from_str(&payload_str)
152 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
153 let record = VectorRecord {
154 id: rid,
155 vector: vec![],
156 payload,
157 text,
158 created_at: ts_to_dt(created_ts),
159 updated_at: ts_to_dt(updated_ts),
160 };
161 Ok((mmap_idx as usize, record))
162 }
163 Err(rusqlite::Error::QueryReturnedNoRows) => {
164 Err(VecDbError::NotFound { id: id.to_string() })
165 }
166 Err(e) => Err(VecDbError::SqliteError(e)),
167 }
168 }
169
170 pub fn delete(&self, id: &str) -> Result<()> {
171 let now = now_unix();
172 let rows = self.conn()?.execute(
173 "UPDATE vectors SET deleted = 1, updated_at = ?1 WHERE id = ?2 AND deleted = 0",
174 params![now, id],
175 )?;
176 if rows == 0 {
177 return Err(VecDbError::NotFound { id: id.to_string() });
178 }
179 Ok(())
180 }
181
182 pub fn list_active(&self) -> Result<Vec<(String, usize)>> {
183 let conn = self.conn()?;
184 let mut stmt = conn.prepare(
185 "SELECT id, mmap_index FROM vectors WHERE deleted = 0 ORDER BY mmap_index ASC",
186 )?;
187 let rows = stmt
188 .query_map([], |row| {
189 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
190 })?
191 .collect::<std::result::Result<Vec<_>, _>>()?;
192 Ok(rows)
193 }
194
195 pub fn count_active(&self) -> Result<usize> {
196 let n: i64 = self.conn()?.query_row(
197 "SELECT COUNT(*) FROM vectors WHERE deleted = 0",
198 [],
199 |row| row.get(0),
200 )?;
201 Ok(n as usize)
202 }
203
204 pub fn save_collection(&self, config: &CollectionConfig) -> Result<()> {
205 let json = serde_json::to_string(config)
206 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
207 let now = now_unix();
208 self.conn()?.execute(
209 "INSERT OR REPLACE INTO collections (name, config, created_at) VALUES (?1, ?2, ?3)",
210 params![config.name, json, now],
211 )?;
212 Ok(())
213 }
214
215 pub fn load_collection(&self, name: &str) -> Result<CollectionConfig> {
216 let conn = self.conn()?;
217 match conn.query_row(
218 "SELECT config FROM collections WHERE name = ?1",
219 params![name],
220 |row| row.get::<_, String>(0),
221 ) {
222 Ok(json) => serde_json::from_str(&json)
223 .map_err(|e| VecDbError::SerializationError(e.to_string())),
224 Err(rusqlite::Error::QueryReturnedNoRows) => {
225 Err(VecDbError::CollectionNotFound(name.to_string()))
226 }
227 Err(e) => Err(VecDbError::SqliteError(e)),
228 }
229 }
230
231 pub fn ensure_payload_indexes(&self, fields: &[String]) -> Result<()> {
234 let conn = self.conn()?;
235 for f in fields {
236 if !is_safe_json_path(f) {
237 continue;
238 }
239 let idx_name = format!("idx_pf_{}", f.replace('.', "_"));
240 let sql = format!(
241 "CREATE INDEX IF NOT EXISTS {idx_name} ON vectors (json_extract(payload, '$.{f}'))"
242 );
243 conn.execute(&sql, [])?;
244 }
245 Ok(())
246 }
247
248 pub fn filter_ids(
250 &self,
251 filter: &serde_json::Value,
252 indexed: &[String],
253 ) -> Result<Vec<crate::types::VectorId>> {
254 Ok(self
255 .filter_ids_indexed(filter, indexed)?
256 .into_iter()
257 .map(|(id, _)| id)
258 .collect())
259 }
260
261 pub fn filter_ids_indexed(
270 &self,
271 filter: &serde_json::Value,
272 indexed: &[String],
273 ) -> Result<Vec<(crate::types::VectorId, usize)>> {
274 if let Some(rows) = self.filter_pushdown(filter, indexed)? {
275 return Ok(rows);
276 }
277 self.filter_scan(filter)
278 }
279
280 fn filter_pushdown(
283 &self,
284 filter: &serde_json::Value,
285 indexed: &[String],
286 ) -> Result<Option<Vec<(crate::types::VectorId, usize)>>> {
287 use crate::planner::executor::{apply_filter_with, collect_filter_fields};
288
289 let indexed_set: std::collections::HashSet<&str> =
290 indexed.iter().map(|s| s.as_str()).collect();
291
292 let mut fields = std::collections::BTreeSet::new();
293 if !collect_filter_fields(filter, &mut fields) || filter_contains_bool(filter) {
294 return Ok(None);
295 }
296 let mut extracted: Vec<String> = Vec::new();
300 for f in &fields {
301 if f == "id" {
302 continue;
303 }
304 if !is_safe_json_path(f) {
305 return Ok(None);
306 }
307 extracted.push(f.clone());
308 }
309
310 let mut params: Vec<rusqlite::types::Value> = Vec::new();
315 let Some(where_super) = filter_to_sql_superset(filter, &mut params, &indexed_set) else {
316 return Ok(None);
317 };
318
319 let mut select = String::from("SELECT id, mmap_index");
320 for f in &extracted {
321 select.push_str(&format!(", json_extract(payload, '$.{f}')"));
322 }
323 select.push_str(" FROM vectors WHERE deleted = 0 AND ");
324 select.push_str(&where_super);
325
326 let conn = self.conn()?;
327 let mut stmt = match conn.prepare(&select) {
328 Ok(s) => s,
329 Err(_) => return Ok(None), };
331 let ncols = extracted.len();
332 let rows_res = stmt.query_map(rusqlite::params_from_iter(params.iter()), |row| {
333 let id: String = row.get(0)?;
334 let idx: i64 = row.get(1)?;
335 let mut vals: Vec<rusqlite::types::Value> = Vec::with_capacity(ncols);
336 for c in 0..ncols {
337 vals.push(row.get::<usize, rusqlite::types::Value>(2 + c)?);
338 }
339 Ok((id, idx as usize, vals))
340 });
341 let rows: Vec<(String, usize, Vec<rusqlite::types::Value>)> = match rows_res {
342 Ok(iter) => iter.collect::<std::result::Result<_, _>>()?,
343 Err(_) => return Ok(None),
344 };
345
346 let mut out = Vec::new();
347 for (id, idx, vals) in rows {
348 let resolve = |field: &str| -> Option<serde_json::Value> {
349 if field == "id" {
350 return Some(serde_json::Value::String(id.clone()));
351 }
352 let pos = extracted.iter().position(|f| f == field)?;
353 sqlite_to_json(&vals[pos])
354 };
355 if apply_filter_with(filter, &resolve) {
356 out.push((id, idx));
357 }
358 }
359 Ok(Some(out))
360 }
361
362 fn filter_scan(
364 &self,
365 filter: &serde_json::Value,
366 ) -> Result<Vec<(crate::types::VectorId, usize)>> {
367 use crate::planner::executor::apply_json_filter;
368 use crate::types::SearchResult;
369
370 let conn = self.conn()?;
371 let mut stmt =
372 conn.prepare("SELECT id, mmap_index, payload, text FROM vectors WHERE deleted = 0")?;
373 let rows: Vec<(String, i64, String, Option<String>)> = stmt
374 .query_map([], |row| {
375 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
376 })?
377 .collect::<std::result::Result<_, _>>()?;
378
379 let mut out = Vec::new();
380 for (id, idx, payload_str, text) in rows {
381 let payload: serde_json::Value = serde_json::from_str(&payload_str)
382 .unwrap_or_else(|_| serde_json::Value::Object(Default::default()));
383 let fake = SearchResult {
384 id: id.clone(),
385 score: 0.0,
386 dense_score: None,
387 sparse_score: None,
388 payload,
389 text,
390 };
391 if apply_json_filter(&fake, filter) {
392 out.push((id, idx as usize));
393 }
394 }
395 Ok(out)
396 }
397
398 pub fn list_collections(&self) -> Result<Vec<CollectionConfig>> {
399 let conn = self.conn()?;
400 let mut stmt = conn.prepare("SELECT config FROM collections ORDER BY created_at ASC")?;
401 let rows = stmt
402 .query_map([], |row| row.get::<_, String>(0))?
403 .collect::<std::result::Result<Vec<_>, _>>()?;
404 rows.iter()
405 .map(|json| {
406 serde_json::from_str(json)
407 .map_err(|e| VecDbError::SerializationError(e.to_string()))
408 })
409 .collect()
410 }
411}
412
413fn is_safe_json_path(field: &str) -> bool {
416 !field.is_empty()
417 && field
418 .chars()
419 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
420}
421
422fn filter_contains_bool(filter: &serde_json::Value) -> bool {
426 match filter {
427 serde_json::Value::Bool(_) => true,
428 serde_json::Value::Array(a) => a.iter().any(filter_contains_bool),
429 serde_json::Value::Object(o) => o.values().any(filter_contains_bool),
430 _ => false,
431 }
432}
433
434fn field_sql_expr(field: &str) -> Option<String> {
437 if field == "id" {
438 return Some("id".to_string());
439 }
440 if !is_safe_json_path(field) {
441 return None;
442 }
443 Some(format!("json_extract(payload, '$.{field}')"))
444}
445
446fn value_forms(v: &serde_json::Value) -> Option<(rusqlite::types::Value, rusqlite::types::Value)> {
450 use rusqlite::types::Value as V;
451 match v {
452 serde_json::Value::String(s) => {
453 let real = s.parse::<f64>().ok().map(V::Real).unwrap_or(V::Null);
454 Some((V::Text(s.clone()), real))
455 }
456 serde_json::Value::Number(n) => {
457 let f = n.as_f64()?;
458 Some((V::Text(n.to_string()), V::Real(f)))
459 }
460 _ => None,
461 }
462}
463
464fn value_as_real(v: &serde_json::Value) -> Option<f64> {
465 match v {
466 serde_json::Value::Number(n) => n.as_f64(),
467 serde_json::Value::String(s) => s.parse::<f64>().ok(),
468 _ => None,
469 }
470}
471
472fn natural_number_param(n: &serde_json::Number) -> Option<rusqlite::types::Value> {
475 use rusqlite::types::Value as V;
476 if let Some(i) = n.as_i64() {
477 Some(V::Integer(i))
478 } else {
479 n.as_f64().map(V::Real)
480 }
481}
482
483fn leaf_sql_superset(
490 field: &str,
491 op: &str,
492 value: &serde_json::Value,
493 params: &mut Vec<rusqlite::types::Value>,
494 indexed: &std::collections::HashSet<&str>,
495) -> Option<String> {
496 use rusqlite::types::Value as V;
497 let expr = field_sql_expr(field)?;
498 let is_indexed = indexed.contains(field);
499
500 if is_indexed {
501 match op {
503 "=" => {
504 let p = match value {
505 serde_json::Value::Number(n) => natural_number_param(n)?,
506 serde_json::Value::String(s) => V::Text(s.clone()),
507 _ => return None,
508 };
509 params.push(p);
510 return Some(format!("{expr} = ?"));
511 }
512 "<" | "<=" | ">" | ">=" => {
513 let r = value_as_real(value)?;
514 params.push(V::Real(r));
515 return Some(format!("{expr} {op} ?"));
516 }
517 "LIKE" => {
518 let s = value.as_str()?;
519 params.push(V::Text(s.to_string()));
520 return Some(format!("{expr} LIKE ?"));
521 }
522 _ => return None,
523 }
524 }
525
526 match op {
528 "=" => {
529 let (text, real) = value_forms(value)?;
530 params.push(text);
531 params.push(real);
532 Some(format!(
533 "(CAST({expr} AS TEXT) = ? OR CAST({expr} AS REAL) = ?)"
534 ))
535 }
536 "<" | "<=" | ">" | ">=" => {
537 let r = value_as_real(value)?;
538 params.push(V::Real(r));
539 Some(format!("CAST({expr} AS REAL) {op} ?"))
540 }
541 "LIKE" => {
542 let s = value.as_str()?;
543 params.push(V::Text(s.to_string()));
544 Some(format!("CAST({expr} AS TEXT) LIKE ?"))
545 }
546 _ => None, }
548}
549
550fn filter_to_sql_superset(
554 filter: &serde_json::Value,
555 params: &mut Vec<rusqlite::types::Value>,
556 indexed: &std::collections::HashSet<&str>,
557) -> Option<String> {
558 match filter {
559 serde_json::Value::Array(cs) => {
560 let mut parts = Vec::with_capacity(cs.len());
561 for c in cs {
562 parts.push(filter_to_sql_superset(c, params, indexed)?);
563 }
564 if parts.is_empty() {
565 return None;
566 }
567 Some(format!("({})", parts.join(" AND ")))
568 }
569 serde_json::Value::Object(obj) => {
570 if obj.contains_key("field") && obj.contains_key("op") && obj.contains_key("value") {
571 let field = obj.get("field")?.as_str()?;
572 let op = obj.get("op")?.as_str()?;
573 let value = obj.get("value")?;
574 leaf_sql_superset(field, op, value, params, indexed)
575 } else {
576 if obj.is_empty() {
577 return None;
578 }
579 let mut parts = Vec::with_capacity(obj.len());
580 for (k, v) in obj {
581 parts.push(leaf_sql_superset(k, "=", v, params, indexed)?);
582 }
583 Some(format!("({})", parts.join(" AND ")))
584 }
585 }
586 _ => None,
587 }
588}
589
590fn sqlite_to_json(v: &rusqlite::types::Value) -> Option<serde_json::Value> {
593 use rusqlite::types::Value as V;
594 match v {
595 V::Null => None,
596 V::Integer(i) => Some(serde_json::Value::Number((*i).into())),
597 V::Real(f) => serde_json::Number::from_f64(*f).map(serde_json::Value::Number),
598 V::Text(s) => Some(serde_json::Value::String(s.clone())),
599 V::Blob(_) => None,
600 }
601}
602
603#[cfg(test)]
604mod pool_tests {
605 use super::*;
606 use chrono::Utc;
607 use serde_json::json;
608 use std::sync::Arc;
609 use tempfile::tempdir;
610
611 use crate::types::VectorRecord;
612
613 fn dummy_record(id: &str) -> VectorRecord {
614 let now = Utc::now();
615 VectorRecord {
616 id: id.to_string(),
617 vector: vec![],
618 payload: json!({ "id": id }),
619 text: None,
620 created_at: now,
621 updated_at: now,
622 }
623 }
624
625 #[test]
627 fn test_pool_concurrent_reads() {
628 let dir = tempdir().unwrap();
629 let path = dir.path().join("conc_reads.db");
630
631 let store = MetadataStore::open(&path).unwrap();
632 for i in 0..5usize {
633 store
634 .upsert(&format!("r{i}"), i, &dummy_record(&format!("r{i}")))
635 .unwrap();
636 }
637
638 let store = Arc::new(store);
639 let mut handles = Vec::new();
640 for _ in 0..4 {
641 let s = Arc::clone(&store);
642 handles.push(std::thread::spawn(move || {
643 let active = s.list_active().expect("list_active failed");
644 assert_eq!(active.len(), 5, "expected 5 active records");
645 }));
646 }
647 for h in handles {
648 h.join().expect("thread panicked");
649 }
650 }
651
652 #[test]
654 fn test_pool_concurrent_upsert_and_read() {
655 let dir = tempdir().unwrap();
656 let path = dir.path().join("conc_rw.db");
657
658 let store = Arc::new(MetadataStore::open(&path).unwrap());
659 let mut handles = Vec::new();
660
661 for t in 0..2usize {
663 let s = Arc::clone(&store);
664 handles.push(std::thread::spawn(move || {
665 for i in 0..10usize {
666 let id = format!("t{t}r{i}");
667 s.upsert(&id, t * 10 + i, &dummy_record(&id))
668 .expect("upsert failed");
669 }
670 }));
671 }
672
673 for _ in 0..2usize {
675 let s = Arc::clone(&store);
676 handles.push(std::thread::spawn(move || {
677 for _ in 0..10 {
678 let _ = s.count_active().expect("count_active failed");
679 }
680 }));
681 }
682
683 for h in handles {
684 h.join().expect("thread panicked");
685 }
686
687 assert_eq!(store.count_active().unwrap(), 20);
689 }
690
691 #[test]
693 fn test_metadata_survives_pool_exhaustion() {
694 let dir = tempdir().unwrap();
695 let path = dir.path().join("pool2.db");
696
697 let store = MetadataStore::open_with_pool_size(&path, 2).unwrap();
698
699 for i in 0..20usize {
701 let id = format!("p{i}");
702 store
703 .upsert(&id, i, &dummy_record(&id))
704 .unwrap_or_else(|e| panic!("op {i} failed: {e}"));
705 }
706
707 assert_eq!(store.count_active().unwrap(), 20);
708 }
709}