From fe1150b0cf5f06ef9ee66f0b60ad6e450814a8da Mon Sep 17 00:00:00 2001 From: gwenn Date: Sun, 20 Mar 2022 11:50:22 +0100 Subject: [PATCH] Port `vtablog` as an example of a writable VTab --- src/vtab/array.rs | 2 +- src/vtab/csvtab.rs | 21 +--- src/vtab/mod.rs | 20 ++- src/vtab/series.rs | 2 +- src/vtab/vtablog.rs | 291 ++++++++++++++++++++++++++++++++++++++++++++ tests/vtab.rs | 2 +- 6 files changed, 316 insertions(+), 22 deletions(-) create mode 100644 src/vtab/vtablog.rs diff --git a/src/vtab/array.rs b/src/vtab/array.rs index adfd9c9..f09ac1a 100644 --- a/src/vtab/array.rs +++ b/src/vtab/array.rs @@ -117,7 +117,7 @@ unsafe impl<'vtab> VTab<'vtab> for ArrayTab { Ok(()) } - fn open(&self) -> Result> { + fn open(&mut self) -> Result> { Ok(ArrayTabCursor::new()) } } diff --git a/src/vtab/csvtab.rs b/src/vtab/csvtab.rs index be2721b..8c38294 100644 --- a/src/vtab/csvtab.rs +++ b/src/vtab/csvtab.rs @@ -30,8 +30,8 @@ use std::str; use crate::ffi; use crate::types::Null; use crate::vtab::{ - dequote, escape_double_quote, parse_boolean, read_only_module, Context, CreateVTab, IndexInfo, - VTab, VTabConfig, VTabConnection, VTabCursor, VTabKind, Values, + escape_double_quote, parse_boolean, read_only_module, Context, CreateVTab, IndexInfo, VTab, + VTabConfig, VTabConnection, VTabCursor, VTabKind, Values, }; use crate::{Connection, Error, Result}; @@ -74,19 +74,6 @@ impl CsvTab { .from_path(&self.filename) } - fn parameter(c_slice: &[u8]) -> Result<(&str, &str)> { - let arg = str::from_utf8(c_slice)?.trim(); - let mut split = arg.split('='); - if let Some(key) = split.next() { - if let Some(value) = split.next() { - let param = key.trim(); - let value = dequote(value); - return Ok((param, value)); - } - } - Err(Error::ModuleError(format!("illegal argument: '{}'", arg))) - } - fn parse_byte(arg: &str) -> Option { if arg.len() == 1 { arg.bytes().next() @@ -122,7 +109,7 @@ unsafe impl<'vtab> VTab<'vtab> for CsvTab { let args = &args[3..]; for c_slice in args { - let (param, value) = CsvTab::parameter(c_slice)?; + let (param, value) = super::parameter(c_slice)?; match param { "filename" => { if !Path::new(value).exists() { @@ -259,7 +246,7 @@ unsafe impl<'vtab> VTab<'vtab> for CsvTab { Ok(()) } - fn open(&self) -> Result> { + fn open(&mut self) -> Result> { Ok(CsvTabCursor::new(self.reader()?)) } } diff --git a/src/vtab/mod.rs b/src/vtab/mod.rs index f775568..0856d96 100644 --- a/src/vtab/mod.rs +++ b/src/vtab/mod.rs @@ -263,7 +263,7 @@ pub unsafe trait VTab<'vtab>: Sized { /// Create a new cursor used for accessing a virtual table. /// (See [SQLite doc](https://sqlite.org/vtab.html#the_xopen_method)) - fn open(&'vtab self) -> Result; + fn open(&'vtab mut self) -> Result; } /// Read-only virtual table instance trait. @@ -882,7 +882,7 @@ pub fn dequote(s: &str) -> &str { } match s.bytes().next() { Some(b) if b == b'"' || b == b'\'' => match s.bytes().rev().next() { - Some(e) if e == b => &s[1..s.len() - 1], + Some(e) if e == b => &s[1..s.len() - 1], // FIXME handle inner escaped quote(s) _ => s, }, _ => s, @@ -912,6 +912,20 @@ pub fn parse_boolean(s: &str) -> Option { } } +/// `=['"]?['"]?` => `(, )` +pub fn parameter(c_slice: &[u8]) -> Result<(&str, &str)> { + let arg = std::str::from_utf8(c_slice)?.trim(); + let mut split = arg.split('='); + if let Some(key) = split.next() { + if let Some(value) = split.next() { + let param = key.trim(); + let value = dequote(value); + return Ok((param, value)); + } + } + Err(Error::ModuleError(format!("illegal argument: '{}'", arg))) +} + // FIXME copy/paste from function.rs unsafe extern "C" fn free_boxed_value(p: *mut c_void) { drop(Box::from_raw(p.cast::())); @@ -1309,6 +1323,8 @@ pub mod csvtab; #[cfg(feature = "series")] #[cfg_attr(docsrs, doc(cfg(feature = "series")))] pub mod series; // SQLite >= 3.9.0 +#[cfg(test)] +mod vtablog; #[cfg(test)] mod test { diff --git a/src/vtab/series.rs b/src/vtab/series.rs index 2dec476..fffbd4d 100644 --- a/src/vtab/series.rs +++ b/src/vtab/series.rs @@ -153,7 +153,7 @@ unsafe impl<'vtab> VTab<'vtab> for SeriesTab { Ok(()) } - fn open(&self) -> Result> { + fn open(&mut self) -> Result> { Ok(SeriesTabCursor::new()) } } diff --git a/src/vtab/vtablog.rs b/src/vtab/vtablog.rs new file mode 100644 index 0000000..51adabd --- /dev/null +++ b/src/vtab/vtablog.rs @@ -0,0 +1,291 @@ +///! Port of C [vtablog](http://www.sqlite.org/cgi/src/finfo?name=ext/misc/vtablog.c) +use std::default::Default; +use std::marker::PhantomData; +use std::os::raw::c_int; +use std::str::FromStr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::vtab::{ + update_module, Context, CreateVTab, IndexInfo, UpdateVTab, VTab, VTabConnection, VTabCursor, + VTabKind, Values, +}; +use crate::{ffi, ValueRef}; +use crate::{Connection, Error, Result}; + +/// Register the "vtablog" module. +pub fn load_module(conn: &Connection) -> Result<()> { + let aux: Option<()> = None; + conn.create_module("vtablog", update_module::(), aux) +} + +/// An instance of the vtablog virtual table +#[repr(C)] +struct VTabLog { + /// Base class. Must be first + base: ffi::sqlite3_vtab, + /// Number of rows in the table + n_row: i64, + /// Instance number for this vtablog table + i_inst: usize, + /// Number of cursors created + n_cursor: usize, +} + +impl VTabLog { + fn connect_create( + _: &mut VTabConnection, + _: Option<&()>, + args: &[&[u8]], + is_create: bool, + ) -> Result<(String, VTabLog)> { + static N_INST: AtomicUsize = AtomicUsize::new(1); + let i_inst = N_INST.fetch_add(1, Ordering::SeqCst); + println!( + "VTabLog::{}(tab={}, args={:?}):", + if is_create { "create" } else { "connect" }, + i_inst, + args, + ); + let mut schema = None; + let mut n_row = None; + + let args = &args[3..]; + for c_slice in args { + let (param, value) = super::parameter(c_slice)?; + match param { + "schema" => { + if schema.is_some() { + return Err(Error::ModuleError(format!( + "more than one '{}' parameter", + param + ))); + } + schema = Some(value.to_owned()) + } + "rows" => { + if n_row.is_some() { + return Err(Error::ModuleError(format!( + "more than one '{}' parameter", + param + ))); + } + if let Ok(n) = i64::from_str(value) { + n_row = Some(n) + } + } + _ => { + return Err(Error::ModuleError(format!( + "unrecognized parameter '{}'", + param + ))); + } + } + } + if schema.is_none() { + return Err(Error::ModuleError("no schema defined".to_owned())); + } + let vtab = VTabLog { + base: ffi::sqlite3_vtab::default(), + n_row: n_row.unwrap_or(10), + i_inst, + n_cursor: 0, + }; + Ok((schema.unwrap(), vtab)) + } +} + +impl Drop for VTabLog { + fn drop(&mut self) { + println!("VTabLog::drop({})", self.i_inst); + } +} + +unsafe impl<'vtab> VTab<'vtab> for VTabLog { + type Aux = (); + type Cursor = VTabLogCursor<'vtab>; + + fn connect( + db: &mut VTabConnection, + aux: Option<&Self::Aux>, + args: &[&[u8]], + ) -> Result<(String, Self)> { + VTabLog::connect_create(db, aux, args, false) + } + + fn best_index(&self, info: &mut IndexInfo) -> Result<()> { + println!("VTabLog::best_index({})", self.i_inst); + info.set_estimated_cost(500.); + info.set_estimated_rows(500); + Ok(()) + } + + fn open(&'vtab mut self) -> Result { + self.n_cursor += 1; + println!( + "VTabLog::open(tab={}, cursor={})", + self.i_inst, self.n_cursor + ); + Ok(VTabLogCursor { + base: ffi::sqlite3_vtab_cursor::default(), + i_cursor: self.n_cursor, + row_id: 0, + phantom: PhantomData, + }) + } +} + +impl<'vtab> CreateVTab<'vtab> for VTabLog { + const KIND: VTabKind = VTabKind::Default; + + fn create( + db: &mut VTabConnection, + aux: Option<&Self::Aux>, + args: &[&[u8]], + ) -> Result<(String, Self)> { + VTabLog::connect_create(db, aux, args, true) + } + + fn destroy(&self) -> Result<()> { + println!("VTabLog::destroy({})", self.i_inst); + Ok(()) + } +} + +impl<'vtab> UpdateVTab<'vtab> for VTabLog { + fn delete(&mut self, arg: ValueRef<'_>) -> Result<()> { + println!("VTabLog::delete({}, {:?})", self.i_inst, arg); + Ok(()) + } + + fn insert(&mut self, args: &Values<'_>) -> Result { + println!( + "VTabLog::insert({}, {:?})", + self.i_inst, + args.iter().collect::>>() + ); + Ok(self.n_row as i64) + } + + fn update(&mut self, args: &Values<'_>) -> Result<()> { + println!( + "VTabLog::update({}, {:?})", + self.i_inst, + args.iter().collect::>>() + ); + Ok(()) + } +} + +/// A cursor for the Series virtual table +#[repr(C)] +struct VTabLogCursor<'vtab> { + /// Base class. Must be first + base: ffi::sqlite3_vtab_cursor, + /// Cursor number + i_cursor: usize, + /// The rowid + row_id: i64, + phantom: PhantomData<&'vtab VTabLog>, +} + +impl VTabLogCursor<'_> { + fn vtab(&self) -> &VTabLog { + unsafe { &*(self.base.pVtab as *const VTabLog) } + } +} + +impl Drop for VTabLogCursor<'_> { + fn drop(&mut self) { + println!( + "VTabLogCursor::drop(tab={}, cursor={})", + self.vtab().i_inst, + self.i_cursor + ); + } +} + +unsafe impl VTabCursor for VTabLogCursor<'_> { + fn filter(&mut self, _: c_int, _: Option<&str>, _: &Values<'_>) -> Result<()> { + println!( + "VTabLogCursor::filter(tab={}, cursor={})", + self.vtab().i_inst, + self.i_cursor + ); + self.row_id = 0; + Ok(()) + } + + fn next(&mut self) -> Result<()> { + println!( + "VTabLogCursor::next(tab={}, cursor={}): rowid {} -> {}", + self.vtab().i_inst, + self.i_cursor, + self.row_id, + self.row_id + 1 + ); + self.row_id += 1; + Ok(()) + } + + fn eof(&self) -> bool { + let eof = self.row_id >= self.vtab().n_row; + println!( + "VTabLogCursor::eof(tab={}, cursor={}): {}", + self.vtab().i_inst, + self.i_cursor, + eof, + ); + eof + } + + fn column(&self, ctx: &mut Context, i: c_int) -> Result<()> { + let value = if i < 26 { + format!( + "{}{}", + "abcdefghijklmnopqrstuvwyz".chars().nth(i as usize).unwrap(), + self.row_id + ) + } else { + format!("{}{}", i, self.row_id) + }; + println!( + "VTabLogCursor::column(tab={}, cursor={}, i={}): {}", + self.vtab().i_inst, + self.i_cursor, + i, + value, + ); + ctx.set_result(&value) + } + + fn rowid(&self) -> Result { + println!( + "VTabLogCursor::rowid(tab={}, cursor={}): {}", + self.vtab().i_inst, + self.i_cursor, + self.row_id, + ); + Ok(self.row_id) + } +} + +#[cfg(test)] +mod test { + use crate::{Connection, Result}; + #[test] + fn test_module() -> Result<()> { + let db = Connection::open_in_memory()?; + super::load_module(&db)?; + + db.execute_batch( + "CREATE VIRTUAL TABLE temp.log USING vtablog( + schema='CREATE TABLE x(a,b,c)', + rows=25 + );", + )?; + let mut stmt = db.prepare("SELECT * FROM log;")?; + let mut rows = stmt.query([])?; + while let Some(_) = rows.next()? {} + Ok(()) + } +} diff --git a/tests/vtab.rs b/tests/vtab.rs index fa26459..bb51356 100644 --- a/tests/vtab.rs +++ b/tests/vtab.rs @@ -39,7 +39,7 @@ fn test_dummy_module() -> rusqlite::Result<()> { Ok(()) } - fn open(&'vtab self) -> Result> { + fn open(&'vtab mut self) -> Result> { Ok(DummyTabCursor::default()) } }