1use fsqlite_error::{FrankenError, Result};
16use fsqlite_types::SqliteValue;
17use fsqlite_types::cx::Cx;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum ConstraintOp {
26 Eq,
27 Gt,
28 Le,
29 Lt,
30 Ge,
31 Match,
32 Like,
33 Glob,
34 Regexp,
35 Ne,
36 IsNot,
37 IsNotNull,
38 IsNull,
39 Is,
40}
41
42#[derive(Debug, Clone)]
44pub struct IndexConstraint {
45 pub column: i32,
47 pub op: ConstraintOp,
49 pub usable: bool,
51}
52
53#[derive(Debug, Clone)]
55pub struct IndexOrderBy {
56 pub column: i32,
58 pub desc: bool,
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct IndexConstraintUsage {
65 pub argv_index: i32,
68 pub omit: bool,
71}
72
73#[derive(Debug, Clone)]
80pub struct IndexInfo {
81 pub constraints: Vec<IndexConstraint>,
83 pub order_by: Vec<IndexOrderBy>,
85 pub constraint_usage: Vec<IndexConstraintUsage>,
87 pub idx_num: i32,
89 pub idx_str: Option<String>,
91 pub order_by_consumed: bool,
93 pub estimated_cost: f64,
95 pub estimated_rows: i64,
97}
98
99impl IndexInfo {
100 #[must_use]
102 pub fn new(constraints: Vec<IndexConstraint>, order_by: Vec<IndexOrderBy>) -> Self {
103 let usage_len = constraints.len();
104 Self {
105 constraints,
106 order_by,
107 constraint_usage: vec![IndexConstraintUsage::default(); usage_len],
108 idx_num: 0,
109 idx_str: None,
110 order_by_consumed: false,
111 estimated_cost: 1_000_000.0,
112 estimated_rows: 1_000_000,
113 }
114 }
115}
116
117#[derive(Debug, Default)]
126pub struct ColumnContext {
127 value: Option<SqliteValue>,
128}
129
130impl ColumnContext {
131 #[must_use]
133 pub fn new() -> Self {
134 Self { value: None }
135 }
136
137 pub fn set_value(&mut self, val: SqliteValue) {
139 self.value = Some(val);
140 }
141
142 pub fn take_value(&mut self) -> Option<SqliteValue> {
144 self.value.take()
145 }
146}
147
148#[allow(clippy::missing_errors_doc)]
167pub trait VirtualTable: Send + Sync {
168 type Cursor: VirtualTableCursor;
170
171 fn create(cx: &Cx, args: &[&str]) -> Result<Self>
176 where
177 Self: Sized,
178 {
179 Self::connect(cx, args)
180 }
181
182 fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
184 where
185 Self: Sized;
186
187 fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
189
190 fn open(&self) -> Result<Self::Cursor>;
192
193 fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
195 Ok(())
196 }
197
198 fn destroy(&mut self, cx: &Cx) -> Result<()> {
202 self.disconnect(cx)
203 }
204
205 fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
215 Err(FrankenError::ReadOnly)
216 }
217
218 fn begin(&mut self, _cx: &Cx) -> Result<()> {
220 Ok(())
221 }
222
223 fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
225 Ok(())
226 }
227
228 fn commit(&mut self, _cx: &Cx) -> Result<()> {
230 Ok(())
231 }
232
233 fn rollback(&mut self, _cx: &Cx) -> Result<()> {
235 Ok(())
236 }
237
238 fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
242 Err(FrankenError::Unsupported)
243 }
244
245 fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
247 Ok(())
248 }
249
250 fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
252 Ok(())
253 }
254
255 fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
257 Ok(())
258 }
259}
260
261#[allow(clippy::missing_errors_doc)]
276pub trait VirtualTableCursor: Send {
277 fn filter(
279 &mut self,
280 cx: &Cx,
281 idx_num: i32,
282 idx_str: Option<&str>,
283 args: &[SqliteValue],
284 ) -> Result<()>;
285
286 fn next(&mut self, cx: &Cx) -> Result<()>;
288
289 fn eof(&self) -> bool;
291
292 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
294
295 fn rowid(&self) -> Result<i64>;
297}
298
299#[cfg(test)]
304#[allow(clippy::too_many_lines)]
305mod tests {
306 use super::*;
307
308 struct GenerateSeries {
311 destroyed: bool,
312 }
313
314 struct GenerateSeriesCursor {
315 start: i64,
316 stop: i64,
317 current: i64,
318 }
319
320 impl VirtualTable for GenerateSeries {
321 type Cursor = GenerateSeriesCursor;
322
323 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
324 Ok(Self { destroyed: false })
325 }
326
327 fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
328 info.estimated_cost = 10.0;
329 info.estimated_rows = 100;
330 info.idx_num = 1;
331
332 if !info.constraints.is_empty() && info.constraints[0].usable {
334 info.constraint_usage[0].argv_index = 1;
335 info.constraint_usage[0].omit = true;
336 }
337 Ok(())
338 }
339
340 fn open(&self) -> Result<GenerateSeriesCursor> {
341 Ok(GenerateSeriesCursor {
342 start: 0,
343 stop: 0,
344 current: 0,
345 })
346 }
347
348 fn destroy(&mut self, _cx: &Cx) -> Result<()> {
349 self.destroyed = true;
350 Ok(())
351 }
352 }
353
354 impl VirtualTableCursor for GenerateSeriesCursor {
355 fn filter(
356 &mut self,
357 _cx: &Cx,
358 _idx_num: i32,
359 _idx_str: Option<&str>,
360 args: &[SqliteValue],
361 ) -> Result<()> {
362 self.start = args.first().map_or(1, SqliteValue::to_integer);
363 self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
364 self.current = self.start;
365 Ok(())
366 }
367
368 fn next(&mut self, _cx: &Cx) -> Result<()> {
369 self.current += 1;
370 Ok(())
371 }
372
373 fn eof(&self) -> bool {
374 self.current > self.stop
375 }
376
377 fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
378 ctx.set_value(SqliteValue::Integer(self.current));
379 Ok(())
380 }
381
382 fn rowid(&self) -> Result<i64> {
383 Ok(self.current)
384 }
385 }
386
387 struct ReadOnlyVtab;
390
391 struct ReadOnlyCursor;
392
393 impl VirtualTable for ReadOnlyVtab {
394 type Cursor = ReadOnlyCursor;
395
396 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
397 Ok(Self)
398 }
399
400 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
401 Ok(())
402 }
403
404 fn open(&self) -> Result<ReadOnlyCursor> {
405 Ok(ReadOnlyCursor)
406 }
407 }
408
409 impl VirtualTableCursor for ReadOnlyCursor {
410 fn filter(
411 &mut self,
412 _cx: &Cx,
413 _idx_num: i32,
414 _idx_str: Option<&str>,
415 _args: &[SqliteValue],
416 ) -> Result<()> {
417 Ok(())
418 }
419
420 fn next(&mut self, _cx: &Cx) -> Result<()> {
421 Ok(())
422 }
423
424 fn eof(&self) -> bool {
425 true
426 }
427
428 fn column(&self, _ctx: &mut ColumnContext, _col: i32) -> Result<()> {
429 Ok(())
430 }
431
432 fn rowid(&self) -> Result<i64> {
433 Ok(0)
434 }
435 }
436
437 struct WritableVtab {
440 rows: Vec<(i64, Vec<SqliteValue>)>,
441 next_rowid: i64,
442 }
443
444 struct WritableCursor {
445 rows: Vec<(i64, Vec<SqliteValue>)>,
446 pos: usize,
447 }
448
449 impl VirtualTable for WritableVtab {
450 type Cursor = WritableCursor;
451
452 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
453 Ok(Self {
454 rows: Vec::new(),
455 next_rowid: 1,
456 })
457 }
458
459 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
460 Ok(())
461 }
462
463 fn open(&self) -> Result<WritableCursor> {
464 Ok(WritableCursor {
465 rows: self.rows.clone(),
466 pos: 0,
467 })
468 }
469
470 fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
471 if args[0].is_null() {
473 let rowid = self.next_rowid;
475 self.next_rowid += 1;
476 let cols: Vec<SqliteValue> = args[2..].to_vec();
477 self.rows.push((rowid, cols));
478 return Ok(Some(rowid));
479 }
480 Ok(None)
481 }
482 }
483
484 impl VirtualTableCursor for WritableCursor {
485 fn filter(
486 &mut self,
487 _cx: &Cx,
488 _idx_num: i32,
489 _idx_str: Option<&str>,
490 _args: &[SqliteValue],
491 ) -> Result<()> {
492 self.pos = 0;
493 Ok(())
494 }
495
496 fn next(&mut self, _cx: &Cx) -> Result<()> {
497 self.pos += 1;
498 Ok(())
499 }
500
501 fn eof(&self) -> bool {
502 self.pos >= self.rows.len()
503 }
504
505 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
506 #[allow(clippy::cast_sign_loss)]
507 let col_idx = col as usize;
508 if let Some((_, cols)) = self.rows.get(self.pos) {
509 if let Some(val) = cols.get(col_idx) {
510 ctx.set_value(val.clone());
511 }
512 }
513 Ok(())
514 }
515
516 fn rowid(&self) -> Result<i64> {
517 self.rows
518 .get(self.pos)
519 .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
520 }
521 }
522
523 #[test]
526 fn test_vtab_create_vs_connect() {
527 let cx = Cx::new();
528
529 let vtab = GenerateSeries::create(&cx, &[]).unwrap();
531 assert!(!vtab.destroyed);
532
533 let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
535 assert!(!vtab2.destroyed);
536 }
537
538 #[test]
539 fn test_vtab_best_index_populates_info() {
540 let cx = Cx::new();
541 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
542
543 let mut info = IndexInfo::new(
544 vec![IndexConstraint {
545 column: 0,
546 op: ConstraintOp::Gt,
547 usable: true,
548 }],
549 vec![],
550 );
551
552 vtab.best_index(&mut info).unwrap();
553
554 assert_eq!(info.idx_num, 1);
555 assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
556 assert_eq!(info.estimated_rows, 100);
557 assert_eq!(info.constraint_usage[0].argv_index, 1);
558 assert!(info.constraint_usage[0].omit);
559 }
560
561 #[test]
562 fn test_vtab_cursor_filter_next_eof() {
563 let cx = Cx::new();
564 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
565 let mut cursor = vtab.open().unwrap();
566
567 cursor
568 .filter(
569 &cx,
570 0,
571 None,
572 &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
573 )
574 .unwrap();
575
576 let mut values = Vec::new();
577 while !cursor.eof() {
578 let mut ctx = ColumnContext::new();
579 cursor.column(&mut ctx, 0).unwrap();
580 let rowid = cursor.rowid().unwrap();
581 values.push((rowid, ctx.take_value().unwrap()));
582 cursor.next(&cx).unwrap();
583 }
584
585 assert_eq!(values.len(), 3);
586 assert_eq!(values[0], (1, SqliteValue::Integer(1)));
587 assert_eq!(values[1], (2, SqliteValue::Integer(2)));
588 assert_eq!(values[2], (3, SqliteValue::Integer(3)));
589 }
590
591 #[test]
592 fn test_vtab_update_insert() {
593 let cx = Cx::new();
594 let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
595
596 let result = vtab
599 .update(
600 &cx,
601 &[
602 SqliteValue::Null,
603 SqliteValue::Null,
604 SqliteValue::Text("hello".to_owned()),
605 ],
606 )
607 .unwrap();
608
609 assert_eq!(result, Some(1));
610 assert_eq!(vtab.rows.len(), 1);
611 assert_eq!(vtab.rows[0].0, 1);
612 }
613
614 #[test]
615 fn test_vtab_update_readonly_default() {
616 let cx = Cx::new();
617 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
618 let err = vtab.update(&cx, &[SqliteValue::Null]).unwrap_err();
619 assert!(matches!(err, FrankenError::ReadOnly));
620 }
621
622 #[test]
623 fn test_vtab_destroy_vs_disconnect() {
624 let cx = Cx::new();
625
626 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
628 vtab.disconnect(&cx).unwrap();
629 vtab.destroy(&cx).unwrap();
630
631 let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
633 assert!(!vtab.destroyed);
634 vtab.destroy(&cx).unwrap();
635 assert!(vtab.destroyed);
636 }
637
638 #[test]
639 fn test_vtab_cursor_send_but_not_sync() {
640 fn assert_send<T: Send>() {}
641 assert_send::<GenerateSeriesCursor>();
642
643 }
650
651 #[test]
652 fn test_column_context_lifecycle() {
653 let mut ctx = ColumnContext::new();
654 assert!(ctx.take_value().is_none());
655
656 ctx.set_value(SqliteValue::Integer(42));
657 assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
658
659 assert!(ctx.take_value().is_none());
661 }
662
663 #[test]
664 fn test_index_info_new() {
665 let info = IndexInfo::new(
666 vec![
667 IndexConstraint {
668 column: 0,
669 op: ConstraintOp::Eq,
670 usable: true,
671 },
672 IndexConstraint {
673 column: 1,
674 op: ConstraintOp::Gt,
675 usable: false,
676 },
677 ],
678 vec![IndexOrderBy {
679 column: 0,
680 desc: false,
681 }],
682 );
683
684 assert_eq!(info.constraints.len(), 2);
685 assert_eq!(info.order_by.len(), 1);
686 assert_eq!(info.constraint_usage.len(), 2);
687 assert_eq!(info.idx_num, 0);
688 assert!(info.idx_str.is_none());
689 assert!(!info.order_by_consumed);
690 }
691}