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 pub(crate) vtab_id: u64,
27 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 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 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 fn open(&self, conn: Arc<Connection>, id: u64) -> crate::Result<ExtVirtualTableCursor> {
375 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 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 conn_ptr: Option<NonNull<turso_ext::Conn>>,
471 implementation: Arc<VTabModuleImpl>,
472 vtab_id: u64,
473}
474
475unsafe 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 let conn = unsafe { Box::from_raw(ptr.as_ptr()) };
561 if !conn._ctx.is_null() {
562 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 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 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 #[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}