use std::any::Any;
use fsqlite_error::{FrankenError, Result};
use fsqlite_types::SqliteValue;
use fsqlite_types::cx::Cx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConstraintOp {
Eq,
Gt,
Le,
Lt,
Ge,
Match,
Like,
Glob,
Regexp,
Ne,
IsNot,
IsNotNull,
IsNull,
Is,
}
#[derive(Debug, Clone)]
pub struct IndexConstraint {
pub column: i32,
pub op: ConstraintOp,
pub usable: bool,
}
#[derive(Debug, Clone)]
pub struct IndexOrderBy {
pub column: i32,
pub desc: bool,
}
#[derive(Debug, Clone, Default)]
pub struct IndexConstraintUsage {
pub argv_index: i32,
pub omit: bool,
}
#[derive(Debug, Clone)]
pub struct IndexInfo {
pub constraints: Vec<IndexConstraint>,
pub order_by: Vec<IndexOrderBy>,
pub constraint_usage: Vec<IndexConstraintUsage>,
pub idx_num: i32,
pub idx_str: Option<String>,
pub order_by_consumed: bool,
pub estimated_cost: f64,
pub estimated_rows: i64,
}
impl IndexInfo {
#[must_use]
pub fn new(constraints: Vec<IndexConstraint>, order_by: Vec<IndexOrderBy>) -> Self {
let usage_len = constraints.len();
Self {
constraints,
order_by,
constraint_usage: vec![IndexConstraintUsage::default(); usage_len],
idx_num: 0,
idx_str: None,
order_by_consumed: false,
estimated_cost: 1_000_000.0,
estimated_rows: 1_000_000,
}
}
}
#[derive(Debug, Default)]
pub struct ColumnContext {
value: Option<SqliteValue>,
}
impl ColumnContext {
#[must_use]
pub fn new() -> Self {
Self { value: None }
}
pub fn set_value(&mut self, val: SqliteValue) {
self.value = Some(val);
}
pub fn take_value(&mut self) -> Option<SqliteValue> {
self.value.take()
}
}
#[derive(Debug, Clone)]
pub struct TransactionalVtabState<S: Clone> {
base_snapshot: Option<S>,
savepoints: Vec<(i32, S)>,
}
impl<S: Clone> Default for TransactionalVtabState<S> {
fn default() -> Self {
Self {
base_snapshot: None,
savepoints: Vec::new(),
}
}
}
impl<S: Clone> TransactionalVtabState<S> {
pub fn begin(&mut self, snapshot: S) {
if self.base_snapshot.is_none() {
self.base_snapshot = Some(snapshot);
self.savepoints.clear();
}
}
pub fn commit(&mut self) {
self.base_snapshot = None;
self.savepoints.clear();
}
pub fn rollback(&mut self) -> Option<S> {
let snapshot = self.base_snapshot.take();
self.savepoints.clear();
snapshot
}
pub fn savepoint(&mut self, level: i32, snapshot: S) {
if self.base_snapshot.is_none() {
return;
}
self.savepoints.retain(|(existing, _)| *existing < level);
self.savepoints.push((level, snapshot));
}
pub fn release(&mut self, level: i32) {
if self.base_snapshot.is_none() {
return;
}
self.savepoints.retain(|(existing, _)| *existing < level);
}
pub fn rollback_to(&mut self, level: i32) -> Option<S> {
self.base_snapshot.as_ref()?;
let snapshot = self
.savepoints
.iter()
.rfind(|(existing, _)| *existing == level)
.map(|(_, snapshot)| snapshot.clone())
.or_else(|| self.base_snapshot.clone());
if snapshot.is_some() {
self.savepoints.retain(|(existing, _)| *existing <= level);
}
snapshot
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ShadowTableKind {
#[default]
Ordinary,
Shadow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShadowTablePolicy {
pub kind: ShadowTableKind,
}
impl ShadowTablePolicy {
#[must_use]
pub const fn ordinary() -> Self {
Self {
kind: ShadowTableKind::Ordinary,
}
}
#[must_use]
pub const fn owned_shadow() -> Self {
Self {
kind: ShadowTableKind::Shadow,
}
}
#[must_use]
pub const fn is_shadow(self) -> bool {
matches!(self.kind, ShadowTableKind::Shadow)
}
}
impl Default for ShadowTablePolicy {
fn default() -> Self {
Self::ordinary()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VtabLifecyclePolicy {
#[default]
Simple,
SeparateCreateAndConnect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VtabIntegrityPolicy {
#[default]
None,
ShadowAware,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct VtabRiskLevel {
pub innocuous: bool,
pub direct_only: bool,
pub uses_all_schemas: bool,
}
impl VtabRiskLevel {
#[must_use]
pub const fn innocuous() -> Self {
Self {
innocuous: true,
direct_only: false,
uses_all_schemas: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VtabModuleMetadata {
pub owns_shadow_tables: bool,
pub lifecycle: VtabLifecyclePolicy,
pub integrity: VtabIntegrityPolicy,
pub risk: VtabRiskLevel,
}
impl VtabModuleMetadata {
#[must_use]
pub const fn ordinary() -> Self {
Self {
owns_shadow_tables: false,
lifecycle: VtabLifecyclePolicy::Simple,
integrity: VtabIntegrityPolicy::None,
risk: VtabRiskLevel::innocuous(),
}
}
#[must_use]
pub const fn shadow_owning(
lifecycle: VtabLifecyclePolicy,
integrity: VtabIntegrityPolicy,
risk: VtabRiskLevel,
) -> Self {
Self {
owns_shadow_tables: true,
lifecycle,
integrity,
risk,
}
}
}
impl Default for VtabModuleMetadata {
fn default() -> Self {
Self::ordinary()
}
}
#[allow(clippy::missing_errors_doc)]
pub trait VirtualTable: Send + Sync {
type Cursor: VirtualTableCursor;
fn module_metadata(_args: &[&str]) -> VtabModuleMetadata
where
Self: Sized,
{
VtabModuleMetadata::ordinary()
}
fn shadow_table_policy(_vtab_name: &str, _table_name: &str) -> ShadowTablePolicy
where
Self: Sized,
{
ShadowTablePolicy::ordinary()
}
fn create(cx: &Cx, args: &[&str]) -> Result<Self>
where
Self: Sized,
{
Self::connect(cx, args)
}
fn connect(cx: &Cx, args: &[&str]) -> Result<Self>
where
Self: Sized;
fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
fn open(&self) -> Result<Self::Cursor>;
fn disconnect(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn destroy(&mut self, cx: &Cx) -> Result<()> {
self.disconnect(cx)
}
fn update(&mut self, _cx: &Cx, _args: &[SqliteValue]) -> Result<Option<i64>> {
Err(FrankenError::ReadOnly)
}
fn begin(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn commit(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn rollback(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn rename(&mut self, _cx: &Cx, _new_name: &str) -> Result<()> {
Err(FrankenError::Unsupported)
}
fn savepoint(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
Ok(())
}
fn release(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
Ok(())
}
fn rollback_to(&mut self, _cx: &Cx, _n: i32) -> Result<()> {
Ok(())
}
}
#[allow(clippy::missing_errors_doc)]
pub trait VirtualTableCursor: Send {
fn filter(
&mut self,
cx: &Cx,
idx_num: i32,
idx_str: Option<&str>,
args: &[SqliteValue],
) -> Result<()>;
fn next(&mut self, cx: &Cx) -> Result<()>;
fn eof(&self) -> bool;
fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
fn rowid(&self) -> Result<i64>;
}
#[allow(clippy::missing_errors_doc)]
pub trait VtabModuleFactory: Send + Sync {
fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>>;
fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
self.create(cx, args)
}
fn column_info(&self, _args: &[&str]) -> Vec<(String, char)> {
Vec::new()
}
fn module_metadata(&self, _args: &[&str]) -> VtabModuleMetadata {
VtabModuleMetadata::ordinary()
}
fn shadow_table_policy(&self, _vtab_name: &str, _table_name: &str) -> ShadowTablePolicy {
ShadowTablePolicy::ordinary()
}
}
#[allow(clippy::missing_errors_doc)]
pub trait ErasedVtabInstance: Send + Sync {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>>;
fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>>;
fn begin(&mut self, cx: &Cx) -> Result<()>;
fn sync_txn(&mut self, cx: &Cx) -> Result<()>;
fn commit(&mut self, cx: &Cx) -> Result<()>;
fn rollback(&mut self, cx: &Cx) -> Result<()>;
fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()>;
fn release(&mut self, cx: &Cx, n: i32) -> Result<()>;
fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()>;
fn destroy(&mut self, cx: &Cx) -> Result<()>;
fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()>;
fn best_index(&self, info: &mut IndexInfo) -> Result<()>;
}
#[allow(clippy::missing_errors_doc)]
pub trait ErasedVtabCursor: Send {
fn erased_filter(
&mut self,
cx: &Cx,
idx_num: i32,
idx_str: Option<&str>,
args: &[SqliteValue],
) -> Result<()>;
fn erased_next(&mut self, cx: &Cx) -> Result<()>;
fn erased_eof(&self) -> bool;
fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()>;
fn erased_rowid(&self) -> Result<i64>;
}
impl<C: VirtualTableCursor + 'static> ErasedVtabCursor for C {
fn erased_filter(
&mut self,
cx: &Cx,
idx_num: i32,
idx_str: Option<&str>,
args: &[SqliteValue],
) -> Result<()> {
VirtualTableCursor::filter(self, cx, idx_num, idx_str, args)
}
fn erased_next(&mut self, cx: &Cx) -> Result<()> {
VirtualTableCursor::next(self, cx)
}
fn erased_eof(&self) -> bool {
VirtualTableCursor::eof(self)
}
fn erased_column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
VirtualTableCursor::column(self, ctx, col)
}
fn erased_rowid(&self) -> Result<i64> {
VirtualTableCursor::rowid(self)
}
}
impl<T: VirtualTable + 'static> ErasedVtabInstance for T
where
T::Cursor: 'static,
{
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn open_cursor(&self) -> Result<Box<dyn ErasedVtabCursor>> {
let cursor = VirtualTable::open(self)?;
Ok(Box::new(cursor))
}
fn update(&mut self, cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
VirtualTable::update(self, cx, args)
}
fn begin(&mut self, cx: &Cx) -> Result<()> {
VirtualTable::begin(self, cx)
}
fn sync_txn(&mut self, cx: &Cx) -> Result<()> {
VirtualTable::sync_txn(self, cx)
}
fn commit(&mut self, cx: &Cx) -> Result<()> {
VirtualTable::commit(self, cx)
}
fn rollback(&mut self, cx: &Cx) -> Result<()> {
VirtualTable::rollback(self, cx)
}
fn savepoint(&mut self, cx: &Cx, n: i32) -> Result<()> {
VirtualTable::savepoint(self, cx, n)
}
fn release(&mut self, cx: &Cx, n: i32) -> Result<()> {
VirtualTable::release(self, cx, n)
}
fn rollback_to(&mut self, cx: &Cx, n: i32) -> Result<()> {
VirtualTable::rollback_to(self, cx, n)
}
fn destroy(&mut self, cx: &Cx) -> Result<()> {
VirtualTable::destroy(self, cx)
}
fn rename(&mut self, cx: &Cx, new_name: &str) -> Result<()> {
VirtualTable::rename(self, cx, new_name)
}
fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
VirtualTable::best_index(self, info)
}
}
pub fn module_factory_from<T>() -> impl VtabModuleFactory
where
T: VirtualTable + 'static,
T::Cursor: 'static,
{
struct Factory<T: Send + Sync>(std::marker::PhantomData<T>);
impl<T: VirtualTable + 'static> VtabModuleFactory for Factory<T>
where
T::Cursor: 'static,
{
fn create(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
let vtab = T::create(cx, args)?;
Ok(Box::new(vtab))
}
fn connect(&self, cx: &Cx, args: &[&str]) -> Result<Box<dyn ErasedVtabInstance>> {
let vtab = T::connect(cx, args)?;
Ok(Box::new(vtab))
}
fn module_metadata(&self, args: &[&str]) -> VtabModuleMetadata {
T::module_metadata(args)
}
fn shadow_table_policy(&self, vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
T::shadow_table_policy(vtab_name, table_name)
}
}
Factory::<T>(std::marker::PhantomData)
}
#[cfg(test)]
#[allow(clippy::too_many_lines)]
mod tests {
use super::*;
struct GenerateSeries {
destroyed: bool,
}
struct GenerateSeriesCursor {
start: i64,
stop: i64,
current: i64,
}
impl VirtualTable for GenerateSeries {
type Cursor = GenerateSeriesCursor;
fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
Ok(Self { destroyed: false })
}
fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
info.estimated_cost = 10.0;
info.estimated_rows = 100;
info.idx_num = 1;
if !info.constraints.is_empty() && info.constraints[0].usable {
info.constraint_usage[0].argv_index = 1;
info.constraint_usage[0].omit = true;
}
Ok(())
}
fn open(&self) -> Result<GenerateSeriesCursor> {
Ok(GenerateSeriesCursor {
start: 0,
stop: 0,
current: 0,
})
}
fn destroy(&mut self, _cx: &Cx) -> Result<()> {
self.destroyed = true;
Ok(())
}
}
impl VirtualTableCursor for GenerateSeriesCursor {
fn filter(
&mut self,
_cx: &Cx,
_idx_num: i32,
_idx_str: Option<&str>,
args: &[SqliteValue],
) -> Result<()> {
self.start = args.first().map_or(1, SqliteValue::to_integer);
self.stop = args.get(1).map_or(10, SqliteValue::to_integer);
self.current = self.start;
Ok(())
}
fn next(&mut self, _cx: &Cx) -> Result<()> {
self.current += 1;
Ok(())
}
fn eof(&self) -> bool {
self.current > self.stop
}
fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
if self.eof() {
ctx.set_value(SqliteValue::Null);
return Ok(());
}
ctx.set_value(SqliteValue::Integer(self.current));
Ok(())
}
fn rowid(&self) -> Result<i64> {
Ok(if self.eof() { 0 } else { self.current })
}
}
struct ReadOnlyVtab;
struct ReadOnlyCursor;
impl VirtualTable for ReadOnlyVtab {
type Cursor = ReadOnlyCursor;
fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
Ok(Self)
}
fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
Ok(())
}
fn open(&self) -> Result<ReadOnlyCursor> {
Ok(ReadOnlyCursor)
}
}
impl VirtualTableCursor for ReadOnlyCursor {
fn filter(
&mut self,
_cx: &Cx,
_idx_num: i32,
_idx_str: Option<&str>,
_args: &[SqliteValue],
) -> Result<()> {
Ok(())
}
fn next(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn eof(&self) -> bool {
true
}
fn column(&self, ctx: &mut ColumnContext, _col: i32) -> Result<()> {
ctx.set_value(SqliteValue::Null);
Ok(())
}
fn rowid(&self) -> Result<i64> {
Ok(0)
}
}
struct WritableVtab {
rows: Vec<(i64, Vec<SqliteValue>)>,
next_rowid: i64,
}
struct WritableCursor {
rows: Vec<(i64, Vec<SqliteValue>)>,
pos: usize,
}
impl VirtualTable for WritableVtab {
type Cursor = WritableCursor;
fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
Ok(Self {
rows: Vec::new(),
next_rowid: 1,
})
}
fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
Ok(())
}
fn open(&self) -> Result<WritableCursor> {
Ok(WritableCursor {
rows: self.rows.clone(),
pos: 0,
})
}
fn update(&mut self, _cx: &Cx, args: &[SqliteValue]) -> Result<Option<i64>> {
if args[0].is_null() {
let rowid = self.next_rowid;
self.next_rowid += 1;
let cols: Vec<SqliteValue> = args[2..].to_vec();
self.rows.push((rowid, cols));
return Ok(Some(rowid));
}
Ok(None)
}
}
impl VirtualTableCursor for WritableCursor {
fn filter(
&mut self,
_cx: &Cx,
_idx_num: i32,
_idx_str: Option<&str>,
_args: &[SqliteValue],
) -> Result<()> {
self.pos = 0;
Ok(())
}
fn next(&mut self, _cx: &Cx) -> Result<()> {
self.pos += 1;
Ok(())
}
fn eof(&self) -> bool {
self.pos >= self.rows.len()
}
fn column(&self, ctx: &mut ColumnContext, col: i32) -> Result<()> {
if self.eof() {
ctx.set_value(SqliteValue::Null);
return Ok(());
}
#[allow(clippy::cast_sign_loss)]
let col_idx = col as usize;
if let Some((_, cols)) = self.rows.get(self.pos) {
if let Some(val) = cols.get(col_idx) {
ctx.set_value(val.clone());
return Ok(());
}
}
ctx.set_value(SqliteValue::Null);
Ok(())
}
fn rowid(&self) -> Result<i64> {
self.rows
.get(self.pos)
.map_or(Ok(0), |(rowid, _)| Ok(*rowid))
}
}
struct ShadowOwningVtab;
impl VirtualTable for ShadowOwningVtab {
type Cursor = ReadOnlyCursor;
fn module_metadata(_args: &[&str]) -> VtabModuleMetadata {
VtabModuleMetadata::shadow_owning(
VtabLifecyclePolicy::SeparateCreateAndConnect,
VtabIntegrityPolicy::ShadowAware,
VtabRiskLevel {
innocuous: false,
direct_only: true,
uses_all_schemas: false,
},
)
}
fn shadow_table_policy(vtab_name: &str, table_name: &str) -> ShadowTablePolicy {
let Some((owner, suffix)) = table_name.rsplit_once('_') else {
return ShadowTablePolicy::ordinary();
};
if owner == vtab_name
&& matches!(suffix, "config" | "content" | "data" | "docsize" | "idx")
{
return ShadowTablePolicy::owned_shadow();
}
ShadowTablePolicy::ordinary()
}
fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
Ok(Self)
}
fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
Ok(())
}
fn open(&self) -> Result<Self::Cursor> {
Ok(ReadOnlyCursor)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct HookSnapshot {
version: i32,
}
struct HookAwareVtab {
version: i32,
syncs: usize,
tx_state: TransactionalVtabState<HookSnapshot>,
}
impl VirtualTable for HookAwareVtab {
type Cursor = ReadOnlyCursor;
fn connect(_cx: &Cx, _args: &[&str]) -> Result<Self> {
Ok(Self {
version: 7,
syncs: 0,
tx_state: TransactionalVtabState::default(),
})
}
fn best_index(&self, _info: &mut IndexInfo) -> Result<()> {
Ok(())
}
fn open(&self) -> Result<Self::Cursor> {
Ok(ReadOnlyCursor)
}
fn begin(&mut self, _cx: &Cx) -> Result<()> {
self.tx_state.begin(HookSnapshot {
version: self.version,
});
Ok(())
}
fn sync_txn(&mut self, _cx: &Cx) -> Result<()> {
self.syncs += 1;
Ok(())
}
fn savepoint(&mut self, _cx: &Cx, n: i32) -> Result<()> {
self.tx_state.savepoint(
n,
HookSnapshot {
version: self.version,
},
);
Ok(())
}
fn release(&mut self, _cx: &Cx, n: i32) -> Result<()> {
self.tx_state.release(n);
Ok(())
}
fn rollback_to(&mut self, _cx: &Cx, n: i32) -> Result<()> {
if let Some(snapshot) = self.tx_state.rollback_to(n) {
self.version = snapshot.version;
}
Ok(())
}
fn commit(&mut self, _cx: &Cx) -> Result<()> {
self.tx_state.commit();
Ok(())
}
fn rollback(&mut self, _cx: &Cx) -> Result<()> {
if let Some(snapshot) = self.tx_state.rollback() {
self.version = snapshot.version;
}
Ok(())
}
}
#[test]
fn test_vtab_create_vs_connect() {
let cx = Cx::new();
let vtab = GenerateSeries::create(&cx, &[]).unwrap();
assert!(!vtab.destroyed);
let vtab2 = GenerateSeries::connect(&cx, &[]).unwrap();
assert!(!vtab2.destroyed);
}
#[test]
fn test_vtab_best_index_populates_info() {
let cx = Cx::new();
let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
let mut info = IndexInfo::new(
vec![IndexConstraint {
column: 0,
op: ConstraintOp::Gt,
usable: true,
}],
vec![],
);
VirtualTable::best_index(&vtab, &mut info).unwrap();
assert_eq!(info.idx_num, 1);
assert!((info.estimated_cost - 10.0).abs() < f64::EPSILON);
assert_eq!(info.estimated_rows, 100);
assert_eq!(info.constraint_usage[0].argv_index, 1);
assert!(info.constraint_usage[0].omit);
}
#[test]
fn test_vtab_cursor_filter_next_eof() {
let cx = Cx::new();
let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
let mut cursor = vtab.open().unwrap();
cursor
.filter(
&cx,
0,
None,
&[SqliteValue::Integer(1), SqliteValue::Integer(3)],
)
.unwrap();
let mut values = Vec::new();
while !cursor.eof() {
let mut ctx = ColumnContext::new();
cursor.column(&mut ctx, 0).unwrap();
let rowid = cursor.rowid().unwrap();
values.push((rowid, ctx.take_value().unwrap()));
cursor.next(&cx).unwrap();
}
assert_eq!(values.len(), 3);
assert_eq!(values[0], (1, SqliteValue::Integer(1)));
assert_eq!(values[1], (2, SqliteValue::Integer(2)));
assert_eq!(values[2], (3, SqliteValue::Integer(3)));
}
#[test]
fn test_generate_series_cursor_past_end_returns_null_and_zero_rowid() {
let cx = Cx::new();
let vtab = GenerateSeries::connect(&cx, &[]).unwrap();
let mut cursor = vtab.open().unwrap();
cursor
.filter(
&cx,
0,
None,
&[SqliteValue::Integer(1), SqliteValue::Integer(1)],
)
.unwrap();
cursor.next(&cx).unwrap();
assert!(cursor.eof());
let mut ctx = ColumnContext::new();
cursor.column(&mut ctx, 0).unwrap();
assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
assert_eq!(cursor.rowid().unwrap(), 0);
}
#[test]
fn test_writable_cursor_missing_column_returns_null() {
let cx = Cx::new();
let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
VirtualTable::update(
&mut vtab,
&cx,
&[
SqliteValue::Null,
SqliteValue::Null,
SqliteValue::Text("hello".into()),
],
)
.unwrap();
let mut cursor = vtab.open().unwrap();
cursor.filter(&cx, 0, None, &[]).unwrap();
let mut ctx = ColumnContext::new();
cursor.column(&mut ctx, 3).unwrap();
assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
cursor.next(&cx).unwrap();
assert!(cursor.eof());
cursor.column(&mut ctx, 0).unwrap();
assert_eq!(ctx.take_value(), Some(SqliteValue::Null));
assert_eq!(cursor.rowid().unwrap(), 0);
}
#[test]
fn test_vtab_update_insert() {
let cx = Cx::new();
let mut vtab = WritableVtab::connect(&cx, &[]).unwrap();
let result = VirtualTable::update(
&mut vtab,
&cx,
&[
SqliteValue::Null,
SqliteValue::Null,
SqliteValue::Text("hello".into()),
],
)
.unwrap();
assert_eq!(result, Some(1));
assert_eq!(vtab.rows.len(), 1);
assert_eq!(vtab.rows[0].0, 1);
}
#[test]
fn test_vtab_update_readonly_default() {
let cx = Cx::new();
let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
let err = VirtualTable::update(&mut vtab, &cx, &[SqliteValue::Null]).unwrap_err();
assert!(matches!(err, FrankenError::ReadOnly));
}
#[test]
fn test_vtab_destroy_vs_disconnect() {
let cx = Cx::new();
let mut vtab = ReadOnlyVtab::connect(&cx, &[]).unwrap();
VirtualTable::disconnect(&mut vtab, &cx).unwrap();
VirtualTable::destroy(&mut vtab, &cx).unwrap();
let mut vtab = GenerateSeries::connect(&cx, &[]).unwrap();
assert!(!vtab.destroyed);
VirtualTable::destroy(&mut vtab, &cx).unwrap();
assert!(vtab.destroyed);
}
#[test]
fn test_vtab_cursor_send_but_not_sync() {
fn assert_send<T: Send>() {}
assert_send::<GenerateSeriesCursor>();
}
#[test]
fn test_column_context_lifecycle() {
let mut ctx = ColumnContext::new();
assert!(ctx.take_value().is_none());
ctx.set_value(SqliteValue::Integer(42));
assert_eq!(ctx.take_value(), Some(SqliteValue::Integer(42)));
assert!(ctx.take_value().is_none());
}
#[test]
fn test_index_info_new() {
let info = IndexInfo::new(
vec![
IndexConstraint {
column: 0,
op: ConstraintOp::Eq,
usable: true,
},
IndexConstraint {
column: 1,
op: ConstraintOp::Gt,
usable: false,
},
],
vec![IndexOrderBy {
column: 0,
desc: false,
}],
);
assert_eq!(info.constraints.len(), 2);
assert_eq!(info.order_by.len(), 1);
assert_eq!(info.constraint_usage.len(), 2);
assert_eq!(info.idx_num, 0);
assert!(info.idx_str.is_none());
assert!(!info.order_by_consumed);
}
#[test]
fn test_transactional_vtab_state_tracks_savepoints() {
let mut state = TransactionalVtabState::default();
state.begin(1_i32);
state.savepoint(0, 2);
state.savepoint(1, 3);
assert_eq!(state.rollback_to(1), Some(3));
state.release(1);
assert_eq!(state.rollback(), Some(1));
assert_eq!(state.rollback(), None);
}
#[test]
fn test_transactional_vtab_state_uses_base_for_late_enlistment() {
let mut state = TransactionalVtabState::default();
state.begin(7_i32);
state.savepoint(2, 9);
assert_eq!(state.rollback_to(1), Some(7));
assert_eq!(state.rollback(), Some(7));
}
#[test]
fn test_shadow_table_policy_defaults_to_ordinary() {
let policy = ReadOnlyVtab::shadow_table_policy("docs", "docs_data");
assert_eq!(policy, ShadowTablePolicy::ordinary());
assert!(!policy.is_shadow());
}
#[test]
fn test_shadow_owning_module_metadata_is_forwarded_by_factory() {
let factory = module_factory_from::<ShadowOwningVtab>();
let metadata = factory.module_metadata(&[]);
assert!(metadata.owns_shadow_tables);
assert_eq!(
metadata.lifecycle,
VtabLifecyclePolicy::SeparateCreateAndConnect
);
assert_eq!(metadata.integrity, VtabIntegrityPolicy::ShadowAware);
assert!(metadata.risk.direct_only);
assert!(!metadata.risk.innocuous);
}
#[test]
fn test_shadow_owning_module_matches_owned_shadow_tables() {
let factory = module_factory_from::<ShadowOwningVtab>();
let owned = factory.shadow_table_policy("docs", "docs_data");
let other_owner = factory.shadow_table_policy("docs", "posts_data");
let unrelated = factory.shadow_table_policy("docs", "docs_segments");
assert_eq!(owned.kind, ShadowTableKind::Shadow);
assert!(!other_owner.is_shadow());
assert!(!unrelated.is_shadow());
}
#[test]
fn test_erased_vtab_instance_forwards_transaction_hooks() {
let cx = Cx::new();
let mut erased: Box<dyn ErasedVtabInstance> =
Box::new(HookAwareVtab::connect(&cx, &[]).unwrap());
erased.begin(&cx).unwrap();
{
let hook = erased
.as_any_mut()
.downcast_mut::<HookAwareVtab>()
.expect("hook-aware vtab");
hook.version = 9;
}
erased.savepoint(&cx, 0).unwrap();
{
let hook = erased
.as_any_mut()
.downcast_mut::<HookAwareVtab>()
.expect("hook-aware vtab");
hook.version = 11;
}
erased.rollback_to(&cx, 0).unwrap();
erased.release(&cx, 0).unwrap();
erased.sync_txn(&cx).unwrap();
erased.rollback(&cx).unwrap();
let hook = erased
.as_any_mut()
.downcast_mut::<HookAwareVtab>()
.expect("hook-aware vtab");
assert_eq!(hook.version, 7);
assert_eq!(hook.syncs, 1);
}
}