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, Default)]
247pub enum ShadowTableAccess {
248 #[default]
250 Allow,
251 Deny,
253}
254
255impl ShadowTableAccess {
256 #[must_use]
258 pub const fn is_allowed(self) -> bool {
259 matches!(self, Self::Allow)
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct ShadowTablePolicy {
267 pub kind: ShadowTableKind,
269 pub direct_dml: ShadowTableAccess,
271 pub schema_ddl: ShadowTableAccess,
273 pub module_internal_write: ShadowTableAccess,
275}
276
277impl ShadowTablePolicy {
278 #[must_use]
280 pub const fn ordinary() -> Self {
281 Self {
282 kind: ShadowTableKind::Ordinary,
283 direct_dml: ShadowTableAccess::Allow,
284 schema_ddl: ShadowTableAccess::Allow,
285 module_internal_write: ShadowTableAccess::Allow,
286 }
287 }
288
289 #[must_use]
291 pub const fn owned_shadow() -> Self {
292 Self {
293 kind: ShadowTableKind::Shadow,
294 direct_dml: ShadowTableAccess::Deny,
295 schema_ddl: ShadowTableAccess::Deny,
296 module_internal_write: ShadowTableAccess::Allow,
297 }
298 }
299
300 #[must_use]
302 pub const fn is_shadow(self) -> bool {
303 matches!(self.kind, ShadowTableKind::Shadow)
304 }
305
306 #[must_use]
308 pub const fn allows_direct_dml(self) -> bool {
309 self.direct_dml.is_allowed()
310 }
311
312 #[must_use]
314 pub const fn allows_schema_ddl(self) -> bool {
315 self.schema_ddl.is_allowed()
316 }
317
318 #[must_use]
320 pub const fn allows_module_internal_write(self) -> bool {
321 self.module_internal_write.is_allowed()
322 }
323}
324
325impl Default for ShadowTablePolicy {
326 fn default() -> Self {
327 Self::ordinary()
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
333pub enum VtabLifecyclePolicy {
334 #[default]
336 Simple,
337 SeparateCreateAndConnect,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
343pub enum VtabIntegrityPolicy {
344 #[default]
346 None,
347 ShadowAware,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
353pub struct VtabRiskLevel {
354 pub innocuous: bool,
356 pub direct_only: bool,
358 pub uses_all_schemas: bool,
360}
361
362impl VtabRiskLevel {
363 #[must_use]
365 pub const fn innocuous() -> Self {
366 Self {
367 innocuous: true,
368 direct_only: false,
369 uses_all_schemas: false,
370 }
371 }
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub struct VtabModuleMetadata {
378 pub owns_shadow_tables: bool,
380 pub lifecycle: VtabLifecyclePolicy,
382 pub integrity: VtabIntegrityPolicy,
384 pub risk: VtabRiskLevel,
386}
387
388impl VtabModuleMetadata {
389 #[must_use]
391 pub const fn ordinary() -> Self {
392 Self {
393 owns_shadow_tables: false,
394 lifecycle: VtabLifecyclePolicy::Simple,
395 integrity: VtabIntegrityPolicy::None,
396 risk: VtabRiskLevel::innocuous(),
397 }
398 }
399
400 #[must_use]
402 pub const fn shadow_owning(
403 lifecycle: VtabLifecyclePolicy,
404 integrity: VtabIntegrityPolicy,
405 risk: VtabRiskLevel,
406 ) -> Self {
407 Self {
408 owns_shadow_tables: true,
409 lifecycle,
410 integrity,
411 risk,
412 }
413 }
414}
415
416impl Default for VtabModuleMetadata {
417 fn default() -> Self {
418 Self::ordinary()
419 }
420}
421
422#[allow(clippy::missing_errors_doc)]
441pub trait VirtualTable: Send + Sync {
442 type Cursor: VirtualTableCursor;
444
445 fn module_metadata(_args: &[&str]) -> VtabModuleMetadata
447 where
448 Self: Sized,
449 {
450 VtabModuleMetadata::ordinary()
451 }
452
453 fn shadow_table_policy(_vtab_name: &str, _table_name: &str) -> ShadowTablePolicy
456 where
457 Self: Sized,
458 {
459 ShadowTablePolicy::ordinary()
460 }
461
462 fn create(cx: &Cx, args: &[&str]) -> Result<Self>
467 where
468 Self: Sized,
469 {
470 Self::connect(cx, args)
471 }
472
473 fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
475 where
476 Self: Sized;
477
478 fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
480
481 fn open(&self) -> Result<Self::Cursor>;
483
484 fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
486 Ok(())
487 }
488
489 fn destroy(&mut self, cx: &Cx) -> Result<()> {
493 self.disconnect(cx)
494 }
495
496 fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
506 Err(FrankenError::ReadOnly)
507 }
508
509 fn begin(&mut self, _cx: &Cx) -> Result<()> {
511 Ok(())
512 }
513
514 fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
516 Ok(())
517 }
518
519 fn commit(&mut self, _cx: &Cx) -> Result<()> {
521 Ok(())
522 }
523
524 fn rollback(&mut self, _cx: &Cx) -> Result<()> {
526 Ok(())
527 }
528
529 fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
533 Err(FrankenError::Unsupported)
534 }
535
536 fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
538 Ok(())
539 }
540
541 fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
543 Ok(())
544 }
545
546 fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
548 Ok(())
549 }
550}
551
552#[allow(clippy::missing_errors_doc)]
567pub trait VirtualTableCursor: Send {
568 fn filter(
570 &mut self,
571 cx: &Cx,
572 idx_num: i32,
573 idx_str: Option<&str>,
574 args: &[SqliteValue],
575 ) -> Result<()>;
576
577 fn next(&mut self, cx: &Cx) -> Result<()>;
579
580 fn eof(&self) -> bool;
582
583 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
585
586 fn rowid(&self) -> Result<i64>;
588}
589
590#[allow(clippy::missing_errors_doc)]
600pub trait VtabModuleFactory: Send + Sync {
601 fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>>;
603
604 fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
606 self.create(cx, args)
607 }
608
609 fn column_info(&self, _args: &[&str]) -> Vec<(String, char)> {
611 Vec::new()
612 }
613
614 fn module_metadata(&self, _args: &[&str]) -> VtabModuleMetadata {
616 VtabModuleMetadata::ordinary()
617 }
618
619 fn shadow_table_policy(&self, _vtab_name: &str, _table_name: &str) -> ShadowTablePolicy {
622 ShadowTablePolicy::ordinary()
623 }
624}
625
626#[allow(clippy::missing_errors_doc)]
628pub trait ErasedVtabInstance: Send + Sync {
629 fn as_any(&self) -> &dyn Any;
631 fn as_any_mut(&mut self) -> &mut dyn Any;
633 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
635 fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
637 fn begin(&mut self, cx: &Cx) -> Result<()>;
639 fn sync_txn(&mut self, cx: &Cx) -> Result<()>;
641 fn commit(&mut self, cx: &Cx) -> Result<()>;
643 fn rollback(&mut self, cx: &Cx) -> Result<()>;
645 fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()>;
647 fn release(&mut self, cx: &Cx, n: i32) -> Result<()>;
649 fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()>;
651 fn destroy(&mut self, cx: &Cx) -> Result<()>;
653 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
655 fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
657}
658
659#[allow(clippy::missing_errors_doc)]
661pub trait ErasedVtabCursor: Send {
662 fn erased_filter(
664 &mut self,
665 cx: &Cx,
666 idx_num: i32,
667 idx_str: Option<&str>,
668 args: &[SqliteValue],
669 ) -> Result<()>;
670 fn erased_next(&mut self, cx: &Cx) -> Result<()>;
672 fn erased_eof(&self) -> bool;
674 fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
676 fn erased_rowid(&self) -> Result<i64>;
678}
679
680impl<C: VirtualTableCursor + 'static> ErasedVtabCursor for C {
682 fn erased_filter(
683 &mut self,
684 cx: &Cx,
685 idx_num: i32,
686 idx_str: Option<&str>,
687 args: &[SqliteValue],
688 ) -> Result<()> {
689 VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
690 }
691 fn erased_next(&mut self, cx: &Cx) -> Result<()> {
692 VirtualTableCursor::next(self, cx)
693 }
694 fn erased_eof(&self) -> bool {
695 VirtualTableCursor::eof(self)
696 }
697 fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
698 VirtualTableCursor::column(self, ctx, col)
699 }
700 fn erased_rowid(&self) -> Result<i64> {
701 VirtualTableCursor::rowid(self)
702 }
703}
704
705impl<T: VirtualTable + 'static> ErasedVtabInstance for T
707where
708 T::Cursor: 'static,
709{
710 fn as_any(&self) -> &dyn Any {
711 self
712 }
713
714 fn as_any_mut(&mut self) -> &mut dyn Any {
715 self
716 }
717
718 fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
719 let cursor = VirtualTable::open(self)?;
720 Ok(Box::new(cursor))
721 }
722 fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
723 VirtualTable::update(self, cx, args)
724 }
725 fn begin(&mut self, cx: &Cx) -> Result<()> {
726 VirtualTable::begin(self, cx)
727 }
728 fn sync_txn(&mut self, cx: &Cx) -> Result<()> {
729 VirtualTable::sync_txn(self, cx)
730 }
731 fn commit(&mut self, cx: &Cx) -> Result<()> {
732 VirtualTable::commit(self, cx)
733 }
734 fn rollback(&mut self, cx: &Cx) -> Result<()> {
735 VirtualTable::rollback(self, cx)
736 }
737 fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()> {
738 VirtualTable::savepoint(self, cx, n)
739 }
740 fn release(&mut self, cx: &Cx, n: i32) -> Result<()> {
741 VirtualTable::release(self, cx, n)
742 }
743 fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()> {
744 VirtualTable::rollback_to(self, cx, n)
745 }
746 fn destroy(&mut self, cx: &Cx) -> Result<()> {
747 VirtualTable::destroy(self, cx)
748 }
749 fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
750 VirtualTable::rename(self, cx, new_name)
751 }
752 fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
753 VirtualTable::best_index(self, info)
754 }
755}
756
757pub fn module_factory_from<T>() -> impl VtabModuleFactory
759where
760 T: VirtualTable + 'static,
761 T::Cursor: 'static,
762{
763 struct Factory<T: Send + Sync>(std::marker::PhantomData<T>);
764
765 impl<T: VirtualTable + 'static> VtabModuleFactory for Factory<T>
766 where
767 T::Cursor: 'static,
768 {
769 fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
770 let vtab = T::create(cx, args)?;
771 Ok(Box::new(vtab))
772 }
773 fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
774 let vtab = T::connect(cx, args)?;
775 Ok(Box::new(vtab))
776 }
777
778 fn module_metadata(&self, args: &[&str]) -> VtabModuleMetadata {
779 T::module_metadata(args)
780 }
781
782 fn shadow_table_policy(&self, vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
783 T::shadow_table_policy(vtab_name, table_name)
784 }
785 }
786
787 Factory::<T>(std::marker::PhantomData)
788}
789
790#[cfg(test)]
795#[allow(clippy::too_many_lines)]
796mod tests {
797 use super::*;
798
799 struct GenerateSeries {
802 destroyed: bool,
803 }
804
805 struct GenerateSeriesCursor {
806 start: i64,
807 stop: i64,
808 current: i64,
809 }
810
811 impl VirtualTable for GenerateSeries {
812 type Cursor = GenerateSeriesCursor;
813
814 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
815 Ok(Self { destroyed: false })
816 }
817
818 fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
819 info.estimated_cost = 10.0;
820 info.estimated_rows = 100;
821 info.idx_num = 1;
822
823 if !info.constraints.is_empty() && info.constraints[0].usable {
825 info.constraint_usage[0].argv_index = 1;
826 info.constraint_usage[0].omit = true;
827 }
828 Ok(())
829 }
830
831 fn open(&self) -> Result<GenerateSeriesCursor> {
832 Ok(GenerateSeriesCursor {
833 start: 0,
834 stop: 0,
835 current: 0,
836 })
837 }
838
839 fn destroy(&mut self, _cx: &Cx) -> Result<()> {
840 self.destroyed = true;
841 Ok(())
842 }
843 }
844
845 impl VirtualTableCursor for GenerateSeriesCursor {
846 fn filter(
847 &mut self,
848 _cx: &Cx,
849 _idx_num: i32,
850 _idx_str: Option<&str>,
851 args: &[SqliteValue],
852 ) -> Result<()> {
853 self.start = args.first().map_or(1, SqliteValue::to_integer);
854 self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
855 self.current = self.start;
856 Ok(())
857 }
858
859 fn next(&mut self, _cx: &Cx) -> Result<()> {
860 self.current += 1;
861 Ok(())
862 }
863
864 fn eof(&self) -> bool {
865 self.current > self.stop
866 }
867
868 fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
869 if self.eof() {
870 ctx.set_value(SqliteValue::Null);
871 return Ok(());
872 }
873 ctx.set_value(SqliteValue::Integer(self.current));
874 Ok(())
875 }
876
877 fn rowid(&self) -> Result<i64> {
878 Ok(if self.eof() { 0 } else { self.current })
879 }
880 }
881
882 struct ReadOnlyVtab;
885
886 struct ReadOnlyCursor;
887
888 impl VirtualTable for ReadOnlyVtab {
889 type Cursor = ReadOnlyCursor;
890
891 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
892 Ok(Self)
893 }
894
895 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
896 Ok(())
897 }
898
899 fn open(&self) -> Result<ReadOnlyCursor> {
900 Ok(ReadOnlyCursor)
901 }
902 }
903
904 impl VirtualTableCursor for ReadOnlyCursor {
905 fn filter(
906 &mut self,
907 _cx: &Cx,
908 _idx_num: i32,
909 _idx_str: Option<&str>,
910 _args: &[SqliteValue],
911 ) -> Result<()> {
912 Ok(())
913 }
914
915 fn next(&mut self, _cx: &Cx) -> Result<()> {
916 Ok(())
917 }
918
919 fn eof(&self) -> bool {
920 true
921 }
922
923 fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
924 ctx.set_value(SqliteValue::Null);
925 Ok(())
926 }
927
928 fn rowid(&self) -> Result<i64> {
929 Ok(0)
930 }
931 }
932
933 struct WritableVtab {
936 rows: Vec<(i64, Vec<SqliteValue>)>,
937 next_rowid: i64,
938 }
939
940 struct WritableCursor {
941 rows: Vec<(i64, Vec<SqliteValue>)>,
942 pos: usize,
943 }
944
945 impl VirtualTable for WritableVtab {
946 type Cursor = WritableCursor;
947
948 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
949 Ok(Self {
950 rows: Vec::new(),
951 next_rowid: 1,
952 })
953 }
954
955 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
956 Ok(())
957 }
958
959 fn open(&self) -> Result<WritableCursor> {
960 Ok(WritableCursor {
961 rows: self.rows.clone(),
962 pos: 0,
963 })
964 }
965
966 fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
967 if args[0].is_null() {
969 let rowid = self.next_rowid;
971 self.next_rowid += 1;
972 let cols: Vec<SqliteValue> = args[2..].to_vec();
973 self.rows.push((rowid, cols));
974 return Ok(Some(rowid));
975 }
976 Ok(None)
977 }
978 }
979
980 impl VirtualTableCursor for WritableCursor {
981 fn filter(
982 &mut self,
983 _cx: &Cx,
984 _idx_num: i32,
985 _idx_str: Option<&str>,
986 _args: &[SqliteValue],
987 ) -> Result<()> {
988 self.pos = 0;
989 Ok(())
990 }
991
992 fn next(&mut self, _cx: &Cx) -> Result<()> {
993 self.pos += 1;
994 Ok(())
995 }
996
997 fn eof(&self) -> bool {
998 self.pos >= self.rows.len()
999 }
1000
1001 fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
1002 if self.eof() {
1003 ctx.set_value(SqliteValue::Null);
1004 return Ok(());
1005 }
1006
1007 #[allow(clippy::cast_sign_loss)]
1008 let col_idx = col as usize;
1009 if let Some((_, cols)) = self.rows.get(self.pos) {
1010 if let Some(val) = cols.get(col_idx) {
1011 ctx.set_value(val.clone());
1012 return Ok(());
1013 }
1014 }
1015 ctx.set_value(SqliteValue::Null);
1016 Ok(())
1017 }
1018
1019 fn rowid(&self) -> Result<i64> {
1020 self.rows
1021 .get(self.pos)
1022 .map_or(Ok(0), |(rowid, _)| Ok(*rowid))
1023 }
1024 }
1025
1026 struct ShadowOwningVtab;
1027
1028 impl VirtualTable for ShadowOwningVtab {
1029 type Cursor = ReadOnlyCursor;
1030
1031 fn module_metadata(_args: &[&str]) -> VtabModuleMetadata {
1032 VtabModuleMetadata::shadow_owning(
1033 VtabLifecyclePolicy::SeparateCreateAndConnect,
1034 VtabIntegrityPolicy::ShadowAware,
1035 VtabRiskLevel {
1036 innocuous: false,
1037 direct_only: true,
1038 uses_all_schemas: false,
1039 },
1040 )
1041 }
1042
1043 fn shadow_table_policy(vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
1044 let Some((owner, suffix)) = table_name.rsplit_once('_') else {
1045 return ShadowTablePolicy::ordinary();
1046 };
1047
1048 if owner == vtab_name
1049 && matches!(suffix, "config" | "content" | "data" | "docsize" | "idx")
1050 {
1051 return ShadowTablePolicy::owned_shadow();
1052 }
1053
1054 ShadowTablePolicy::ordinary()
1055 }
1056
1057 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1058 Ok(Self)
1059 }
1060
1061 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1062 Ok(())
1063 }
1064
1065 fn open(&self) -> Result<Self::Cursor> {
1066 Ok(ReadOnlyCursor)
1067 }
1068 }
1069
1070 #[derive(Debug, Clone, PartialEq, Eq)]
1071 struct HookSnapshot {
1072 version: i32,
1073 }
1074
1075 struct HookAwareVtab {
1076 version: i32,
1077 syncs: usize,
1078 tx_state: TransactionalVtabState<HookSnapshot>,
1079 }
1080
1081 impl VirtualTable for HookAwareVtab {
1082 type Cursor = ReadOnlyCursor;
1083
1084 fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
1085 Ok(Self {
1086 version: 7,
1087 syncs: 0,
1088 tx_state: TransactionalVtabState::default(),
1089 })
1090 }
1091
1092 fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
1093 Ok(())
1094 }
1095
1096 fn open(&self) -> Result<Self::Cursor> {
1097 Ok(ReadOnlyCursor)
1098 }
1099
1100 fn begin(&mut self, _cx: &Cx) -> Result<()> {
1101 self.tx_state.begin(HookSnapshot {
1102 version: self.version,
1103 });
1104 Ok(())
1105 }
1106
1107 fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
1108 self.syncs += 1;
1109 Ok(())
1110 }
1111
1112 fn savepoint(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1113 self.tx_state.savepoint(
1114 n,
1115 HookSnapshot {
1116 version: self.version,
1117 },
1118 );
1119 Ok(())
1120 }
1121
1122 fn release(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1123 self.tx_state.release(n);
1124 Ok(())
1125 }
1126
1127 fn rollback_to(&mut self, _cx: &Cx, n: i32) -> Result<()> {
1128 if let Some(snapshot) = self.tx_state.rollback_to(n) {
1129 self.version = snapshot.version;
1130 }
1131 Ok(())
1132 }
1133
1134 fn commit(&mut self, _cx: &Cx) -> Result<()> {
1135 self.tx_state.commit();
1136 Ok(())
1137 }
1138
1139 fn rollback(&mut self, _cx: &Cx) -> Result<()> {
1140 if let Some(snapshot) = self.tx_state.rollback() {
1141 self.version = snapshot.version;
1142 }
1143 Ok(())
1144 }
1145 }
1146
1147 #[test]
1150 fn test_vtab_create_vs_connect() {
1151 let cx = Cx::new();
1152
1153 let vtab = GenerateSeries::create(&cx, &[]).unwrap();
1155 assert!(!vtab.destroyed);
1156
1157 let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
1159 assert!(!vtab2.destroyed);
1160 }
1161
1162 #[test]
1163 fn test_vtab_best_index_populates_info() {
1164 let cx = Cx::new();
1165 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1166
1167 let mut info = IndexInfo::new(
1168 vec![IndexConstraint {
1169 column: 0,
1170 op: ConstraintOp::Gt,
1171 usable: true,
1172 }],
1173 vec![],
1174 );
1175
1176 VirtualTable::best_index(&vtab, &mut info).unwrap();
1177
1178 assert_eq!(info.idx_num, 1);
1179 assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
1180 assert_eq!(info.estimated_rows, 100);
1181 assert_eq!(info.constraint_usage[0].argv_index, 1);
1182 assert!(info.constraint_usage[0].omit);
1183 }
1184
1185 #[test]
1186 fn test_vtab_cursor_filter_next_eof() {
1187 let cx = Cx::new();
1188 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1189 let mut cursor = vtab.open().unwrap();
1190
1191 cursor
1192 .filter(
1193 &cx,
1194 0,
1195 None,
1196 &[SqliteValue::Integer(1), SqliteValue::Integer(3)],
1197 )
1198 .unwrap();
1199
1200 let mut values = Vec::new();
1201 while !cursor.eof() {
1202 let mut ctx = ColumnContext::new();
1203 cursor.column(&mut ctx, 0).unwrap();
1204 let rowid = cursor.rowid().unwrap();
1205 values.push((rowid, ctx.take_value().unwrap()));
1206 cursor.next(&cx).unwrap();
1207 }
1208
1209 assert_eq!(values.len(), 3);
1210 assert_eq!(values[0], (1, SqliteValue::Integer(1)));
1211 assert_eq!(values[1], (2, SqliteValue::Integer(2)));
1212 assert_eq!(values[2], (3, SqliteValue::Integer(3)));
1213 }
1214
1215 #[test]
1216 fn test_generate_series_cursor_past_end_returns_null_and_zero_rowid() {
1217 let cx = Cx::new();
1218 let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1219 let mut cursor = vtab.open().unwrap();
1220
1221 cursor
1222 .filter(
1223 &cx,
1224 0,
1225 None,
1226 &[SqliteValue::Integer(1), SqliteValue::Integer(1)],
1227 )
1228 .unwrap();
1229 cursor.next(&cx).unwrap();
1230 assert!(cursor.eof());
1231
1232 let mut ctx = ColumnContext::new();
1233 cursor.column(&mut ctx, 0).unwrap();
1234 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1235 assert_eq!(cursor.rowid().unwrap(), 0);
1236 }
1237
1238 #[test]
1239 fn test_writable_cursor_missing_column_returns_null() {
1240 let cx = Cx::new();
1241 let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1242 VirtualTable::update(
1243 &mut vtab,
1244 &cx,
1245 &[
1246 SqliteValue::Null,
1247 SqliteValue::Null,
1248 SqliteValue::Text("hello".into()),
1249 ],
1250 )
1251 .unwrap();
1252
1253 let mut cursor = vtab.open().unwrap();
1254 cursor.filter(&cx, 0, None, &[]).unwrap();
1255
1256 let mut ctx = ColumnContext::new();
1257 cursor.column(&mut ctx, 3).unwrap();
1258 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1259
1260 cursor.next(&cx).unwrap();
1261 assert!(cursor.eof());
1262 cursor.column(&mut ctx, 0).unwrap();
1263 assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
1264 assert_eq!(cursor.rowid().unwrap(), 0);
1265 }
1266
1267 #[test]
1268 fn test_vtab_update_insert() {
1269 let cx = Cx::new();
1270 let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
1271
1272 let result = VirtualTable::update(
1275 &mut vtab,
1276 &cx,
1277 &[
1278 SqliteValue::Null,
1279 SqliteValue::Null,
1280 SqliteValue::Text("hello".into()),
1281 ],
1282 )
1283 .unwrap();
1284
1285 assert_eq!(result, Some(1));
1286 assert_eq!(vtab.rows.len(), 1);
1287 assert_eq!(vtab.rows[0].0, 1);
1288 }
1289
1290 #[test]
1291 fn test_vtab_update_readonly_default() {
1292 let cx = Cx::new();
1293 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1294 let err = VirtualTable::update(&mut vtab, &cx, &[SqliteValue::Null]).unwrap_err();
1295 assert!(matches!(err, FrankenError::ReadOnly));
1296 }
1297
1298 #[test]
1299 fn test_vtab_destroy_vs_disconnect() {
1300 let cx = Cx::new();
1301
1302 let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
1304 VirtualTable::disconnect(&mut vtab, &cx).unwrap();
1305 VirtualTable::destroy(&mut vtab, &cx).unwrap();
1306
1307 let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
1309 assert!(!vtab.destroyed);
1310 VirtualTable::destroy(&mut vtab, &cx).unwrap();
1311 assert!(vtab.destroyed);
1312 }
1313
1314 #[test]
1315 fn test_vtab_cursor_send_but_not_sync() {
1316 fn assert_send<T: Send>() {}
1317 assert_send::<GenerateSeriesCursor>();
1318
1319 }
1326
1327 #[test]
1328 fn test_column_context_lifecycle() {
1329 let mut ctx = ColumnContext::new();
1330 assert!(ctx.take_value().is_none());
1331
1332 ctx.set_value(SqliteValue::Integer(42));
1333 assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
1334
1335 assert!(ctx.take_value().is_none());
1337 }
1338
1339 #[test]
1340 fn test_index_info_new() {
1341 let info = IndexInfo::new(
1342 vec![
1343 IndexConstraint {
1344 column: 0,
1345 op: ConstraintOp::Eq,
1346 usable: true,
1347 },
1348 IndexConstraint {
1349 column: 1,
1350 op: ConstraintOp::Gt,
1351 usable: false,
1352 },
1353 ],
1354 vec![IndexOrderBy {
1355 column: 0,
1356 desc: false,
1357 }],
1358 );
1359
1360 assert_eq!(info.constraints.len(), 2);
1361 assert_eq!(info.order_by.len(), 1);
1362 assert_eq!(info.constraint_usage.len(), 2);
1363 assert_eq!(info.idx_num, 0);
1364 assert!(info.idx_str.is_none());
1365 assert!(!info.order_by_consumed);
1366 }
1367
1368 #[test]
1369 fn test_transactional_vtab_state_tracks_savepoints() {
1370 let mut state = TransactionalVtabState::default();
1371
1372 state.begin(1_i32);
1373 state.savepoint(0, 2);
1374 state.savepoint(1, 3);
1375 assert_eq!(state.rollback_to(1), Some(3));
1376 state.release(1);
1377 assert_eq!(state.rollback(), Some(1));
1378 assert_eq!(state.rollback(), None);
1379 }
1380
1381 #[test]
1382 fn test_transactional_vtab_state_uses_base_for_late_enlistment() {
1383 let mut state = TransactionalVtabState::default();
1384
1385 state.begin(7_i32);
1386 state.savepoint(2, 9);
1387
1388 assert_eq!(state.rollback_to(1), Some(7));
1389 assert_eq!(state.rollback(), Some(7));
1390 }
1391
1392 #[test]
1393 fn test_shadow_table_policy_defaults_to_ordinary() {
1394 let policy = ReadOnlyVtab::shadow_table_policy("docs", "docs_data");
1395 assert_eq!(policy, ShadowTablePolicy::ordinary());
1396 assert!(!policy.is_shadow());
1397 assert!(policy.allows_direct_dml());
1398 assert!(policy.allows_schema_ddl());
1399 assert!(policy.allows_module_internal_write());
1400 }
1401
1402 #[test]
1403 fn test_owned_shadow_policy_blocks_user_dml_and_schema_ddl() {
1404 let policy = ShadowTablePolicy::owned_shadow();
1405
1406 assert!(policy.is_shadow());
1407 assert!(!policy.allows_direct_dml());
1408 assert!(!policy.allows_schema_ddl());
1409 assert!(policy.allows_module_internal_write());
1410 }
1411
1412 #[test]
1413 fn test_shadow_owning_module_metadata_is_forwarded_by_factory() {
1414 let factory = module_factory_from::<ShadowOwningVtab>();
1415 let metadata = factory.module_metadata(&[]);
1416
1417 assert!(metadata.owns_shadow_tables);
1418 assert_eq!(
1419 metadata.lifecycle,
1420 VtabLifecyclePolicy::SeparateCreateAndConnect
1421 );
1422 assert_eq!(metadata.integrity, VtabIntegrityPolicy::ShadowAware);
1423 assert!(metadata.risk.direct_only);
1424 assert!(!metadata.risk.innocuous);
1425 }
1426
1427 #[test]
1428 fn test_shadow_owning_module_matches_owned_shadow_tables() {
1429 let factory = module_factory_from::<ShadowOwningVtab>();
1430
1431 let owned = factory.shadow_table_policy("docs", "docs_data");
1432 let other_owner = factory.shadow_table_policy("docs", "posts_data");
1433 let unrelated = factory.shadow_table_policy("docs", "docs_segments");
1434
1435 assert_eq!(owned.kind, ShadowTableKind::Shadow);
1436 assert!(!owned.allows_direct_dml());
1437 assert!(!owned.allows_schema_ddl());
1438 assert!(owned.allows_module_internal_write());
1439 assert!(!other_owner.is_shadow());
1440 assert!(!unrelated.is_shadow());
1441 assert!(unrelated.allows_direct_dml());
1442 }
1443
1444 #[test]
1445 fn test_erased_vtab_instance_forwards_transaction_hooks() {
1446 let cx = Cx::new();
1447 let mut erased: Box<dyn ErasedVtabInstance> =
1448 Box::new(HookAwareVtab::connect(&cx, &[]).unwrap());
1449
1450 erased.begin(&cx).unwrap();
1451 {
1452 let hook = erased
1453 .as_any_mut()
1454 .downcast_mut::<HookAwareVtab>()
1455 .expect("hook-aware vtab");
1456 hook.version = 9;
1457 }
1458 erased.savepoint(&cx, 0).unwrap();
1459 {
1460 let hook = erased
1461 .as_any_mut()
1462 .downcast_mut::<HookAwareVtab>()
1463 .expect("hook-aware vtab");
1464 hook.version = 11;
1465 }
1466 erased.rollback_to(&cx, 0).unwrap();
1467 erased.release(&cx, 0).unwrap();
1468 erased.sync_txn(&cx).unwrap();
1469 erased.rollback(&cx).unwrap();
1470
1471 let hook = erased
1472 .as_any_mut()
1473 .downcast_mut::<HookAwareVtab>()
1474 .expect("hook-aware vtab");
1475 assert_eq!(hook.version, 7);
1476 assert_eq!(hook.syncs, 1);
1477 }
1478}