Register now for early access to concurrent writes in the Turso Cloud. Join the waitlist

Turso v0.6.0

Pekka EnbergPekka Enberg
Glauber CostaGlauber Costa
Cover image for Turso v0.6.0

#What’s new?

  • Multi-process support
  • Vacuum
  • Turso CLI on npm
  • Generated columns
  • Temporary tables
  • ...and more

Today we are announcing the release of Turso 0.6. This release feels special: this is the first release of Turso where all the major features of SQLite are now supported. Two developments in particular got us to this point: Turso now supports multi-process access, as well as vacuum of the databases. In subsequent releases, we will be working to bring those features to production level.

But 0.6 also includes a lot more. There are improvements to Turso’s strict type system, improvements in SQLite compatibility, bug fixes and performance enhancements.

#Multi-process support

Turso now has experimental support for multi-process access (#6236) to the same database — enabled with dsn?experimental=multiprocess_wal — so users can inspect or interact with your .db files while they are open in your live application. This is useful for things like ETL, or simply running queries against your production database directly from the CLI without shutting down your app. On Linux, Turso uses byte-range per-fd OFD locking, and regular fcntl locking on other Unix systems. On Windows, the experimental IOCP backend must be used for multi-process support (contributed by Marc-André Moreau).

Note that currently only WAL journal mode is supported for multiprocess access, which means BEGIN CONCURRENT is not supported with multi-process right now.

#Vacuum

Turso now supports VACUUM INTO and VACUUM, which allows you to rebuild the database file to reclaim space for unused pages.

The VACUUM INTO statement writes a compacted copy of the database to a new file:

VACUUM INTO 'compacted.db';

The VACUUM statement, in contrast, rewrites the source database itself:

VACUUM;

The in-place VACUUM (#6402) is marked as experimental in Turso 0.6 and requires you to enable it with --experimental-vacuum on the CLI. As the statement updates the database file in-place, make sure you have backups of your data and use it with caution.

#SQL features

#Triggers

Triggers are no longer experimental (#5768). CREATE TRIGGER is now enabled by default, with no flag needed. Turso supports BEFORE, AFTER, and INSTEAD OF triggers on INSERT, UPDATE, and DELETE, as well as WHEN clauses and the usual NEW. and OLD. row references. The release also has many smaller fixes across the board to make triggers production-ready, covering things like foreign-key cascade interactions, RETURNING inside trigger bodies, BEFORE INSERT raw-value affinity, nested trigger subqueries, and RAISE() argument handling.

#Tables

TEMPORARY tables (#6323) are now fully supported via the CREATE TEMP TABLE and CREATE TEMP TRIGGER statements, for connection-scoped ephemeral tables and triggers.

Generated columns (#5964, #6134) are now supported as an experimental feature. Enable them with --experimental-generated-columns. A generated column is a column whose value is computed from an expression over the other columns in the row:

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    price REAL,
    quantity INTEGER,
    total REAL AS (price * quantity)
);

In 0.6, generated columns are VIRTUAL, recomputed on read rather than stored on disk, and behave correctly with the usual table machinery: CHECK and NOT NULL constraints, UNIQUE indexes, foreign keys, and ALTER TABLE ADD COLUMN. STORED generated columns are not yet supported, and virtual generated columns in STRICT tables are not type-checked yet.

CREATE TABLE AS SELECT is now supported (#6422). It creates a new table whose column definitions are derived from the result set of a SELECT and populates it with the query's rows in one statement, which is handy for materializing query results or quickly cloning a table:

CREATE TABLE active_users AS
SELECT id, name, email FROM users WHERE last_seen > '2026-01-01';

WITHOUT ROWID tables (#6530, contributed by Marc-André Moreau) are now supported as an experimental feature. Enable them with --experimental-without-rowid. In a regular SQLite/Turso table, every row has an implicit 64-bit rowid and the table is stored as a B-tree keyed by that rowid; a WITHOUT ROWID table instead stores rows directly in a B-tree keyed by the declared PRIMARY KEY. That's a better fit when the natural key is a string or a composite — you save the rowid lookup and avoid duplicating the key across a separate index:

CREATE TABLE inventory (
    sku TEXT PRIMARY KEY,
    qty INTEGER
) WITHOUT ROWID;

In 0.6, the supported subset is opening and querying existing WITHOUT ROWID databases, creating new ones, and INSERT. UPDATE, DELETE, secondary indexes, UPSERT/REPLACE, foreign keys, MVCC writes, and CDC on WITHOUT ROWID tables are rejected explicitly for now.

#Queries

CROSS JOIN is now supported as an explicit join syntax (#5879). A cross join produces the Cartesian product of two tables — every row on the left paired with every row on the right — and is the same thing you get from a comma-separated FROM list, but spelled out as a join so the intent is explicit:

SELECT s.name, p.label
FROM sizes s CROSS JOIN paints p;

UPDATE ... FROM is now supported (#6427). The optional FROM clause on UPDATE lets you compute new values for a target table by joining against other tables, which is the standard way to apply per-row updates derived from another table without resorting to correlated subqueries:

UPDATE inventory
   SET qty = inventory.qty + delta.amount
  FROM delta
 WHERE inventory.sku = delta.sku;

The INDEXED BY and NOT INDEXED BY clauses are now supported (#6029). These query-planner hints let you override Turso's index selection on a per-table basis: INDEXED BY <name> forces the specified index to be used for accessing the table, while NOT INDEXED forces a full table scan. They're useful for pinning the access method when the planner picks something suboptimal:

SELECT * FROM orders INDEXED BY orders_by_customer WHERE customer_id = 42;
SELECT * FROM orders NOT INDEXED WHERE status = 'pending';

The FILTER clause on aggregate functions is now supported (#6510). FILTER (WHERE ...) attached to an aggregate restricts which rows that aggregate sees — without splitting the query into separate SELECTs or stuffing CASE expressions inside the aggregate. It's especially useful for computing several conditional totals over the same grouping in one pass:

SELECT
    user_id,
    COUNT(*) FILTER (WHERE status = 'success') AS successes,
    COUNT(*) FILTER (WHERE status = 'failed')  AS failures
FROM jobs
GROUP BY user_id;

Three ORDER BY and compound-SELECT gaps got filled in. Previously, NULLS FIRST was silently ignored and NULLS LAST was rejected outright — Turso now threads NULLS FIRST / NULLS LAST through the planner and sorter so NULL placement is independent of ASC/DESC (#6171). Compound SELECTs (UNION, UNION ALL, INTERSECT, EXCEPT) now respect ORDER BY and LIMIT/OFFSET on the merged result rather than returning rows in arbitrary order (#6063). And compound SELECTs are now also allowed inside WHERE-clause subqueries, which used to be rejected with a parse error (#6242):

SELECT id FROM events
WHERE user_id IN (
    SELECT id FROM users WHERE region = 'eu'
    UNION
    SELECT id FROM admins
)
ORDER BY created_at DESC NULLS LAST;

The DEFAULT keyword in INSERT VALUES (#6058) lets you fall back to a column's declared default on a per-row, per-column basis instead of having to omit the column from the insert list entirely:

CREATE TABLE events (
    id INTEGER PRIMARY KEY,
    occurred_at TEXT DEFAULT CURRENT_TIMESTAMP,
    payload TEXT
);

INSERT INTO events (occurred_at, payload) VALUES
    (DEFAULT, 'first'),         -- use the default timestamp
    ('2026-01-01', 'backfill'); -- explicit value

This is standard SQL (SQL:2016) but not something SQLite supports.

#Functions

ROW_NUMBER() (#5477) is now supported as a window function — it returns a 1-based sequence number within each partition and is the most common building block for "top N per group" queries.

Two new scalar functions from SQLite 3.50. unistr(X) (#6132) expands \uXXXX / \UXXXXXXXX Unicode escape sequences in a string, and unistr_quote(X) (#6216) is the inverse, producing an SQL literal that round-trips through unistr().

#Pragmas

Turso 0.6 fills in several pragmas that show up in SQLite tooling and ORMs:

  • PRAGMA foreign_key_list(table) (#6621) returns the foreign-key relationships declared on a table, matching SQLite's output columns and reverse-declaration ordering. The table-valued form pragma_foreign_key_list is also supported.
  • PRAGMA full_column_names and PRAGMA short_column_names (#6200) control how column names appear in result sets (TABLE.COLUMN vs. just COLUMN). These are deprecated in SQLite but still needed by some test suites and clients.
  • PRAGMA locking_mode (#6232) is now readable (the value can be queried but not changed).
  • .dbconfig dqs_dml on/off (#6234) now actually controls whether unresolved double-quoted identifiers fall back to string literals — SQLite's infamous "DQS" misfeature — instead of being silently ignored.
  • PRAGMA require_where (#5772, alias PRAGMA i_am_a_dummy as an homage to MySQL) rejects UPDATE and DELETE statements that have no WHERE clause — a syntactic guardrail against accidentally rewriting or wiping an entire table. Off by default, per-connection.

#Type system improvements

Turso 0.5 introduced full support for STRICT tables, and an experimental custom type system (enabled with --experimental-custom-types), which allows you to define your own types via CREATE TYPE and use them as column types in strict tables. Turso ships a handful of built-ins, such as Numeric and Datetime. Turso 0.6 builds on that with three new improvements: array types, structs and unions, and domains.

#Array types

With array types, it is possible to define arrays as first class citizens. Example:

CREATE TABLE t (id INTEGER PRIMARY KEY, items TEXT[]) STRICT;
INSERT INTO t VALUES (1, ARRAY['first', 'second', 'third']);

SELECT items[1], items[2], items[3] FROM t;
-- first|second|third

SELECT items[100] FROM t;
-- NULL

SELECT items[-1] FROM t;
-- NULL

Multidimensional arrays, slice-notation, array aggregation, are supported

#Structs and unions

Unions and structs are ways to represent the complex, composite data. With dot-notation accessors, it allows you to encode expressions without having to manually define your own access notations. Example:

CREATE TYPE platform_id AS UNION(telegram INT, slack TEXT, signal TEXT);

CREATE TABLE contacts (
    id INTEGER PRIMARY KEY,
    platform platform_id
) STRICT;

INSERT INTO contacts VALUES (1, union_value('telegram', 12345));
INSERT INTO contacts VALUES (2, union_value('slack', 'U0ABC'));
INSERT INTO contacts VALUES (3, union_value('signal', '+1555'));

-- Dot notation extracts the variant value (NULL if tag doesn't match)
SELECT id, platform.telegram, platform.slack FROM contacts ORDER BY id;
-- 1|12345|
-- 2||U0ABC
-- 3||

-- Combine with union_tag() to filter by variant
SELECT id, platform.slack FROM contacts WHERE union_tag(platform) = 'slack';
-- 2|U0ABC

#Domains

Domains are “mini-types”. They encode a type together with a sequence of validation functions. For example, “positive integer”

Domains are also present in Postgres, and are now part of Turso 0.6

Here is an example of how to use Domains:

CREATE DOMAIN positive_int AS integer CHECK (value > 0);

CREATE TABLE measurements (
    id INTEGER PRIMARY KEY,
    reading positive_int
) STRICT;

INSERT INTO measurements VALUES (1, 42);
-- OK

INSERT INTO measurements VALUES (2, -5);
-- Error: domain positive_int constraint violation

#Encryption

Encryption for MVCC databases (#5740, #5836) is now fully supported. The MVCC logical log (.db-log) is encrypted at rest using the same AEAD cipher infrastructure as the pager: each transaction frame's payload is encrypted on write and decrypted on read, while the frame header and trailer remain in plaintext so recovery and CRC validation still work without the key.

Chunked encryption for large frames (#6174). Because transaction frames in the logical log are variable-sized (unlike fixed 4 KiB WAL pages), large payloads are split into 32 KiB chunks and encrypted independently. This keeps memory usage bounded — readers and writers only hold one chunk in memory at a time instead of the entire frame.

#SDKs

#JavaScript SDKs

The Turso CLI is now available on npm. To run the Turso shell, type:

npx -y turso

And you will be greeted with the Turso shell:

Turso v0.6.0
Enter ".help" for usage hints.
Did you know that Turso supports vector search? Type .manual vector to learn more.
This software is in BETA, use caution with production data and ensure you have backups.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database
turso>

Per-connection and per-query timeouts (#6109, #6196). Both @tursodatabase/database and @tursodatabase/serverless now support a configurable query timeout that interrupts long-running statements after a deadline. You can set a connection-level default and override it per call:

const db = new Database("app.db", { defaultQueryTimeout: 5_000 });

// Inherits the 5s default.
db.prepare("SELECT * FROM users").all();

// Per-query override.
db.prepare("SELECT * FROM big_table").all(undefined, { queryTimeout: 100 });
db.exec("VACUUM", { queryTimeout: 60_000 });

For the serverless package, timeouts also abort the underlying fetch(), so a stalled network call no longer blocks subsequent queries on the connection. Timed-out queries throw a TimeoutError (code: "TIMEOUT") so callers can distinguish them and decide whether to retry.

Database.close() now finalizes statements (#6060, #6064). Previously, calling Database.close() closed the database handle but left unfinalized prepared statements dangling.

@tursodatabase/serverless now properly supports interactive transactions (#6465). Previously, prepared statements obtained via Connection.prepare() allocated their own session per call, so in the following example the insert was not rolled back:

const insert = await db.prepare("INSERT INTO users(name) VALUES (?)");

await db.exec("BEGIN");
await insert.run(["Alice"]);
await db.exec("ROLLBACK");

Database.prepare() now returns a promise everywhere (#6718). The serverless package already had an async prepare(); in this release @tursodatabase/database is changed to do the same, unifying the interface between serverless and local database SDKs.

Bind parameter support on the Database object (#6589). Database.all(), Database.get(), and Database.iterate() now accept bind parameters directly, so applications no longer have to prepare a statement just to bind a value. This is useful, for example, for serverless applications to avoid the extra network round-trip that the prepare step otherwise incurs.

#Python

SQLAlchemy asyncio dialect (#6551, contributed by Akira Noda). The Python bindings now register a sqlite+aioturso:// SQLAlchemy dialect, so SQLAlchemy's create_async_engine() can talk to a local Turso database without a separate driver shim — analogous to sqlite+aiosqlite://.

Named parameter support (#6526, contributed by Akira Noda). Bind parameters can now be passed by name in the Python API as well as by position.

Wheel coverage and Python versions. Python 3.9 is dropped and Python 3.14 is added (#6054). CI now builds wheels for Linux aarch64 and macOS x86_64 alongside the existing Linux x86_64 and macOS arm64 targets (#6067). The bindings were also ported to PyO3's declarative module syntax ahead of the deprecation of function-based modules (#6122). All three contributed by Kilian Hu.

#Rust

Custom I/O on Builder (#6313, contributed by Hugues Morisset). The Rust Builder now accepts a custom I/O implementation via with_io_impl(...), so embedders can plug in their own async runtime or storage backend instead of being tied to the default.

Dynamic auth token on the sync engine (#6765). The sync engine's auth token can be a closure that returns a fresh token rather than a fixed string, letting applications rotate credentials without restarting.

Statement::n_change() (#6448, contributed by wyhaya). When you run arbitrary SQL through .query(), you only get rows back; to also see how many rows were affected without re-running with .execute(), you can now call .n_change() on the statement.

experimental_index_method on the sync Builder (#6601, contributed by Mark Hinshaw). The local Rust Builder already had experimental_index_method(bool) for FTS / vector index methods; the sync Builder now mirrors it, unblocking synced databases that use CREATE INDEX ... USING fts(...).

#SQLite C API

SQLite C API coverage improvements. João Lucas and Lucas Schwalm Silva improved the SQLite C API, which, for example, helps in supporting Diesel and sqlx, the Rust ORMs.

The following SQLite C API functions were added:

  • sqlite3_column_value
  • sqlite3_complete
  • sqlite3_context_db_handle
  • sqlite3_interrupt
  • sqlite3_prepare_v3
  • sqlite3_progress_handler
  • sqlite3_result_int
  • sqlite3_stmt_readonly
  • sqlite3_stmt_status
  • sqlite3_stricmp
  • sqlite3_value_dup/free
  • sqlite3_value_int

The sqlite3_open_v2() function now also supports URI filename parsing.

#Performance

  • Unified single-index and multi-index access path selection in the optimizer (#5827)
  • IN (list / uncorrelated subquery) as a scan+seek driver (#5822)
  • Reworked CTE / FROM-subquery materialization and costing; subqueries are now order-aware (#5880)
  • Stats-less heuristic tuning, plus a richer EXPLAIN QUERY PLAN (#5741)
  • Simple COUNT(*) and MIN()/MAX() optimizations (#5894)
  • Prepared-statement recompilation check replaced with a generation counter (#5785); other step() fast-path wins (#5771)
  • Trigger execution fast paths: dedicated step_subprogram, statement caching, sparse OLD./NEW. binding (#6217)
  • GROUP BY: deduplicated column reads, smaller sorter records (#5923)
  • Lock-free buffer pool slot bitmap (#5977)
  • Grace hash join algorithm for disk-spilling joins (#5981)
  • BitSet / ColumnMask replacing HashSet<usize> and Vec<usize> across the planner (#6430)

#Summary

Turso 0.6 closes out the last big SQLite feature gaps: multi-process access to the same database, in-place VACUUM, generated columns, and TEMPORARY tables. Triggers come out of experimental and the type system grows arrays, structs, unions, and domains. The Turso CLI now ships on npm so npx turso is all you need to drop into a shell. Over 500 merged PRs from 70+ contributors went into this release since v0.5.0. The full changelog is on GitHub. If you run into issues or have ideas, open an issue on GitHub or join us on Discord.