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