activerecord-turso
ActiveRecord adapter for Turso (SQLite compatible).
Status
This adapter is under active development. It can run basic ActiveRecord operations against Turso today, but several production features are still being hardened. See Limitations and Risks before using it in production.
Requirements
- Ruby >= 3.0
- ActiveRecord >= 8.0, < 8.2
- The
tursoRuby gem (local bindings at~/Projects/turso/bindings/rubyduring development)
Installation
Add to your Gemfile:
gem "activerecord-turso", path: "~/Projects/activerecord-turso"
Then configure database.yml:
development:
adapter: turso
database: "path/to/db.sqlite3"
For an in-memory database:
test:
adapter: turso
database: ":memory:"
Usage
Most standard ActiveRecord operations work the same as with the SQLite3 adapter:
class Post < ActiveRecord::Base
end
Post.create!(title: "Hello", body: "World", published: true)
MVCC / BEGIN CONCURRENT
Turso supports BEGIN CONCURRENT for optimistic, multi-writer transactions. To opt in, pass concurrent: true to transaction:
ActiveRecord::Base.transaction(concurrent: true) do
user.update!(balance: user.balance - 100)
order.create!(amount: 100)
end
If the commit detects a snapshot conflict, the adapter will automatically retry the block up to a configured limit with exponential backoff.
Configure retry behavior in database.yml:
development:
adapter: turso
database: "path/to/db.sqlite3"
turso_mvcc_max_retries: 50
turso_mvcc_base_delay_ms: 10
Important caveats:
transaction(concurrent: true)requires the same database connection to be held for the entire retry loop. Rails' connection pool may reap the connection between retries, which can silently break MVCC semantics. Use this only when you understand the pooling behavior of your app.lock!,with_lock, andlock_versionare not meaningful under MVCC. Do not use them inside concurrent transactions.- If the retry limit is exhausted, the conflict is raised as
ActiveRecord::StatementInvalid. You must handle it in application code.
Full-text search (Tantivy)
Turso provides full-text search through Tantivy. Use CREATE INDEX ... USING fts:
CREATE INDEX fts_posts ON posts USING fts (title, body);
From migrations:
class AddFtsToPosts < ActiveRecord::Migration[8.1]
def change
add_fts_index :posts, [:title, :body], tokenizer: :default
end
end
Query with Tantivy functions:
SELECT * FROM posts WHERE fts_match(title, body, 'database');
Note: fts5 virtual tables are not supported. Use USING fts indexes instead.
Concurrent transactions
Turso supports multi-writer concurrency with MVCC. Enable it in database.yml:
development:
adapter: turso
database: db/dev.sqlite3
journal_mode: mvcc
busy_timeout: 5000
Use concurrent: true inside the same pinned connection:
ActiveRecord::Base.connection.transaction(concurrent: true) do
# read-modify-write
end
Caveats:
- The connection must stay checked out for the whole transaction; avoid work that yields the ActiveRecord connection back to the pool.
busy_timeoutis used for lock waits, not MVCC snapshot conflicts. Snapshot conflicts retry with bounded backoff.
Limitations and Risks
The following limitations apply to the current implementation. Read this section carefully before deploying to production.
1. Result type metadata uses column declared types
The adapter builds column type maps from column_decltype metadata. This works for most column definitions but may not capture type information for computed expressions or subquery columns.
2. Batch SQL execution uses a simple string splitter
The adapter's batch execution path splits multi-statement SQL on semicolons. This means SQL containing semicolons inside string literals, triggers, or stored expressions may be split incorrectly. Avoid relying on multi-statement strings other than simple schema dumps.
3. MVCC requires opt-in and has pooling caveats
BEGIN CONCURRENT is powerful but breaks ActiveRecord's default assumptions:
- ActiveRecord expects transactions to commit unless the database returns an error. With
BEGIN CONCURRENT, the commit can fail with a snapshot conflict and must be retried. - The retry loop must run on the same connection. Rails' connection pool is not MVCC-aware and may return the connection to the pool between retries.
- Do not combine concurrent transactions with pessimistic locking (
lock!,with_lock,lock_version).
Only use transaction(concurrent: true) after testing it under your app's concurrency patterns.
4. Prepared statement cache is disabled
The adapter returns false for default_prepared_statements and uses a no-op statement pool. Each query is prepared and finalized individually. This is slower than the upstream SQLite3 adapter for high-volume repeated queries, but avoids correctness issues until the Turso bindings expose stable prepared-statement reuse.
5. ActiveRecord 8.0 support is CI-tested, not locally tested
Only ActiveRecord 8.1 is installed in the primary development environment. ActiveRecord 8.0 compatibility is validated through CI. If you run into 8.0-specific issues, please report them.
6. Some SQLite-specific features are unsupported or conservatively flagged
- Transaction isolation levels other than the default are reported as unsupported (
supports_transaction_isolation?returnsfalse) because Turso remote connections do not provide shared-cache read-uncommitted semantics. insert_returningis enabled only when the reported SQLite version is>= 3.35.0.insert_on_conflictis enabled only when the reported SQLite version is>= 3.24.0.
7. execute_batch in the underlying bindings is a Ruby-side fallback
The turso gem provides DB#execute_batch as a convenience that splits and executes statements one by one. It does not use a native batch API, so it carries the same semicolon-splitting risk as item 2 above.
Development
Run the local test suite:
bundle install
bundle exec rake test
To run against the official Rails Active Record test suite, see the CI workflow in .github/workflows/test.yml and the project design doc in docs/superpowers/specs/.
License
MIT