From 9c1dca72db69f3e7fc2847936bc39a5b11bbd74f Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Mon, 19 May 2025 06:29:20 +0200 Subject: [PATCH] Introduce VTable This allows storing table arguments parsed in the VTabModule::create method. --- core/lib.rs | 17 ++- core/translate/emitter.rs | 6 +- core/translate/insert.rs | 1 - core/translate/schema.rs | 2 +- core/vdbe/explain.rs | 5 +- core/vdbe/insn.rs | 3 +- extensions/completion/src/lib.rs | 97 +++++++------- extensions/core/README.md | 38 +++--- extensions/core/src/lib.rs | 2 +- extensions/core/src/vtabs.rs | 71 +++++++---- extensions/series/src/lib.rs | 107 ++++++++-------- extensions/tests/src/lib.rs | 208 ++++++++++++++++--------------- macros/src/ext/vtab_derive.rs | 91 ++++++++------ macros/src/lib.rs | 111 ++++++++++------- 14 files changed, 424 insertions(+), 335 deletions(-) diff --git a/core/lib.rs b/core/lib.rs index 7899177d2..7df3689e2 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -45,6 +45,7 @@ use limbo_ext::{ConstraintInfo, IndexInfo, OrderByInfo, ResultCode, VTabKind, VT use limbo_sqlite3_parser::{ast, ast::Cmd, lexer::sql::Parser}; use parking_lot::RwLock; use schema::{Column, Schema}; +use std::ffi::c_void; use std::{ borrow::Cow, cell::{Cell, RefCell, UnsafeCell}, @@ -674,6 +675,7 @@ pub struct VirtualTable { pub implementation: Rc, columns: Vec, kind: VTabKind, + table_ptr: *const c_void, } impl VirtualTable { @@ -719,7 +721,7 @@ impl VirtualTable { ))); } }; - let schema = module.implementation.as_ref().init_schema(args)?; + let (schema, table_ptr) = module.implementation.as_ref().create(args)?; let mut parser = Parser::new(schema.as_bytes()); if let ast::Cmd::Stmt(ast::Stmt::CreateTable { body, .. }) = parser.next()?.ok_or( LimboError::ParseError("Failed to parse schema from virtual table module".to_string()), @@ -731,6 +733,7 @@ impl VirtualTable { columns, args: exprs, kind, + table_ptr, }); return Ok(vtab); } @@ -740,7 +743,7 @@ impl VirtualTable { } pub fn open(&self) -> crate::Result { - let cursor = unsafe { (self.implementation.open)(self.implementation.ctx) }; + let cursor = unsafe { (self.implementation.open)(self.table_ptr) }; VTabOpaqueCursor::new(cursor, self.implementation.close) } @@ -797,10 +800,9 @@ impl VirtualTable { let arg_count = args.len(); let ext_args = args.iter().map(|arg| arg.to_ffi()).collect::>(); let newrowid = 0i64; - let implementation = self.implementation.as_ref(); let rc = unsafe { (self.implementation.update)( - implementation as *const VTabModuleImpl as *const std::ffi::c_void, + self.table_ptr, arg_count as i32, ext_args.as_ptr(), &newrowid as *const _ as *mut i64, @@ -819,12 +821,7 @@ impl VirtualTable { } pub fn destroy(&self) -> Result<()> { - let implementation = self.implementation.as_ref(); - let rc = unsafe { - (self.implementation.destroy)( - implementation as *const VTabModuleImpl as *const std::ffi::c_void, - ) - }; + let rc = unsafe { (self.implementation.destroy)(self.table_ptr) }; match rc { ResultCode::OK => Ok(()), _ => Err(LimboError::ExtensionError(rc.to_string())), diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 28d992a54..15d0e8035 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -461,7 +461,7 @@ fn emit_delete_insns( dest: key_reg, }); - if let Some(vtab) = table_reference.virtual_table() { + if let Some(_) = table_reference.virtual_table() { let conflict_action = 0u16; let start_reg = key_reg; @@ -474,7 +474,6 @@ fn emit_delete_insns( cursor_id, arg_count: 2, start_reg, - vtab_ptr: vtab.implementation.as_ref().ctx as usize, conflict_action, }); } else { @@ -1039,13 +1038,12 @@ fn emit_update_insns( flag: 0, table_name: table_ref.identifier.clone(), }); - } else if let Some(vtab) = table_ref.virtual_table() { + } else if let Some(_) = table_ref.virtual_table() { let arg_count = table_ref.columns().len() + 2; program.emit_insn(Insn::VUpdate { cursor_id, arg_count, start_reg: beg, - vtab_ptr: vtab.implementation.as_ref().ctx as usize, conflict_action: 0u16, }); } diff --git a/core/translate/insert.rs b/core/translate/insert.rs index 1c3169b4c..195dfa582 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -714,7 +714,6 @@ fn translate_virtual_table_insert( cursor_id, arg_count: column_mappings.len() + 2, start_reg: registers_start, - vtab_ptr: virtual_table.implementation.as_ref().ctx as usize, conflict_action, }); diff --git a/core/translate/schema.rs b/core/translate/schema.rs index 2c88a21e7..7e2c865a8 100644 --- a/core/translate/schema.rs +++ b/core/translate/schema.rs @@ -505,7 +505,7 @@ fn create_vtable_body_to_str(vtab: &CreateVirtualTable, module: Rc) -> .collect::>(); let schema = module .implementation - .init_schema(ext_args) + .create_schema(ext_args) .unwrap_or_default(); let vtab_args = if let Some(first_paren) = schema.find('(') { let closing_paren = schema.rfind(')').unwrap_or_default(); diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 4de07b2e5..888cf32bf 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -416,14 +416,13 @@ pub fn insn_to_str( cursor_id, arg_count, // P2: Number of arguments in argv[] start_reg, // P3: Start register for argv[] - vtab_ptr, // P4: vtab pointer - conflict_action, // P5: Conflict resolution flags + conflict_action, // P4: Conflict resolution flags } => ( "VUpdate", *cursor_id as i32, *arg_count as i32, *start_reg as i32, - Value::build_text(&format!("vtab:{}", vtab_ptr)), + Value::build_text(""), *conflict_action, format!("args=r[{}..{}]", start_reg, start_reg + arg_count - 1), ), diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 7063ac050..f5ff8352d 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -323,8 +323,7 @@ pub enum Insn { cursor_id: usize, // P1: Virtual table cursor number arg_count: usize, // P2: Number of arguments in argv[] start_reg: usize, // P3: Start register for argv[] - vtab_ptr: usize, // P4: vtab pointer - conflict_action: u16, // P5: Conflict resolution flags + conflict_action: u16, // P4: Conflict resolution flags }, /// Advance the virtual table cursor to the next row. diff --git a/extensions/completion/src/lib.rs b/extensions/completion/src/lib.rs index 94f8b4689..bfd03756c 100644 --- a/extensions/completion/src/lib.rs +++ b/extensions/completion/src/lib.rs @@ -4,10 +4,12 @@ mod keywords; use keywords::KEYWORDS; -use limbo_ext::{register_extension, ResultCode, VTabCursor, VTabModule, VTabModuleDerive, Value}; +use limbo_ext::{ + register_extension, ResultCode, VTabCursor, VTabModule, VTabModuleDerive, VTable, Value, +}; register_extension! { - vtabs: { CompletionVTab } + vtabs: { CompletionVTabModule } } macro_rules! try_option { @@ -57,61 +59,34 @@ impl Into for CompletionPhase { /// A virtual table that generates candidate completions #[derive(Debug, Default, VTabModuleDerive)] -struct CompletionVTab {} +struct CompletionVTabModule {} -impl VTabModule for CompletionVTab { - type VCursor = CompletionCursor; +impl VTabModule for CompletionVTabModule { + type Table = CompletionTable; const NAME: &'static str = "completion"; const VTAB_KIND: limbo_ext::VTabKind = limbo_ext::VTabKind::TableValuedFunction; - type Error = ResultCode; - fn create_schema(_args: &[Value]) -> String { - "CREATE TABLE completion( + fn create(_args: &[Value]) -> Result<(String, Self::Table), ResultCode> { + let schema = "CREATE TABLE completion( candidate TEXT, prefix TEXT HIDDEN, wholeline TEXT HIDDEN, phase INT HIDDEN )" - .to_string() + .to_string(); + Ok((schema, CompletionTable {})) } +} - fn open(&self) -> Result { +struct CompletionTable {} + +impl VTable for CompletionTable { + type Cursor = CompletionCursor; + type Error = ResultCode; + + fn open(&self) -> Result { Ok(CompletionCursor::default()) } - - fn filter(cursor: &mut Self::VCursor, args: &[Value], _: Option<(&str, i32)>) -> ResultCode { - if args.is_empty() || args.len() > 2 { - return ResultCode::InvalidArgs; - } - cursor.reset(); - let prefix = try_option!(args[0].to_text(), ResultCode::InvalidArgs); - - let wholeline = args.get(1).map(|v| v.to_text().unwrap_or("")).unwrap_or(""); - - cursor.line = wholeline.to_string(); - cursor.prefix = prefix.to_string(); - - // Currently best index is not implemented so the correct arg parsing is not done here - if !cursor.line.is_empty() && cursor.prefix.is_empty() { - let mut i = cursor.line.len(); - while let Some(ch) = cursor.line.chars().next() { - if i > 0 && (ch.is_alphanumeric() || ch == '_') { - i -= 1; - } else { - break; - } - } - if cursor.line.len() - i > 0 { - // TODO see if need to inclusive range - cursor.prefix = cursor.line[..i].to_string(); - } - } - - cursor.rowid = 0; - cursor.phase = CompletionPhase::Keywords; - - cursor.next() - } } /// The cursor for iterating over the completions @@ -138,6 +113,40 @@ impl CompletionCursor { impl VTabCursor for CompletionCursor { type Error = ResultCode; + fn filter(&mut self, args: &[Value], _: Option<(&str, i32)>) -> ResultCode { + if args.is_empty() || args.len() > 2 { + return ResultCode::InvalidArgs; + } + self.reset(); + let prefix = try_option!(args[0].to_text(), ResultCode::InvalidArgs); + + let wholeline = args.get(1).map(|v| v.to_text().unwrap_or("")).unwrap_or(""); + + self.line = wholeline.to_string(); + self.prefix = prefix.to_string(); + + // Currently best index is not implemented so the correct arg parsing is not done here + if !self.line.is_empty() && self.prefix.is_empty() { + let mut i = self.line.len(); + while let Some(ch) = self.line.chars().next() { + if i > 0 && (ch.is_alphanumeric() || ch == '_') { + i -= 1; + } else { + break; + } + } + if self.line.len() - i > 0 { + // TODO see if need to inclusive range + self.prefix = self.line[..i].to_string(); + } + } + + self.rowid = 0; + self.phase = CompletionPhase::Keywords; + + self.next() + } + fn next(&mut self) -> ResultCode { self.rowid += 1; diff --git a/extensions/core/README.md b/extensions/core/README.md index 2b3bce4b9..0493eead0 100644 --- a/extensions/core/README.md +++ b/extensions/core/README.md @@ -172,29 +172,35 @@ impl AggFunc for Percentile { /// Example: A virtual table that operates on a CSV file as a database table. /// This example assumes that the CSV file is located at "data.csv" in the current directory. #[derive(Debug, VTabModuleDerive)] -struct CsvVTable; +struct CsvVTableModule; -impl VTabModule for CsvVTable { - type VCursor = CsvCursor; - /// Define your error type. Must impl Display and match VCursor::Error - type Error = &'static str; +impl VTabModule for CsvVTableModule { + type Table = CsvTable; /// Declare the name for your virtual table const NAME: &'static str = "csv_data"; - /// Declare the type of vtable (TableValuedFunction or VirtualTable) const VTAB_KIND: VTabKind = VTabKind::VirtualTable; - /// Function to initialize the schema of your vtable - fn create_schema(_args: &[Value]) -> &'static str { - "CREATE TABLE csv_data( + /// Declare your virtual table and its schema + fn create(args: &[Value]) -> Result<(String, Self::Table), ResultCode> { + let schema = "CREATE TABLE csv_data( name TEXT, age TEXT, city TEXT - )" + )".into(); + Ok((schema, CsvTable {})) } +} + +struct CsvTable {} + +impl VTable for CsvTable { + type Cursor = CsvCursor; + /// Define your error type. Must impl Display and match Cursor::Error + type Error = &'static str; /// Open to return a new cursor: In this simple example, the CSV file is read completely into memory on connect. - fn open(&self) -> Result { + fn open(&self) -> Result { // Read CSV file contents from "data.csv" let csv_content = fs::read_to_string("data.csv").unwrap_or_default(); // For simplicity, we'll ignore the header row. @@ -210,11 +216,6 @@ impl VTabModule for CsvVTable { Ok(CsvCursor { rows, index: 0 }) } - /// Filter through result columns. (not used in this simple example) - fn filter(_cursor: &mut Self::VCursor, _args: &[Value]) -> ResultCode { - ResultCode::OK - } - /// *Optional* methods for non-readonly tables /// Update the value at rowid @@ -243,6 +244,11 @@ struct CsvCursor { impl VTabCursor for CsvCursor { type Error = &'static str; + /// Filter through result columns. (not used in this simple example) + fn filter(&mut self, _args: &[Value], _idx_info: Option<(&str, i32)>) -> ResultCode { + ResultCode::OK + } + /// Next advances the cursor to the next row. fn next(&mut self) -> ResultCode { if self.index < self.rows.len() - 1 { diff --git a/extensions/core/src/lib.rs b/extensions/core/src/lib.rs index 99729de6c..75ea1c77d 100644 --- a/extensions/core/src/lib.rs +++ b/extensions/core/src/lib.rs @@ -17,7 +17,7 @@ pub use vfs_modules::{RegisterVfsFn, VfsExtension, VfsFile, VfsFileImpl, VfsImpl use vtabs::RegisterModuleFn; pub use vtabs::{ ConstraintInfo, ConstraintOp, ConstraintUsage, ExtIndexInfo, IndexInfo, OrderByInfo, - VTabCursor, VTabKind, VTabModule, VTabModuleImpl, + VTabCreateResult, VTabCursor, VTabKind, VTabModule, VTabModuleImpl, VTable, }; pub type ExtResult = std::result::Result; diff --git a/extensions/core/src/vtabs.rs b/extensions/core/src/vtabs.rs index a456c4d12..316e0a5c0 100644 --- a/extensions/core/src/vtabs.rs +++ b/extensions/core/src/vtabs.rs @@ -11,9 +11,8 @@ pub type RegisterModuleFn = unsafe extern "C" fn( #[repr(C)] #[derive(Clone, Debug)] pub struct VTabModuleImpl { - pub ctx: *const c_void, pub name: *const c_char, - pub create_schema: VtabFnCreateSchema, + pub create: VtabFnCreate, pub open: VtabFnOpen, pub close: VtabFnClose, pub filter: VtabFnFilter, @@ -26,24 +25,50 @@ pub struct VTabModuleImpl { pub best_idx: BestIdxFn, } +#[repr(C)] +pub struct VTabCreateResult { + pub code: ResultCode, + pub schema: *const c_char, + pub table: *const c_void, +} + #[cfg(feature = "core_only")] impl VTabModuleImpl { - pub fn init_schema(&self, args: Vec) -> crate::ExtResult { - let schema = unsafe { (self.create_schema)(args.as_ptr(), args.len() as i32) }; - if schema.is_null() { - return Err(ResultCode::InvalidArgs); - } + pub fn create(&self, args: Vec) -> crate::ExtResult<(String, *const c_void)> { + let result = unsafe { (self.create)(args.as_ptr(), args.len() as i32) }; for arg in args { unsafe { arg.__free_internal_type() }; } - let schema = unsafe { std::ffi::CString::from_raw(schema) }; - Ok(schema.to_string_lossy().to_string()) + if !result.code.is_ok() { + return Err(result.code); + } + let schema = unsafe { std::ffi::CString::from_raw(result.schema as *mut _) }; + Ok((schema.to_string_lossy().to_string(), result.table)) + } + + // TODO: This function is temporary and should eventually be removed. + // The only difference from `create` is that it takes ownership of the table instance. + // Currently, it is used to generate virtual table column names that are stored in + // `sqlite_schema` alongside the table's schema. + // However, storing column names is not necessary to match SQLite's behavior. + // SQLite computes the list of columns dynamically each time the `.schema` command + // is executed, using the `shell_add_schema` UDF function. + pub fn create_schema(&self, args: Vec) -> crate::ExtResult { + self.create(args).and_then(|(schema, table)| { + // Drop the allocated table instance to avoid a memory leak. + let result = unsafe { (self.destroy)(table) }; + if result.is_ok() { + Ok(schema) + } else { + Err(result) + } + }) } } -pub type VtabFnCreateSchema = unsafe extern "C" fn(args: *const Value, argc: i32) -> *mut c_char; +pub type VtabFnCreate = unsafe extern "C" fn(args: *const Value, argc: i32) -> VTabCreateResult; -pub type VtabFnOpen = unsafe extern "C" fn(*const c_void) -> *const c_void; +pub type VtabFnOpen = unsafe extern "C" fn(table: *const c_void) -> *const c_void; pub type VtabFnClose = unsafe extern "C" fn(cursor: *const c_void) -> ResultCode; @@ -64,13 +89,14 @@ pub type VtabFnEof = unsafe extern "C" fn(cursor: *const c_void) -> bool; pub type VtabRowIDFn = unsafe extern "C" fn(cursor: *const c_void) -> i64; pub type VtabFnUpdate = unsafe extern "C" fn( - vtab: *const c_void, + table: *const c_void, argc: i32, argv: *const Value, p_out_rowid: *mut i64, ) -> ResultCode; -pub type VtabFnDestroy = unsafe extern "C" fn(vtab: *const c_void) -> ResultCode; +pub type VtabFnDestroy = unsafe extern "C" fn(table: *const c_void) -> ResultCode; + pub type BestIdxFn = unsafe extern "C" fn( constraints: *const ConstraintInfo, constraint_len: i32, @@ -86,18 +112,20 @@ pub enum VTabKind { } pub trait VTabModule: 'static { - type VCursor: VTabCursor; + type Table: VTable; const VTAB_KIND: VTabKind; const NAME: &'static str; + + /// Creates a new instance of a virtual table. + /// Returns a tuple where the first element is the table's schema. + fn create(args: &[Value]) -> Result<(String, Self::Table), ResultCode>; +} + +pub trait VTable { + type Cursor: VTabCursor; type Error: std::fmt::Display; - fn create_schema(args: &[Value]) -> String; - fn open(&self) -> Result; - fn filter( - cursor: &mut Self::VCursor, - args: &[Value], - idx_info: Option<(&str, i32)>, - ) -> ResultCode; + fn open(&self) -> Result; fn update(&mut self, _rowid: i64, _args: &[Value]) -> Result<(), Self::Error> { Ok(()) } @@ -130,6 +158,7 @@ pub trait VTabModule: 'static { pub trait VTabCursor: Sized { type Error: std::fmt::Display; + fn filter(&mut self, args: &[Value], idx_info: Option<(&str, i32)>) -> ResultCode; fn rowid(&self) -> i64; fn column(&self, idx: u32) -> Result; fn eof(&self) -> bool; diff --git a/extensions/series/src/lib.rs b/extensions/series/src/lib.rs index c5ee979da..f609ded91 100644 --- a/extensions/series/src/lib.rs +++ b/extensions/series/src/lib.rs @@ -1,9 +1,10 @@ use limbo_ext::{ - register_extension, ResultCode, VTabCursor, VTabKind, VTabModule, VTabModuleDerive, Value, + register_extension, ResultCode, VTabCursor, VTabKind, VTabModule, VTabModuleDerive, VTable, + Value, }; register_extension! { - vtabs: { GenerateSeriesVTab } + vtabs: { GenerateSeriesVTabModule } } macro_rules! try_option { @@ -17,26 +18,32 @@ macro_rules! try_option { /// A virtual table that generates a sequence of integers #[derive(Debug, VTabModuleDerive, Default)] -struct GenerateSeriesVTab; +struct GenerateSeriesVTabModule; -impl VTabModule for GenerateSeriesVTab { - type VCursor = GenerateSeriesCursor; - type Error = ResultCode; +impl VTabModule for GenerateSeriesVTabModule { + type Table = GenerateSeriesTable; const NAME: &'static str = "generate_series"; const VTAB_KIND: VTabKind = VTabKind::TableValuedFunction; - fn create_schema(_args: &[Value]) -> String { - // Create table schema - "CREATE TABLE generate_series( + fn create(_args: &[Value]) -> Result<(String, Self::Table), ResultCode> { + let schema = "CREATE TABLE generate_series( value INTEGER, start INTEGER HIDDEN, stop INTEGER HIDDEN, step INTEGER HIDDEN )" - .into() + .into(); + Ok((schema, GenerateSeriesTable {})) } +} - fn open(&self) -> Result { +struct GenerateSeriesTable {} + +impl VTable for GenerateSeriesTable { + type Cursor = GenerateSeriesCursor; + type Error = ResultCode; + + fn open(&self) -> Result { Ok(GenerateSeriesCursor { start: 0, stop: 0, @@ -44,41 +51,6 @@ impl VTabModule for GenerateSeriesVTab { current: 0, }) } - - fn filter(cursor: &mut Self::VCursor, args: &[Value], _: Option<(&str, i32)>) -> ResultCode { - // args are the start, stop, and step - if args.is_empty() || args.len() > 3 { - return ResultCode::InvalidArgs; - } - let start = try_option!(args[0].to_integer(), ResultCode::InvalidArgs); - let stop = try_option!( - args.get(1).map(|v| v.to_integer().unwrap_or(i64::MAX)), - ResultCode::EOF // Sqlite returns an empty series for wacky args - ); - let mut step = args - .get(2) - .map(|v| v.to_integer().unwrap_or(1)) - .unwrap_or(1); - - // Convert zero step to 1, matching SQLite behavior - if step == 0 { - step = 1; - } - - cursor.start = start; - cursor.step = step; - cursor.stop = stop; - - // Set initial value based on range validity - // For invalid input SQLite returns an empty series - cursor.current = if cursor.is_invalid_range() { - return ResultCode::EOF; - } else { - start - }; - - ResultCode::OK - } } /// The cursor for iterating over the generated sequence @@ -116,6 +88,41 @@ impl GenerateSeriesCursor { impl VTabCursor for GenerateSeriesCursor { type Error = ResultCode; + fn filter(&mut self, args: &[Value], _: Option<(&str, i32)>) -> ResultCode { + // args are the start, stop, and step + if args.is_empty() || args.len() > 3 { + return ResultCode::InvalidArgs; + } + let start = try_option!(args[0].to_integer(), ResultCode::InvalidArgs); + let stop = try_option!( + args.get(1).map(|v| v.to_integer().unwrap_or(i64::MAX)), + ResultCode::EOF // Sqlite returns an empty series for wacky args + ); + let mut step = args + .get(2) + .map(|v| v.to_integer().unwrap_or(1)) + .unwrap_or(1); + + // Convert zero step to 1, matching SQLite behavior + if step == 0 { + step = 1; + } + + self.start = start; + self.step = step; + self.stop = stop; + + // Set initial value based on range validity + // For invalid input SQLite returns an empty series + self.current = if self.is_invalid_range() { + return ResultCode::EOF; + } else { + start + }; + + ResultCode::OK + } + fn next(&mut self) -> ResultCode { if self.eof() { return ResultCode::EOF; @@ -217,7 +224,7 @@ mod tests { } // Helper function to collect all values from a cursor, returns Result with error code fn collect_series(series: Series) -> Result, ResultCode> { - let tbl = GenerateSeriesVTab; + let tbl = GenerateSeriesTable {}; let mut cursor = tbl.open()?; // Create args array for filter @@ -228,7 +235,7 @@ mod tests { ]; // Initialize cursor through filter - match GenerateSeriesVTab::filter(&mut cursor, &args, None) { + match cursor.filter(&args, None) { ResultCode::OK => (), ResultCode::EOF => return Ok(vec![]), err => return Err(err), @@ -534,7 +541,7 @@ mod tests { let start = series.start; let stop = series.stop; let step = series.step; - let tbl = GenerateSeriesVTab {}; + let tbl = GenerateSeriesTable {}; let mut cursor = tbl.open().unwrap(); let args = vec![ @@ -544,7 +551,7 @@ mod tests { ]; // Initialize cursor through filter - GenerateSeriesVTab::filter(&mut cursor, &args, None); + cursor.filter(&args, None); let mut rowids = vec![]; while !cursor.eof() { diff --git a/extensions/tests/src/lib.rs b/extensions/tests/src/lib.rs index f5138fad8..b70b5497b 100644 --- a/extensions/tests/src/lib.rs +++ b/extensions/tests/src/lib.rs @@ -1,7 +1,8 @@ use lazy_static::lazy_static; use limbo_ext::{ register_extension, scalar, ConstraintInfo, ConstraintOp, ConstraintUsage, ExtResult, - IndexInfo, OrderByInfo, ResultCode, VTabCursor, VTabKind, VTabModule, VTabModuleDerive, Value, + IndexInfo, OrderByInfo, ResultCode, VTabCursor, VTabKind, VTabModule, VTabModuleDerive, VTable, + Value, }; #[cfg(not(target_family = "wasm"))] use limbo_ext::{VfsDerive, VfsExtension, VfsFile}; @@ -11,7 +12,7 @@ use std::io::{Read, Seek, SeekFrom, Write}; use std::sync::Mutex; register_extension! { - vtabs: { KVStoreVTab }, + vtabs: { KVStoreVTabModule }, scalars: { test_scalar }, vfs: { TestFS }, } @@ -21,7 +22,7 @@ lazy_static! { } #[derive(VTabModuleDerive, Default)] -pub struct KVStoreVTab; +pub struct KVStoreVTabModule; /// the cursor holds a snapshot of (rowid, key, value) in memory. pub struct KVStoreCursor { @@ -29,17 +30,114 @@ pub struct KVStoreCursor { index: Option, } -impl VTabModule for KVStoreVTab { - type VCursor = KVStoreCursor; +impl VTabModule for KVStoreVTabModule { + type Table = KVStoreTable; const VTAB_KIND: VTabKind = VTabKind::VirtualTable; const NAME: &'static str = "kv_store"; + + fn create(_args: &[Value]) -> Result<(String, Self::Table), ResultCode> { + let schema = "CREATE TABLE x (key TEXT PRIMARY KEY, value TEXT);".to_string(); + Ok((schema, KVStoreTable {})) + } +} + +fn hash_key(key: &str) -> i64 { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + key.hash(&mut hasher); + hasher.finish() as i64 +} + +impl VTabCursor for KVStoreCursor { type Error = String; - fn create_schema(_args: &[Value]) -> String { - "CREATE TABLE x (key TEXT PRIMARY KEY, value TEXT);".to_string() + fn filter(&mut self, args: &[Value], idx_str: Option<(&str, i32)>) -> ResultCode { + match idx_str { + Some(("key_eq", 1)) => { + let key = args + .first() + .and_then(|v| v.to_text()) + .map(|s| s.to_string()); + log::debug!("idx_str found: key_eq\n value: {:?}", key); + if let Some(key) = key { + let rowid = hash_key(&key); + let store = GLOBAL_STORE.lock().unwrap(); + if let Some((k, v)) = store.get(&rowid) { + self.rows.push((rowid, k.clone(), v.clone())); + self.index = Some(0); + } else { + self.rows.clear(); + self.index = None; + return ResultCode::EOF; + } + return ResultCode::OK; + } + self.rows.clear(); + self.index = None; + ResultCode::OK + } + _ => { + let store = GLOBAL_STORE.lock().unwrap(); + self.rows = store + .iter() + .map(|(&rowid, (k, v))| (rowid, k.clone(), v.clone())) + .collect(); + self.rows.sort_by_key(|(rowid, _, _)| *rowid); + if self.rows.is_empty() { + self.index = None; + ResultCode::EOF + } else { + self.index = Some(0); + ResultCode::OK + } + } + } } - fn open(&self) -> Result { + fn rowid(&self) -> i64 { + if self.index.is_some_and(|c| c < self.rows.len()) { + self.rows[self.index.unwrap_or(0)].0 + } else { + log::error!("rowid: -1"); + -1 + } + } + + fn column(&self, idx: u32) -> Result { + if self.index.is_some_and(|c| c >= self.rows.len()) { + return Err("cursor out of range".into()); + } + if let Some((_, ref key, ref val)) = self.rows.get(self.index.unwrap_or(0)) { + match idx { + 0 => Ok(Value::from_text(key.clone())), // key + 1 => Ok(Value::from_text(val.clone())), // value + _ => Err("Invalid column".into()), + } + } else { + Err("Invalid Column".into()) + } + } + + fn eof(&self) -> bool { + self.index.is_some_and(|s| s >= self.rows.len()) || self.index.is_none() + } + + fn next(&mut self) -> ResultCode { + self.index = Some(self.index.unwrap_or(0) + 1); + if self.index.is_some_and(|c| c >= self.rows.len()) { + return ResultCode::EOF; + } + ResultCode::OK + } +} + +pub struct KVStoreTable {} + +impl VTable for KVStoreTable { + type Cursor = KVStoreCursor; + type Error = String; + + fn open(&self) -> Result { let _ = env_logger::try_init(); Ok(KVStoreCursor { rows: Vec::new(), @@ -88,53 +186,6 @@ impl VTabModule for KVStoreVTab { } } - fn filter( - cursor: &mut Self::VCursor, - args: &[Value], - idx_str: Option<(&str, i32)>, - ) -> ResultCode { - match idx_str { - Some(("key_eq", 1)) => { - let key = args - .first() - .and_then(|v| v.to_text()) - .map(|s| s.to_string()); - log::debug!("idx_str found: key_eq\n value: {:?}", key); - if let Some(key) = key { - let rowid = hash_key(&key); - let store = GLOBAL_STORE.lock().unwrap(); - if let Some((k, v)) = store.get(&rowid) { - cursor.rows.push((rowid, k.clone(), v.clone())); - cursor.index = Some(0); - } else { - cursor.rows.clear(); - cursor.index = None; - return ResultCode::EOF; - } - return ResultCode::OK; - } - cursor.rows.clear(); - cursor.index = None; - ResultCode::OK - } - _ => { - let store = GLOBAL_STORE.lock().unwrap(); - cursor.rows = store - .iter() - .map(|(&rowid, (k, v))| (rowid, k.clone(), v.clone())) - .collect(); - cursor.rows.sort_by_key(|(rowid, _, _)| *rowid); - if cursor.rows.is_empty() { - cursor.index = None; - ResultCode::EOF - } else { - cursor.index = Some(0); - ResultCode::OK - } - } - } - } - fn insert(&mut self, values: &[Value]) -> Result { let key = values .first() @@ -175,53 +226,6 @@ impl VTabModule for KVStoreVTab { } } -fn hash_key(key: &str) -> i64 { - use std::hash::{Hash, Hasher}; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - key.hash(&mut hasher); - hasher.finish() as i64 -} - -impl VTabCursor for KVStoreCursor { - type Error = String; - - fn rowid(&self) -> i64 { - if self.index.is_some_and(|c| c < self.rows.len()) { - self.rows[self.index.unwrap_or(0)].0 - } else { - log::error!("rowid: -1"); - -1 - } - } - - fn column(&self, idx: u32) -> Result { - if self.index.is_some_and(|c| c >= self.rows.len()) { - return Err("cursor out of range".into()); - } - if let Some((_, ref key, ref val)) = self.rows.get(self.index.unwrap_or(0)) { - match idx { - 0 => Ok(Value::from_text(key.clone())), // key - 1 => Ok(Value::from_text(val.clone())), // value - _ => Err("Invalid column".into()), - } - } else { - Err("Invalid Column".into()) - } - } - - fn eof(&self) -> bool { - self.index.is_some_and(|s| s >= self.rows.len()) || self.index.is_none() - } - - fn next(&mut self) -> ResultCode { - self.index = Some(self.index.unwrap_or(0) + 1); - if self.index.is_some_and(|c| c >= self.rows.len()) { - return ResultCode::EOF; - } - ResultCode::OK - } -} - pub struct TestFile { file: File, } diff --git a/macros/src/ext/vtab_derive.rs b/macros/src/ext/vtab_derive.rs index d096f2f94..0bcb3c53a 100644 --- a/macros/src/ext/vtab_derive.rs +++ b/macros/src/ext/vtab_derive.rs @@ -7,7 +7,7 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { let struct_name = &ast.ident; let register_fn_name = format_ident!("register_{}", struct_name); - let create_schema_fn_name = format_ident!("create_schema_{}", struct_name); + let create_fn_name = format_ident!("create_{}", struct_name); let open_fn_name = format_ident!("open_{}", struct_name); let close_fn_name = format_ident!("close_{}", struct_name); let filter_fn_name = format_ident!("filter_{}", struct_name); @@ -22,26 +22,40 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { let expanded = quote! { impl #struct_name { #[no_mangle] - unsafe extern "C" fn #create_schema_fn_name( + unsafe extern "C" fn #create_fn_name( argv: *const ::limbo_ext::Value, argc: i32 - ) -> *mut ::std::ffi::c_char { + ) -> ::limbo_ext::VTabCreateResult { let args = if argv.is_null() { &Vec::new() } else { ::std::slice::from_raw_parts(argv, argc as usize) }; - let sql = <#struct_name as ::limbo_ext::VTabModule>::create_schema(&args); - ::std::ffi::CString::new(sql).unwrap().into_raw() + match <#struct_name as ::limbo_ext::VTabModule>::create(&args) { + Ok((schema, table)) => { + ::limbo_ext::VTabCreateResult { + code: ::limbo_ext::ResultCode::OK, + schema: ::std::ffi::CString::new(schema).unwrap().into_raw(), + table: ::std::boxed::Box::into_raw(::std::boxed::Box::new(table)) as *const ::std::ffi::c_void, + } + }, + Err(e) => { + ::limbo_ext::VTabCreateResult { + code: e, + schema: ::std::ptr::null(), + table: ::std::ptr::null(), + } + } + } } #[no_mangle] - unsafe extern "C" fn #open_fn_name(ctx: *const ::std::ffi::c_void) -> *const ::std::ffi::c_void { - if ctx.is_null() { + unsafe extern "C" fn #open_fn_name(table: *const ::std::ffi::c_void) -> *const ::std::ffi::c_void { + if table.is_null() { return ::std::ptr::null(); } - let ctx = ctx as *const #struct_name; - let ctx: &#struct_name = &*ctx; - if let Ok(cursor) = <#struct_name as ::limbo_ext::VTabModule>::open(ctx) { + let table = table as *const <#struct_name as ::limbo_ext::VTabModule>::Table; + let table: &<#struct_name as ::limbo_ext::VTabModule>::Table = &*table; + if let Ok(cursor) = <#struct_name as ::limbo_ext::VTabModule>::Table::open(table) { return ::std::boxed::Box::into_raw(::std::boxed::Box::new(cursor)) as *const ::std::ffi::c_void; } else { return ::std::ptr::null(); @@ -55,8 +69,9 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { if cursor.is_null() { return ::limbo_ext::ResultCode::Error; } - let boxed_cursor = ::std::boxed::Box::from_raw(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor); - boxed_cursor.close() + let cursor = cursor as *mut <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor; + let cursor = ::std::boxed::Box::from_raw(cursor); + <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor::close(&*cursor) } #[no_mangle] @@ -70,14 +85,14 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { if cursor.is_null() { return ::limbo_ext::ResultCode::Error; } - let cursor = unsafe { &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor) }; + let cursor = &mut *(cursor as *mut <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor); let args = ::std::slice::from_raw_parts(argv, argc as usize); let idx_str = if idx_str.is_null() { None } else { Some((unsafe { ::std::ffi::CStr::from_ptr(idx_str).to_str().unwrap() }, idx_num)) }; - <#struct_name as ::limbo_ext::VTabModule>::filter(cursor, args, idx_str) + <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor::filter(cursor, args, idx_str) } #[no_mangle] @@ -88,8 +103,8 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { if cursor.is_null() { return ::limbo_ext::Value::error(::limbo_ext::ResultCode::Error); } - let cursor = unsafe { &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor) }; - match <<#struct_name as ::limbo_ext::VTabModule>::VCursor as ::limbo_ext::VTabCursor>::column(cursor, idx) { + let cursor = &*(cursor as *const <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor); + match <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor::column(cursor, idx) { Ok(val) => val, Err(e) => ::limbo_ext::Value::error_with_message(e.to_string()) } @@ -102,8 +117,8 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { if cursor.is_null() { return ::limbo_ext::ResultCode::Error; } - let cursor = &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor); - <<#struct_name as ::limbo_ext::VTabModule>::VCursor as ::limbo_ext::VTabCursor>::next(cursor) + let cursor = &mut *(cursor as *mut <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor); + <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor::next(cursor) } #[no_mangle] @@ -113,22 +128,22 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { if cursor.is_null() { return true; } - let cursor = &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor); - <<#struct_name as ::limbo_ext::VTabModule>::VCursor as ::limbo_ext::VTabCursor>::eof(cursor) + let cursor = &*(cursor as *const <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor); + <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor::eof(cursor) } #[no_mangle] unsafe extern "C" fn #update_fn_name( - vtab: *const ::std::ffi::c_void, + table: *const ::std::ffi::c_void, argc: i32, argv: *const ::limbo_ext::Value, p_out_rowid: *mut i64, ) -> ::limbo_ext::ResultCode { - if vtab.is_null() { + if table.is_null() { return ::limbo_ext::ResultCode::Error; } - let vtab = &mut *(vtab as *mut #struct_name); + let table = &mut *(table as *mut <#struct_name as ::limbo_ext::VTabModule>::Table); let args = ::std::slice::from_raw_parts(argv, argc as usize); let old_rowid = match args.get(0).map(|v| v.value_type()) { @@ -143,21 +158,21 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { match (old_rowid, new_rowid) { // DELETE: old_rowid provided, no new_rowid (Some(old), None) => { - if <#struct_name as VTabModule>::delete(vtab, old).is_err() { + if <#struct_name as VTabModule>::Table::delete(table, old).is_err() { return ::limbo_ext::ResultCode::Error; } return ::limbo_ext::ResultCode::OK; } // UPDATE: old_rowid provided and new_rowid may exist (Some(old), Some(new)) => { - if <#struct_name as VTabModule>::update(vtab, old, &columns).is_err() { + if <#struct_name as VTabModule>::Table::update(table, old, &columns).is_err() { return ::limbo_ext::ResultCode::Error; } return ::limbo_ext::ResultCode::OK; } // INSERT: no old_rowid (old_rowid = None) (None, _) => { - if let Ok(rowid) = <#struct_name as VTabModule>::insert(vtab, &columns) { + if let Ok(rowid) = <#struct_name as VTabModule>::Table::insert(table, &columns) { if !p_out_rowid.is_null() { *p_out_rowid = rowid; return ::limbo_ext::ResultCode::RowID; @@ -170,24 +185,26 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { } #[no_mangle] - pub unsafe extern "C" fn #rowid_fn_name(ctx: *const ::std::ffi::c_void) -> i64 { - if ctx.is_null() { + pub unsafe extern "C" fn #rowid_fn_name(cursor: *const ::std::ffi::c_void) -> i64 { + if cursor.is_null() { return -1; } - let cursor = &*(ctx as *const <#struct_name as ::limbo_ext::VTabModule>::VCursor); - <<#struct_name as ::limbo_ext::VTabModule>::VCursor as ::limbo_ext::VTabCursor>::rowid(cursor) + let cursor = &*(cursor as *const <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor); + <<#struct_name as ::limbo_ext::VTabModule>::Table as ::limbo_ext::VTable>::Cursor::rowid(cursor) } #[no_mangle] unsafe extern "C" fn #destroy_fn_name( - vtab: *const ::std::ffi::c_void, + table: *const ::std::ffi::c_void, ) -> ::limbo_ext::ResultCode { - if vtab.is_null() { + if table.is_null() { return ::limbo_ext::ResultCode::Error; } - let vtab = &mut *(vtab as *mut #struct_name); - if <#struct_name as VTabModule>::destroy(vtab).is_err() { + // Take ownership of the table so it can be properly dropped. + let mut table: ::std::boxed::Box<<#struct_name as ::limbo_ext::VTabModule>::Table> = + ::std::boxed::Box::from_raw(table as *mut <#struct_name as ::limbo_ext::VTabModule>::Table); + if <#struct_name as VTabModule>::Table::destroy(&mut *table).is_err() { return ::limbo_ext::ResultCode::Error; } @@ -203,7 +220,7 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { ) -> ::limbo_ext::ExtIndexInfo { let constraints = if n_constraints > 0 { std::slice::from_raw_parts(constraints, n_constraints as usize) } else { &[] }; let order_by = if n_order_by > 0 { std::slice::from_raw_parts(order_by, n_order_by as usize) } else { &[] }; - <#struct_name as ::limbo_ext::VTabModule>::best_index(constraints, order_by).to_ffi() + <#struct_name as ::limbo_ext::VTabModule>::Table::best_index(constraints, order_by).to_ffi() } #[no_mangle] @@ -216,11 +233,9 @@ pub fn derive_vtab_module(input: TokenStream) -> TokenStream { let api = &*api; let name = <#struct_name as ::limbo_ext::VTabModule>::NAME; let name_c = ::std::ffi::CString::new(name).unwrap().into_raw() as *const ::std::ffi::c_char; - let table_instance = ::std::boxed::Box::into_raw(::std::boxed::Box::new(#struct_name::default())); let module = ::limbo_ext::VTabModuleImpl { - ctx: table_instance as *const ::std::ffi::c_void, name: name_c, - create_schema: Self::#create_schema_fn_name, + create: Self::#create_fn_name, open: Self::#open_fn_name, close: Self::#close_fn_name, filter: Self::#filter_fn_name, diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 154dfb63c..b65af2234 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -230,63 +230,77 @@ pub fn derive_agg_func(input: TokenStream) -> TokenStream { /// Macro to derive a VTabModule for your extension. This macro will generate /// the necessary functions to register your module with core. You must implement -/// the VTabModule trait for your struct, and the VTabCursor trait for your cursor. +/// the VTabModule, VTable, and VTabCursor traits. /// ```ignore -///#[derive(Debug, VTabModuleDerive)] -///struct CsvVTab; -///impl VTabModule for CsvVTab { -/// type VCursor = CsvCursor; -/// const NAME: &'static str = "csv_data"; +/// #[derive(Debug, VTabModuleDerive)] +/// struct CsvVTabModule; +/// +/// impl VTabModule for CsvVTabModule { +/// type Table = CsvTable; +/// const NAME: &'static str = "csv_data"; +/// const VTAB_KIND: VTabKind = VTabKind::VirtualTable; +/// +/// /// Declare your virtual table and its schema +/// fn create(args: &[Value]) -> Result<(String, Self::Table), ResultCode> { +/// let schema = "CREATE TABLE csv_data( +/// name TEXT, +/// age TEXT, +/// city TEXT +/// )".into(); +/// Ok((schema, CsvTable {})) +/// } +/// } +/// +/// struct CsvTable {} +/// +/// // Implement the VTable trait for your virtual table +/// impl VTable for CsvTable { +/// type Cursor = CsvCursor; +/// type Error = &'static str; +/// +/// /// Open the virtual table and return a cursor +/// fn open(&self) -> Result { +/// let csv_content = fs::read_to_string("data.csv").unwrap_or_default(); +/// let rows: Vec> = csv_content +/// .lines() +/// .skip(1) +/// .map(|line| { +/// line.split(',') +/// .map(|s| s.trim().to_string()) +/// .collect() +/// }) +/// .collect(); +/// Ok(CsvCursor { rows, index: 0 }) +/// } /// -/// /// Declare the schema for your virtual table -/// fn create_schema(args: &[&str]) -> &'static str { -/// let sql = "CREATE TABLE csv_data( -/// name TEXT, -/// age TEXT, -/// city TEXT -/// )" -/// } -/// /// Open the virtual table and return a cursor -/// fn open() -> Self::VCursor { -/// let csv_content = fs::read_to_string("data.csv").unwrap_or_default(); -/// let rows: Vec> = csv_content -/// .lines() -/// .skip(1) -/// .map(|line| { -/// line.split(',') -/// .map(|s| s.trim().to_string()) -/// .collect() -/// }) -/// .collect(); -/// CsvCursor { rows, index: 0 } -/// } -/// /// Filter the virtual table based on arguments (omitted here for simplicity) -/// fn filter(_cursor: &mut Self::VCursor, _arg_count: i32, _args: &[Value]) -> ResultCode { -/// ResultCode::OK -/// } /// /// **Optional** methods for non-readonly tables: /// /// /// Update the row with the provided values, return the new rowid /// fn update(&mut self, rowid: i64, args: &[Value]) -> Result, Self::Error> { /// Ok(None)// return Ok(None) for read-only /// } +/// /// /// Insert a new row with the provided values, return the new rowid /// fn insert(&mut self, args: &[Value]) -> Result<(), Self::Error> { /// Ok(()) // /// } +/// /// /// Delete the row with the provided rowid /// fn delete(&mut self, rowid: i64) -> Result<(), Self::Error> { /// Ok(()) /// } +/// /// /// Destroy the virtual table. Any cleanup logic for when the table is deleted comes heres /// fn destroy(&mut self) -> Result<(), Self::Error> { /// Ok(()) /// } +/// } /// /// #[derive(Debug)] /// struct CsvCursor { /// rows: Vec>, /// index: usize, +/// } /// /// impl CsvCursor { /// /// Returns the value for a given column index. @@ -298,27 +312,40 @@ pub fn derive_agg_func(input: TokenStream) -> TokenStream { /// Value::null() /// } /// } +/// } +/// /// // Implement the VTabCursor trait for your virtual cursor /// impl VTabCursor for CsvCursor { -/// /// Move the cursor to the next row -/// fn next(&mut self) -> ResultCode { -/// if self.index < self.rows.len() - 1 { -/// self.index += 1; -/// ResultCode::OK -/// } else { -/// ResultCode::EOF -/// } -/// } +/// type Error = &'static str; +/// +/// /// Filter the virtual table based on arguments (omitted here for simplicity) +/// fn filter(&mut self, _args: &[Value], _idx_info: Option<(&str, i32)>) -> ResultCode { +/// ResultCode::OK +/// } +/// +/// /// Move the cursor to the next row +/// fn next(&mut self) -> ResultCode { +/// if self.index < self.rows.len() - 1 { +/// self.index += 1; +/// ResultCode::OK +/// } else { +/// ResultCode::EOF +/// } +/// } +/// /// fn eof(&self) -> bool { /// self.index >= self.rows.len() /// } +/// /// /// Return the value for a given column index -/// fn column(&self, idx: u32) -> Value { +/// fn column(&self, idx: u32) -> Result { /// self.column(idx) /// } +/// /// fn rowid(&self) -> i64 { /// self.index as i64 /// } +/// } /// #[proc_macro_derive(VTabModuleDerive)] pub fn derive_vtab_module(input: TokenStream) -> TokenStream {