We'd love to have you contribute to Turso!
This document is a quick helper to get you going.
- Contributing to Turso
Turso is a rewrite of SQLite in Rust. If you are new to SQLite, the following articles and books are a good starting point:
- Architecture of SQLite
- Sibsankar Haldar. SQLite Database System Design and Implementation (2nd Edition). 2016
- Jay Kreibich. Using SQLite: Small. Fast. Reliable. Choose Any Three. 1st Edition. 2010
If you are new to Rust, the following books are recommended reading:
- Jim Blandy et al. Programming Rust, 2nd Edition. 2021
- Steve Klabnik and Carol Nichols. The Rust Programming Language. 2022
Examples of contributing
To build and run tursodb CLI:
cargo run --package turso_cli --bin tursodb database.dbRun tests:
cargo build -p turso_sqlite3 --features capi
cargo testThe mold linker (Linux only) can reduce your build time from a minute to just few seconds.
First, install mold:
# Fedora/RHEL
sudo dnf install mold
# Ubuntu/Debian
sudo apt install moldThen configure Cargo to use mold by creating .cargo/config.toml:
For Linux:
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]Note
These steps have been tested on Ubuntu Noble 24.04.2 LTS
Running tests on Linux and getting them pass requires a few additional steps
- Install SQLite headers
sudo apt install sqlite3 libsqlite3-dev- Install Python3 dev files
sudo apt install python3.12 python3.12-dev- Set env var for Maturin
export PYO3_PYTHON=$(which python3)- Build Cargo
cargo build -p turso_sqlite3 --features capi- Run tests
cargo testTest coverage report:
cargo tarpaulin -o html
Note
Generation of coverage report requires tarpaulin binary to be installed.
You can install it with cargo install cargo-tarpaulin
Tip
If coverage fails with "Test failed during run" error and all of the tests passed it might be the result of tarpaulin bug. You can temporarily set dynamic libraries linking manually as a workaround, e.g. for linux LD_LIBRARY_PATH="$(rustc --print=target-libdir)" cargo tarpaulin -o html.
Run benchmarks:
cargo bench --profile bench-profile --bench benchmarkRun benchmarks and generate flamegraphs:
echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
cargo bench --profile bench-profile --bench benchmark -- --profile-time=5You're welcome to develop Turso with AI coding agents such as Claude Code, Codex, or OpenCode. Used well, they can help you explore the codebase, draft tests, and polish your contributions. To make the most of them — and to get your PRs merged — keep the following in mind.
We expect you to understand the code you submit. The best AI-assisted contributions come from people who treat the agent as a collaborator, not a substitute: you direct the work, review it critically, and own the result. A PR you can explain and defend is far more valuable than a large one you can't.
To give your PR the best chance of being merged:
- Keep it small and focused. Describe the change in your own words, or heavily edit any AI-generated summary so it reads naturally and accurately.
- Include regression tests. Verify that your tests actually FAIL without your changes — this is especially important when the tests are AI-generated.
- Contribute in areas you understand. If you spot a bug in something like the MVCC or b-tree layer but aren't familiar with it, the most helpful thing you can do is file a clear bug report rather than submit an AI-generated fix.
- Do a self-review. LLMs tend to make the same mistakes repeatedly: removing existing comments, adding verbose new ones, writing overly elaborate tests instead of using existing test helpers, etc. Always self-review your code before submitting - make sure it's correct and follows the existing code standards in this repo.
Finally, a well-written bug report with a solid reproducer is often more valuable to maintainers than a sloppy PR. If you're not sure your change is ready, opening an issue is always a great contribution.
Turso aims towards SQLite compatibility. If you find a query that has different behavior than SQLite, the first step is to check what the generated bytecode looks like.
To do that, first run the EXPLAIN command in sqlite3 shell:
sqlite> EXPLAIN SELECT first_name FROM users;
addr opcode p1 p2 p3 p4 p5 comment
---- ------------- ---- ---- ---- ------------- -- -------------
0 Init 0 7 0 0 Start at 7
1 OpenRead 0 2 0 2 0 root=2 iDb=0; users
2 Rewind 0 6 0 0
3 Column 0 1 1 0 r[1]= cursor 0 column 1
4 ResultRow 1 1 0 0 output=r[1]
5 Next 0 3 0 1
6 Halt 0 0 0 0
7 Transaction 0 0 1 0 1 usesStmtJournal=0
8 Goto 0 1 0 0
and then run the same command in Turso's shell.
If the bytecode is different, that's the bug -- work towards fixing code generation. If the bytecode is the same, but query results are different, then the bug is somewhere in the virtual machine interpreter or storage layer.
If you suspect a multi-threading issue, you can run the stress test with ThreadSanitizer enabled as follows:
rustup toolchain install nightly
rustup override set nightly
cargo run -Zbuild-std --target x86_64-unknown-linux-gnu -p turso_stress -- --vfs syscall --nr-threads 4 --nr-iterations 1000The issue tracker has issues tagged with good first issue, which are considered to be things to work on to get going. If you're interested in working on one of them, comment on the issue tracker, and we're happy to help you get going.
You don't need to ask "can I work on this?" The answer is always yes. Pick something, work on it, and open a pull request. A few things to keep in mind:
- You don't need permission to start. Anyone can work on any open issue at any time.
- Commenting that you're working on something doesn't reserve it. Someone else may work on the same issue in parallel, so don't assume an issue is yours just because you claimed it.
- Claiming an issue does not obligate a maintainer to merge your work, and asking to work on something is not a commitment from us to review or accept it. Work gets merged on its merits.
If you want to coordinate or ask for guidance on an approach, that's welcome. Just don't wait for a permission slip before getting started.
Fork the repository and open a pull request to submit your work.
The CI checks for formatting, Clippy warnings, and test failures so remember to run the following before submitting your pull request:
cargo fmtandcargo clippy --workspace --all-features --all-targets -- --deny=warningsto keep the code formatting in check.make testto run the test suite.
Keep your pull requests focused and as small as possible, but not smaller. IOW, when preparing a pull request, ensure it focuses on a single thing and that your commits align with that. For example, a good pull request might fix a specific bug or a group of related bugs. Or a good pull request might add a new feature and test for it. Conversely, a bad pull request might fix a bug, add a new feature, and refactor some code.
The commits in your pull request tell the story of your change. Break your pull request into multiple commits when needed to make it easier to review and ensure that future developers can also understand the change as they are in the middle of a git bisect run to debug a nasty bug. A developer should be able to reconstruct the intent of your change and how you got to the end-result by reading the commits. To keep a clean commit history, make sure the commits are atomic:
- Keep commits as small as possible. The smaller the commit, the easier it is to review, but also easier
git revertwhen things go bad. - Don't mix logic and cleanups in same commit. If you need to refactor the code, do it in a commit of its own. Mixing refactoring with logic changes makes it very hard to review a commit.
- Don't mix logic and formatting changes in same commit. Resist the urge to fix random formatting issues in the same commit as your logic changes, because it only makes it harder to review the commit.
- Write a good commit message. You know your commit is atomic when it's easy to write a short commit message that describes the intent of the change. Follow the commit message style below.
Use an optional component scope followed by a lowercase imperative summary.
Do not add a trailing period. Conventional Commit prefixes such as
feat(scope): are not required.
[scope: ]<imperative summary>
<why the change is needed and what invariant or bug it addresses>
<non-obvious implementation details or tradeoffs, if needed>
Tests: <relevant validation, if useful>
Fixes #1234
For example:
core/mvcc: preserve B-tree cleanup markers in commit logs
Commit-log canonicalization could collapse a cleanup marker into the
replacement row, leaving stale physical B-tree state after checkpoint.
Keep B-tree-resident delete markers when collapsing adjacent versions.
Tests: added an update/reopen/delete/checkpoint regression test
Fixes #1234
The body should explain the intent and why the change is needed, rather than narrating the diff. Include non-obvious implementation details and tradeoffs that will matter to reviewers or future maintainers. The body, test summary, and issue reference are optional when they do not add useful information.
To produce pull requests like this, you should learn how to use Git's interactive rebase (git rebase -i).
For a longer discussion on good commits, see Al Tenhundfeld's What makes a good git commit, for example.
The testing/test.all is a starting point for adding functional tests using a similar syntax to SQLite.
The purpose of these tests is to verify behavior matches with SQLite and Turso.
- Cargo-c is needed for building C-ABI compatible library. You can get it via:
cargo install cargo-c --version 0.10.16 --locked- SQLite is needed for compatibility checking. You can install it using
brewon macOS/Linux:
brew install sqliteOr using choco on Windows:
choco install sqliteThe Rust C compatibility tests also require a static SQLite installation from vcpkg:
$env:VCPKG_ROOT = "C:\path\to\vcpkg"
vcpkg install sqlite3:x64-windows-static-mdTo run the test suite with Turso, simply run:
make test
To run the test suite with SQLite, type:
SQLITE_EXEC=sqlite3 SQLITE_FLAGS="" make test
When working on a new feature, please consider adding a test case for it.
The sqltest crate provides a dedicated test runner with a custom DSL for writing SQL tests.
Tests should be added to sqlite/conformance/sqlite-sqltests/ using the .sqltest format.
To run tests:
make -C sqlite/conformance runFor full documentation on the DSL syntax and CLI usage, see the sqltest docs.
TPC-H is a standard benchmark for testing database performance. To try out Turso's performance against a TPC-H compatible workload, you can generate or download a TPC-H compatible SQLite database e.g. here.
The simulator directory contains a deterministic simulator for testing.
What this means is that the behavior of a test run is deterministic based on the seed value.
If the simulator catches a bug, you can always reproduce the exact same sequence of events by passing the same seed.
The simulator also performs fault injection to discover interesting bugs.
Whopper is a DST that, unlike simulator, performs concurrent query execution.
To run Whopper for your local changes, run:
./testing/concurrent-simulator/bin/runThe output of the simulation run looks as follows:
mode = fast
seed = 11621338508193870992
. I/U/D/C
. 22/17/15/0
. 41/34/20/3
| 62/43/27/4
| 88/55/30/5
╱|╲ 97/58/30/6
╱╲|╱╲ 108/62/30/7
╱╲╱|╲╱╲ 115/67/32/7
╱╲╱╲|╱╲╱╲ 121/74/35/7
╱╲╱╲╱|╲╱╲╱╲ 125/80/38/7
╱╲╱╲╱╲|╱╲╱╲╱╲ 141/94/43/8
real 0m1.250s
user 0m0.843s
sys 0m0.043s
The simulator prints ten progress indication lines, regardless of how long a run takes. The progress indicator line shows the following stats:
I-- the number ofINSERTstatements executedU-- the number ofUPDATEstatements executedD-- the number ofDELETEstatements executedC-- the number ofPRAGMA integrity_checkstatements executed
This will do a short sanity check run in using the fast mode.
If you need to reproduce a run, just defined the SEED environment variable as follows:
SEED=1234 ./testing/concurrent-simulator/bin/runYou can also run Whopper in exploration mode to find more serious bugs:
./testing/concurrent-simulator/bin/exploreNote that exploration uses the chaos mode so if you need to reproduce a run, use:
SEED=1234 ./testing/concurrent-simulator/bin/run --mode chaosBoth explore and run accept the --enable-checksums and --enable-encryption flags for per page checksums and encryption respectively.
Turso provides Python bindings built on top of the PyO3 project.
To compile the Python bindings locally, you first need to create and activate a Python virtual environment (for example, with Python 3.12):
python3.12 -m venv venv
source venv/bin/activateThen, install Maturin:
pip install maturinOnce Maturin is installed, you can build the crate and install it as a Python module directly into the current virtual environment by running:
cd bindings/python && maturin developFirst, build the unreliable libc:
cd testing/unreliable-libc
make
The run the stress testing tool with fault injection enabled:
RUST_BACKTRACE=1 LD_PRELOAD=./testing/unreliable-libc/unreliable-libc.so cargo run -p turso_stress -- --nr-iterations 10000
Antithesis is a testing platform for finding bugs with reproducibility. In Turso, we use Antithesis in addition to our own deterministic simulation testing (DST) tool for the following:
- Discovering bugs that the DST did not catch (and improve the DST)
- Discovering bugs that the DST does not cover (for example, non-simulated I/O)
If you have an Antithesis account, you first need to configure some environment variables:
export ANTITHESIS_USER=
export ANTITHESIS_TENANT=
export ANTITHESIS_PASSWD=
export ANTITHESIS_DOCKER_HOST=
export ANTITHESIS_DOCKER_REPO=
export ANTITHESIS_EMAIL=You can then publish a new Antithesis workflow with:
scripts/antithesis/publish-workload.shAnd launch an Antithesis test run with:
scripts/antithesis/launch.shTurso uses Aristo to capture design intent that the code alone doesn't spell out — invariants a refactor could silently break — as #[aristo::intent("...")] annotations attached to the code. They're optional; reach for one only when a property is invisible from the signature and not already guarded by a test. For example, on the WAL trait in core/storage/wal.rs:
#[aristo::intent(
"An append-only log that records page-level changes before they are \
applied to the database, so a system crash can be recovered by \
replaying the log.",
verify = "neural",
id = "wal_records_changes_before_apply",
)]
pub trait Wal: Debug + Send + Sync { ... }The macros are a workspace dependency, so annotated code builds normally. To author and lint annotations, install the CLI:
cargo install aristo-cli # provides the `aristo` command
aristo lint # lint annotation prose (also runs in CI on every PR)Annotations with verify = "neural" are machine-checked by an agent. The skills
that drive this are not committed to this repository — they are generated by the
CLI and tied to its version — so install them into your local agent on demand:
aristo install-skills --agent claude-code --user # installs into ~/.claude/skills/, not the repoThen, from Claude Code, run /aristo-verify. The skill produces a verdict for each
annotation pending verification and writes the proofs under .aristo/proofs/. To
validate those proofs and apply them to the annotation index, run:
aristo verify --apply-verdictsTo run deep verification, run this prompt with claude:
Run Aristo canon match
Some intents assert invariants over internal state the public API doesn't expose. Rather than widen the API for tests, mark that state with the aristo::instrument macros (#[derive(Inspect)], #[expose_pub], yield_point!) behind a Cargo feature, so the verification harness can read it while normal builds compile it out entirely. See the /aristo-instrumenting skill for when and how.
Write intents as you write the code — one at a time, in the same change, while the rationale is fresh; a batched "annotate it later" pass recovers the what but not the why. aristo status shows where a module stands and aristo nudge suggests the next action. Inside an AI agent (see Developing with AI coding agents) the loop runs through the installed skills rather than the bare CLI:
/aristo-status— read the board (what's annotated, verified, awaiting review)./aristo-authoring— write good intents while you're in the code./aristo-verify— verify the annotations your change touched./aristo-intent-suggestionsand/aristo-authored-review— review what was authored.
Only the durable, trust-gating artifacts are committed: aristo.toml, the annotations themselves, .aristo/proofs/*.proof, and .aristo/doc/. The annotation index (.aristo/index.toml) is a regenerable, gitignored cache — every read command rebuilds it from source, so there's nothing to stage.
When you want to add third party dependencies, please follow these steps:
- Add Licenses: Place the appropriate licenses for the third-party dependencies under the licenses directory. Ensure that each license is in a separate file and named appropriately.
- Update NOTICE.md: Specify the licenses for the third-party dependencies in the NOTICE.md file. Include the name of the dependency, the license file path, and the homepage of the dependency.
By following these steps, you ensure that all third-party dependencies are properly documented and their licenses are included in the project.
Releases are made using the scripts/update-version.py script, which updates version numbers across all Cargo.toml, package.json, package-lock.json, gradle.properties, and Directory.Build.props files in the workspace, creates a git commit, and adds a version tag.
The process is:
- Run the version update script with the desired version number.
- Push the commit and tag to the remote.
Pre-releases use a version suffix such as -pre.N:
./scripts/update-version.py 0.6.0-pre.9
git push origin main v0.6.0-pre.9Releases use a plain version number:
./scripts/update-version.py 0.6.0
git push origin main v0.6.0PyPI has a storage quota for the pyturso package. If you run out of storage, you need to delete old release candidate (RC) packages to free up space.
Use the scripts/pypi-cleanup script to manage this:
# Dry run — lists RC packages older than 90 days (safe, no changes made)
./scripts/pypi-cleanup
# Actually delete the packages
./scripts/pypi-cleanup --executeAlways run the dry run first to review what will be deleted before executing.