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, PartialEq, Eq)]
46pub struct IndexConstraint {
47 pub column: i32,
49 pub op: ConstraintOp,
51 pub usable: bool,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
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,
72 pub omit: bool,
77}
78
79#[derive(Debug, Clone)]
86pub struct IndexInfo {
87 pub constraints: Vec<IndexConstraint>,
90 pub order_by: Vec<IndexOrderBy>,
93 pub constraint_usage: Vec<IndexConstraintUsage>,
97 pub idx_num: i32,
99 pub idx_str: Option<String>,
101 pub order_by_consumed: bool,
103 pub estimated_cost: f64,
105 pub estimated_rows: i64,
107}
108
109impl IndexInfo {
110 #[must_use]
112 pub fn new(constraints: Vec<IndexConstraint>, order_by: Vec<IndexOrderBy>) -> Self {
113 let usage_len = constraints.len();
114 Self {
115 constraints,
116 order_by,
117 constraint_usage: vec![IndexConstraintUsage::default(); usage_len],
118 idx_num: 0,
119 idx_str: None,
120 order_by_consumed: false,
121 estimated_cost: 1_000_000.0,
122 estimated_rows: 1_000_000,
123 }
124 }
125}
126
127#[derive(Debug, Default)]
136pub struct ColumnContext {
137 value: Option<SqliteValue>,
138}
139
140impl ColumnContext {
141 #[must_use]
143 pub fn new() -> Self {
144 Self { value: None }
145 }
146
147 pub fn set_value(&mut self, val: SqliteValue) {
149 self.value = Some(val);
150 }
151
152 pub fn take_value(&mut self) -> Option<SqliteValue> {
154 self.value.take()
155 }
156}
157
158#[derive(Debug, Clone)]
164pub struct TransactionalVtabState<S: Clone> {
165 base_snapshot: Option<S>,
166 savepoints: Vec<(i32, S)>,
167}
168
169impl<S: Clone> Default for TransactionalVtabState<S> {
170 fn default() -> Self {
171 Self {
172 base_snapshot: None,
173 savepoints: Vec::new(),
174 }
175 }
176}
177
178impl<S: Clone> TransactionalVtabState<S> {
179 pub fn begin(&mut self, snapshot: S) {
181 if self.base_snapshot.is_none() {
182 self.base_snapshot = Some(snapshot);
183 self.savepoints.clear();
184 }
185 }
186
187 pub fn commit(&mut self) {
189 self.base_snapshot = None;
190 self.savepoints.clear();
191 }
192
193 pub fn rollback(&mut self) -> Option<S> {
195 let snapshot = self.base_snapshot.take();
196 self.savepoints.clear();
197 snapshot
198 }
199
200 pub fn savepoint(&mut self, level: i32, snapshot: S) {
202 if self.base_snapshot.is_none() {
203 return;
204 }
205 self.savepoints.retain(|(existing, _)| *existing < level);
206 self.savepoints.push((level, snapshot));
207 }
208
209 pub fn release(&mut self, level: i32) {
211 if self.base_snapshot.is_none() {
212 return;
213 }
214 self.savepoints.retain(|(existing, _)| *existing < level);
215 }
216
217 pub fn rollback_to(&mut self, level: i32) -> Option<S> {
225 self.base_snapshot.as_ref()?;
226 let snapshot = self
227 .savepoints
228 .iter()
229 .rfind(|(existing, _)| *existing == level)
230 .map(|(_, snapshot)| snapshot.clone())
231 .or_else(|| self.base_snapshot.clone());
232 if snapshot.is_some() {
233 self.savepoints.retain(|(existing, _)| *existing <= level);
234 }
235 snapshot
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245pub enum ShadowTableKind {
246 #[default]
248 Ordinary,
249 Shadow,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
255pub enum ShadowTableAccess {
256 #[default]
258 Allow,
259 Deny,
261}
262
263impl ShadowTableAccess {
264 #[must_use]
266 pub const fn is_allowed(self) -> bool {
267 matches!(self, Self::Allow)
268 }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct ShadowTablePolicy {
275 pub kind: ShadowTableKind,
277 pub direct_dml: ShadowTableAccess,
279 pub schema_ddl: ShadowTableAccess,
281 pub module_internal_write: ShadowTableAccess,
283}
284
285impl ShadowTablePolicy {
286 #[must_use]
288 pub const fn ordinary() -> Self {
289 Self {
290 kind: ShadowTableKind::Ordinary,
291 direct_dml: ShadowTableAccess::Allow,
292 schema_ddl: ShadowTableAccess::Allow,
293 module_internal_write: ShadowTableAccess::Allow,
294 }
295 }
296
297 #[must_use]
299 pub const fn owned_shadow() -> Self {
300 Self {
301 kind: ShadowTableKind::Shadow,
302 direct_dml: ShadowTableAccess::Deny,
303 schema_ddl: ShadowTableAccess::Deny,
304 module_internal_write: ShadowTableAccess::Allow,
305 }
306 }
307
308 #[must_use]
310 pub const fn is_shadow(self) -> bool {
311 matches!(self.kind, ShadowTableKind::Shadow)
312 }
313
314 #[must_use]
316 pub const fn allows_direct_dml(self) -> bool {
317 self.direct_dml.is_allowed()
318 }
319
320 #[must_use]
322 pub const fn allows_schema_ddl(self) -> bool {
323 self.schema_ddl.is_allowed()
324 }
325
326 #[must_use]
328 pub const fn allows_module_internal_write(self) -> bool {
329 self.module_internal_write.is_allowed()
330 }
331}
332
333impl Default for ShadowTablePolicy {
334 fn default() -> Self {
335 Self::ordinary()
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
341pub enum VtabLifecyclePolicy {
342 #[default]
344 Simple,
345 SeparateCreateAndConnect,
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
351pub enum VtabIntegrityPolicy {
352 #[default]
354 None,
355 ShadowAware,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
361pub struct VtabRiskLevel {
362 pub innocuous: bool,
364 pub direct_only: bool,
366 pub uses_all_schemas: bool,
368}
369
370impl VtabRiskLevel {
371 #[must_use]
373 pub const fn innocuous() -> Self {
374 Self {
375 innocuous: true,
376 direct_only: false,
377 uses_all_schemas: false,
378 }
379 }
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub struct VtabModuleMetadata {
386 pub owns_shadow_tables: bool,
388 pub lifecycle: VtabLifecyclePolicy,
390 pub integrity: VtabIntegrityPolicy,
392 pub risk: VtabRiskLevel,
394}
395
396impl VtabModuleMetadata {
397 #[must_use]
399 pub const fn ordinary() -> Self {
400 Self {
401 owns_shadow_tables: false,
402 lifecycle: VtabLifecyclePolicy::Simple,
403 integrity: VtabIntegrityPolicy::None,
404 risk: VtabRiskLevel::innocuous(),
405 }
406 }
407
408 #[must_use]
410 pub const fn shadow_owning(
411 lifecycle: VtabLifecyclePolicy,
412 integrity: VtabIntegrityPolicy,
413 risk: VtabRiskLevel,
414 ) -> Self {
415 Self {
416 owns_shadow_tables: true,
417 lifecycle,
418 integrity,
419 risk,
420 }
421 }
422}
423
424impl Default for VtabModuleMetadata {
425 fn default() -> Self {
426 Self::ordinary()
427 }
428}
429
430#[allow(clippy::missing_errors_doc)]
449pub trait VirtualTable: Send + Sync {
450 type Cursor: VirtualTableCursor;
452
453 fn module_metadata(_args: &[&str]) -> VtabModuleMetadata
455 where
456 Self: Sized,
457 {
458 VtabModuleMetadata::ordinary()
459 }
460
461 fn shadow_table_policy(_vtab_name: &str, _table_name: &str) -> ShadowTablePolicy
464 where
465 Self: Sized,
466 {
467 ShadowTablePolicy::ordinary()
468 }
469
470 fn create(cx: &Cx, args: &[&str]) -> Result<Self>
479 where
480 Self: Sized,
481 {
482 Self::connect(cx, args)
483 }
484
485 fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
488 where
489 Self: Sized;
490
491 fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
497
498 fn open(&self) -> Result<Self::Cursor>;
500
501 fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
503 Ok(())
504 }
505
506 fn destroy(&mut self, cx: &Cx) -> Result<()> {
510 self.disconnect(cx)
511 }
512
513 fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
523 Err(FrankenError::ReadOnly)
524 }
525
526 fn begin(&mut self, _cx: &Cx) -> Result<()> {
528 Ok(())
529 }
530
531 fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
533 Ok(())
534 }
535
536 fn commit(&mut self, _cx: &Cx) -> Result<()> {
538 Ok(())
539 }
540
541 fn rollback(&mut self, _cx: &Cx) -> Result<()> {
543 Ok(())
544 }
545
546 fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
550 Err(FrankenError::Unsupported)
551 }
552
553 fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
555 Ok(())
556 }
557
558 fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
560 Ok(())
561 }
562
563 fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
565 Ok(())
566 }
567}
568
569#[allow(clippy::missing_errors_doc)]
584pub trait VirtualTableCursor: Send {
585 fn filter(
587 &mut self,
588 cx: &Cx,
589 idx_num: i32,
590 idx_str: Option<&str>,
591 args: &[SqliteValue],
592 ) -> Result<()>;
593
594 fn next(&mut self, cx: &Cx) -> Result<()>;
596
597 fn eof(&self) -> bool;
599
600 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
602
603 fn rowid(&self) -> Result<i64>;
605}
606
607#[allow(clippy::missing_errors_doc)]
617pub trait VtabModuleFactory: Send + Sync {
618 fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>>;
623
624 fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
627 self.create(cx, args)
628 }
629
630 fn column_info(&self, _args: &[&str]) -> Vec<(String, char)> {
633 Vec::new()
634 }
635
636 fn module_metadata(&self, _args: &[&str]) -> VtabModuleMetadata {
638 VtabModuleMetadata::ordinary()
639 }
640
641 fn shadow_table_policy(&self, _vtab_name: &str, _table_name: &str) -> ShadowTablePolicy {
644 ShadowTablePolicy::ordinary()
645 }
646}
647
648mod erased_instance_sealed {
649 use super::VirtualTable;
650
651 pub trait Sealed {}
652
653 impl<T: VirtualTable + 'static> Sealed for T where T::Cursor: 'static {}
654}
655
656#[allow(clippy::missing_errors_doc)]
662#[allow(private_bounds)]
663pub trait ErasedVtabInstance: Send + Sync + erased_instance_sealed::Sealed {
664 fn as_any(&self) -> &dyn Any;
666 fn as_any_mut(&mut self) -> &mut dyn Any;
668 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
670 fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
672 fn begin(&mut self, cx: &Cx) -> Result<()>;
674 fn sync_txn(&mut self, cx: &Cx) -> Result<()>;
676 fn commit(&mut self, cx: &Cx) -> Result<()>;
678 fn rollback(&mut self, cx: &Cx) -> Result<()>;
680 fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()>;
682 fn release(&mut self, cx: &Cx, n: i32) -> Result<()>;
684 fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()>;
686 fn disconnect(&mut self, cx: &Cx) -> Result<()>;
688 fn destroy(&mut self, cx: &Cx) -> Result<()>;
690 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
692 fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
694}
695
696#[allow(clippy::missing_errors_doc)]
698pub trait ErasedVtabCursor: Send {
699 fn erased_filter(
701 &mut self,
702 cx: &Cx,
703 idx_num: i32,
704 idx_str: Option<&str>,
705 args: &[SqliteValue],
706 ) -> Result<()>;
707 fn erased_next(&mut self, cx: &Cx) -> Result<()>;
709 fn erased_eof(&self) -> bool;
711 fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
713 fn erased_rowid(&self) -> Result<i64>;
715}
716
717impl<C: VirtualTableCursor + 'static> ErasedVtabCursor for C {
719 fn erased_filter(
720 &mut self,
721 cx: &Cx,
722 idx_num: i32,
723 idx_str: Option<&str>,
724 args: &[SqliteValue],
725 ) -> Result<()> {
726 VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
727 }
728 fn erased_next(&mut self, cx: &Cx) -> Result<()> {
729 VirtualTableCursor::next(self, cx)
730 }
731 fn erased_eof(&self) -> bool {
732 VirtualTableCursor::eof(self)
733 }
734 fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
735 VirtualTableCursor::column(self, ctx, col)
736 }
737 fn erased_rowid(&self) -> Result<i64> {
738 VirtualTableCursor::rowid(self)
739 }
740}
741
742impl<T: VirtualTable + 'static> ErasedVtabInstance for T
744where
745 T::Cursor: 'static,
746{
747 fn as_any(&self) -> &dyn Any {
748 self
749 }
750
751 fn as_any_mut(&mut self) -> &mut dyn Any {
752 self
753 }
754
755 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
756 let cursor = VirtualTable::open(self)?;
757 Ok(Box::new(cursor))
758 }
759 fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
760 VirtualTable::update(self, cx, args)
761 }
762 fn begin(&mut self, cx: &Cx) -> Result<()> {
763 VirtualTable::begin(self, cx)
764 }
765 fn sync_txn(&mut self, cx: &Cx) -> Result<()> {
766 VirtualTable::sync_txn(self, cx)
767 }
768 fn commit(&mut self, cx: &Cx) -> Result<()> {
769 VirtualTable::commit(self, cx)
770 }
771 fn rollback(&mut self, cx: &Cx) -> Result<()> {
772 VirtualTable::rollback(self, cx)
773 }
774 fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()> {
775 VirtualTable::savepoint(self, cx, n)
776 }
777 fn release(&mut self, cx: &Cx, n: i32) -> Result<()> {
778 VirtualTable::release(self, cx, n)
779 }
780 fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()> {
781 VirtualTable::rollback_to(self, cx, n)
782 }
783 fn disconnect(&mut self, cx: &Cx) -> Result<()> {
784 VirtualTable::disconnect(self, cx)
785 }
786 fn destroy(&mut self, cx: &Cx) -> Result<()> {
787 VirtualTable::destroy(self, cx)
788 }
789 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
790 VirtualTable::rename(self, cx, new_name)
791 }
792 fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
793 VirtualTable::best_index(self, info)
794 }
795}
796
797pub fn module_factory_from<T>() -> impl VtabModuleFactory
799where
800 T: VirtualTable + 'static,
801 T::Cursor: 'static,
802{
803 struct Factory<T: Send + Sync>(std::marker::PhantomData<T>);
804
805 impl<T: VirtualTable + 'static> VtabModuleFactory for Factory<T>
806 where
807 T::Cursor: 'static,
808 {
809 fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
810 let vtab = T::create(cx, args)?;
811 Ok(Box::new(vtab))
812 }
813 fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
814 let vtab = T::connect(cx, args)?;
815 Ok(Box::new(vtab))
816 }
817
818 fn module_metadata(&self, args: &[&str]) -> VtabModuleMetadata {
819 T::module_metadata(args)
820 }
821
822 fn shadow_table_policy(&self, vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
823 T::shadow_table_policy(vtab_name, table_name)
824 }
825 }
826
827 Factory::<T>(std::marker::PhantomData)
828}
829
830#[cfg(test)]
835#[allow(clippy::too_many_lines)]
836mod tests {
837 use super::*;
838
839 struct GenerateSeries {
842 destroyed: bool,
843 }
844
845 struct GenerateSeriesCursor {
846 start: i64,
847 stop: i64,
848 current: i64,
849 }
850
851 impl VirtualTable for GenerateSeries {
852 type Cursor = GenerateSeriesCursor;
853
854 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
855 Ok(Self { destroyed: false })
856 }
857
858 fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
859 info.estimated_cost = 10.0;
860 info.estimated_rows = 100;
861 info.idx_num = 1;
862
863 if !info.constraints.is_empty() && info.constraints[0].usable {
865 info.constraint_usage[0].argv_index = 1;
866 info.constraint_usage[0].omit = true;
867 }
868 Ok(())
869 }
870
871 fn open(&self) -> Result<GenerateSeriesCursor> {
872 Ok(GenerateSeriesCursor {
873 start: 0,
874 stop: 0,
875 current: 0,
876 })
877 }
878
879 fn destroy(&mut self, _cx: &Cx) -> Result<()> {
880 self.destroyed = true;
881 Ok(())
882 }
883 }
884
885 impl VirtualTableCursor for GenerateSeriesCursor {
886 fn filter(
887 &mut self,
888 _cx: &Cx,
889 _idx_num: i32,
890 _idx_str: Option<&str>,
891 args: &[SqliteValue],
892 ) -> Result<()> {
893 self.start = args.first().map_or(1, SqliteValue::to_integer);
894 self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
895 self.current = self.start;
896 Ok(())
897 }
898
899 fn next(&mut self, _cx: &Cx) -> Result<()> {
900 self.current += 1;
901 Ok(())
902 }
903
904 fn eof(&self) -> bool {
905 self.current > self.stop
906 }
907
908 fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
909 if self.eof() {
910 ctx.set_value(SqliteValue::Null);
911 return Ok(());
912 }
913 ctx.set_value(SqliteValue::Integer(self.current));
914 Ok(())
915 }
916
917 fn rowid(&self) -> Result<i64> {
918 Ok(if self.eof() { 0 } else { self.current })
919 }
920 }
921
922 struct ReadOnlyVtab;
925
926 struct ReadOnlyCursor;
927
928 impl VirtualTable for ReadOnlyVtab {
929 type Cursor = ReadOnlyCursor;
930
931 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
932 Ok(Self)
933 }
934
935 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
936 Ok(())
937 }
938
939 fn open(&self) -> Result<ReadOnlyCursor> {
940 Ok(ReadOnlyCursor)
941 }
942 }
943
944 impl VirtualTableCursor for ReadOnlyCursor {
945 fn filter(
946 &mut self,
947 _cx: &Cx,
948 _idx_num: i32,
949 _idx_str: Option<&str>,
950 _args: &[SqliteValue],
951 ) -> Result<()> {
952 Ok(())
953 }
954
955 fn next(&mut self, _cx: &Cx) -> Result<()> {
956 Ok(())
957 }
958
959 fn eof(&self) -> bool {
960 true
961 }
962
963 fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
964 ctx.set_value(SqliteValue::Null);
965 Ok(())
966 }
967
968 fn rowid(&self) -> Result<i64> {
969 Ok(0)
970 }
971 }
972
973 struct WritableVtab {
976 rows: Vec<(i64, Vec<SqliteValue>)>,
977 next_rowid: i64,
978 }
979
980 struct WritableCursor {
981 rows: Vec<(i64, Vec<SqliteValue>)>,
982 pos: usize,
983 }
984
985 impl VirtualTable for WritableVtab {
986 type Cursor = WritableCursor;
987
988 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
989 Ok(Self {
990 rows: Vec::new(),
991 next_rowid: 1,
992 })
993 }
994
995 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
996 Ok(())
997 }
998
999 fn open(&self) -> Result<WritableCursor> {
1000 Ok(WritableCursor {
1001 rows: self.rows.clone(),
1002 pos: 0,
1003 })
1004 }
1005
1006 fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
1007 if args[0].is_null() {
1009 let rowid = self.next_rowid;
1011 self.next_rowid += 1;
1012 let cols: Vec<SqliteValue> = args[2..].to_vec();
1013 self.rows.push((rowid, cols));
1014 return Ok(Some(rowid));
1015 }
1016 Ok(None)
1017 }
1018 }
1019
1020 impl VirtualTableCursor for WritableCursor {
1021 fn filter(
1022 &mut self,
1023 _cx: &Cx,
1024 _idx_num: i32,
1025 _idx_str: Option<&str>,
1026 _args: &[SqliteValue],
1027 ) -> Result<()> {
1028 self.pos = 0;
1029 Ok(())
1030 }
1031
1032 fn next(&mut self, _cx: &Cx) -> Result<()> {
1033 self.pos += 1;
1034 Ok(())
1035 }
1036
1037 fn eof(&self) -> bool {
1038 self.pos >= self.rows.len()
1039 }
1040
1041 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
1042 if self.eof() {
1043 ctx.set_value(SqliteValue::Null);
1044 return Ok(());
1045 }
1046
1047 #[allow(clippy::cast_sign_loss)]
1048 let col_idx = col as usize;
1049 if let Some((_, cols)) = self.rows.get(self.pos)
1050 && let Some(val) = cols.get(col_idx)
1051 {
1052 ctx.set_value(val.clone());
1053 return Ok(());
1054 }
1055 ctx.set_value(SqliteValue::Null);
1056 Ok(())
1057 }
1058
1059 fn rowid(&self) -> Result<i64> {
1060 self.rows
1061 .get(self.pos)
1062 .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
1063 }
1064 }
1065
1066 struct ShadowOwningVtab;
1067
1068 impl VirtualTable for ShadowOwningVtab {
1069 type Cursor = ReadOnlyCursor;
1070
1071 fn module_metadata(_args: &[&str]) -> VtabModuleMetadata {
1072 VtabModuleMetadata::shadow_owning(
1073 VtabLifecyclePolicy::SeparateCreateAndConnect,
1074 VtabIntegrityPolicy::ShadowAware,
1075 VtabRiskLevel {
1076 innocuous: false,
1077 direct_only: true,
1078 uses_all_schemas: false,
1079 },
1080 )
1081 }
1082
1083 fn shadow_table_policy(vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
1084 let Some((owner, suffix)) = table_name.rsplit_once('_') else {
1085 return ShadowTablePolicy::ordinary();
1086 };
1087
1088 if owner == vtab_name
1089 && matches!(suffix, "config" | "content" | "data" | "docsize" | "idx")
1090 {
1091 return ShadowTablePolicy::owned_shadow();
1092 }
1093
1094 ShadowTablePolicy::ordinary()
1095 }
1096
1097 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1098 Ok(Self)
1099 }
1100
1101 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1102 Ok(())
1103 }
1104
1105 fn open(&self) -> Result<Self::Cursor> {
1106 Ok(ReadOnlyCursor)
1107 }
1108 }
1109
1110 #[derive(Debug, Clone, PartialEq, Eq)]
1111 struct HookSnapshot {
1112 version: i32,
1113 }
1114
1115 struct HookAwareVtab {
1116 version: i32,
1117 syncs: usize,
1118 tx_state: TransactionalVtabState<HookSnapshot>,
1119 }
1120
1121 impl VirtualTable for HookAwareVtab {
1122 type Cursor = ReadOnlyCursor;
1123
1124 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1125 Ok(Self {
1126 version: 7,
1127 syncs: 0,
1128 tx_state: TransactionalVtabState::default(),
1129 })
1130 }
1131
1132 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1133 Ok(())
1134 }
1135
1136 fn open(&self) -> Result<Self::Cursor> {
1137 Ok(ReadOnlyCursor)
1138 }
1139
1140 fn begin(&mut self, _cx: &Cx) -> Result<()> {
1141 self.tx_state.begin(HookSnapshot {
1142 version: self.version,
1143 });
1144 Ok(())
1145 }
1146
1147 fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
1148 self.syncs += 1;
1149 Ok(())
1150 }
1151
1152 fn savepoint(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1153 self.tx_state.savepoint(
1154 n,
1155 HookSnapshot {
1156 version: self.version,
1157 },
1158 );
1159 Ok(())
1160 }
1161
1162 fn release(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1163 self.tx_state.release(n);
1164 Ok(())
1165 }
1166
1167 fn rollback_to(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1168 if let Some(snapshot) = self.tx_state.rollback_to(n) {
1169 self.version = snapshot.version;
1170 }
1171 Ok(())
1172 }
1173
1174 fn commit(&mut self, _cx: &Cx) -> Result<()> {
1175 self.tx_state.commit();
1176 Ok(())
1177 }
1178
1179 fn rollback(&mut self, _cx: &Cx) -> Result<()> {
1180 if let Some(snapshot) = self.tx_state.rollback() {
1181 self.version = snapshot.version;
1182 }
1183 Ok(())
1184 }
1185 }
1186
1187 #[test]
1190 fn test_vtab_create_vs_connect() {
1191 let cx = Cx::new();
1192
1193 let vtab = GenerateSeries::create(&cx, &[]).unwrap();
1195 assert!(!vtab.destroyed);
1196
1197 let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
1199 assert!(!vtab2.destroyed);
1200 }
1201
1202 #[test]
1203 fn test_vtab_best_index_populates_info() {
1204 let cx = Cx::new();
1205 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1206
1207 let mut info = IndexInfo::new(
1208 vec![IndexConstraint {
1209 column: 0,
1210 op: ConstraintOp::Gt,
1211 usable: true,
1212 }],
1213 vec![],
1214 );
1215
1216 VirtualTable::best_index(&vtab, &mut info).unwrap();
1217
1218 assert_eq!(info.idx_num, 1);
1219 assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
1220 assert_eq!(info.estimated_rows, 100);
1221 assert_eq!(info.constraint_usage[0].argv_index, 1);
1222 assert!(info.constraint_usage[0].omit);
1223 }
1224
1225 #[test]
1226 fn test_vtab_cursor_filter_next_eof() {
1227 let cx = Cx::new();
1228 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1229 let mut cursor = vtab.open().unwrap();
1230
1231 cursor
1232 .filter(
1233 &cx,
1234 0,
1235 None,
1236 &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
1237 )
1238 .unwrap();
1239
1240 let mut values = Vec::new();
1241 while !cursor.eof() {
1242 let mut ctx = ColumnContext::new();
1243 cursor.column(&mut ctx, 0).unwrap();
1244 let rowid = cursor.rowid().unwrap();
1245 values.push((rowid, ctx.take_value().unwrap()));
1246 cursor.next(&cx).unwrap();
1247 }
1248
1249 assert_eq!(values.len(), 3);
1250 assert_eq!(values[0], (1, SqliteValue::Integer(1)));
1251 assert_eq!(values[1], (2, SqliteValue::Integer(2)));
1252 assert_eq!(values[2], (3, SqliteValue::Integer(3)));
1253 }
1254
1255 #[test]
1256 fn test_generate_series_cursor_past_end_returns_null_and_zero_rowid() {
1257 let cx = Cx::new();
1258 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1259 let mut cursor = vtab.open().unwrap();
1260
1261 cursor
1262 .filter(
1263 &cx,
1264 0,
1265 None,
1266 &[SqliteValue::Integer(1), SqliteValue::Integer(1)],
1267 )
1268 .unwrap();
1269 cursor.next(&cx).unwrap();
1270 assert!(cursor.eof());
1271
1272 let mut ctx = ColumnContext::new();
1273 cursor.column(&mut ctx, 0).unwrap();
1274 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1275 assert_eq!(cursor.rowid().unwrap(), 0);
1276 }
1277
1278 #[test]
1279 fn test_writable_cursor_missing_column_returns_null() {
1280 let cx = Cx::new();
1281 let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1282 VirtualTable::update(
1283 &mut vtab,
1284 &cx,
1285 &[
1286 SqliteValue::Null,
1287 SqliteValue::Null,
1288 SqliteValue::Text("hello".into()),
1289 ],
1290 )
1291 .unwrap();
1292
1293 let mut cursor = vtab.open().unwrap();
1294 cursor.filter(&cx, 0, None, &[]).unwrap();
1295
1296 let mut ctx = ColumnContext::new();
1297 cursor.column(&mut ctx, 3).unwrap();
1298 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1299
1300 cursor.next(&cx).unwrap();
1301 assert!(cursor.eof());
1302 cursor.column(&mut ctx, 0).unwrap();
1303 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1304 assert_eq!(cursor.rowid().unwrap(), 0);
1305 }
1306
1307 #[test]
1308 fn test_vtab_update_insert() {
1309 let cx = Cx::new();
1310 let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1311
1312 let result = VirtualTable::update(
1315 &mut vtab,
1316 &cx,
1317 &[
1318 SqliteValue::Null,
1319 SqliteValue::Null,
1320 SqliteValue::Text("hello".into()),
1321 ],
1322 )
1323 .unwrap();
1324
1325 assert_eq!(result, Some(1));
1326 assert_eq!(vtab.rows.len(), 1);
1327 assert_eq!(vtab.rows[0].0, 1);
1328 }
1329
1330 #[test]
1331 fn test_vtab_update_readonly_default() {
1332 let cx = Cx::new();
1333 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1334 let err = VirtualTable::update(&mut vtab, &cx, &[SqliteValue::Null]).unwrap_err();
1335 assert!(matches!(err, FrankenError::ReadOnly));
1336 }
1337
1338 #[test]
1339 fn test_vtab_destroy_vs_disconnect() {
1340 let cx = Cx::new();
1341
1342 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1344 VirtualTable::disconnect(&mut vtab, &cx).unwrap();
1345 VirtualTable::destroy(&mut vtab, &cx).unwrap();
1346
1347 let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1349 assert!(!vtab.destroyed);
1350 VirtualTable::destroy(&mut vtab, &cx).unwrap();
1351 assert!(vtab.destroyed);
1352 }
1353
1354 #[test]
1355 fn test_vtab_cursor_send_but_not_sync() {
1356 fn assert_send<T: Send>() {}
1357 assert_send::<GenerateSeriesCursor>();
1358
1359 }
1366
1367 #[test]
1368 fn test_column_context_lifecycle() {
1369 let mut ctx = ColumnContext::new();
1370 assert!(ctx.take_value().is_none());
1371
1372 ctx.set_value(SqliteValue::Integer(42));
1373 assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
1374
1375 assert!(ctx.take_value().is_none());
1377 }
1378
1379 #[test]
1380 fn test_index_info_new() {
1381 let info = IndexInfo::new(
1382 vec![
1383 IndexConstraint {
1384 column: 0,
1385 op: ConstraintOp::Eq,
1386 usable: true,
1387 },
1388 IndexConstraint {
1389 column: 1,
1390 op: ConstraintOp::Gt,
1391 usable: false,
1392 },
1393 ],
1394 vec![IndexOrderBy {
1395 column: 0,
1396 desc: false,
1397 }],
1398 );
1399
1400 assert_eq!(info.constraints.len(), 2);
1401 assert_eq!(info.order_by.len(), 1);
1402 assert_eq!(info.constraint_usage.len(), 2);
1403 assert_eq!(info.idx_num, 0);
1404 assert!(info.idx_str.is_none());
1405 assert!(!info.order_by_consumed);
1406 }
1407
1408 #[test]
1409 fn test_transactional_vtab_state_tracks_savepoints() {
1410 let mut state = TransactionalVtabState::default();
1411
1412 state.begin(1_i32);
1413 state.savepoint(0, 2);
1414 state.savepoint(1, 3);
1415 assert_eq!(state.rollback_to(1), Some(3));
1416 state.release(1);
1417 assert_eq!(state.rollback(), Some(1));
1418 assert_eq!(state.rollback(), None);
1419 }
1420
1421 #[test]
1422 fn test_transactional_vtab_state_uses_base_for_late_enlistment() {
1423 let mut state = TransactionalVtabState::default();
1424
1425 state.begin(7_i32);
1426 state.savepoint(2, 9);
1427
1428 assert_eq!(state.rollback_to(1), Some(7));
1429 assert_eq!(state.rollback(), Some(7));
1430 }
1431
1432 #[test]
1433 fn test_shadow_table_policy_defaults_to_ordinary() {
1434 let policy = ReadOnlyVtab::shadow_table_policy("docs", "docs_data");
1435 assert_eq!(policy, ShadowTablePolicy::ordinary());
1436 assert!(!policy.is_shadow());
1437 assert!(policy.allows_direct_dml());
1438 assert!(policy.allows_schema_ddl());
1439 assert!(policy.allows_module_internal_write());
1440 }
1441
1442 #[test]
1443 fn test_owned_shadow_policy_blocks_user_dml_and_schema_ddl() {
1444 let policy = ShadowTablePolicy::owned_shadow();
1445
1446 assert!(policy.is_shadow());
1447 assert!(!policy.allows_direct_dml());
1448 assert!(!policy.allows_schema_ddl());
1449 assert!(policy.allows_module_internal_write());
1450 }
1451
1452 #[test]
1453 fn test_shadow_owning_module_metadata_is_forwarded_by_factory() {
1454 let factory = module_factory_from::<ShadowOwningVtab>();
1455 let metadata = factory.module_metadata(&[]);
1456
1457 assert!(metadata.owns_shadow_tables);
1458 assert_eq!(
1459 metadata.lifecycle,
1460 VtabLifecyclePolicy::SeparateCreateAndConnect
1461 );
1462 assert_eq!(metadata.integrity, VtabIntegrityPolicy::ShadowAware);
1463 assert!(metadata.risk.direct_only);
1464 assert!(!metadata.risk.innocuous);
1465 }
1466
1467 #[test]
1468 fn test_shadow_owning_module_matches_owned_shadow_tables() {
1469 let factory = module_factory_from::<ShadowOwningVtab>();
1470
1471 let owned = factory.shadow_table_policy("docs", "docs_data");
1472 let other_owner = factory.shadow_table_policy("docs", "posts_data");
1473 let unrelated = factory.shadow_table_policy("docs", "docs_segments");
1474
1475 assert_eq!(owned.kind, ShadowTableKind::Shadow);
1476 assert!(!owned.allows_direct_dml());
1477 assert!(!owned.allows_schema_ddl());
1478 assert!(owned.allows_module_internal_write());
1479 assert!(!other_owner.is_shadow());
1480 assert!(!unrelated.is_shadow());
1481 assert!(unrelated.allows_direct_dml());
1482 }
1483
1484 #[test]
1485 fn test_erased_vtab_instance_forwards_transaction_hooks() {
1486 let cx = Cx::new();
1487 let mut erased: Box<dyn ErasedVtabInstance> =
1488 Box::new(HookAwareVtab::connect(&cx, &[]).unwrap());
1489
1490 erased.begin(&cx).unwrap();
1491 {
1492 let hook = erased
1493 .as_any_mut()
1494 .downcast_mut::<HookAwareVtab>()
1495 .expect("hook-aware vtab");
1496 hook.version = 9;
1497 }
1498 erased.savepoint(&cx, 0).unwrap();
1499 {
1500 let hook = erased
1501 .as_any_mut()
1502 .downcast_mut::<HookAwareVtab>()
1503 .expect("hook-aware vtab");
1504 hook.version = 11;
1505 }
1506 erased.rollback_to(&cx, 0).unwrap();
1507 erased.release(&cx, 0).unwrap();
1508 erased.sync_txn(&cx).unwrap();
1509 erased.rollback(&cx).unwrap();
1510
1511 let hook = erased
1512 .as_any_mut()
1513 .downcast_mut::<HookAwareVtab>()
1514 .expect("hook-aware vtab");
1515 assert_eq!(hook.version, 7);
1516 assert_eq!(hook.syncs, 1);
1517 }
1518}