1use std::any::Any;
16
17use fsqlite_error::{FrankenError, Result};
18use fsqlite_types::SqliteValue;
19use fsqlite_types::cx::Cx;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum ConstraintOp {
28 Eq,
29 Gt,
30 Le,
31 Lt,
32 Ge,
33 Match,
34 Like,
35 Glob,
36 Regexp,
37 Ne,
38 IsNot,
39 IsNotNull,
40 IsNull,
41 Is,
42}
43
44#[derive(Debug, Clone)]
46pub struct IndexConstraint {
47 pub column: i32,
49 pub op: ConstraintOp,
51 pub usable: bool,
53}
54
55#[derive(Debug, Clone)]
57pub struct IndexOrderBy {
58 pub column: i32,
60 pub desc: bool,
62}
63
64#[derive(Debug, Clone, Default)]
66pub struct IndexConstraintUsage {
67 pub argv_index: i32,
70 pub omit: bool,
73}
74
75#[derive(Debug, Clone)]
82pub struct IndexInfo {
83 pub constraints: Vec<IndexConstraint>,
85 pub order_by: Vec<IndexOrderBy>,
87 pub constraint_usage: Vec<IndexConstraintUsage>,
89 pub idx_num: i32,
91 pub idx_str: Option<String>,
93 pub order_by_consumed: bool,
95 pub estimated_cost: f64,
97 pub estimated_rows: i64,
99}
100
101impl IndexInfo {
102 #[must_use]
104 pub fn new(constraints: Vec<IndexConstraint>, order_by: Vec<IndexOrderBy>) -> Self {
105 let usage_len = constraints.len();
106 Self {
107 constraints,
108 order_by,
109 constraint_usage: vec![IndexConstraintUsage::default(); usage_len],
110 idx_num: 0,
111 idx_str: None,
112 order_by_consumed: false,
113 estimated_cost: 1_000_000.0,
114 estimated_rows: 1_000_000,
115 }
116 }
117}
118
119#[derive(Debug, Default)]
128pub struct ColumnContext {
129 value: Option<SqliteValue>,
130}
131
132impl ColumnContext {
133 #[must_use]
135 pub fn new() -> Self {
136 Self { value: None }
137 }
138
139 pub fn set_value(&mut self, val: SqliteValue) {
141 self.value = Some(val);
142 }
143
144 pub fn take_value(&mut self) -> Option<SqliteValue> {
146 self.value.take()
147 }
148}
149
150#[derive(Debug, Clone)]
156pub struct TransactionalVtabState<S: Clone> {
157 base_snapshot: Option<S>,
158 savepoints: Vec<(i32, S)>,
159}
160
161impl<S: Clone> Default for TransactionalVtabState<S> {
162 fn default() -> Self {
163 Self {
164 base_snapshot: None,
165 savepoints: Vec::new(),
166 }
167 }
168}
169
170impl<S: Clone> TransactionalVtabState<S> {
171 pub fn begin(&mut self, snapshot: S) {
173 if self.base_snapshot.is_none() {
174 self.base_snapshot = Some(snapshot);
175 self.savepoints.clear();
176 }
177 }
178
179 pub fn commit(&mut self) {
181 self.base_snapshot = None;
182 self.savepoints.clear();
183 }
184
185 pub fn rollback(&mut self) -> Option<S> {
187 let snapshot = self.base_snapshot.take();
188 self.savepoints.clear();
189 snapshot
190 }
191
192 pub fn savepoint(&mut self, level: i32, snapshot: S) {
194 if self.base_snapshot.is_none() {
195 return;
196 }
197 self.savepoints.retain(|(existing, _)| *existing < level);
198 self.savepoints.push((level, snapshot));
199 }
200
201 pub fn release(&mut self, level: i32) {
203 if self.base_snapshot.is_none() {
204 return;
205 }
206 self.savepoints.retain(|(existing, _)| *existing < level);
207 }
208
209 pub fn rollback_to(&mut self, level: i32) -> Option<S> {
217 self.base_snapshot.as_ref()?;
218 let snapshot = self
219 .savepoints
220 .iter()
221 .rfind(|(existing, _)| *existing == level)
222 .map(|(_, snapshot)| snapshot.clone())
223 .or_else(|| self.base_snapshot.clone());
224 if snapshot.is_some() {
225 self.savepoints.retain(|(existing, _)| *existing <= level);
226 }
227 snapshot
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
237pub enum ShadowTableKind {
238 #[default]
240 Ordinary,
241 Shadow,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub struct ShadowTablePolicy {
249 pub kind: ShadowTableKind,
251}
252
253impl ShadowTablePolicy {
254 #[must_use]
256 pub const fn ordinary() -> Self {
257 Self {
258 kind: ShadowTableKind::Ordinary,
259 }
260 }
261
262 #[must_use]
264 pub const fn owned_shadow() -> Self {
265 Self {
266 kind: ShadowTableKind::Shadow,
267 }
268 }
269
270 #[must_use]
272 pub const fn is_shadow(self) -> bool {
273 matches!(self.kind, ShadowTableKind::Shadow)
274 }
275}
276
277impl Default for ShadowTablePolicy {
278 fn default() -> Self {
279 Self::ordinary()
280 }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
285pub enum VtabLifecyclePolicy {
286 #[default]
288 Simple,
289 SeparateCreateAndConnect,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
295pub enum VtabIntegrityPolicy {
296 #[default]
298 None,
299 ShadowAware,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
305pub struct VtabRiskLevel {
306 pub innocuous: bool,
308 pub direct_only: bool,
310 pub uses_all_schemas: bool,
312}
313
314impl VtabRiskLevel {
315 #[must_use]
317 pub const fn innocuous() -> Self {
318 Self {
319 innocuous: true,
320 direct_only: false,
321 uses_all_schemas: false,
322 }
323 }
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329pub struct VtabModuleMetadata {
330 pub owns_shadow_tables: bool,
332 pub lifecycle: VtabLifecyclePolicy,
334 pub integrity: VtabIntegrityPolicy,
336 pub risk: VtabRiskLevel,
338}
339
340impl VtabModuleMetadata {
341 #[must_use]
343 pub const fn ordinary() -> Self {
344 Self {
345 owns_shadow_tables: false,
346 lifecycle: VtabLifecyclePolicy::Simple,
347 integrity: VtabIntegrityPolicy::None,
348 risk: VtabRiskLevel::innocuous(),
349 }
350 }
351
352 #[must_use]
354 pub const fn shadow_owning(
355 lifecycle: VtabLifecyclePolicy,
356 integrity: VtabIntegrityPolicy,
357 risk: VtabRiskLevel,
358 ) -> Self {
359 Self {
360 owns_shadow_tables: true,
361 lifecycle,
362 integrity,
363 risk,
364 }
365 }
366}
367
368impl Default for VtabModuleMetadata {
369 fn default() -> Self {
370 Self::ordinary()
371 }
372}
373
374#[allow(clippy::missing_errors_doc)]
393pub trait VirtualTable: Send + Sync {
394 type Cursor: VirtualTableCursor;
396
397 fn module_metadata(_args: &[&str]) -> VtabModuleMetadata
399 where
400 Self: Sized,
401 {
402 VtabModuleMetadata::ordinary()
403 }
404
405 fn shadow_table_policy(_vtab_name: &str, _table_name: &str) -> ShadowTablePolicy
408 where
409 Self: Sized,
410 {
411 ShadowTablePolicy::ordinary()
412 }
413
414 fn create(cx: &Cx, args: &[&str]) -> Result<Self>
419 where
420 Self: Sized,
421 {
422 Self::connect(cx, args)
423 }
424
425 fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
427 where
428 Self: Sized;
429
430 fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
432
433 fn open(&self) -> Result<Self::Cursor>;
435
436 fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
438 Ok(())
439 }
440
441 fn destroy(&mut self, cx: &Cx) -> Result<()> {
445 self.disconnect(cx)
446 }
447
448 fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
458 Err(FrankenError::ReadOnly)
459 }
460
461 fn begin(&mut self, _cx: &Cx) -> Result<()> {
463 Ok(())
464 }
465
466 fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
468 Ok(())
469 }
470
471 fn commit(&mut self, _cx: &Cx) -> Result<()> {
473 Ok(())
474 }
475
476 fn rollback(&mut self, _cx: &Cx) -> Result<()> {
478 Ok(())
479 }
480
481 fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
485 Err(FrankenError::Unsupported)
486 }
487
488 fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
490 Ok(())
491 }
492
493 fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
495 Ok(())
496 }
497
498 fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
500 Ok(())
501 }
502}
503
504#[allow(clippy::missing_errors_doc)]
519pub trait VirtualTableCursor: Send {
520 fn filter(
522 &mut self,
523 cx: &Cx,
524 idx_num: i32,
525 idx_str: Option<&str>,
526 args: &[SqliteValue],
527 ) -> Result<()>;
528
529 fn next(&mut self, cx: &Cx) -> Result<()>;
531
532 fn eof(&self) -> bool;
534
535 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
537
538 fn rowid(&self) -> Result<i64>;
540}
541
542#[allow(clippy::missing_errors_doc)]
552pub trait VtabModuleFactory: Send + Sync {
553 fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>>;
555
556 fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
558 self.create(cx, args)
559 }
560
561 fn column_info(&self, _args: &[&str]) -> Vec<(String, char)> {
563 Vec::new()
564 }
565
566 fn module_metadata(&self, _args: &[&str]) -> VtabModuleMetadata {
568 VtabModuleMetadata::ordinary()
569 }
570
571 fn shadow_table_policy(&self, _vtab_name: &str, _table_name: &str) -> ShadowTablePolicy {
574 ShadowTablePolicy::ordinary()
575 }
576}
577
578#[allow(clippy::missing_errors_doc)]
580pub trait ErasedVtabInstance: Send + Sync {
581 fn as_any(&self) -> &dyn Any;
583 fn as_any_mut(&mut self) -> &mut dyn Any;
585 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
587 fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
589 fn begin(&mut self, cx: &Cx) -> Result<()>;
591 fn sync_txn(&mut self, cx: &Cx) -> Result<()>;
593 fn commit(&mut self, cx: &Cx) -> Result<()>;
595 fn rollback(&mut self, cx: &Cx) -> Result<()>;
597 fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()>;
599 fn release(&mut self, cx: &Cx, n: i32) -> Result<()>;
601 fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()>;
603 fn destroy(&mut self, cx: &Cx) -> Result<()>;
605 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
607 fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
609}
610
611#[allow(clippy::missing_errors_doc)]
613pub trait ErasedVtabCursor: Send {
614 fn erased_filter(
616 &mut self,
617 cx: &Cx,
618 idx_num: i32,
619 idx_str: Option<&str>,
620 args: &[SqliteValue],
621 ) -> Result<()>;
622 fn erased_next(&mut self, cx: &Cx) -> Result<()>;
624 fn erased_eof(&self) -> bool;
626 fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
628 fn erased_rowid(&self) -> Result<i64>;
630}
631
632impl<C: VirtualTableCursor + 'static> ErasedVtabCursor for C {
634 fn erased_filter(
635 &mut self,
636 cx: &Cx,
637 idx_num: i32,
638 idx_str: Option<&str>,
639 args: &[SqliteValue],
640 ) -> Result<()> {
641 VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
642 }
643 fn erased_next(&mut self, cx: &Cx) -> Result<()> {
644 VirtualTableCursor::next(self, cx)
645 }
646 fn erased_eof(&self) -> bool {
647 VirtualTableCursor::eof(self)
648 }
649 fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
650 VirtualTableCursor::column(self, ctx, col)
651 }
652 fn erased_rowid(&self) -> Result<i64> {
653 VirtualTableCursor::rowid(self)
654 }
655}
656
657impl<T: VirtualTable + 'static> ErasedVtabInstance for T
659where
660 T::Cursor: 'static,
661{
662 fn as_any(&self) -> &dyn Any {
663 self
664 }
665
666 fn as_any_mut(&mut self) -> &mut dyn Any {
667 self
668 }
669
670 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
671 let cursor = VirtualTable::open(self)?;
672 Ok(Box::new(cursor))
673 }
674 fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
675 VirtualTable::update(self, cx, args)
676 }
677 fn begin(&mut self, cx: &Cx) -> Result<()> {
678 VirtualTable::begin(self, cx)
679 }
680 fn sync_txn(&mut self, cx: &Cx) -> Result<()> {
681 VirtualTable::sync_txn(self, cx)
682 }
683 fn commit(&mut self, cx: &Cx) -> Result<()> {
684 VirtualTable::commit(self, cx)
685 }
686 fn rollback(&mut self, cx: &Cx) -> Result<()> {
687 VirtualTable::rollback(self, cx)
688 }
689 fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()> {
690 VirtualTable::savepoint(self, cx, n)
691 }
692 fn release(&mut self, cx: &Cx, n: i32) -> Result<()> {
693 VirtualTable::release(self, cx, n)
694 }
695 fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()> {
696 VirtualTable::rollback_to(self, cx, n)
697 }
698 fn destroy(&mut self, cx: &Cx) -> Result<()> {
699 VirtualTable::destroy(self, cx)
700 }
701 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
702 VirtualTable::rename(self, cx, new_name)
703 }
704 fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
705 VirtualTable::best_index(self, info)
706 }
707}
708
709pub fn module_factory_from<T>() -> impl VtabModuleFactory
711where
712 T: VirtualTable + 'static,
713 T::Cursor: 'static,
714{
715 struct Factory<T: Send + Sync>(std::marker::PhantomData<T>);
716
717 impl<T: VirtualTable + 'static> VtabModuleFactory for Factory<T>
718 where
719 T::Cursor: 'static,
720 {
721 fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
722 let vtab = T::create(cx, args)?;
723 Ok(Box::new(vtab))
724 }
725 fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
726 let vtab = T::connect(cx, args)?;
727 Ok(Box::new(vtab))
728 }
729
730 fn module_metadata(&self, args: &[&str]) -> VtabModuleMetadata {
731 T::module_metadata(args)
732 }
733
734 fn shadow_table_policy(&self, vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
735 T::shadow_table_policy(vtab_name, table_name)
736 }
737 }
738
739 Factory::<T>(std::marker::PhantomData)
740}
741
742#[cfg(test)]
747#[allow(clippy::too_many_lines)]
748mod tests {
749 use super::*;
750
751 struct GenerateSeries {
754 destroyed: bool,
755 }
756
757 struct GenerateSeriesCursor {
758 start: i64,
759 stop: i64,
760 current: i64,
761 }
762
763 impl VirtualTable for GenerateSeries {
764 type Cursor = GenerateSeriesCursor;
765
766 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
767 Ok(Self { destroyed: false })
768 }
769
770 fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
771 info.estimated_cost = 10.0;
772 info.estimated_rows = 100;
773 info.idx_num = 1;
774
775 if !info.constraints.is_empty() && info.constraints[0].usable {
777 info.constraint_usage[0].argv_index = 1;
778 info.constraint_usage[0].omit = true;
779 }
780 Ok(())
781 }
782
783 fn open(&self) -> Result<GenerateSeriesCursor> {
784 Ok(GenerateSeriesCursor {
785 start: 0,
786 stop: 0,
787 current: 0,
788 })
789 }
790
791 fn destroy(&mut self, _cx: &Cx) -> Result<()> {
792 self.destroyed = true;
793 Ok(())
794 }
795 }
796
797 impl VirtualTableCursor for GenerateSeriesCursor {
798 fn filter(
799 &mut self,
800 _cx: &Cx,
801 _idx_num: i32,
802 _idx_str: Option<&str>,
803 args: &[SqliteValue],
804 ) -> Result<()> {
805 self.start = args.first().map_or(1, SqliteValue::to_integer);
806 self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
807 self.current = self.start;
808 Ok(())
809 }
810
811 fn next(&mut self, _cx: &Cx) -> Result<()> {
812 self.current += 1;
813 Ok(())
814 }
815
816 fn eof(&self) -> bool {
817 self.current > self.stop
818 }
819
820 fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
821 if self.eof() {
822 ctx.set_value(SqliteValue::Null);
823 return Ok(());
824 }
825 ctx.set_value(SqliteValue::Integer(self.current));
826 Ok(())
827 }
828
829 fn rowid(&self) -> Result<i64> {
830 Ok(if self.eof() { 0 } else { self.current })
831 }
832 }
833
834 struct ReadOnlyVtab;
837
838 struct ReadOnlyCursor;
839
840 impl VirtualTable for ReadOnlyVtab {
841 type Cursor = ReadOnlyCursor;
842
843 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
844 Ok(Self)
845 }
846
847 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
848 Ok(())
849 }
850
851 fn open(&self) -> Result<ReadOnlyCursor> {
852 Ok(ReadOnlyCursor)
853 }
854 }
855
856 impl VirtualTableCursor for ReadOnlyCursor {
857 fn filter(
858 &mut self,
859 _cx: &Cx,
860 _idx_num: i32,
861 _idx_str: Option<&str>,
862 _args: &[SqliteValue],
863 ) -> Result<()> {
864 Ok(())
865 }
866
867 fn next(&mut self, _cx: &Cx) -> Result<()> {
868 Ok(())
869 }
870
871 fn eof(&self) -> bool {
872 true
873 }
874
875 fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
876 ctx.set_value(SqliteValue::Null);
877 Ok(())
878 }
879
880 fn rowid(&self) -> Result<i64> {
881 Ok(0)
882 }
883 }
884
885 struct WritableVtab {
888 rows: Vec<(i64, Vec<SqliteValue>)>,
889 next_rowid: i64,
890 }
891
892 struct WritableCursor {
893 rows: Vec<(i64, Vec<SqliteValue>)>,
894 pos: usize,
895 }
896
897 impl VirtualTable for WritableVtab {
898 type Cursor = WritableCursor;
899
900 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
901 Ok(Self {
902 rows: Vec::new(),
903 next_rowid: 1,
904 })
905 }
906
907 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
908 Ok(())
909 }
910
911 fn open(&self) -> Result<WritableCursor> {
912 Ok(WritableCursor {
913 rows: self.rows.clone(),
914 pos: 0,
915 })
916 }
917
918 fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
919 if args[0].is_null() {
921 let rowid = self.next_rowid;
923 self.next_rowid += 1;
924 let cols: Vec<SqliteValue> = args[2..].to_vec();
925 self.rows.push((rowid, cols));
926 return Ok(Some(rowid));
927 }
928 Ok(None)
929 }
930 }
931
932 impl VirtualTableCursor for WritableCursor {
933 fn filter(
934 &mut self,
935 _cx: &Cx,
936 _idx_num: i32,
937 _idx_str: Option<&str>,
938 _args: &[SqliteValue],
939 ) -> Result<()> {
940 self.pos = 0;
941 Ok(())
942 }
943
944 fn next(&mut self, _cx: &Cx) -> Result<()> {
945 self.pos += 1;
946 Ok(())
947 }
948
949 fn eof(&self) -> bool {
950 self.pos >= self.rows.len()
951 }
952
953 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
954 if self.eof() {
955 ctx.set_value(SqliteValue::Null);
956 return Ok(());
957 }
958
959 #[allow(clippy::cast_sign_loss)]
960 let col_idx = col as usize;
961 if let Some((_, cols)) = self.rows.get(self.pos) {
962 if let Some(val) = cols.get(col_idx) {
963 ctx.set_value(val.clone());
964 return Ok(());
965 }
966 }
967 ctx.set_value(SqliteValue::Null);
968 Ok(())
969 }
970
971 fn rowid(&self) -> Result<i64> {
972 self.rows
973 .get(self.pos)
974 .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
975 }
976 }
977
978 struct ShadowOwningVtab;
979
980 impl VirtualTable for ShadowOwningVtab {
981 type Cursor = ReadOnlyCursor;
982
983 fn module_metadata(_args: &[&str]) -> VtabModuleMetadata {
984 VtabModuleMetadata::shadow_owning(
985 VtabLifecyclePolicy::SeparateCreateAndConnect,
986 VtabIntegrityPolicy::ShadowAware,
987 VtabRiskLevel {
988 innocuous: false,
989 direct_only: true,
990 uses_all_schemas: false,
991 },
992 )
993 }
994
995 fn shadow_table_policy(vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
996 let Some((owner, suffix)) = table_name.rsplit_once('_') else {
997 return ShadowTablePolicy::ordinary();
998 };
999
1000 if owner == vtab_name
1001 && matches!(suffix, "config" | "content" | "data" | "docsize" | "idx")
1002 {
1003 return ShadowTablePolicy::owned_shadow();
1004 }
1005
1006 ShadowTablePolicy::ordinary()
1007 }
1008
1009 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1010 Ok(Self)
1011 }
1012
1013 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1014 Ok(())
1015 }
1016
1017 fn open(&self) -> Result<Self::Cursor> {
1018 Ok(ReadOnlyCursor)
1019 }
1020 }
1021
1022 #[derive(Debug, Clone, PartialEq, Eq)]
1023 struct HookSnapshot {
1024 version: i32,
1025 }
1026
1027 struct HookAwareVtab {
1028 version: i32,
1029 syncs: usize,
1030 tx_state: TransactionalVtabState<HookSnapshot>,
1031 }
1032
1033 impl VirtualTable for HookAwareVtab {
1034 type Cursor = ReadOnlyCursor;
1035
1036 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1037 Ok(Self {
1038 version: 7,
1039 syncs: 0,
1040 tx_state: TransactionalVtabState::default(),
1041 })
1042 }
1043
1044 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1045 Ok(())
1046 }
1047
1048 fn open(&self) -> Result<Self::Cursor> {
1049 Ok(ReadOnlyCursor)
1050 }
1051
1052 fn begin(&mut self, _cx: &Cx) -> Result<()> {
1053 self.tx_state.begin(HookSnapshot {
1054 version: self.version,
1055 });
1056 Ok(())
1057 }
1058
1059 fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
1060 self.syncs += 1;
1061 Ok(())
1062 }
1063
1064 fn savepoint(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1065 self.tx_state.savepoint(
1066 n,
1067 HookSnapshot {
1068 version: self.version,
1069 },
1070 );
1071 Ok(())
1072 }
1073
1074 fn release(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1075 self.tx_state.release(n);
1076 Ok(())
1077 }
1078
1079 fn rollback_to(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1080 if let Some(snapshot) = self.tx_state.rollback_to(n) {
1081 self.version = snapshot.version;
1082 }
1083 Ok(())
1084 }
1085
1086 fn commit(&mut self, _cx: &Cx) -> Result<()> {
1087 self.tx_state.commit();
1088 Ok(())
1089 }
1090
1091 fn rollback(&mut self, _cx: &Cx) -> Result<()> {
1092 if let Some(snapshot) = self.tx_state.rollback() {
1093 self.version = snapshot.version;
1094 }
1095 Ok(())
1096 }
1097 }
1098
1099 #[test]
1102 fn test_vtab_create_vs_connect() {
1103 let cx = Cx::new();
1104
1105 let vtab = GenerateSeries::create(&cx, &[]).unwrap();
1107 assert!(!vtab.destroyed);
1108
1109 let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
1111 assert!(!vtab2.destroyed);
1112 }
1113
1114 #[test]
1115 fn test_vtab_best_index_populates_info() {
1116 let cx = Cx::new();
1117 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1118
1119 let mut info = IndexInfo::new(
1120 vec![IndexConstraint {
1121 column: 0,
1122 op: ConstraintOp::Gt,
1123 usable: true,
1124 }],
1125 vec![],
1126 );
1127
1128 VirtualTable::best_index(&vtab, &mut info).unwrap();
1129
1130 assert_eq!(info.idx_num, 1);
1131 assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
1132 assert_eq!(info.estimated_rows, 100);
1133 assert_eq!(info.constraint_usage[0].argv_index, 1);
1134 assert!(info.constraint_usage[0].omit);
1135 }
1136
1137 #[test]
1138 fn test_vtab_cursor_filter_next_eof() {
1139 let cx = Cx::new();
1140 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1141 let mut cursor = vtab.open().unwrap();
1142
1143 cursor
1144 .filter(
1145 &cx,
1146 0,
1147 None,
1148 &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
1149 )
1150 .unwrap();
1151
1152 let mut values = Vec::new();
1153 while !cursor.eof() {
1154 let mut ctx = ColumnContext::new();
1155 cursor.column(&mut ctx, 0).unwrap();
1156 let rowid = cursor.rowid().unwrap();
1157 values.push((rowid, ctx.take_value().unwrap()));
1158 cursor.next(&cx).unwrap();
1159 }
1160
1161 assert_eq!(values.len(), 3);
1162 assert_eq!(values[0], (1, SqliteValue::Integer(1)));
1163 assert_eq!(values[1], (2, SqliteValue::Integer(2)));
1164 assert_eq!(values[2], (3, SqliteValue::Integer(3)));
1165 }
1166
1167 #[test]
1168 fn test_generate_series_cursor_past_end_returns_null_and_zero_rowid() {
1169 let cx = Cx::new();
1170 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1171 let mut cursor = vtab.open().unwrap();
1172
1173 cursor
1174 .filter(
1175 &cx,
1176 0,
1177 None,
1178 &[SqliteValue::Integer(1), SqliteValue::Integer(1)],
1179 )
1180 .unwrap();
1181 cursor.next(&cx).unwrap();
1182 assert!(cursor.eof());
1183
1184 let mut ctx = ColumnContext::new();
1185 cursor.column(&mut ctx, 0).unwrap();
1186 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1187 assert_eq!(cursor.rowid().unwrap(), 0);
1188 }
1189
1190 #[test]
1191 fn test_writable_cursor_missing_column_returns_null() {
1192 let cx = Cx::new();
1193 let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1194 VirtualTable::update(
1195 &mut vtab,
1196 &cx,
1197 &[
1198 SqliteValue::Null,
1199 SqliteValue::Null,
1200 SqliteValue::Text("hello".into()),
1201 ],
1202 )
1203 .unwrap();
1204
1205 let mut cursor = vtab.open().unwrap();
1206 cursor.filter(&cx, 0, None, &[]).unwrap();
1207
1208 let mut ctx = ColumnContext::new();
1209 cursor.column(&mut ctx, 3).unwrap();
1210 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1211
1212 cursor.next(&cx).unwrap();
1213 assert!(cursor.eof());
1214 cursor.column(&mut ctx, 0).unwrap();
1215 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1216 assert_eq!(cursor.rowid().unwrap(), 0);
1217 }
1218
1219 #[test]
1220 fn test_vtab_update_insert() {
1221 let cx = Cx::new();
1222 let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1223
1224 let result = VirtualTable::update(
1227 &mut vtab,
1228 &cx,
1229 &[
1230 SqliteValue::Null,
1231 SqliteValue::Null,
1232 SqliteValue::Text("hello".into()),
1233 ],
1234 )
1235 .unwrap();
1236
1237 assert_eq!(result, Some(1));
1238 assert_eq!(vtab.rows.len(), 1);
1239 assert_eq!(vtab.rows[0].0, 1);
1240 }
1241
1242 #[test]
1243 fn test_vtab_update_readonly_default() {
1244 let cx = Cx::new();
1245 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1246 let err = VirtualTable::update(&mut vtab, &cx, &[SqliteValue::Null]).unwrap_err();
1247 assert!(matches!(err, FrankenError::ReadOnly));
1248 }
1249
1250 #[test]
1251 fn test_vtab_destroy_vs_disconnect() {
1252 let cx = Cx::new();
1253
1254 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1256 VirtualTable::disconnect(&mut vtab, &cx).unwrap();
1257 VirtualTable::destroy(&mut vtab, &cx).unwrap();
1258
1259 let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1261 assert!(!vtab.destroyed);
1262 VirtualTable::destroy(&mut vtab, &cx).unwrap();
1263 assert!(vtab.destroyed);
1264 }
1265
1266 #[test]
1267 fn test_vtab_cursor_send_but_not_sync() {
1268 fn assert_send<T: Send>() {}
1269 assert_send::<GenerateSeriesCursor>();
1270
1271 }
1278
1279 #[test]
1280 fn test_column_context_lifecycle() {
1281 let mut ctx = ColumnContext::new();
1282 assert!(ctx.take_value().is_none());
1283
1284 ctx.set_value(SqliteValue::Integer(42));
1285 assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
1286
1287 assert!(ctx.take_value().is_none());
1289 }
1290
1291 #[test]
1292 fn test_index_info_new() {
1293 let info = IndexInfo::new(
1294 vec![
1295 IndexConstraint {
1296 column: 0,
1297 op: ConstraintOp::Eq,
1298 usable: true,
1299 },
1300 IndexConstraint {
1301 column: 1,
1302 op: ConstraintOp::Gt,
1303 usable: false,
1304 },
1305 ],
1306 vec![IndexOrderBy {
1307 column: 0,
1308 desc: false,
1309 }],
1310 );
1311
1312 assert_eq!(info.constraints.len(), 2);
1313 assert_eq!(info.order_by.len(), 1);
1314 assert_eq!(info.constraint_usage.len(), 2);
1315 assert_eq!(info.idx_num, 0);
1316 assert!(info.idx_str.is_none());
1317 assert!(!info.order_by_consumed);
1318 }
1319
1320 #[test]
1321 fn test_transactional_vtab_state_tracks_savepoints() {
1322 let mut state = TransactionalVtabState::default();
1323
1324 state.begin(1_i32);
1325 state.savepoint(0, 2);
1326 state.savepoint(1, 3);
1327 assert_eq!(state.rollback_to(1), Some(3));
1328 state.release(1);
1329 assert_eq!(state.rollback(), Some(1));
1330 assert_eq!(state.rollback(), None);
1331 }
1332
1333 #[test]
1334 fn test_transactional_vtab_state_uses_base_for_late_enlistment() {
1335 let mut state = TransactionalVtabState::default();
1336
1337 state.begin(7_i32);
1338 state.savepoint(2, 9);
1339
1340 assert_eq!(state.rollback_to(1), Some(7));
1341 assert_eq!(state.rollback(), Some(7));
1342 }
1343
1344 #[test]
1345 fn test_shadow_table_policy_defaults_to_ordinary() {
1346 let policy = ReadOnlyVtab::shadow_table_policy("docs", "docs_data");
1347 assert_eq!(policy, ShadowTablePolicy::ordinary());
1348 assert!(!policy.is_shadow());
1349 }
1350
1351 #[test]
1352 fn test_shadow_owning_module_metadata_is_forwarded_by_factory() {
1353 let factory = module_factory_from::<ShadowOwningVtab>();
1354 let metadata = factory.module_metadata(&[]);
1355
1356 assert!(metadata.owns_shadow_tables);
1357 assert_eq!(
1358 metadata.lifecycle,
1359 VtabLifecyclePolicy::SeparateCreateAndConnect
1360 );
1361 assert_eq!(metadata.integrity, VtabIntegrityPolicy::ShadowAware);
1362 assert!(metadata.risk.direct_only);
1363 assert!(!metadata.risk.innocuous);
1364 }
1365
1366 #[test]
1367 fn test_shadow_owning_module_matches_owned_shadow_tables() {
1368 let factory = module_factory_from::<ShadowOwningVtab>();
1369
1370 let owned = factory.shadow_table_policy("docs", "docs_data");
1371 let other_owner = factory.shadow_table_policy("docs", "posts_data");
1372 let unrelated = factory.shadow_table_policy("docs", "docs_segments");
1373
1374 assert_eq!(owned.kind, ShadowTableKind::Shadow);
1375 assert!(!other_owner.is_shadow());
1376 assert!(!unrelated.is_shadow());
1377 }
1378
1379 #[test]
1380 fn test_erased_vtab_instance_forwards_transaction_hooks() {
1381 let cx = Cx::new();
1382 let mut erased: Box<dyn ErasedVtabInstance> =
1383 Box::new(HookAwareVtab::connect(&cx, &[]).unwrap());
1384
1385 erased.begin(&cx).unwrap();
1386 {
1387 let hook = erased
1388 .as_any_mut()
1389 .downcast_mut::<HookAwareVtab>()
1390 .expect("hook-aware vtab");
1391 hook.version = 9;
1392 }
1393 erased.savepoint(&cx, 0).unwrap();
1394 {
1395 let hook = erased
1396 .as_any_mut()
1397 .downcast_mut::<HookAwareVtab>()
1398 .expect("hook-aware vtab");
1399 hook.version = 11;
1400 }
1401 erased.rollback_to(&cx, 0).unwrap();
1402 erased.release(&cx, 0).unwrap();
1403 erased.sync_txn(&cx).unwrap();
1404 erased.rollback(&cx).unwrap();
1405
1406 let hook = erased
1407 .as_any_mut()
1408 .downcast_mut::<HookAwareVtab>()
1409 .expect("hook-aware vtab");
1410 assert_eq!(hook.version, 7);
1411 assert_eq!(hook.syncs, 1);
1412 }
1413}