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