Embedded SQLite (Turso)
Bashkit can embed a SQLite-compatible engine inside a sandbox by enabling
the sqlite feature. The engine is Turso
(turso_core), a pure-Rust rewrite of SQLite that exposes a pluggable
IO trait — letting us bind it to bashkit’s virtual filesystem.
⚠️ Turso is BETA upstream. The feature is opt-in at the cargo level and at runtime via
BASHKIT_ALLOW_INPROCESS_SQLITE=1. Enable it only for trusted scripts until upstream cuts a stable release.
Quick start
# Cargo.toml
bashkit = { version = "0.2", features = ["sqlite"] }
use bashkit::Bash;
#[tokio::main]
async fn main() -> bashkit::Result<()> {
let mut bash = Bash::builder()
.sqlite()
.env("BASHKIT_ALLOW_INPROCESS_SQLITE", "1")
.build();
let r = bash
.exec(r#"sqlite /tmp/notes.sqlite '
CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT);
INSERT INTO notes(body) VALUES (\"hello\");
'"#)
.await?;
println!("write: {}", r.stderr);
let r = bash.exec(r#"sqlite -header /tmp/notes.sqlite "SELECT * FROM notes""#).await?;
print!("{}", r.stdout);
Ok(())
}
Two backends
SqliteBackend::Memory (default) loads the database file from the VFS into
turso’s MemoryIO, runs the SQL, and flushes the resulting bytes back to
the VFS at command boundary. SqliteBackend::Vfs plugs the bashkit
FileSystem directly into turso via a custom IO impl; same observable
semantics, different code path.
use bashkit::{Bash, SqliteBackend, SqliteLimits};
let bash = Bash::builder()
.sqlite_with_limits(
SqliteLimits::default()
.backend(SqliteBackend::Vfs)
.max_db_bytes(8 * 1024 * 1024),
)
.env("BASHKIT_ALLOW_INPROCESS_SQLITE", "1")
.build();
You can also flip the backend per-invocation with -backend memory|vfs.
In-memory databases
Use :memory: as the database argument when you don’t need persistence:
sqlite :memory: '
WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM r WHERE n<10)
SELECT sum(n) FROM r
'
Output modes
| Flag | Mode | Notes |
|---|---|---|
| (default) | list | ` |
-csv | csv | RFC 4180 quoting |
-tabs | tabs | Tab-separated |
-line | line | name = value blocks |
-column | column | Column-aligned with -- separator |
-box | box | ASCII box-drawing borders |
-json | json | Array of objects via serde_json |
-markdown | markdown | GitHub-flavoured table |
.headers on adds a header row; -header is its CLI equivalent.
Dot-commands
.help Show command list
.quit / .exit Stop execution
.tables [PAT] List tables (LIKE pattern)
.schema [PAT] Print CREATE statements
.indexes [PAT] List indexes
.headers on|off Toggle column headers
.mode MODE Switch output mode
.separator SEP Set field separator (escapes: \t \n \r \0)
.nullvalue STR Set NULL placeholder
.dump Dump schema + data as SQL
.read PATH Execute SQL from a VFS file
Dot-commands must be on their own line. .read is bounded to 16 levels
of nesting to prevent stack overflow on self-referential scripts.
Resource limits
SqliteLimits caps every resource the engine can use to push back against
malicious or runaway SQL. Defaults:
| Limit | Default |
|---|---|
max_script_bytes | 4 MiB |
max_rows_per_query | 1,000,000 |
max_db_bytes | 256 MiB |
max_duration | 30 s |
max_statements | 10,000 |
MAX_DOT_READ_DEPTH | 16 (constant) |
max_duration is enforced via a wall-clock deadline shared across every
statement in the invocation. The step loop checks the deadline on each
turn and calls Statement::interrupt() once it expires. Pass
std::time::Duration::ZERO to opt out (useful for CI hosts with bursty
schedulers).
Tune per workload via SqliteLimits::default().max_*(...).
Security
See specs/sqlite-builtin.md § “Trust Model & Threats” for the full
threat table. Highlights:
- No host filesystem access. All paths resolve through the bashkit
VFS; even with
-backend vfsand an absolute path like/etc/passwd, the engine reads from the VFS only. - Default-disabled at runtime. Without
BASHKIT_ALLOW_INPROCESS_SQLITE=1, the builtin refuses to execute. ATTACH/DETACHrejected by policy. A pre-execution check refuses both keywords (case-insensitive, comment-aware) so cross-DB access can’t bypass VFS isolation.- PRAGMA deny list.
SqliteLimits::pragma_denyblocks resource and filesystem-shaped knobs (cache_size,mmap_size,page_size,temp_store_directory,compile_options,locking_mode, …) by default. Common operational PRAGMAs (user_version,wal_checkpoint,foreign_keys,journal_mode) pass through. Override withSqliteLimits::pragma_deny([...]). - Bounded recursion in
.read.
Compatibility with the sqlite3 shell
The CLI surface is intentionally a subset of sqlite3’s. The pinned
parity tests live in tests/sqlite_compat_tests.rs. Notable differences:
- No interactive REPL.
- No
ATTACH,DETACH,.load,.eqp, or.fullschema. - Dot-commands must be on their own line (no inline
;mixing).
See also:
python_guide— the embedded Python builtin, same opt-in pattern.live_mounts_guide— mount points the sqlite builtin reads from / writes to.threat_model— overall security model.