From 5d1cc0f2fc737528e6b9e90ca9f9313a8404f922 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 19 Jan 2025 15:10:47 +0200 Subject: [PATCH] WIP: Initial virtual table implementation --- Cargo.lock | 8 ++ Cargo.toml | 1 + core/ext/mod.rs | 139 +++++++++++++++++++++++++++++++- core/lib.rs | 102 +++++++++++++++++++++++- core/schema.rs | 14 ++++ core/translate/expr.rs | 9 +++ core/translate/main_loop.rs | 150 ++++++++++++++++++++++++----------- core/translate/optimizer.rs | 6 +- core/translate/plan.rs | 16 +++- core/translate/planner.rs | 33 +++++++- core/vdbe/builder.rs | 6 +- core/vdbe/explain.rs | 57 +++++++++++++ core/vdbe/insn.rs | 29 +++++++ core/vdbe/mod.rs | 86 ++++++++++++++++++++ extensions/core/src/lib.rs | 75 +++++++++++++++++- extensions/core/src/types.rs | 5 +- extensions/series/Cargo.toml | 15 ++++ extensions/series/src/lib.rs | 136 +++++++++++++++++++++++++++++++ macros/src/args.rs | 10 ++- macros/src/lib.rs | 115 +++++++++++++++++++++++++++ 20 files changed, 951 insertions(+), 61 deletions(-) create mode 100644 extensions/series/Cargo.toml create mode 100644 extensions/series/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 18a1db8fe..3a67bee35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,14 @@ dependencies = [ "regex", ] +[[package]] +name = "limbo_series" +version = "0.0.13" +dependencies = [ + "limbo_ext", + "log", +] + [[package]] name = "limbo_sim" version = "0.0.13" diff --git a/Cargo.toml b/Cargo.toml index bc188deee..83a8a8332 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "simulator", "sqlite3", "test", "extensions/percentile", + "extensions/series", ] exclude = ["perf/latency/limbo"] diff --git a/core/ext/mod.rs b/core/ext/mod.rs index cf3fa6109..e858f969f 100644 --- a/core/ext/mod.rs +++ b/core/ext/mod.rs @@ -1,6 +1,15 @@ -use crate::{function::ExternalFunc, Database}; -use limbo_ext::{ExtensionApi, InitAggFunction, ResultCode, ScalarFunction}; +use crate::{ + function::ExternalFunc, + schema::{Column, Type}, + Database, VirtualTable, +}; +use fallible_iterator::FallibleIterator; +use limbo_ext::{ExtensionApi, InitAggFunction, ResultCode, ScalarFunction, VTabModuleImpl}; pub use limbo_ext::{FinalizeFunction, StepFunction, Value as ExtValue, ValueType as ExtValueType}; +use sqlite3_parser::{ + ast::{Cmd, CreateTableBody, Stmt}, + lexer::sql::Parser, +}; use std::{ ffi::{c_char, c_void, CStr}, rc::Rc, @@ -13,6 +22,7 @@ unsafe extern "C" fn register_scalar_function( func: ScalarFunction, ) -> ResultCode { let c_str = unsafe { CStr::from_ptr(name) }; + println!("Scalar??"); let name_str = match c_str.to_str() { Ok(s) => s.to_string(), Err(_) => return ResultCode::InvalidArgs, @@ -32,6 +42,7 @@ unsafe extern "C" fn register_aggregate_function( step_func: StepFunction, finalize_func: FinalizeFunction, ) -> ResultCode { + println!("Aggregate??"); let c_str = unsafe { CStr::from_ptr(name) }; let name_str = match c_str.to_str() { Ok(s) => s.to_string(), @@ -44,6 +55,48 @@ unsafe extern "C" fn register_aggregate_function( db.register_aggregate_function_impl(&name_str, args, (init_func, step_func, finalize_func)) } +unsafe extern "C" fn register_module( + ctx: *mut c_void, + name: *const c_char, + module: VTabModuleImpl, +) -> ResultCode { + let c_str = unsafe { CStr::from_ptr(name) }; + let name_str = match c_str.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ResultCode::Error, + }; + if ctx.is_null() { + return ResultCode::Error; + } + let db = unsafe { &*(ctx as *const Database) }; + + db.register_module_impl(&name_str, module) +} + +unsafe extern "C" fn declare_vtab( + ctx: *mut c_void, + name: *const c_char, + sql: *const c_char, +) -> ResultCode { + let c_str = unsafe { CStr::from_ptr(name) }; + let name_str = match c_str.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ResultCode::Error, + }; + + let c_str = unsafe { CStr::from_ptr(sql) }; + let sql_str = match c_str.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ResultCode::Error, + }; + + if ctx.is_null() { + return ResultCode::Error; + } + let db = unsafe { &*(ctx as *const Database) }; + db.declare_vtab_impl(&name_str, &sql_str) +} + impl Database { fn register_scalar_function_impl(&self, name: &str, func: ScalarFunction) -> ResultCode { self.syms.borrow_mut().functions.insert( @@ -66,11 +119,93 @@ impl Database { ResultCode::OK } + fn register_module_impl(&self, name: &str, module: VTabModuleImpl) -> ResultCode { + self.vtab_modules + .borrow_mut() + .insert(name.to_string(), Rc::new(module)); + ResultCode::OK + } + + fn declare_vtab_impl(&self, name: &str, sql: &str) -> ResultCode { + let mut parser = Parser::new(sql.as_bytes()); + let cmd = parser.next().unwrap().unwrap(); + let Cmd::Stmt(stmt) = cmd else { + return ResultCode::Error; + }; + let Stmt::CreateTable { body, .. } = stmt else { + return ResultCode::Error; + }; + let CreateTableBody::ColumnsAndConstraints { columns, .. } = body else { + return ResultCode::Error; + }; + + let columns = columns + .into_iter() + .filter_map(|(name, column_def)| { + // if column_def.col_type includes HIDDEN, omit it for now + if let Some(data_type) = column_def.col_type.as_ref() { + if data_type.name.as_str().contains("HIDDEN") { + return None; + } + } + let column = Column { + name: name.0.clone(), + // TODO extract to util, we use this elsewhere too. + ty: match column_def.col_type { + Some(data_type) => { + // https://www.sqlite.org/datatype3.html + let type_name = data_type.name.as_str().to_uppercase(); + if type_name.contains("INT") { + Type::Integer + } else if type_name.contains("CHAR") + || type_name.contains("CLOB") + || type_name.contains("TEXT") + { + Type::Text + } else if type_name.contains("BLOB") || type_name.is_empty() { + Type::Blob + } else if type_name.contains("REAL") + || type_name.contains("FLOA") + || type_name.contains("DOUB") + { + Type::Real + } else { + Type::Numeric + } + } + None => Type::Null, + }, + primary_key: column_def.constraints.iter().any(|c| { + matches!( + c.constraint, + sqlite3_parser::ast::ColumnConstraint::PrimaryKey { .. } + ) + }), + is_rowid_alias: false, + }; + Some(column) + }) + .collect::>(); + let vtab_module = self.vtab_modules.borrow().get(name).unwrap().clone(); + let vtab = VirtualTable { + name: name.to_string(), + implementation: vtab_module, + columns, + }; + self.syms + .borrow_mut() + .vtabs + .insert(name.to_string(), Rc::new(vtab)); + ResultCode::OK + } + pub fn build_limbo_ext(&self) -> ExtensionApi { ExtensionApi { ctx: self as *const _ as *mut c_void, register_scalar_function, register_aggregate_function, + register_module, + declare_vtab, } } } diff --git a/core/lib.rs b/core/lib.rs index 840b4cfc8..4cf8d8afd 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -22,10 +22,11 @@ use fallible_iterator::FallibleIterator; #[cfg(not(target_family = "wasm"))] use libloading::{Library, Symbol}; #[cfg(not(target_family = "wasm"))] -use limbo_ext::{ExtensionApi, ExtensionEntryPoint}; +use limbo_ext::{ExtensionApi, ExtensionEntryPoint, ResultCode}; +use limbo_ext::{VTabModuleImpl, Value as ExtValue, ValueType}; use log::trace; -use schema::Schema; -use sqlite3_parser::ast; +use schema::{Column, Schema}; +use sqlite3_parser::ast::{self}; use sqlite3_parser::{ast::Cmd, lexer::sql::Parser}; use std::cell::Cell; use std::collections::HashMap; @@ -40,8 +41,10 @@ use storage::pager::allocate_page; use storage::sqlite3_ondisk::{DatabaseHeader, DATABASE_HEADER_SIZE}; pub use storage::wal::WalFile; pub use storage::wal::WalFileShared; +use types::OwnedValue; pub use types::Value; use util::parse_schema_rows; +use vdbe::VTabOpaqueCursor; pub use error::LimboError; use translate::select::prepare_select_plan; @@ -75,6 +78,7 @@ pub struct Database { schema: Rc>, header: Rc>, syms: Rc>, + vtab_modules: Rc>>>, // Shared structures of a Database are the parts that are common to multiple threads that might // create DB connections. _shared_page_cache: Arc>, @@ -137,6 +141,7 @@ impl Database { _shared_page_cache: _shared_page_cache.clone(), _shared_wal: shared_wal.clone(), syms, + vtab_modules: Rc::new(RefCell::new(HashMap::new())), }; let db = Arc::new(db); let conn = Rc::new(Connection { @@ -509,10 +514,100 @@ impl Rows { } } +#[derive(Clone, Debug)] +pub struct VirtualTable { + name: String, + pub implementation: Rc, + columns: Vec, +} + +impl VirtualTable { + pub fn open(&self) -> VTabOpaqueCursor { + let cursor = unsafe { (self.implementation.open)() }; + VTabOpaqueCursor::new(cursor) + } + + pub fn filter( + &self, + cursor: &VTabOpaqueCursor, + arg_count: usize, + args: Vec, + ) -> Result<()> { + let mut filter_args = Vec::with_capacity(arg_count); + for i in 0..arg_count { + let ownedvalue_arg = args.get(i).unwrap(); + let extvalue_arg: ExtValue = match ownedvalue_arg { + OwnedValue::Null => Ok(ExtValue::null()), + OwnedValue::Integer(i) => Ok(ExtValue::from_integer(*i)), + OwnedValue::Float(f) => Ok(ExtValue::from_float(*f)), + OwnedValue::Text(t) => Ok(ExtValue::from_text((*t.value).clone())), + OwnedValue::Blob(b) => Ok(ExtValue::from_blob((**b).clone())), + other => Err(LimboError::ExtensionError(format!( + "Unsupported value type: {:?}", + other + ))), + }?; + filter_args.push(extvalue_arg); + } + let rc = unsafe { + (self.implementation.filter)(cursor.as_ptr(), arg_count as i32, filter_args.as_ptr()) + }; + match rc { + ResultCode::OK => Ok(()), + _ => Err(LimboError::ExtensionError("Filter failed".to_string())), + } + } + + pub fn column(&self, cursor: &VTabOpaqueCursor, column: usize) -> Result { + let val = unsafe { (self.implementation.column)(cursor.as_ptr(), column as u32) }; + match &val.value_type { + ValueType::Null => Ok(OwnedValue::Null), + ValueType::Integer => match val.to_integer() { + Some(i) => Ok(OwnedValue::Integer(i)), + None => Err(LimboError::ExtensionError( + "Failed to convert integer value".to_string(), + )), + }, + ValueType::Float => match val.to_float() { + Some(f) => Ok(OwnedValue::Float(f)), + None => Err(LimboError::ExtensionError( + "Failed to convert float value".to_string(), + )), + }, + ValueType::Text => match val.to_text() { + Some(t) => Ok(OwnedValue::build_text(Rc::new(t))), + None => Err(LimboError::ExtensionError( + "Failed to convert text value".to_string(), + )), + }, + ValueType::Blob => match val.to_blob() { + Some(b) => Ok(OwnedValue::Blob(Rc::new(b))), + None => Err(LimboError::ExtensionError( + "Failed to convert blob value".to_string(), + )), + }, + ValueType::Error => Err(LimboError::ExtensionError(format!( + "Error value in column {}", + column + ))), + } + } + + pub fn next(&self, cursor: &VTabOpaqueCursor) -> Result { + let rc = unsafe { (self.implementation.next)(cursor.as_ptr()) }; + match rc { + ResultCode::OK => Ok(true), + ResultCode::EOF => Ok(false), + _ => Err(LimboError::ExtensionError("Next failed".to_string())), + } + } +} + pub(crate) struct SymbolTable { pub functions: HashMap>, #[cfg(not(target_family = "wasm"))] extensions: Vec<(Library, *const ExtensionApi)>, + pub vtabs: HashMap>, } impl std::fmt::Debug for SymbolTable { @@ -554,6 +649,7 @@ impl SymbolTable { pub fn new() -> Self { Self { functions: HashMap::new(), + vtabs: HashMap::new(), // TODO: wasm libs will be very different #[cfg(not(target_family = "wasm"))] extensions: Vec::new(), diff --git a/core/schema.rs b/core/schema.rs index fda6c12ba..c02bb3dce 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -1,3 +1,4 @@ +use crate::VirtualTable; use crate::{util::normalize_ident, Result}; use core::fmt; use fallible_iterator::FallibleIterator; @@ -47,6 +48,7 @@ impl Schema { pub enum Table { BTree(Rc), Pseudo(Rc), + Virtual(Rc), } impl Table { @@ -54,6 +56,7 @@ impl Table { match self { Table::BTree(table) => table.root_page, Table::Pseudo(_) => unimplemented!(), + Table::Virtual(_) => unimplemented!(), } } @@ -61,6 +64,7 @@ impl Table { match self { Self::BTree(table) => &table.name, Self::Pseudo(_) => "", + Self::Virtual(table) => &table.name, } } @@ -68,6 +72,7 @@ impl Table { match self { Self::BTree(table) => table.columns.get(index).unwrap(), Self::Pseudo(table) => table.columns.get(index).unwrap(), + Self::Virtual(_) => unimplemented!(), } } @@ -75,6 +80,7 @@ impl Table { match self { Self::BTree(table) => &table.columns, Self::Pseudo(table) => &table.columns, + Self::Virtual(table) => &table.columns, } } @@ -82,6 +88,13 @@ impl Table { match self { Self::BTree(table) => Some(table.clone()), Self::Pseudo(_) => None, + Self::Virtual(_) => None, + } + } + pub fn virtual_table(&self) -> Option> { + match self { + Self::Virtual(table) => Some(table.clone()), + _ => None, } } } @@ -91,6 +104,7 @@ impl PartialEq for Table { match (self, other) { (Self::BTree(a), Self::BTree(b)) => Rc::ptr_eq(a, b), (Self::Pseudo(a), Self::Pseudo(b)) => Rc::ptr_eq(a, b), + (Self::Virtual(a), Self::Virtual(b)) => Rc::ptr_eq(a, b), _ => false, } } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 3d07459cb..e86843459 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1575,6 +1575,15 @@ pub fn translate_expr( }); Ok(target_register) } + TableReferenceType::VirtualTable { .. } => { + let cursor_id = program.resolve_cursor_id(&tbl_ref.table_identifier); + program.emit_insn(Insn::VColumn { + cursor_id: cursor_id, + column: *column, + dest: target_register, + }); + Ok(target_register) + } } } ast::Expr::RowId { database: _, table } => { diff --git a/core/translate/main_loop.rs b/core/translate/main_loop.rs index cc62d3986..d52d87554 100644 --- a/core/translate/main_loop.rs +++ b/core/translate/main_loop.rs @@ -17,6 +17,7 @@ use super::{ order_by::{order_by_sorter_insert, sorter_insert}, plan::{ IterationDirection, Search, SelectPlan, SelectQueryType, SourceOperator, TableReference, + TableReferenceType, }, }; @@ -82,27 +83,41 @@ pub fn init_loop( SourceOperator::Scan { table_reference, .. } => { + let reftype = &table_reference.reference_type; let cursor_id = program.alloc_cursor_id( Some(table_reference.table_identifier.clone()), - CursorType::BTreeTable(table_reference.btree().unwrap().clone()), + match reftype { + TableReferenceType::BTreeTable => { + CursorType::BTreeTable(table_reference.btree().unwrap().clone()) + } + TableReferenceType::VirtualTable { .. } => { + CursorType::VirtualTable(table_reference.virtual_table().unwrap().clone()) + } + other => panic!("Invalid table reference type in Scan: {:?}", other), + }, ); - let root_page = table_reference.table.get_root_page(); - match mode { - OperationMode::SELECT => { + match (mode, reftype) { + (OperationMode::SELECT, TableReferenceType::BTreeTable) => { + let root_page = table_reference.table.get_root_page(); program.emit_insn(Insn::OpenReadAsync { cursor_id, root_page, }); program.emit_insn(Insn::OpenReadAwait {}); } - OperationMode::DELETE => { + (OperationMode::DELETE, TableReferenceType::BTreeTable) => { + let root_page = table_reference.table.get_root_page(); program.emit_insn(Insn::OpenWriteAsync { cursor_id, root_page, }); program.emit_insn(Insn::OpenWriteAwait {}); } + (OperationMode::SELECT, TableReferenceType::VirtualTable { .. }) => { + program.emit_insn(Insn::VOpenAsync { cursor_id }); + program.emit_insn(Insn::VOpenAwait {}); + } _ => { unimplemented!() } @@ -308,14 +323,18 @@ pub fn open_loop( predicates, iter_dir, } => { + let ref_type = &table_reference.reference_type; let cursor_id = program.resolve_cursor_id(&table_reference.table_identifier); - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - program.emit_insn(Insn::LastAsync { cursor_id }); - } else { - program.emit_insn(Insn::RewindAsync { cursor_id }); + + if !matches!(ref_type, TableReferenceType::VirtualTable { .. }) { + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + program.emit_insn(Insn::LastAsync { cursor_id }); + } else { + program.emit_insn(Insn::RewindAsync { cursor_id }); + } } let LoopLabels { loop_start, @@ -325,22 +344,46 @@ pub fn open_loop( .labels_main_loop .get(id) .expect("scan has no loop labels"); - program.emit_insn( - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - Insn::LastAwait { - cursor_id, - pc_if_empty: loop_end, + + match ref_type { + TableReferenceType::BTreeTable => program.emit_insn( + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + Insn::LastAwait { + cursor_id, + pc_if_empty: loop_end, + } + } else { + Insn::RewindAwait { + cursor_id, + pc_if_empty: loop_end, + } + }, + ), + TableReferenceType::VirtualTable { args, .. } => { + let start_reg = program.alloc_registers(args.len()); + let mut cur_reg = start_reg; + for arg in args { + let reg = cur_reg; + cur_reg += 1; + translate_expr( + program, + Some(referenced_tables), + arg, + reg, + &t_ctx.resolver, + )?; } - } else { - Insn::RewindAwait { + program.emit_insn(Insn::VFilter { cursor_id, - pc_if_empty: loop_end, - } - }, - ); + arg_count: args.len(), + args_reg: start_reg, + }); + } + other => panic!("Unsupported table reference type: {:?}", other), + } program.resolve_label(loop_start, program.offset()); if let Some(preds) = predicates { @@ -790,29 +833,42 @@ pub fn close_loop( iter_dir, .. } => { + let ref_type = &table_reference.reference_type; program.resolve_label(loop_labels.next, program.offset()); let cursor_id = program.resolve_cursor_id(&table_reference.table_identifier); - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - program.emit_insn(Insn::PrevAsync { cursor_id }); - } else { - program.emit_insn(Insn::NextAsync { cursor_id }); - } - if iter_dir - .as_ref() - .is_some_and(|dir| *dir == IterationDirection::Backwards) - { - program.emit_insn(Insn::PrevAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }); - } else { - program.emit_insn(Insn::NextAwait { - cursor_id, - pc_if_next: loop_labels.loop_start, - }); + + match ref_type { + TableReferenceType::BTreeTable { .. } => { + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + program.emit_insn(Insn::PrevAsync { cursor_id }); + } else { + program.emit_insn(Insn::NextAsync { cursor_id }); + } + if iter_dir + .as_ref() + .is_some_and(|dir| *dir == IterationDirection::Backwards) + { + program.emit_insn(Insn::PrevAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); + } else { + program.emit_insn(Insn::NextAwait { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); + } + } + TableReferenceType::VirtualTable { .. } => { + program.emit_insn(Insn::VNext { + cursor_id, + pc_if_next: loop_labels.loop_start, + }); + } + other => unreachable!("Unsupported table reference type: {:?}", other), } } SourceOperator::Search { diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index d6caba85e..0b0cff3d0 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -509,7 +509,11 @@ fn push_predicate( .iter() .position(|t| { t.table_identifier == table_reference.table_identifier - && t.reference_type == TableReferenceType::BTreeTable + && matches!( + t.reference_type, + TableReferenceType::BTreeTable { .. } + | TableReferenceType::VirtualTable { .. } + ) }) .unwrap(); diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 6164428fb..7447219f6 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -9,7 +9,7 @@ use crate::{ function::AggFunc, schema::{BTreeTable, Column, Index, Table}, vdbe::BranchOffset, - Result, + Result, VirtualTable, }; use crate::{ schema::{PseudoTable, Type}, @@ -237,6 +237,11 @@ pub enum TableReferenceType { /// The index of the first register in the query plan that contains the result columns of the subquery. result_columns_start_reg: usize, }, + /// A virtual table. + VirtualTable { + /// Arguments to pass e.g. generate_series(1, 10, 2) + args: Vec, + }, } /// A query plan has a list of TableReference objects, each of which represents a table or subquery. @@ -256,9 +261,16 @@ pub struct TableReference { impl TableReference { pub fn btree(&self) -> Option> { - match self.reference_type { + match &self.reference_type { TableReferenceType::BTreeTable => self.table.btree(), TableReferenceType::Subquery { .. } => None, + TableReferenceType::VirtualTable { .. } => None, + } + } + pub fn virtual_table(&self) -> Option> { + match &self.reference_type { + TableReferenceType::VirtualTable { .. } => self.table.virtual_table(), + _ => None, } } pub fn new_subquery(identifier: String, table_index: usize, plan: &SelectPlan) -> Self { diff --git a/core/translate/planner.rs b/core/translate/planner.rs index 86132fb60..9543a64db 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -331,7 +331,38 @@ fn parse_from_clause_table( }, )) } - _ => todo!(), + ast::SelectTable::TableCall(qualified_name, mut maybe_args, maybe_alias) => { + let normalized_name = normalize_ident(qualified_name.name.0.as_str()); + let Some(vtab) = syms.vtabs.get(&normalized_name) else { + crate::bail_parse_error!("Virtual table {} not found", normalized_name); + }; + let alias = maybe_alias + .as_ref() + .map(|a| match a { + ast::As::As(id) => id.0.clone(), + ast::As::Elided(id) => id.0.clone(), + }) + .unwrap_or(normalized_name); + + let table_reference = TableReference { + table: Table::Virtual(vtab.clone()), + table_identifier: alias.clone(), + table_index: cur_table_index, + reference_type: TableReferenceType::VirtualTable { + args: maybe_args.take().unwrap_or_default(), + }, + }; + Ok(( + table_reference.clone(), + SourceOperator::Scan { + table_reference, + predicates: None, + id: operator_id_counter.get_next_id(), + iter_dir: None, + }, + )) + } + other => todo!("unsupported table type: {:?}", other), } } diff --git a/core/vdbe/builder.rs b/core/vdbe/builder.rs index 82be12f1c..fa871bf50 100644 --- a/core/vdbe/builder.rs +++ b/core/vdbe/builder.rs @@ -8,7 +8,7 @@ use crate::{ parameters::Parameters, schema::{BTreeTable, Index, PseudoTable}, storage::sqlite3_ondisk::DatabaseHeader, - Connection, + Connection, VirtualTable, }; use super::{BranchOffset, CursorID, Insn, InsnReference, Program}; @@ -38,6 +38,7 @@ pub enum CursorType { BTreeIndex(Rc), Pseudo(Rc), Sorter, + VirtualTable(Rc), } impl CursorType { @@ -305,6 +306,9 @@ impl ProgramBuilder { Insn::IsNull { src: _, target_pc } => { resolve(target_pc, "IsNull"); } + Insn::VNext { pc_if_next, .. } => { + resolve(pc_if_next, "VNext"); + } _ => continue, } } diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 8ab330f24..17eb525ea 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -344,6 +344,62 @@ pub fn insn_to_str( 0, "".to_string(), ), + Insn::VOpenAsync { cursor_id } => ( + "VOpenAsync", + *cursor_id as i32, + 0, + 0, + OwnedValue::build_text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::VOpenAwait => ( + "VOpenAwait", + 0, + 0, + 0, + OwnedValue::build_text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::VFilter { + cursor_id, + arg_count, + args_reg, + } => ( + "VFilter", + *cursor_id as i32, + *arg_count as i32, + *args_reg as i32, + OwnedValue::build_text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::VColumn { + cursor_id, + column, + dest, + } => ( + "VColumn", + *cursor_id as i32, + *column as i32, + *dest as i32, + OwnedValue::build_text(Rc::new("".to_string())), + 0, + "".to_string(), + ), + Insn::VNext { + cursor_id, + pc_if_next, + } => ( + "VNext", + *cursor_id as i32, + pc_if_next.to_debug_int(), + 0, + OwnedValue::build_text(Rc::new("".to_string())), + 0, + "".to_string(), + ), Insn::OpenPseudo { cursor_id, content_reg, @@ -401,6 +457,7 @@ pub fn insn_to_str( Some(&pseudo_table.columns.get(*column).unwrap().name) } CursorType::Sorter => None, + CursorType::VirtualTable(v) => Some(&v.columns.get(*column).unwrap().name), }; ( "Column", diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 8d812846b..e8b15490b 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -154,6 +154,35 @@ pub enum Insn { // Await for the completion of open cursor. OpenReadAwait, + /// Open a cursor for a virtual table. + VOpenAsync { + cursor_id: CursorID, + }, + + /// Await for the completion of open cursor for a virtual table. + VOpenAwait, + + /// Initialize the position of the virtual table cursor. + VFilter { + cursor_id: CursorID, + arg_count: usize, + args_reg: usize, + }, + + /// Read a column from the current row of the virtual table cursor. + VColumn { + cursor_id: CursorID, + column: usize, + dest: usize, + }, + + /// Advance the virtual table cursor to the next row. + /// TODO: async + VNext { + cursor_id: CursorID, + pc_if_next: BranchOffset, + }, + // Open a cursor for a pseudo-table that contains a single row. OpenPseudo { cursor_id: CursorID, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 6e134a6bc..8c92aa9bb 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -57,6 +57,7 @@ use sorter::Sorter; use std::borrow::BorrowMut; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; +use std::ffi::c_void; use std::num::NonZero; use std::rc::{Rc, Weak}; @@ -190,6 +191,18 @@ impl RegexCache { } } +pub struct VTabOpaqueCursor(*mut c_void); + +impl VTabOpaqueCursor { + pub fn new(cursor: *mut c_void) -> Self { + Self(cursor) + } + + pub fn as_ptr(&self) -> *mut c_void { + self.0 + } +} + /// The program state describes the environment in which the program executes. pub struct ProgramState { pub pc: InsnReference, @@ -197,6 +210,7 @@ pub struct ProgramState { btree_index_cursors: RefCell>, pseudo_cursors: RefCell>, sorter_cursors: RefCell>, + virtual_cursors: RefCell>, registers: Vec, last_compare: Option, deferred_seek: Option<(CursorID, CursorID)>, @@ -220,6 +234,7 @@ impl ProgramState { btree_index_cursors, pseudo_cursors, sorter_cursors, + virtual_cursors: RefCell::new(BTreeMap::new()), registers, last_compare: None, deferred_seek: None, @@ -267,6 +282,7 @@ macro_rules! must_be_btree_cursor { CursorType::BTreeIndex(_) => $btree_index_cursors.get_mut(&$cursor_id).unwrap(), CursorType::Pseudo(_) => panic!("{} on pseudo cursor", $insn_name), CursorType::Sorter => panic!("{} on sorter cursor", $insn_name), + CursorType::VirtualTable(_) => panic!("{} on virtual table cursor", $insn_name), }; cursor }}; @@ -318,6 +334,7 @@ impl Program { let mut btree_index_cursors = state.btree_index_cursors.borrow_mut(); let mut pseudo_cursors = state.pseudo_cursors.borrow_mut(); let mut sorter_cursors = state.sorter_cursors.borrow_mut(); + let mut virtual_cursors = state.virtual_cursors.borrow_mut(); match insn { Insn::Init { target_pc } => { assert!(target_pc.is_offset()); @@ -660,12 +677,73 @@ impl Program { CursorType::Sorter => { panic!("OpenReadAsync on sorter cursor"); } + CursorType::VirtualTable(_) => { + panic!("OpenReadAsync on virtual table cursor, use Insn::VOpenAsync instead"); + } } state.pc += 1; } Insn::OpenReadAwait => { state.pc += 1; } + Insn::VOpenAsync { cursor_id } => { + let (_, cursor_type) = self.cursor_ref.get(*cursor_id).unwrap(); + let CursorType::VirtualTable(virtual_table) = cursor_type else { + panic!("VOpenAsync on non-virtual table cursor"); + }; + let cursor = virtual_table.open(); + virtual_cursors.insert(*cursor_id, cursor); + state.pc += 1; + } + Insn::VOpenAwait => { + state.pc += 1; + } + Insn::VFilter { + cursor_id, + arg_count, + args_reg, + } => { + let (_, cursor_type) = self.cursor_ref.get(*cursor_id).unwrap(); + let CursorType::VirtualTable(virtual_table) = cursor_type else { + panic!("VFilter on non-virtual table cursor"); + }; + let cursor = virtual_cursors.get(cursor_id).unwrap(); + let mut args = Vec::new(); + for i in 0..*arg_count { + args.push(state.registers[args_reg + i].clone()); + } + virtual_table.filter(cursor, *arg_count, args)?; + state.pc += 1; + } + Insn::VColumn { + cursor_id, + column, + dest, + } => { + let (_, cursor_type) = self.cursor_ref.get(*cursor_id).unwrap(); + let CursorType::VirtualTable(virtual_table) = cursor_type else { + panic!("VColumn on non-virtual table cursor"); + }; + let cursor = virtual_cursors.get(cursor_id).unwrap(); + state.registers[*dest] = virtual_table.column(cursor, *column)?; + state.pc += 1; + } + Insn::VNext { + cursor_id, + pc_if_next, + } => { + let (_, cursor_type) = self.cursor_ref.get(*cursor_id).unwrap(); + let CursorType::VirtualTable(virtual_table) = cursor_type else { + panic!("VNextAsync on non-virtual table cursor"); + }; + let cursor = virtual_cursors.get_mut(cursor_id).unwrap(); + let has_more = virtual_table.next(cursor)?; + if has_more { + state.pc = pc_if_next.to_offset_int(); + } else { + state.pc += 1; + } + } Insn::OpenPseudo { cursor_id, content_reg: _, @@ -789,6 +867,11 @@ impl Program { state.registers[*dest] = OwnedValue::Null; } } + CursorType::VirtualTable(_) => { + panic!( + "Insn::Column on virtual table cursor, use Insn::VColumn instead" + ); + } } state.pc += 1; @@ -2212,6 +2295,9 @@ impl Program { CursorType::Sorter => { let _ = sorter_cursors.remove(cursor_id); } + CursorType::VirtualTable(_) => { + let _ = virtual_cursors.remove(cursor_id); + } } state.pc += 1; } diff --git a/extensions/core/src/lib.rs b/extensions/core/src/lib.rs index 5f9bb09c5..fec363c44 100644 --- a/extensions/core/src/lib.rs +++ b/extensions/core/src/lib.rs @@ -1,5 +1,5 @@ mod types; -pub use limbo_macros::{register_extension, scalar, AggregateDerive}; +pub use limbo_macros::{register_extension, scalar, AggregateDerive, VTabModuleDerive}; use std::os::raw::{c_char, c_void}; pub use types::{ResultCode, Value, ValueType}; @@ -21,6 +21,30 @@ pub struct ExtensionApi { step_func: StepFunction, finalize_func: FinalizeFunction, ) -> ResultCode, + + pub register_module: unsafe extern "C" fn( + ctx: *mut c_void, + name: *const c_char, + module: VTabModuleImpl, + ) -> ResultCode, + + pub declare_vtab: unsafe extern "C" fn( + ctx: *mut c_void, + name: *const c_char, + sql: *const c_char, + ) -> ResultCode, +} + +impl ExtensionApi { + pub fn declare_virtual_table(&self, name: &str, sql: &str) -> ResultCode { + let Ok(name) = std::ffi::CString::new(name) else { + return ResultCode::Error; + }; + let Ok(sql) = std::ffi::CString::new(sql) else { + return ResultCode::Error; + }; + unsafe { (self.declare_vtab)(self.ctx, name.as_ptr(), sql.as_ptr()) } + } } pub type ExtensionEntryPoint = unsafe extern "C" fn(api: *const ExtensionApi) -> ResultCode; @@ -47,3 +71,52 @@ pub trait AggFunc { fn step(state: &mut Self::State, args: &[Value]); fn finalize(state: Self::State) -> Value; } + +#[repr(C)] +#[derive(Clone, Debug)] +pub struct VTabModuleImpl { + pub name: *const c_char, + pub connect: VtabFnConnect, + pub open: VtabFnOpen, + pub filter: VtabFnFilter, + pub column: VtabFnColumn, + pub next: VtabFnNext, + pub eof: VtabFnEof, +} + +pub type VtabFnConnect = unsafe extern "C" fn(api: *const c_void) -> ResultCode; + +pub type VtabFnOpen = unsafe extern "C" fn() -> *mut c_void; + +pub type VtabFnFilter = + unsafe extern "C" fn(cursor: *mut c_void, argc: i32, argv: *const Value) -> ResultCode; + +pub type VtabFnColumn = unsafe extern "C" fn(cursor: *mut c_void, idx: u32) -> Value; + +pub type VtabFnNext = unsafe extern "C" fn(cursor: *mut c_void) -> ResultCode; + +pub type VtabFnEof = unsafe extern "C" fn(cursor: *mut c_void) -> bool; + +pub trait VTabModule: 'static { + type VCursor: VTabCursor; + + fn name() -> &'static str; + fn connect(api: &ExtensionApi) -> ResultCode; + fn open() -> Self::VCursor; + fn filter(cursor: &mut Self::VCursor, arg_count: i32, args: &[Value]) -> ResultCode; + fn column(cursor: &Self::VCursor, idx: u32) -> Value; + fn next(cursor: &mut Self::VCursor) -> ResultCode; + fn eof(cursor: &Self::VCursor) -> bool; +} + +pub trait VTabCursor: Sized { + fn rowid(&self) -> i64; + fn column(&self, idx: u32) -> Value; + fn eof(&self) -> bool; + fn next(&mut self) -> ResultCode; +} + +#[repr(C)] +pub struct VTabImpl { + pub module: VTabModuleImpl, +} diff --git a/extensions/core/src/types.rs b/extensions/core/src/types.rs index 9d69aa942..726410b62 100644 --- a/extensions/core/src/types.rs +++ b/extensions/core/src/types.rs @@ -3,6 +3,7 @@ use std::{fmt::Display, os::raw::c_void}; /// Error type is of type ExtError which can be /// either a user defined error or an error code #[repr(C)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum ResultCode { OK = 0, Error = 1, @@ -18,6 +19,7 @@ pub enum ResultCode { Unimplemented = 11, Internal = 12, Unavailable = 13, + EOF = 14, } impl ResultCode { @@ -43,6 +45,7 @@ impl Display for ResultCode { ResultCode::Unimplemented => write!(f, "Unimplemented"), ResultCode::Internal => write!(f, "Internal Error"), ResultCode::Unavailable => write!(f, "Unavailable"), + ResultCode::EOF => write!(f, "EOF"), } } } @@ -60,7 +63,7 @@ pub enum ValueType { #[repr(C)] pub struct Value { - value_type: ValueType, + pub value_type: ValueType, value: *mut c_void, } diff --git a/extensions/series/Cargo.toml b/extensions/series/Cargo.toml new file mode 100644 index 000000000..73a634ac7 --- /dev/null +++ b/extensions/series/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "limbo_series" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + + +[dependencies] +limbo_ext = { path = "../core"} +log = "0.4.20" diff --git a/extensions/series/src/lib.rs b/extensions/series/src/lib.rs new file mode 100644 index 000000000..f438c6fce --- /dev/null +++ b/extensions/series/src/lib.rs @@ -0,0 +1,136 @@ +use limbo_ext::{ + register_extension, ExtensionApi, ResultCode, VTabCursor, VTabModule, VTabModuleDerive, Value, + ValueType, +}; + +register_extension! { + vtabs: { GenerateSeriesVTab } +} + +/// A virtual table that generates a sequence of integers +#[derive(Debug, VTabModuleDerive)] +struct GenerateSeriesVTab; + +impl VTabModule for GenerateSeriesVTab { + type VCursor = GenerateSeriesCursor; + fn name() -> &'static str { + "generate_series" + } + + fn connect(api: &ExtensionApi) -> ResultCode { + // Create table schema + let sql = "CREATE TABLE generate_series( + value INTEGER, + start INTEGER HIDDEN, + stop INTEGER HIDDEN, + step INTEGER HIDDEN + )"; + let name = Self::name(); + api.declare_virtual_table(name, sql) + } + + fn open() -> Self::VCursor { + GenerateSeriesCursor { + start: 0, + stop: 0, + step: 0, + current: 0, + } + } + + fn filter(cursor: &mut Self::VCursor, arg_count: i32, args: &[Value]) -> ResultCode { + // args are the start, stop, and step + if arg_count == 0 || arg_count > 3 { + return ResultCode::InvalidArgs; + } + let start = { + if args[0].value_type() == ValueType::Integer { + args[0].to_integer().unwrap() + } else { + return ResultCode::InvalidArgs; + } + }; + let stop = if args.len() == 1 { + i64::MAX + } else { + if args[1].value_type() == ValueType::Integer { + args[1].to_integer().unwrap() + } else { + return ResultCode::InvalidArgs; + } + }; + let step = if args.len() <= 2 { + 1 + } else { + if args[2].value_type() == ValueType::Integer { + args[2].to_integer().unwrap() + } else { + return ResultCode::InvalidArgs; + } + }; + cursor.start = start; + cursor.current = start; + cursor.stop = stop; + cursor.step = step; + ResultCode::OK + } + + fn column(cursor: &Self::VCursor, idx: u32) -> Value { + cursor.column(idx) + } + + fn next(cursor: &mut Self::VCursor) -> ResultCode { + GenerateSeriesCursor::next(cursor) + } + + fn eof(cursor: &Self::VCursor) -> bool { + cursor.eof() + } +} + +/// The cursor for iterating over the generated sequence +#[derive(Debug)] +struct GenerateSeriesCursor { + start: i64, + stop: i64, + step: i64, + current: i64, +} + +impl GenerateSeriesCursor { + fn next(&mut self) -> ResultCode { + let current = self.current; + + // Check if we've reached the end + if (self.step > 0 && current >= self.stop) || (self.step < 0 && current <= self.stop) { + return ResultCode::EOF; + } + + self.current = current.saturating_add(self.step); + ResultCode::OK + } +} + +impl VTabCursor for GenerateSeriesCursor { + fn next(&mut self) -> ResultCode { + GenerateSeriesCursor::next(self) + } + + fn eof(&self) -> bool { + (self.step > 0 && self.current > self.stop) || (self.step < 0 && self.current < self.stop) + } + + fn column(&self, idx: u32) -> Value { + match idx { + 0 => Value::from_integer(self.current), + 1 => Value::from_integer(self.start), + 2 => Value::from_integer(self.stop), + 3 => Value::from_integer(self.step), + _ => Value::null(), + } + } + + fn rowid(&self) -> i64 { + ((self.current - self.start) / self.step) + 1 + } +} diff --git a/macros/src/args.rs b/macros/src/args.rs index ec65be8b4..25454dfaf 100644 --- a/macros/src/args.rs +++ b/macros/src/args.rs @@ -6,13 +6,14 @@ use syn::{Ident, LitStr, Token}; pub(crate) struct RegisterExtensionInput { pub aggregates: Vec, pub scalars: Vec, + pub vtabs: Vec, } impl syn::parse::Parse for RegisterExtensionInput { fn parse(input: syn::parse::ParseStream) -> syn::Result { let mut aggregates = Vec::new(); let mut scalars = Vec::new(); - + let mut vtabs = Vec::new(); while !input.is_empty() { if input.peek(syn::Ident) && input.peek2(Token![:]) { let section_name: Ident = input.parse()?; @@ -28,11 +29,15 @@ impl syn::parse::Parse for RegisterExtensionInput { scalars = Punctuated::::parse_terminated(&content)? .into_iter() .collect(); + } else if section_name == "vtabs" { + vtabs = Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); } else { return Err(syn::Error::new(section_name.span(), "Unknown section")); } } else { - return Err(input.error("Expected aggregates: or scalars: section")); + return Err(input.error("Expected aggregates:, scalars:, or vtabs: section")); } if input.peek(Token![,]) { @@ -43,6 +48,7 @@ impl syn::parse::Parse for RegisterExtensionInput { Ok(Self { aggregates, scalars, + vtabs, }) } } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index ffb1e1524..92d1a39a5 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -324,6 +324,103 @@ pub fn derive_agg_func(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } +#[proc_macro_derive(VTabModuleDerive)] +pub fn derive_vtab_module(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let struct_name = &ast.ident; + + let register_fn_name = format_ident!("register_{}", struct_name); + let connect_fn_name = format_ident!("connect_{}", struct_name); + let open_fn_name = format_ident!("open_{}", struct_name); + let filter_fn_name = format_ident!("filter_{}", struct_name); + let column_fn_name = format_ident!("column_{}", struct_name); + let next_fn_name = format_ident!("next_{}", struct_name); + let eof_fn_name = format_ident!("eof_{}", struct_name); + + let expanded = quote! { + impl #struct_name { + #[no_mangle] + unsafe extern "C" fn #connect_fn_name( + db: *const ::std::ffi::c_void, + ) -> ::limbo_ext::ResultCode { + let api = unsafe { &*(db as *const ExtensionApi) }; + <#struct_name as ::limbo_ext::VTabModule>::connect(api) + } + + #[no_mangle] + unsafe extern "C" fn #open_fn_name( + ) -> *mut ::std::ffi::c_void { + let cursor = <#struct_name as ::limbo_ext::VTabModule>::open(); + Box::into_raw(Box::new(cursor)) as *mut ::std::ffi::c_void + } + + #[no_mangle] + unsafe extern "C" fn #filter_fn_name( + cursor: *mut ::std::ffi::c_void, + argc: i32, + argv: *const ::limbo_ext::Value, + ) -> ::limbo_ext::ResultCode { + let cursor = unsafe { &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor) }; + let args = std::slice::from_raw_parts(argv, argc as usize); + <#struct_name as ::limbo_ext::VTabModule>::filter(cursor, argc, args) + } + + #[no_mangle] + unsafe extern "C" fn #column_fn_name( + cursor: *mut ::std::ffi::c_void, + idx: u32, + ) -> ::limbo_ext::Value { + let cursor = unsafe { &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor) }; + <#struct_name as ::limbo_ext::VTabModule>::column(cursor, idx) + } + + #[no_mangle] + unsafe extern "C" fn #next_fn_name( + cursor: *mut ::std::ffi::c_void, + ) -> ::limbo_ext::ResultCode { + let cursor = unsafe { &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor) }; + <#struct_name as ::limbo_ext::VTabModule>::next(cursor) + } + + #[no_mangle] + unsafe extern "C" fn #eof_fn_name( + cursor: *mut ::std::ffi::c_void, + ) -> bool { + let cursor = unsafe { &mut *(cursor as *mut <#struct_name as ::limbo_ext::VTabModule>::VCursor) }; + <#struct_name as ::limbo_ext::VTabModule>::eof(cursor) + } + + #[no_mangle] + pub unsafe extern "C" fn #register_fn_name( + api: *const ::limbo_ext::ExtensionApi + ) -> ::limbo_ext::ResultCode { + if api.is_null() { + return ::limbo_ext::ResultCode::Error; + } + + let api = &*api; + let name = <#struct_name as ::limbo_ext::VTabModule>::name(); + // name needs to be a c str FFI compatible, NOT CString + let name_c = std::ffi::CString::new(name).unwrap(); + + let module = ::limbo_ext::VTabModuleImpl { + name: name_c.as_ptr(), + connect: Self::#connect_fn_name, + open: Self::#open_fn_name, + filter: Self::#filter_fn_name, + column: Self::#column_fn_name, + next: Self::#next_fn_name, + eof: Self::#eof_fn_name, + }; + + (api.register_module)(api.ctx, name_c.as_ptr(), module) + } + } + }; + + TokenStream::from(expanded) +} + /// Register your extension with 'core' by providing the relevant functions ///```ignore ///use limbo_ext::{register_extension, scalar, Value, AggregateDerive, AggFunc}; @@ -363,6 +460,7 @@ pub fn register_extension(input: TokenStream) -> TokenStream { let RegisterExtensionInput { aggregates, scalars, + vtabs, } = input_ast; let scalar_calls = scalars.iter().map(|scalar_ident| { @@ -390,6 +488,21 @@ pub fn register_extension(input: TokenStream) -> TokenStream { } }); + let vtab_calls = vtabs.iter().map(|vtab_ident| { + let register_fn = syn::Ident::new(&format!("register_{}", vtab_ident), vtab_ident.span()); + quote! { + { + let result = unsafe{ #vtab_ident::#register_fn(api)}; + if result == ::limbo_ext::ResultCode::OK { + let result = <#vtab_ident as ::limbo_ext::VTabModule>::connect(api); + return result; + } else { + return result; + } + } + } + }); + let expanded = quote! { #[no_mangle] pub extern "C" fn register_extension(api: &::limbo_ext::ExtensionApi) -> ::limbo_ext::ResultCode { @@ -398,6 +511,8 @@ pub fn register_extension(input: TokenStream) -> TokenStream { #(#aggregate_calls)* + #(#vtab_calls)* + ::limbo_ext::ResultCode::OK } };