1use std::collections::BTreeMap;
16
17use rusqlite::{params, OptionalExtension};
18use serde::{Deserialize, Serialize};
19use uqa_core::{ArrayValue, DecimalValue, DocId, TemporalValue, Value};
20
21use super::{ManagedConnection, Result, SQLiteError};
22use crate::value_index_key::SQLiteValueIndexKey;
23
24fn encode_doc_id(doc_id: DocId) -> Result<i64> {
25 i64::try_from(doc_id).map_err(|_| {
26 SQLiteError::StorageBackend(format!(
27 "document id {doc_id} does not fit in SQLite INTEGER"
28 ))
29 })
30}
31
32fn decode_doc_id(doc_id: i64) -> Result<DocId> {
33 DocId::try_from(doc_id).map_err(|_| {
34 SQLiteError::StorageBackend(format!(
35 "invalid negative document id {doc_id} in persisted B-tree index"
36 ))
37 })
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(tag = "type", content = "value")]
42enum StoredValue {
43 Null,
44 Void,
45 Bool(bool),
46 Int(i64),
47 Float(u64),
49 Str(String),
50 FixedChar(String),
51 Bytes(Vec<u8>),
52 Temporal(TemporalValue),
53 Decimal(DecimalValue),
54 Json(String),
55 JsonB(String),
56 Array(ArrayValue),
57 List(Vec<StoredValue>),
58 Row(Vec<StoredValue>),
59 Record(Vec<(String, StoredValue)>),
60 Map(BTreeMap<String, StoredValue>),
61}
62
63impl From<&Value> for StoredValue {
64 fn from(value: &Value) -> Self {
65 match value {
66 Value::Null => Self::Null,
67 Value::Void => Self::Void,
68 Value::Bool(value) => Self::Bool(*value),
69 Value::Int(value) => Self::Int(*value),
70 Value::Float(value) => Self::Float(value.to_bits()),
71 Value::Str(value) => Self::Str(value.clone()),
72 Value::FixedChar(value) => Self::FixedChar(value.clone()),
73 Value::Bytes(value) => Self::Bytes(value.clone()),
74 Value::Temporal(value) => Self::Temporal(value.clone()),
75 Value::Decimal(value) => Self::Decimal(value.clone()),
76 Value::Json(value) => Self::Json(value.clone()),
77 Value::JsonB(value) => Self::JsonB(value.clone()),
78 Value::Array(value) => Self::Array(value.clone()),
79 Value::List(values) => Self::List(values.iter().map(Self::from).collect()),
80 Value::Row(values) => Self::Row(values.iter().map(Self::from).collect()),
81 Value::Record(fields) => Self::Record(
82 fields
83 .iter()
84 .map(|(name, value)| (name.clone(), Self::from(value)))
85 .collect(),
86 ),
87 Value::Map(values) => Self::Map(
88 values
89 .iter()
90 .map(|(key, value)| (key.clone(), Self::from(value)))
91 .collect(),
92 ),
93 }
94 }
95}
96
97impl StoredValue {
98 fn into_value(self) -> Value {
99 match self {
100 Self::Null => Value::Null,
101 Self::Void => Value::Void,
102 Self::Bool(value) => Value::Bool(value),
103 Self::Int(value) => Value::Int(value),
104 Self::Float(bits) => Value::Float(f64::from_bits(bits)),
105 Self::Str(value) => Value::Str(value),
106 Self::FixedChar(value) => Value::FixedChar(value),
107 Self::Bytes(value) => Value::Bytes(value),
108 Self::Temporal(value) => Value::Temporal(value),
109 Self::Decimal(value) => Value::Decimal(value),
110 Self::Json(value) => Value::Json(value),
111 Self::JsonB(value) => Value::JsonB(value),
112 Self::Array(value) => Value::Array(value),
113 Self::List(values) => Value::List(values.into_iter().map(Self::into_value).collect()),
114 Self::Row(values) => Value::Row(values.into_iter().map(Self::into_value).collect()),
115 Self::Record(fields) => Value::Record(
116 fields
117 .into_iter()
118 .map(|(name, value)| (name, value.into_value()))
119 .collect(),
120 ),
121 Self::Map(values) => Value::Map(
122 values
123 .into_iter()
124 .map(|(key, value)| (key, value.into_value()))
125 .collect(),
126 ),
127 }
128 }
129}
130
131fn encode_value(value: &Value) -> Result<String> {
132 Ok(serde_json::to_string(&StoredValue::from(value))?)
133}
134
135fn decode_value(encoded: &str) -> Result<Value> {
136 Ok(serde_json::from_str::<StoredValue>(encoded)?.into_value())
137}
138
139#[derive(Clone)]
140pub struct SQLiteBTreeIndexStore {
141 conn: ManagedConnection,
142}
143
144impl SQLiteBTreeIndexStore {
145 pub fn new(conn: ManagedConnection) -> Self {
146 Self { conn }
147 }
148
149 pub fn fields(&self, table: &str) -> Result<Vec<uqa_storage::ValueIndexKey>> {
150 self.conn.with(|conn| {
151 let mut stmt = conn.prepare_cached(
152 "SELECT field FROM _btree_indexes
153 WHERE table_name = ?1
154 ORDER BY field",
155 )?;
156 let rows = stmt.query_map([table], |row| {
157 row.get::<_, SQLiteValueIndexKey<uqa_storage::ValueIndexKey>>(0)
158 .map(|key| key.0)
159 })?;
160 let mut fields = Vec::new();
161 for row in rows {
162 fields.push(row?);
163 }
164 Ok(fields)
165 })
166 }
167
168 pub fn repairs(&self) -> Result<Vec<(String, uqa_storage::ValueIndexKey)>> {
169 self.conn.with(|conn| {
170 let mut stmt = conn.prepare_cached(
171 "SELECT table_name, field FROM _btree_index_repairs ORDER BY table_name, field",
172 )?;
173 let rows = stmt.query_map([], |row| {
174 Ok((
175 row.get::<_, String>(0)?,
176 row.get::<_, SQLiteValueIndexKey<uqa_storage::ValueIndexKey>>(1)?
177 .0,
178 ))
179 })?;
180 let mut repairs = Vec::new();
181 for row in rows {
182 repairs.push(row?);
183 }
184 Ok(repairs)
185 })
186 }
187
188 pub fn clear_repair(&self, table: &str, field: &uqa_storage::ValueIndexKey) -> Result<()> {
189 self.conn.with(|conn| {
190 conn.execute(
191 "DELETE FROM _btree_index_repairs
192 WHERE table_name = ?1 AND field = ?2",
193 params![table, SQLiteValueIndexKey(field)],
194 )?;
195 Ok(())
196 })
197 }
198
199 pub fn load(
202 &self,
203 table: &str,
204 field: &uqa_storage::ValueIndexKey,
205 ) -> Result<Option<Vec<(DocId, Value)>>> {
206 self.conn.with(|conn| {
207 let exists = conn
208 .prepare_cached(
209 "SELECT 1 FROM _btree_indexes
210 WHERE table_name = ?1 AND field = ?2",
211 )?
212 .query_row(params![table, SQLiteValueIndexKey(field)], |row| {
213 row.get::<_, i64>(0)
214 })
215 .optional()?
216 .is_some();
217 if !exists {
218 return Ok(None);
219 }
220
221 let mut stmt = conn.prepare_cached(
222 "SELECT doc_id, value_json
223 FROM _btree_index_entries
224 WHERE table_name = ?1 AND field = ?2
225 ORDER BY doc_id",
226 )?;
227 let mut rows = stmt.query(params![table, SQLiteValueIndexKey(field)])?;
228 let mut values = Vec::new();
229 while let Some(row) = rows.next()? {
230 let doc_id = decode_doc_id(row.get::<_, i64>(0)?)?;
231 let encoded = row.get::<_, String>(1)?;
232 values.push((doc_id, decode_value(&encoded)?));
233 }
234 Ok(Some(values))
235 })
236 }
237
238 pub fn replace(
240 &self,
241 table: &str,
242 field: &uqa_storage::ValueIndexKey,
243 values: &[(DocId, Value)],
244 ) -> Result<()> {
245 self.replace_many(table, &[(field, values)])
246 }
247
248 pub fn repair(
252 &self,
253 table: &str,
254 field: &uqa_storage::ValueIndexKey,
255 stale_doc_ids: &[DocId],
256 missing: &[(DocId, Value)],
257 ) -> Result<()> {
258 let stale_doc_ids = stale_doc_ids
259 .iter()
260 .map(|doc_id| encode_doc_id(*doc_id))
261 .collect::<Result<Vec<_>>>()?;
262 let missing = missing
263 .iter()
264 .map(|(doc_id, value)| Ok((encode_doc_id(*doc_id)?, encode_value(value)?)))
265 .collect::<Result<Vec<_>>>()?;
266 self.conn.with_mut(|conn| {
267 let tx = conn.savepoint()?;
268 tx.execute(
269 "INSERT OR IGNORE INTO _btree_indexes (table_name, field)
270 VALUES (?1, ?2)",
271 params![table, SQLiteValueIndexKey(field)],
272 )?;
273 {
274 let mut delete = tx.prepare_cached(
275 "DELETE FROM _btree_index_entries
276 WHERE table_name = ?1 AND field = ?2 AND doc_id = ?3",
277 )?;
278 for doc_id in &stale_doc_ids {
279 delete.execute(params![table, SQLiteValueIndexKey(field), doc_id])?;
280 }
281 }
282 {
283 let mut insert = tx.prepare_cached(
284 "INSERT INTO _btree_index_entries
285 (table_name, field, doc_id, value_json)
286 VALUES (?1, ?2, ?3, ?4)
287 ON CONFLICT (table_name, field, doc_id)
288 DO UPDATE SET value_json = excluded.value_json",
289 )?;
290 for (doc_id, value_json) in &missing {
291 insert.execute(params![
292 table,
293 SQLiteValueIndexKey(field),
294 doc_id,
295 value_json
296 ])?;
297 }
298 }
299 tx.commit()?;
300 Ok(())
301 })
302 }
303
304 pub fn replace_many(
308 &self,
309 table: &str,
310 indexes: &[(&uqa_storage::ValueIndexKey, &[(DocId, Value)])],
311 ) -> Result<()> {
312 let encoded = indexes
313 .iter()
314 .map(|(field, values)| {
315 let values = values
316 .iter()
317 .map(|(doc_id, value)| Ok((encode_doc_id(*doc_id)?, encode_value(value)?)))
318 .collect::<Result<Vec<_>>>()?;
319 Ok((*field, values))
320 })
321 .collect::<Result<Vec<_>>>()?;
322 self.conn.with_mut(|conn| {
323 let tx = conn.savepoint()?;
324 {
325 let mut delete = tx.prepare_cached(
326 "DELETE FROM _btree_index_entries
327 WHERE table_name = ?1 AND field = ?2",
328 )?;
329 let mut mark = tx.prepare_cached(
330 "INSERT OR IGNORE INTO _btree_indexes (table_name, field)
331 VALUES (?1, ?2)",
332 )?;
333 let mut insert = tx.prepare_cached(
334 "INSERT INTO _btree_index_entries
335 (table_name, field, doc_id, value_json)
336 VALUES (?1, ?2, ?3, ?4)",
337 )?;
338 for (field, values) in &encoded {
339 delete.execute(params![table, SQLiteValueIndexKey(*field)])?;
340 mark.execute(params![table, SQLiteValueIndexKey(*field)])?;
341 for (doc_id, value_json) in values {
342 insert.execute(params![
343 table,
344 SQLiteValueIndexKey(*field),
345 doc_id,
346 value_json
347 ])?;
348 }
349 }
350 }
351 tx.commit()?;
352 Ok(())
353 })
354 }
355
356 pub fn apply_write(
360 &self,
361 table: &str,
362 doc_id: DocId,
363 values: Option<&BTreeMap<uqa_storage::ValueIndexKey, Value>>,
364 ) -> Result<()> {
365 let doc_id = encode_doc_id(doc_id)?;
366 let encoded = values
367 .map(|values| {
368 values
369 .iter()
370 .map(|(field, value)| Ok((field.clone(), encode_value(value)?)))
371 .collect::<Result<Vec<_>>>()
372 })
373 .transpose()?;
374 self.conn.with_mut(|conn| {
375 let tx = conn.savepoint()?;
376 match encoded.as_ref() {
377 Some(values) => {
378 let mut stmt = tx.prepare_cached(
379 "INSERT INTO _btree_index_entries
380 (table_name, field, doc_id, value_json)
381 SELECT ?1, ?2, ?3, ?4
382 WHERE EXISTS (
383 SELECT 1 FROM _btree_indexes
384 WHERE table_name = ?1 AND field = ?2
385 )
386 ON CONFLICT (table_name, field, doc_id)
387 DO UPDATE SET value_json = excluded.value_json",
388 )?;
389 for (field, value_json) in values {
390 stmt.execute(params![
391 table,
392 SQLiteValueIndexKey(field),
393 doc_id,
394 value_json
395 ])?;
396 }
397 }
398 None => {
399 tx.execute(
400 "DELETE FROM _btree_index_entries
401 WHERE table_name = ?1 AND doc_id = ?2",
402 params![table, doc_id],
403 )?;
404 }
405 }
406 tx.commit()?;
407 Ok(())
408 })
409 }
410
411 pub fn drop_index(&self, table: &str, field: &uqa_storage::ValueIndexKey) -> Result<()> {
412 self.conn.with_mut(|conn| {
413 let tx = conn.savepoint()?;
414 tx.execute(
415 "DELETE FROM _btree_index_entries
416 WHERE table_name = ?1 AND field = ?2",
417 params![table, SQLiteValueIndexKey(field)],
418 )?;
419 tx.execute(
420 "DELETE FROM _btree_indexes
421 WHERE table_name = ?1 AND field = ?2",
422 params![table, SQLiteValueIndexKey(field)],
423 )?;
424 tx.commit()?;
425 Ok(())
426 })
427 }
428
429 pub fn clear_table(&self, table: &str) -> Result<()> {
431 self.conn.with(|conn| {
432 conn.execute(
433 "DELETE FROM _btree_index_entries WHERE table_name = ?1",
434 params![table],
435 )?;
436 Ok(())
437 })
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::Catalog;
445
446 fn store() -> SQLiteBTreeIndexStore {
447 let conn = ManagedConnection::open_in_memory().unwrap();
448 let _catalog = Catalog::open(conn.clone()).unwrap();
449 conn.with(|connection| {
450 connection.execute_batch(
451 "INSERT INTO _documents (table_name, doc_id, body)
452 VALUES ('messages', 1, '{}'), ('messages', 2, '{}');",
453 )?;
454 Ok(())
455 })
456 .unwrap();
457 SQLiteBTreeIndexStore::new(conn)
458 }
459
460 #[test]
461 fn tagged_values_round_trip_without_untagged_serde_ambiguity() {
462 let values = vec![
463 Value::Null,
464 Value::Bool(true),
465 Value::Int(7),
466 Value::Float(-0.0),
467 Value::Float(f64::NAN),
468 Value::Str("seven".into()),
469 Value::FixedChar("seven ".into()),
470 Value::Bytes(vec![1, 2, 3]),
471 Value::Json("{\"b\":2,\"a\":1}".into()),
472 Value::JsonB("{\"a\": 1, \"b\": 2}".into()),
473 Value::List(vec![Value::Int(1), Value::Int(2)]),
474 Value::Map(BTreeMap::from([("k".into(), Value::Str("v".into()))])),
475 ];
476 for value in values {
477 let decoded = decode_value(&encode_value(&value).unwrap()).unwrap();
478 match (&value, &decoded) {
479 (Value::Float(left), Value::Float(right)) if left.is_nan() => {
480 assert!(right.is_nan());
481 }
482 _ => assert_eq!(decoded, value),
483 }
484 }
485 }
486
487 #[test]
488 fn replace_load_write_delete_and_clear_round_trip() {
489 let store = store();
490 store
491 .replace(
492 "messages",
493 &"public_id".into(),
494 &[(1, Value::Str("m1".into())), (2, Value::Null)],
495 )
496 .unwrap();
497 assert_eq!(
498 store.load("messages", &"public_id".into()).unwrap(),
499 Some(vec![(1, Value::Str("m1".into())), (2, Value::Null)])
500 );
501 assert_eq!(
502 store.fields("messages").unwrap(),
503 vec![uqa_storage::ValueIndexKey::from("public_id")]
504 );
505
506 store
507 .apply_write(
508 "messages",
509 2,
510 Some(&BTreeMap::from([
511 ("public_id".into(), Value::Str("m2".into())),
512 ("not_built".into(), Value::Int(9)),
513 ])),
514 )
515 .unwrap();
516 store.apply_write("messages", 1, None).unwrap();
517 assert_eq!(
518 store.load("messages", &"public_id".into()).unwrap(),
519 Some(vec![(2, Value::Str("m2".into()))])
520 );
521
522 store.clear_table("messages").unwrap();
523 assert_eq!(
524 store.load("messages", &"public_id".into()).unwrap(),
525 Some(vec![])
526 );
527 store.drop_index("messages", &"public_id".into()).unwrap();
528 assert_eq!(store.load("messages", &"public_id".into()).unwrap(), None);
529 }
530
531 #[test]
532 fn sparse_repair_preserves_valid_postings() {
533 let store = store();
534 store
535 .replace(
536 "messages",
537 &"public_id".into(),
538 &[(1, Value::Str("m1".into())), (2, Value::Str("m2".into()))],
539 )
540 .unwrap();
541 let row_id_before: i64 = store
542 .conn
543 .with(|conn| {
544 Ok(conn.query_row(
545 "SELECT rowid FROM _btree_index_entries
546 WHERE table_name = 'messages'
547 AND field = 'public_id' AND doc_id = 2",
548 [],
549 |row| row.get(0),
550 )?)
551 })
552 .unwrap();
553 store
554 .conn
555 .with(|conn| {
556 conn.execute(
557 "INSERT INTO _documents (table_name, doc_id, body)
558 VALUES ('messages', 3, '{}')",
559 [],
560 )?;
561 Ok(())
562 })
563 .unwrap();
564
565 store
566 .repair(
567 "messages",
568 &"public_id".into(),
569 &[1],
570 &[(3, Value::Str("m3".into()))],
571 )
572 .unwrap();
573
574 assert_eq!(
575 store.load("messages", &"public_id".into()).unwrap(),
576 Some(vec![
577 (2, Value::Str("m2".into())),
578 (3, Value::Str("m3".into()))
579 ])
580 );
581 let row_id_after: i64 = store
582 .conn
583 .with(|conn| {
584 Ok(conn.query_row(
585 "SELECT rowid FROM _btree_index_entries
586 WHERE table_name = 'messages'
587 AND field = 'public_id' AND doc_id = 2",
588 [],
589 |row| row.get(0),
590 )?)
591 })
592 .unwrap();
593 assert_eq!(row_id_after, row_id_before);
594 }
595
596 #[test]
597 fn out_of_range_document_ids_fail_before_replacing_existing_entries() {
598 let store = store();
599 store
600 .replace(
601 "messages",
602 &"public_id".into(),
603 &[(1, Value::Str("m1".into()))],
604 )
605 .unwrap();
606
607 let error = store
608 .replace(
609 "messages",
610 &"public_id".into(),
611 &[(u64::MAX, Value::Str("overflow".into()))],
612 )
613 .unwrap_err();
614 assert!(error.to_string().contains("does not fit in SQLite INTEGER"));
615 assert_eq!(
616 store.load("messages", &"public_id".into()).unwrap(),
617 Some(vec![(1, Value::Str("m1".into()))])
618 );
619
620 let error = store
621 .apply_write(
622 "messages",
623 u64::MAX,
624 Some(&BTreeMap::from([(
625 "public_id".into(),
626 Value::Str("overflow".into()),
627 )])),
628 )
629 .unwrap_err();
630 assert!(error.to_string().contains("does not fit in SQLite INTEGER"));
631 }
632
633 #[test]
634 fn negative_persisted_document_id_is_reported_as_corruption() {
635 let store = store();
636 store
637 .replace(
638 "messages",
639 &"public_id".into(),
640 &[(1, Value::Str("m1".into()))],
641 )
642 .unwrap();
643 store
644 .conn
645 .with(|conn| {
646 conn.execute(
647 "INSERT INTO _documents (table_name, doc_id, body)
648 VALUES ('messages', -1, '{}')",
649 [],
650 )?;
651 conn.execute(
652 "INSERT INTO _btree_index_entries
653 (table_name, field, doc_id, value_json)
654 VALUES (?1, ?2, ?3, ?4)",
655 params![
656 "messages",
657 "public_id",
658 -1_i64,
659 encode_value(&Value::Str("corrupt".into()))?
660 ],
661 )?;
662 Ok(())
663 })
664 .unwrap();
665
666 let error = store.load("messages", &"public_id".into()).unwrap_err();
667 assert!(error
668 .to_string()
669 .contains("invalid negative document id -1"));
670 }
671}