Skip to main content

clt_database/
vtab.rs

1use crate::pragma::{PragmaVirtualTable, PragmaVirtualTableCursor};
2use crate::schema::Column;
3use crate::sync::atomic::{AtomicPtr, AtomicU64, Ordering};
4use crate::sync::{Arc, RwLock, Weak};
5use crate::util::columns_from_create_table_body;
6use crate::{Connection, LimboError, SymbolTable, Value};
7use std::ffi::c_void;
8use std::ptr::NonNull;
9use turso_ext::{ConstraintInfo, IndexInfo, OrderByInfo, ResultCode, VTabKind, VTabModuleImpl};
10use turso_parser::{ast, parser::Parser};
11
12#[derive(Debug, Clone)]
13pub(crate) enum VirtualTableType {
14    Pragma(PragmaVirtualTable),
15    External(ExtVirtualTable),
16    Internal(Arc<RwLock<dyn InternalVirtualTable>>),
17}
18
19#[derive(Clone, Debug)]
20pub struct VirtualTable {
21    pub(crate) name: String,
22    pub(crate) columns: Vec<Column>,
23    pub(crate) kind: VTabKind,
24    pub(crate) vtab_type: VirtualTableType,
25    // identifier to tie a cursor to a specific instantiated virtual table instance
26    pub(crate) vtab_id: u64,
27    // Whether this virtual table is safe to use from within triggers and views.
28    // Corresponds to SQLite's SQLITE_VTAB_INNOCUOUS flag.
29    pub(crate) innocuous: bool,
30}
31
32impl VirtualTable {
33    pub(crate) fn id(&self) -> u64 {
34        self.vtab_id
35    }
36    pub(crate) fn readonly(&self) -> bool {
37        match &self.vtab_type {
38            VirtualTableType::Pragma(_) => true,
39            VirtualTableType::External(table) => table.readonly(),
40            VirtualTableType::Internal(_) => true,
41        }
42    }
43
44    /// Wrap an `InternalVirtualTable` implementation so it can appear in a
45    /// `Schema`'s catalog. The table's `name()` becomes the catalog name and
46    /// its `sql()` is parsed to derive the column metadata. Returns an error
47    /// if the SQL string is not a valid `CREATE TABLE` statement.
48    pub(crate) fn wrap_internal_table<T>(table: T) -> crate::Result<Arc<VirtualTable>>
49    where
50        T: InternalVirtualTable + 'static,
51    {
52        let name = table.name();
53        let sql = table.sql();
54        let columns = Self::resolve_columns(sql)?;
55        Ok(Arc::new(VirtualTable {
56            name,
57            columns,
58            kind: VTabKind::TableValuedFunction,
59            vtab_type: VirtualTableType::Internal(Arc::new(RwLock::new(table))),
60            vtab_id: 0,
61            innocuous: true,
62        }))
63    }
64
65    pub(crate) fn function(name: &str, syms: &SymbolTable) -> crate::Result<Arc<VirtualTable>> {
66        let module = syms.vtab_modules.get(name);
67        let (vtab_type, schema) = if module.is_some() {
68            ExtVirtualTable::create(name, module, Vec::new(), VTabKind::TableValuedFunction)
69                .map(|(vtab, columns)| (VirtualTableType::External(vtab), columns))?
70        } else {
71            return Err(LimboError::ParseError(format!(
72                "No such table-valued function: {name}"
73            )));
74        };
75
76        let vtab = VirtualTable {
77            name: name.to_owned(),
78            columns: Self::resolve_columns(schema)?,
79            kind: VTabKind::TableValuedFunction,
80            vtab_type,
81            vtab_id: 0,
82            innocuous: false,
83        };
84        Ok(Arc::new(vtab))
85    }
86
87    pub fn table(
88        tbl_name: Option<&str>,
89        module_name: &str,
90        args: Vec<turso_ext::Value>,
91        syms: &SymbolTable,
92    ) -> crate::Result<Arc<VirtualTable>> {
93        let module = syms.vtab_modules.get(module_name);
94        let (table, schema) =
95            ExtVirtualTable::create(module_name, module, args, VTabKind::VirtualTable)?;
96        let vtab = VirtualTable {
97            name: tbl_name.unwrap_or(module_name).to_owned(),
98            columns: Self::resolve_columns(schema)?,
99            kind: VTabKind::VirtualTable,
100            vtab_type: VirtualTableType::External(table),
101            vtab_id: VTAB_ID_COUNTER.fetch_add(1, Ordering::Acquire),
102            innocuous: false,
103        };
104        Ok(Arc::new(vtab))
105    }
106
107    pub(crate) fn resolve_columns(schema: String) -> crate::Result<Vec<Column>> {
108        let mut parser = Parser::new(schema.as_bytes());
109        if let ast::Cmd::Stmt(ast::Stmt::CreateTable { body, .. }) =
110            parser.next_cmd()?.ok_or_else(|| {
111                LimboError::ParseError(
112                    "Failed to parse schema from virtual table module".to_string(),
113                )
114            })?
115        {
116            columns_from_create_table_body(&body)
117        } else {
118            Err(LimboError::ParseError(
119                "Failed to parse schema from virtual table module".to_string(),
120            ))
121        }
122    }
123
124    pub(crate) fn open(&self, conn: Arc<Connection>) -> crate::Result<VirtualTableCursor> {
125        match &self.vtab_type {
126            VirtualTableType::Pragma(table) => {
127                Ok(VirtualTableCursor::new_pragma(table.open(conn)?))
128            }
129            VirtualTableType::External(table) => Ok(VirtualTableCursor::new_external(
130                table.open(conn, self.vtab_id)?,
131            )),
132            VirtualTableType::Internal(table) => {
133                Ok(VirtualTableCursor::new_internal(table.read().open(conn)?))
134            }
135        }
136    }
137
138    pub(crate) fn update(&self, args: &[Value]) -> crate::Result<Option<i64>> {
139        match &self.vtab_type {
140            VirtualTableType::Pragma(_) => Err(LimboError::ReadOnly),
141            VirtualTableType::External(table) => table.update(args),
142            VirtualTableType::Internal(_) => Err(LimboError::ReadOnly),
143        }
144    }
145
146    pub(crate) fn destroy(&self) -> crate::Result<()> {
147        match &self.vtab_type {
148            VirtualTableType::Pragma(_) => Ok(()),
149            VirtualTableType::External(table) => table.destroy(),
150            VirtualTableType::Internal(_) => Ok(()),
151        }
152    }
153
154    pub(crate) fn best_index(
155        &self,
156        constraints: &[ConstraintInfo],
157        order_by: &[OrderByInfo],
158    ) -> Result<IndexInfo, ResultCode> {
159        match &self.vtab_type {
160            VirtualTableType::Pragma(table) => table.best_index(constraints),
161            VirtualTableType::External(table) => table.best_index(constraints, order_by),
162            VirtualTableType::Internal(table) => table.read().best_index(constraints, order_by),
163        }
164    }
165
166    pub(crate) fn begin(&self) -> crate::Result<()> {
167        match &self.vtab_type {
168            VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError(
169                "Pragma virtual tables do not support transactions".to_string(),
170            )),
171            VirtualTableType::External(table) => table.begin(),
172            VirtualTableType::Internal(_) => Err(LimboError::ExtensionError(
173                "Internal virtual tables currently do not support transactions".to_string(),
174            )),
175        }
176    }
177
178    pub(crate) fn commit(&self) -> crate::Result<()> {
179        match &self.vtab_type {
180            VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError(
181                "Pragma virtual tables do not support transactions".to_string(),
182            )),
183            VirtualTableType::External(table) => table.commit(),
184            VirtualTableType::Internal(_) => Err(LimboError::ExtensionError(
185                "Internal virtual tables currently do not support transactions".to_string(),
186            )),
187        }
188    }
189
190    pub(crate) fn rollback(&self) -> crate::Result<()> {
191        match &self.vtab_type {
192            VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError(
193                "Pragma virtual tables do not support transactions".to_string(),
194            )),
195            VirtualTableType::External(table) => table.rollback(),
196            VirtualTableType::Internal(_) => Err(LimboError::ExtensionError(
197                "Internal virtual tables currently do not support transactions".to_string(),
198            )),
199        }
200    }
201
202    pub(crate) fn rename(&self, new_name: &str) -> crate::Result<()> {
203        match &self.vtab_type {
204            VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError(
205                "Pragma virtual tables do not support renaming".to_string(),
206            )),
207            VirtualTableType::External(table) => table.rename(new_name),
208            VirtualTableType::Internal(_) => Err(LimboError::ExtensionError(
209                "Internal virtual tables currently do not support renaming".to_string(),
210            )),
211        }
212    }
213}
214
215enum VirtualTableCursorInner {
216    Pragma(Box<PragmaVirtualTableCursor>),
217    External(ExtVirtualTableCursor),
218    Internal(Arc<RwLock<dyn InternalVirtualTableCursor>>),
219}
220
221pub struct VirtualTableCursor {
222    inner: VirtualTableCursorInner,
223    null_flag: bool,
224}
225
226crate::assert::assert_send_sync!(VirtualTableCursor);
227
228impl VirtualTableCursor {
229    pub(crate) fn new_pragma(cursor: PragmaVirtualTableCursor) -> Self {
230        Self {
231            inner: VirtualTableCursorInner::Pragma(Box::new(cursor)),
232            null_flag: false,
233        }
234    }
235
236    pub(crate) fn new_external(cursor: ExtVirtualTableCursor) -> Self {
237        Self {
238            inner: VirtualTableCursorInner::External(cursor),
239            null_flag: false,
240        }
241    }
242
243    pub(crate) fn new_internal(cursor: Arc<RwLock<dyn InternalVirtualTableCursor>>) -> Self {
244        Self {
245            inner: VirtualTableCursorInner::Internal(cursor),
246            null_flag: false,
247        }
248    }
249
250    pub(crate) fn set_null_flag(&mut self, flag: bool) {
251        self.null_flag = flag;
252    }
253
254    pub(crate) fn next(&mut self) -> crate::Result<bool> {
255        self.null_flag = false;
256        match &mut self.inner {
257            VirtualTableCursorInner::Pragma(cursor) => cursor.next(),
258            VirtualTableCursorInner::External(cursor) => cursor.next(),
259            VirtualTableCursorInner::Internal(cursor) => cursor.write().next(),
260        }
261    }
262
263    pub(crate) fn rowid(&self) -> i64 {
264        match &self.inner {
265            VirtualTableCursorInner::Pragma(cursor) => cursor.rowid(),
266            VirtualTableCursorInner::External(cursor) => cursor.rowid(),
267            VirtualTableCursorInner::Internal(cursor) => cursor.read().rowid(),
268        }
269    }
270
271    pub(crate) fn column(&self, column: usize) -> crate::Result<Value> {
272        if self.null_flag {
273            return Ok(Value::Null);
274        }
275        match &self.inner {
276            VirtualTableCursorInner::Pragma(cursor) => cursor.column(column),
277            VirtualTableCursorInner::External(cursor) => cursor.column(column),
278            VirtualTableCursorInner::Internal(cursor) => cursor.read().column(column),
279        }
280    }
281
282    pub(crate) fn filter(
283        &mut self,
284        idx_num: i32,
285        idx_str: Option<String>,
286        arg_count: usize,
287        args: Vec<Value>,
288    ) -> crate::Result<bool> {
289        self.null_flag = false;
290        match &mut self.inner {
291            VirtualTableCursorInner::Pragma(cursor) => cursor.filter(args),
292            VirtualTableCursorInner::External(cursor) => {
293                cursor.filter(idx_num, idx_str, arg_count, args)
294            }
295            VirtualTableCursorInner::Internal(cursor) => {
296                cursor.write().filter(&args, idx_str, idx_num)
297            }
298        }
299    }
300
301    pub(crate) fn vtab_id(&self) -> Option<u64> {
302        match &self.inner {
303            VirtualTableCursorInner::Pragma(_) => None,
304            VirtualTableCursorInner::External(cursor) => cursor.vtab_id.into(),
305            VirtualTableCursorInner::Internal(_) => None,
306        }
307    }
308}
309
310#[derive(Debug)]
311pub(crate) struct ExtVirtualTable {
312    implementation: Arc<VTabModuleImpl>,
313    table_ptr: AtomicPtr<c_void>,
314}
315static VTAB_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
316
317impl Clone for ExtVirtualTable {
318    fn clone(&self) -> Self {
319        Self {
320            implementation: self.implementation.clone(),
321            table_ptr: AtomicPtr::new(self.table_ptr.load(Ordering::SeqCst)),
322        }
323    }
324}
325
326impl ExtVirtualTable {
327    pub(crate) fn readonly(&self) -> bool {
328        self.implementation.readonly
329    }
330    fn best_index(
331        &self,
332        constraints: &[ConstraintInfo],
333        order_by: &[OrderByInfo],
334    ) -> Result<IndexInfo, ResultCode> {
335        unsafe {
336            IndexInfo::from_ffi((self.implementation.best_idx)(
337                constraints.as_ptr(),
338                constraints.len() as i32,
339                order_by.as_ptr(),
340                order_by.len() as i32,
341            ))
342        }
343    }
344
345    /// takes ownership of the provided Args
346    fn create(
347        module_name: &str,
348        module: Option<&Arc<crate::ext::VTabImpl>>,
349        args: Vec<turso_ext::Value>,
350        kind: VTabKind,
351    ) -> crate::Result<(Self, String)> {
352        let module = module.ok_or_else(|| {
353            LimboError::ExtensionError(format!("Virtual table module not found: {module_name}"))
354        })?;
355        if kind != module.module_kind {
356            let expected = match kind {
357                VTabKind::VirtualTable => "virtual table",
358                VTabKind::TableValuedFunction => "table-valued function",
359            };
360            return Err(LimboError::ExtensionError(format!(
361                "{module_name} is not a {expected} module"
362            )));
363        }
364        let (schema, table_ptr) = module.implementation.create(args)?;
365        let vtab = ExtVirtualTable {
366            implementation: module.implementation.clone(),
367            table_ptr: AtomicPtr::new(table_ptr as *mut c_void),
368        };
369        Ok((vtab, schema))
370    }
371
372    /// Accepts a pointer connection that owns the VTable, that the module
373    /// can optionally use to query the other tables.
374    fn open(&self, conn: Arc<Connection>, id: u64) -> crate::Result<ExtVirtualTableCursor> {
375        // we need a Weak<Connection> to upgrade and call from the extension.
376        let weak = Arc::downgrade(&conn);
377        let weak_box = Box::into_raw(Box::new(weak));
378        let conn = turso_ext::Conn::new(
379            weak_box as *mut c_void,
380            crate::ext::prepare_stmt,
381            crate::ext::execute,
382        );
383        let ext_conn_ptr = NonNull::new(Box::into_raw(Box::new(conn))).expect("null pointer");
384        // store the leaked connection pointer on the table so it can be freed on drop
385        let Some(cursor) = NonNull::new(unsafe {
386            (self.implementation.open)(
387                self.table_ptr.load(Ordering::SeqCst) as *const c_void,
388                ext_conn_ptr.as_ptr(),
389            ) as *mut c_void
390        }) else {
391            return Err(LimboError::ExtensionError("Open returned null".to_string()));
392        };
393        ExtVirtualTableCursor::new(cursor, ext_conn_ptr, self.implementation.clone(), id)
394    }
395
396    fn update(&self, args: &[Value]) -> crate::Result<Option<i64>> {
397        let arg_count = args.len();
398        let ext_args = args.iter().map(|arg| arg.to_ffi()).collect::<Vec<_>>();
399        let newrowid = 0i64;
400        let rc = unsafe {
401            (self.implementation.update)(
402                self.table_ptr.load(Ordering::SeqCst) as *const c_void,
403                arg_count as i32,
404                ext_args.as_ptr(),
405                &newrowid as *const _ as *mut i64,
406            )
407        };
408        for arg in ext_args {
409            unsafe {
410                arg.__free_internal_type();
411            }
412        }
413        match rc {
414            ResultCode::OK => Ok(None),
415            ResultCode::RowID => Ok(Some(newrowid)),
416            _ => Err(LimboError::ExtensionError(rc.to_string())),
417        }
418    }
419
420    fn destroy(&self) -> crate::Result<()> {
421        let rc = unsafe {
422            (self.implementation.destroy)(self.table_ptr.load(Ordering::SeqCst) as *const c_void)
423        };
424        match rc {
425            ResultCode::OK => Ok(()),
426            _ => Err(LimboError::ExtensionError(rc.to_string())),
427        }
428    }
429
430    fn commit(&self) -> crate::Result<()> {
431        let rc = unsafe { (self.implementation.commit)(self.table_ptr.load(Ordering::SeqCst)) };
432        match rc {
433            ResultCode::OK => Ok(()),
434            _ => Err(LimboError::ExtensionError("Commit failed".to_string())),
435        }
436    }
437
438    fn begin(&self) -> crate::Result<()> {
439        let rc = unsafe { (self.implementation.begin)(self.table_ptr.load(Ordering::SeqCst)) };
440        match rc {
441            ResultCode::OK => Ok(()),
442            _ => Err(LimboError::ExtensionError("Begin failed".to_string())),
443        }
444    }
445
446    fn rollback(&self) -> crate::Result<()> {
447        let rc = unsafe { (self.implementation.rollback)(self.table_ptr.load(Ordering::SeqCst)) };
448        match rc {
449            ResultCode::OK => Ok(()),
450            _ => Err(LimboError::ExtensionError("Rollback failed".to_string())),
451        }
452    }
453
454    fn rename(&self, new_name: &str) -> crate::Result<()> {
455        let c_new_name = std::ffi::CString::new(new_name).unwrap();
456        let rc = unsafe {
457            (self.implementation.rename)(self.table_ptr.load(Ordering::SeqCst), c_new_name.as_ptr())
458        };
459        match rc {
460            ResultCode::OK => Ok(()),
461            _ => Err(LimboError::ExtensionError("Rename failed".to_string())),
462        }
463    }
464}
465
466pub struct ExtVirtualTableCursor {
467    cursor: NonNull<c_void>,
468    // the core `[Connection]` pointer the vtab module needs to
469    // query other internal tables.
470    conn_ptr: Option<NonNull<turso_ext::Conn>>,
471    implementation: Arc<VTabModuleImpl>,
472    vtab_id: u64,
473}
474
475// SAFETY: Extension provider must guarantee Send + Sync on their side
476// we cannot properly infer Send + Sync for dynamic libraries
477unsafe impl Send for ExtVirtualTableCursor {}
478unsafe impl Sync for ExtVirtualTableCursor {}
479crate::assert::assert_send_sync!(ExtVirtualTableCursor);
480
481impl ExtVirtualTableCursor {
482    fn new(
483        cursor: NonNull<c_void>,
484        conn_ptr: NonNull<turso_ext::Conn>,
485        implementation: Arc<VTabModuleImpl>,
486        id: u64,
487    ) -> crate::Result<Self> {
488        Ok(Self {
489            cursor,
490            conn_ptr: Some(conn_ptr),
491            implementation,
492            vtab_id: id,
493        })
494    }
495
496    fn rowid(&self) -> i64 {
497        unsafe { (self.implementation.rowid)(self.cursor.as_ptr()) }
498    }
499
500    #[tracing::instrument(skip(self))]
501    fn filter(
502        &self,
503        idx_num: i32,
504        idx_str: Option<String>,
505        arg_count: usize,
506        args: Vec<Value>,
507    ) -> crate::Result<bool> {
508        tracing::trace!("xFilter");
509        let ext_args = args.iter().map(|arg| arg.to_ffi()).collect::<Vec<_>>();
510        let idx_str = match idx_str {
511            Some(idx_str) => Some(std::ffi::CString::new(idx_str).map_err(|e| {
512                crate::LimboError::InternalError(format!("failed to convert idx_str string: {e}"))
513            })?),
514            None => None,
515        };
516        let c_idx_str_ptr = idx_str
517            .as_ref()
518            .map(|s| s.as_ptr())
519            .unwrap_or(std::ptr::null_mut());
520        let rc = unsafe {
521            (self.implementation.filter)(
522                self.cursor.as_ptr(),
523                arg_count as i32,
524                ext_args.as_ptr(),
525                c_idx_str_ptr,
526                idx_num,
527            )
528        };
529        for arg in ext_args {
530            unsafe {
531                arg.__free_internal_type();
532            }
533        }
534        match rc {
535            ResultCode::OK => Ok(true),
536            ResultCode::EOF => Ok(false),
537            _ => Err(LimboError::ExtensionError(rc.to_string())),
538        }
539    }
540
541    fn column(&self, column: usize) -> crate::Result<Value> {
542        let val = unsafe { (self.implementation.column)(self.cursor.as_ptr(), column as u32) };
543        Value::from_ffi(val)
544    }
545
546    fn next(&self) -> crate::Result<bool> {
547        let rc = unsafe { (self.implementation.next)(self.cursor.as_ptr()) };
548        match rc {
549            ResultCode::OK => Ok(true),
550            ResultCode::EOF => Ok(false),
551            _ => Err(LimboError::ExtensionError("Next failed".to_string())),
552        }
553    }
554}
555
556impl Drop for ExtVirtualTableCursor {
557    fn drop(&mut self) {
558        if let Some(ptr) = self.conn_ptr.take() {
559            // first free the boxed turso_ext::Conn pointer itself
560            let conn = unsafe { Box::from_raw(ptr.as_ptr()) };
561            if !conn._ctx.is_null() {
562                // we also leaked the Weak 'ctx' pointer, so free this as well
563                let _ = unsafe { Box::from_raw(conn._ctx as *mut Weak<Connection>) };
564            }
565        }
566        let result = unsafe { (self.implementation.close)(self.cursor.as_ptr()) };
567        if !result.is_ok() {
568            tracing::error!("Failed to close virtual table cursor");
569        }
570    }
571}
572
573pub trait InternalVirtualTable: std::fmt::Debug + Send + Sync {
574    fn name(&self) -> String;
575    fn open(
576        &self,
577        conn: Arc<Connection>,
578    ) -> crate::Result<Arc<RwLock<dyn InternalVirtualTableCursor>>>;
579    /// best_index is used by the optimizer. See the comment on `Table::best_index`.
580    fn best_index(
581        &self,
582        constraints: &[turso_ext::ConstraintInfo],
583        order_by: &[turso_ext::OrderByInfo],
584    ) -> Result<turso_ext::IndexInfo, ResultCode>;
585    fn sql(&self) -> String;
586}
587
588pub trait InternalVirtualTableCursor: Send + Sync {
589    /// next returns `Ok(true)` if there are more rows, and `Ok(false)` otherwise.
590    fn next(&mut self) -> Result<bool, LimboError>;
591    fn rowid(&self) -> i64;
592    fn column(&self, column: usize) -> Result<Value, LimboError>;
593    fn filter(
594        &mut self,
595        args: &[Value],
596        idx_str: Option<String>,
597        idx_num: i32,
598    ) -> Result<bool, LimboError>;
599}
600
601#[cfg(all(clt_turso_tests, clt_turso_feature = "fs"))]
602mod tests {
603    use super::*;
604    use crate::{Database, DatabaseOpts, MemoryIO, OpenFlags};
605
606    /// Minimal `InternalVirtualTable` that exposes a fixed two-row table. Used
607    /// to verify that callers can register an arbitrary catalog table at
608    /// database open time and query it like any other table.
609    #[derive(Debug)]
610    struct StaticTable {
611        name: &'static str,
612    }
613
614    impl InternalVirtualTable for StaticTable {
615        fn name(&self) -> String {
616            self.name.to_string()
617        }
618        fn sql(&self) -> String {
619            format!("CREATE TABLE {}(key TEXT, value INTEGER)", self.name)
620        }
621        fn open(
622            &self,
623            _conn: Arc<Connection>,
624        ) -> crate::Result<Arc<RwLock<dyn InternalVirtualTableCursor>>> {
625            Ok(Arc::new(RwLock::new(StaticCursor {
626                rows: vec![("alpha".to_string(), 1), ("beta".to_string(), 2)],
627                position: -1,
628            })))
629        }
630        fn best_index(
631            &self,
632            constraints: &[turso_ext::ConstraintInfo],
633            _order_by: &[turso_ext::OrderByInfo],
634        ) -> std::result::Result<turso_ext::IndexInfo, ResultCode> {
635            Ok(turso_ext::IndexInfo {
636                idx_num: 0,
637                idx_str: None,
638                order_by_consumed: false,
639                estimated_cost: 1.0,
640                estimated_rows: 2,
641                constraint_usages: constraints
642                    .iter()
643                    .map(|_| turso_ext::ConstraintUsage {
644                        argv_index: None,
645                        omit: false,
646                    })
647                    .collect(),
648            })
649        }
650    }
651
652    struct StaticCursor {
653        rows: Vec<(String, i64)>,
654        position: i64,
655    }
656
657    impl InternalVirtualTableCursor for StaticCursor {
658        fn next(&mut self) -> Result<bool, LimboError> {
659            self.position += 1;
660            Ok((self.position as usize) < self.rows.len())
661        }
662        fn rowid(&self) -> i64 {
663            self.position
664        }
665        fn column(&self, column: usize) -> Result<Value, LimboError> {
666            let (key, value) = &self.rows[self.position as usize];
667            match column {
668                0 => Ok(Value::build_text(key.clone())),
669                1 => Ok(Value::from_i64(*value)),
670                _ => Err(LimboError::InternalError(format!(
671                    "column index {column} out of range"
672                ))),
673            }
674        }
675        fn filter(
676            &mut self,
677            _args: &[Value],
678            _idx_str: Option<String>,
679            _idx_num: i32,
680        ) -> Result<bool, LimboError> {
681            self.position = -1;
682            self.next()
683        }
684    }
685
686    #[test]
687    fn registered_internal_vtab_is_visible_to_connections() {
688        let io: Arc<dyn crate::IO> = Arc::new(MemoryIO::new());
689        let db = Database::open_file_with_flags(
690            io,
691            crate::util::MEMORY_PATH,
692            OpenFlags::Create,
693            DatabaseOpts::new(),
694            None,
695        )
696        .unwrap();
697        let name = db
698            .register_internal_vtab(StaticTable {
699                name: "external_metadata",
700            })
701            .unwrap();
702        assert_eq!(name, "external_metadata");
703
704        let conn = db.connect().unwrap();
705        let mut stmt = conn
706            .prepare("SELECT key, value FROM external_metadata")
707            .unwrap();
708        let rows = stmt.run_collect_rows().unwrap();
709        let mapped: Vec<(String, i64)> = rows
710            .into_iter()
711            .map(|cols| {
712                let key = match &cols[0] {
713                    Value::Text(t) => t.as_str().to_string(),
714                    other => panic!("unexpected key type {other:?}"),
715                };
716                let value = match &cols[1] {
717                    Value::Numeric(crate::Numeric::Integer(i)) => *i,
718                    other => panic!("unexpected value type {other:?}"),
719                };
720                (key, value)
721            })
722            .collect();
723        assert_eq!(
724            mapped,
725            vec![("alpha".to_string(), 1), ("beta".to_string(), 2)]
726        );
727    }
728
729    #[test]
730    fn registered_internal_vtab_lookup_folds_ascii_only() {
731        let io: Arc<dyn crate::IO> = Arc::new(MemoryIO::new());
732        let db = Database::open_file_with_flags(
733            io,
734            crate::util::MEMORY_PATH,
735            OpenFlags::Create,
736            DatabaseOpts::new(),
737            None,
738        )
739        .unwrap();
740        let name = db
741            .register_internal_vtab(StaticTable {
742                name: "External_ΔΥΣ",
743            })
744            .unwrap();
745        assert_eq!(name, "External_ΔΥΣ");
746
747        let conn = db.connect().unwrap();
748        let mut stmt = conn.prepare("SELECT key, value FROM external_ΔΥΣ").unwrap();
749        let rows = stmt.run_collect_rows().unwrap();
750        assert_eq!(rows.len(), 2);
751
752        assert!(conn.prepare("SELECT key, value FROM external_δυσ").is_err());
753    }
754}