Files
turso/core/vtab.rs
Piotr Rzysko 61234eeb19 Add ResultCode to best_index result
The `best_index` implementation now returns a ResultCode along with the
IndexInfo. This allows it to signal specific outcomes, such as errors or
constraint violations. This change aligns better with SQLite’s xBestIndex
contract, where cases like missing constraints or invalid combinations of
constraints must not result in a valid plan.
2025-08-04 20:18:44 +02:00

382 lines
12 KiB
Rust

use crate::pragma::{PragmaVirtualTable, PragmaVirtualTableCursor};
use crate::schema::Column;
use crate::util::columns_from_create_table_body;
use crate::{Connection, LimboError, SymbolTable, Value};
use fallible_iterator::FallibleIterator;
use std::cell::RefCell;
use std::ffi::c_void;
use std::rc::Rc;
use std::sync::Arc;
use turso_ext::{ConstraintInfo, IndexInfo, OrderByInfo, ResultCode, VTabKind, VTabModuleImpl};
use turso_sqlite3_parser::{ast, lexer::sql::Parser};
#[derive(Debug, Clone)]
pub(crate) enum VirtualTableType {
Pragma(PragmaVirtualTable),
External(ExtVirtualTable),
}
#[derive(Clone, Debug)]
pub struct VirtualTable {
pub(crate) name: String,
pub(crate) columns: Vec<Column>,
pub(crate) kind: VTabKind,
vtab_type: VirtualTableType,
}
impl VirtualTable {
pub(crate) fn readonly(self: &Arc<VirtualTable>) -> bool {
match &self.vtab_type {
VirtualTableType::Pragma(_) => true,
VirtualTableType::External(table) => table.readonly(),
}
}
pub(crate) fn builtin_functions() -> Vec<Arc<VirtualTable>> {
PragmaVirtualTable::functions()
.into_iter()
.map(|(tab, schema)| {
let vtab = VirtualTable {
name: format!("pragma_{}", tab.pragma_name),
columns: Self::resolve_columns(schema)
.expect("built-in function schema resolution should not fail"),
kind: VTabKind::TableValuedFunction,
vtab_type: VirtualTableType::Pragma(tab),
};
Arc::new(vtab)
})
.collect()
}
pub(crate) fn function(name: &str, syms: &SymbolTable) -> crate::Result<Arc<VirtualTable>> {
let module = syms.vtab_modules.get(name);
let (vtab_type, schema) = if module.is_some() {
ExtVirtualTable::create(name, module, Vec::new(), VTabKind::TableValuedFunction)
.map(|(vtab, columns)| (VirtualTableType::External(vtab), columns))?
} else {
return Err(LimboError::ParseError(format!(
"No such table-valued function: {name}"
)));
};
let vtab = VirtualTable {
name: name.to_owned(),
columns: Self::resolve_columns(schema)?,
kind: VTabKind::TableValuedFunction,
vtab_type,
};
Ok(Arc::new(vtab))
}
pub fn table(
tbl_name: Option<&str>,
module_name: &str,
args: Vec<turso_ext::Value>,
syms: &SymbolTable,
) -> crate::Result<Arc<VirtualTable>> {
let module = syms.vtab_modules.get(module_name);
let (table, schema) =
ExtVirtualTable::create(module_name, module, args, VTabKind::VirtualTable)?;
let vtab = VirtualTable {
name: tbl_name.unwrap_or(module_name).to_owned(),
columns: Self::resolve_columns(schema)?,
kind: VTabKind::VirtualTable,
vtab_type: VirtualTableType::External(table),
};
Ok(Arc::new(vtab))
}
fn resolve_columns(schema: String) -> crate::Result<Vec<Column>> {
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()),
)? {
columns_from_create_table_body(&body)
} else {
Err(LimboError::ParseError(
"Failed to parse schema from virtual table module".to_string(),
))
}
}
pub(crate) fn open(&self, conn: Arc<Connection>) -> crate::Result<VirtualTableCursor> {
match &self.vtab_type {
VirtualTableType::Pragma(table) => {
Ok(VirtualTableCursor::Pragma(Box::new(table.open(conn)?)))
}
VirtualTableType::External(table) => {
Ok(VirtualTableCursor::External(table.open(conn)?))
}
}
}
pub(crate) fn update(&self, args: &[Value]) -> crate::Result<Option<i64>> {
match &self.vtab_type {
VirtualTableType::Pragma(_) => Err(LimboError::ReadOnly),
VirtualTableType::External(table) => table.update(args),
}
}
pub(crate) fn destroy(&self) -> crate::Result<()> {
match &self.vtab_type {
VirtualTableType::Pragma(_) => Ok(()),
VirtualTableType::External(table) => table.destroy(),
}
}
pub(crate) fn best_index(
&self,
constraints: &[ConstraintInfo],
order_by: &[OrderByInfo],
) -> Result<IndexInfo, ResultCode> {
match &self.vtab_type {
VirtualTableType::Pragma(table) => table.best_index(constraints),
VirtualTableType::External(table) => table.best_index(constraints, order_by),
}
}
}
pub enum VirtualTableCursor {
Pragma(Box<PragmaVirtualTableCursor>),
External(ExtVirtualTableCursor),
}
impl VirtualTableCursor {
pub(crate) fn next(&mut self) -> crate::Result<bool> {
match self {
VirtualTableCursor::Pragma(cursor) => cursor.next(),
VirtualTableCursor::External(cursor) => cursor.next(),
}
}
pub(crate) fn rowid(&self) -> i64 {
match self {
VirtualTableCursor::Pragma(cursor) => cursor.rowid(),
VirtualTableCursor::External(cursor) => cursor.rowid(),
}
}
pub(crate) fn column(&self, column: usize) -> crate::Result<Value> {
match self {
VirtualTableCursor::Pragma(cursor) => cursor.column(column),
VirtualTableCursor::External(cursor) => cursor.column(column),
}
}
pub(crate) fn filter(
&mut self,
idx_num: i32,
idx_str: Option<String>,
arg_count: usize,
args: Vec<Value>,
) -> crate::Result<bool> {
match self {
VirtualTableCursor::Pragma(cursor) => cursor.filter(args),
VirtualTableCursor::External(cursor) => {
cursor.filter(idx_num, idx_str, arg_count, args)
}
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct ExtVirtualTable {
implementation: Rc<VTabModuleImpl>,
table_ptr: *const c_void,
connection_ptr: RefCell<Option<*mut turso_ext::Conn>>,
}
impl Drop for ExtVirtualTable {
fn drop(&mut self) {
if let Some(conn) = self.connection_ptr.borrow_mut().take() {
if conn.is_null() {
return;
}
// free the memory for the turso_ext::Conn itself
let mut conn = unsafe { Box::from_raw(conn) };
// frees the boxed Weak pointer
conn.close();
}
*self.connection_ptr.borrow_mut() = None;
}
}
impl ExtVirtualTable {
pub(crate) fn readonly(&self) -> bool {
self.implementation.readonly
}
fn best_index(
&self,
constraints: &[ConstraintInfo],
order_by: &[OrderByInfo],
) -> Result<IndexInfo, ResultCode> {
unsafe {
IndexInfo::from_ffi((self.implementation.best_idx)(
constraints.as_ptr(),
constraints.len() as i32,
order_by.as_ptr(),
order_by.len() as i32,
))
}
}
/// takes ownership of the provided Args
fn create(
module_name: &str,
module: Option<&Rc<crate::ext::VTabImpl>>,
args: Vec<turso_ext::Value>,
kind: VTabKind,
) -> crate::Result<(Self, String)> {
let module = module.ok_or(LimboError::ExtensionError(format!(
"Virtual table module not found: {module_name}"
)))?;
if kind != module.module_kind {
let expected = match kind {
VTabKind::VirtualTable => "virtual table",
VTabKind::TableValuedFunction => "table-valued function",
};
return Err(LimboError::ExtensionError(format!(
"{module_name} is not a {expected} module"
)));
}
let (schema, table_ptr) = module.implementation.create(args)?;
let vtab = ExtVirtualTable {
connection_ptr: RefCell::new(None),
implementation: module.implementation.clone(),
table_ptr,
};
Ok((vtab, schema))
}
/// Accepts a pointer connection that owns the VTable, that the module
/// can optionally use to query the other tables.
fn open(&self, conn: Arc<Connection>) -> crate::Result<ExtVirtualTableCursor> {
// we need a Weak<Connection> to upgrade and call from the extension.
let weak_box: *mut Arc<Connection> = Box::into_raw(Box::new(conn));
let conn = turso_ext::Conn::new(
weak_box.cast(),
crate::ext::prepare_stmt,
crate::ext::execute,
crate::ext::close,
);
let ext_conn_ptr = Box::into_raw(Box::new(conn));
// store the leaked connection pointer on the table so it can be freed on drop
*self.connection_ptr.borrow_mut() = Some(ext_conn_ptr);
let cursor = unsafe { (self.implementation.open)(self.table_ptr, ext_conn_ptr) };
ExtVirtualTableCursor::new(cursor, self.implementation.clone())
}
fn update(&self, args: &[Value]) -> crate::Result<Option<i64>> {
let arg_count = args.len();
let ext_args = args.iter().map(|arg| arg.to_ffi()).collect::<Vec<_>>();
let newrowid = 0i64;
let rc = unsafe {
(self.implementation.update)(
self.table_ptr,
arg_count as i32,
ext_args.as_ptr(),
&newrowid as *const _ as *mut i64,
)
};
for arg in ext_args {
unsafe {
arg.__free_internal_type();
}
}
match rc {
ResultCode::OK => Ok(None),
ResultCode::RowID => Ok(Some(newrowid)),
_ => Err(LimboError::ExtensionError(rc.to_string())),
}
}
fn destroy(&self) -> crate::Result<()> {
let rc = unsafe { (self.implementation.destroy)(self.table_ptr) };
match rc {
ResultCode::OK => Ok(()),
_ => Err(LimboError::ExtensionError(rc.to_string())),
}
}
}
pub struct ExtVirtualTableCursor {
cursor: *const c_void,
implementation: Rc<VTabModuleImpl>,
}
impl ExtVirtualTableCursor {
fn new(cursor: *const c_void, implementation: Rc<VTabModuleImpl>) -> crate::Result<Self> {
if cursor.is_null() {
return Err(LimboError::InternalError(
"VirtualTableCursor: cursor is null".into(),
));
}
Ok(Self {
cursor,
implementation,
})
}
fn rowid(&self) -> i64 {
unsafe { (self.implementation.rowid)(self.cursor) }
}
#[tracing::instrument(skip(self))]
fn filter(
&self,
idx_num: i32,
idx_str: Option<String>,
arg_count: usize,
args: Vec<Value>,
) -> crate::Result<bool> {
tracing::trace!("xFilter");
let ext_args = args.iter().map(|arg| arg.to_ffi()).collect::<Vec<_>>();
let c_idx_str = idx_str
.map(|s| std::ffi::CString::new(s).unwrap())
.map(|cstr| cstr.into_raw())
.unwrap_or(std::ptr::null_mut());
let rc = unsafe {
(self.implementation.filter)(
self.cursor,
arg_count as i32,
ext_args.as_ptr(),
c_idx_str,
idx_num,
)
};
for arg in ext_args {
unsafe {
arg.__free_internal_type();
}
}
match rc {
ResultCode::OK => Ok(true),
ResultCode::EOF => Ok(false),
_ => Err(LimboError::ExtensionError(rc.to_string())),
}
}
fn column(&self, column: usize) -> crate::Result<Value> {
let val = unsafe { (self.implementation.column)(self.cursor, column as u32) };
Value::from_ffi(val)
}
fn next(&self) -> crate::Result<bool> {
let rc = unsafe { (self.implementation.next)(self.cursor) };
match rc {
ResultCode::OK => Ok(true),
ResultCode::EOF => Ok(false),
_ => Err(LimboError::ExtensionError("Next failed".to_string())),
}
}
}
impl Drop for ExtVirtualTableCursor {
fn drop(&mut self) {
let result = unsafe { (self.implementation.close)(self.cursor) };
if !result.is_ok() {
tracing::error!("Failed to close virtual table cursor");
}
}
}