How to Extend Turso with Virtual Tables (vtab): Implementation Guide

Turso supports three virtual table mechanisms—pragma, external, and internal—that let developers extend queryable functionality without modifying the core storage engine.

Turso, the open-source edge database maintained by tursodatabase/turso, exposes a modular virtual table (vtab) subsystem for adding custom table-like objects. By implementing specific traits or dynamic library interfaces, you can expose system metadata, integrate external data sources, or create lightweight Rust-based tables that participate in SQL queries alongside regular tables.

Types of Virtual Tables in Turso

Turso categorizes virtual tables based on where the implementation lives and how it is loaded:

Kind Location Typical Use
Pragma Built-in tables exposing pragma information Read-only system metadata
External Dynamic library loaded at runtime Custom extensions (CSV, regexp, crypto)
Internal Rust structs implementing InternalVirtualTable Lightweight tables exposing internal state

The core type unifying all implementations is VirtualTable in [core/vtab.rs](https://github.com/tursodatabase/turso/blob/main/core/vtab.rs#L19-L25). It stores the table name, column metadata, a VTabKind discriminant, and a VirtualTableType that determines cursor construction during query execution.

Implementing Internal Virtual Tables in Rust

Internal virtual tables are pure-Rust objects that implement the InternalVirtualTable trait defined in core/vtab.rs. This approach requires no external shared libraries and keeps the extension code compiled directly into your Turso application.

The InternalVirtualTable Trait

To create an internal table, implement the following interface:

fn name(&self) -> String;
fn sql(&self) -> String;  // CREATE TABLE statement for column parsing
fn open(&self, conn: Arc<Connection>) -> Result<Arc<RwLock<dyn InternalVirtualTableCursor>>>;
fn best_index(&self, constraints: &[turso_ext::ConstraintInfo],
              order_by: &[turso_ext::OrderByInfo]) -> Result<turs_ext::IndexInfo, ResultCode>;

Registration Flow

  1. Implement InternalVirtualTable for your struct
  2. Call Database::register_internal_vtab (defined in [core/lib.rs](https://github.com/tursodatabase/turso/blob/main/core/lib.rs#L2704-L2708))
  3. This forwards to Schema::register_internal_vtab ([core/schema.rs](https://github.com/tursodatabase/turso/blob/main/core/schema.rs#L956-L967)), which wraps your object via VirtualTable::wrap_internal_table and inserts it into the catalog

Complete Rust Example

use turso::{Database, Connection, Value, Result};

/// Simple read-only internal table returning two rows.
#[derive(Debug)]
struct SimpleTable;

impl turso::vtab::InternalVirtualTable for SimpleTable {
    fn name(&self) -> String { 
        "simple_demo".into() 
    }
    
    fn sql(&self) -> String { 
        "CREATE TABLE simple_demo(key TEXT, value INTEGER)".into() 
    }
    
    fn open(&self, _conn: std::sync::Arc<Connection>)
        -> turso::Result<std::sync::Arc<std::sync::RwLock<dyn turso::vtab::InternalVirtualTableCursor>>>
    {
        // Cursor implementation omitted—see the test suite's StaticCursor for a minimal example
        unimplemented!()
    }
    
    fn best_index(&self, _c: &[turso_ext::ConstraintInfo],
                  _o: &[turso_ext::OrderByInfo])
        -> std::result::Result<turso_ext::IndexInfo, ResultCode>
    {
        // No special indexing; any query plan works
        Ok(turso_ext::IndexInfo::default())
    }
}

fn main() -> Result<()> {
    let db = Database::open_file_with_flags(
        std::sync::Arc::new(turso::MemoryIO::new()),
        turso::util::MEMORY_PATH,
        turso::OpenFlags::Create,
        turso::DatabaseOpts::new(),
        None,
    )?;
    
    // Register the table; Turso returns the name actually inserted
    let name = db.register_internal_vtab(SimpleTable)?;
    assert_eq!(name, "simple_demo");

    let conn = db.connect()?;
    let mut stmt = conn.prepare("SELECT key, value FROM simple_demo")?;
    let rows = stmt.run_collect_rows()?;
    Ok(())
}

This pattern mirrors the repository's own test suite (testing/system/vtab.test and the StaticTable/StaticCursor structs in core/vtab.rs).

Creating External Virtual Table Extensions

External virtual tables reside in separate dynamic libraries that implement the VTabModule trait (see [extensions/core/src/vtabs.rs](https://github.com/tursodatabase/turso/blob/main/extensions/core/src/vtabs.rs#L35-L45)). These extensions communicate with Turso through a C-compatible ABI defined in VTabModuleImpl.

Extension Lifecycle

When Turso loads an extension via Database::load_extension ([core/lib.rs](https://github.com/tursodatabase/turso/blob/main/core/lib.rs)), it registers the module name in the global SymbolTable. Upon executing CREATE VIRTUAL TABLE ... USING module_name, Turso calls ExtVirtualTable::create ([core/vtab.rs](https://github.com/tursodatabase/turso/blob/main/core/vtab.rs#L45-L70)), which:

  1. Invokes the module's create entry point to obtain schema definitions and an instance pointer
  2. Stores the pointer in an atomic field (table_ptr) for subsequent operations
  3. Returns a VirtualTable of type External

The cursor implementation ExtVirtualTableCursor ([core/vtab.rs](https://github.com/tursodatabase/turso/blob/main/core/vtab.rs#L66-L100)) forwards filter, column, next, and eof calls to the function pointers supplied by your extension.

External Module Skeleton (C-ABI)

#include "turso_ext.h"

typedef struct MyTable {
    // custom state
} MyTable;

typedef struct MyCursor {
    MyTable *tbl;
    int pos;
} MyCursor;

VTabCreateResult my_create(const Value *args, int argc) {
    MyTable *tbl = malloc(sizeof(MyTable));
    const char *schema = "CREATE TABLE x(col TEXT, val INTEGER)";
    return (VTabCreateResult){
        .code = RESULT_OK,
        .schema = schema,
        .table = tbl
    };
}

void *my_open(void *table, const Conn *conn) {
    MyCursor *cur = malloc(sizeof(MyCursor));
    cur->tbl = table;
    cur->pos = -1;
    return cur;
}

ResultCode my_filter(void *cursor, int argc, const Value *argv,
                    const char *idx_str, int idx_num) {
    // Apply constraints, prepare iteration
    return RESULT_OK;
}

Value my_column(void *cursor, unsigned int col) {
    return Value_from_text("hello");
}

ResultCode my_next(void *cursor) {
    // Advance cursor; return RESULT_EOF at end
    return RESULT_OK;
}

ResultCode turso_register_module(void *ctx) {
    VTabModuleImpl impl = {
        .name = "my_mod",
        .readonly = false,
        .create = my_create,
        .open = my_open,
        .filter = my_filter,
        .column = my_column,
        .next = my_next,
        // ... other callbacks
    };
    return RegisterModule(ctx, "my_mod", impl, VTAB_KIND_VIRTUAL_TABLE);
}

Loading External Extensions

After compiling the library (e.g., libmy_mod.so), load it at runtime:

let conn = db.connect()?;
conn.load_extension("libmy_mod.so")?;  // registers the module
conn.execute("CREATE VIRTUAL TABLE demo USING my_mod()")?;
let mut stmt = conn.prepare("SELECT * FROM demo")?;
let rows = stmt.run_collect_rows()?;

Core Architecture and Wiring

Understanding how Turso wires virtual tables together helps debug extension behavior and optimize query plans:

Summary

  • Three virtual table kinds: Pragma (system metadata), External (dynamic libraries), and Internal (Rust structs)

  • Internal registration: Implement InternalVirtualTable, then call Database::register_internal_vtab to insert into the catalog via Schema::register_internal_vtab

  • External modules: Implement VTabModuleImpl in a shared library, load with Connection::load_extension, and create tables using CREATE VIRTUAL TABLE ... USING module_name

  • Key implementation files: core/vtab.rs (core types and dispatch), core/schema.rs (registration), core/lib.rs (public API), and extensions/core/src/vtabs.rs (module interface)

  • Cursor lifecycle: Turso calls open to create a cursor, filter to apply constraints, then iterates with next and column until eof returns true

Frequently Asked Questions

What is the difference between internal and external virtual tables in Turso?

Internal virtual tables are Rust structs implementing the InternalVirtualTable trait that compile directly into your application binary, ideal for exposing lightweight state like metrics or configuration. External virtual tables are dynamic libraries implementing the C-compatible VTabModuleImpl interface, suitable for complex data sources or extensions written in other languages that must be loaded at runtime.

How do I register a custom virtual table with Turso?

For internal tables, call Database::register_internal_vtab with your struct implementing InternalVirtualTable, which stores the table in the schema catalog via Schema::register_internal_vtab. For external tables, compile your extension as a shared library, load it using Connection::load_extension, then execute CREATE VIRTUAL TABLE with your module name.

Can external virtual tables interact with regular Turso tables?

Yes. When Turso calls your extension's open callback ([core/vtab.rs](https://github.com/tursodatabase/turso/blob/main/core/vtab.rs)), it passes a Conn pointer that allows the extension to execute queries against the database. This enables joining external data with standard tables or accessing schema information from within the virtual table implementation.

Where are virtual table cursor implementations defined in the source code?

Cursor implementations reside in [core/vtab.rs](https://github.com/tursodatabase/turso/blob/main/core/vtab.rs). The file contains ExtVirtualTableCursor (lines 66-100) for external modules, PragmaVirtualTableCursor for system tables, and the InternalVirtualTableCursor trait for Rust-based internal tables. The VirtualTableCursor enum dispatches calls to these concrete implementations based on the virtual table type.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →