Migrate from Turso
Turso is a managed database built on libSQL, a fork of SQLite. DB9 is PostgreSQL-compatible. Migrating from Turso requires converting your schema and queries from SQLite to PostgreSQL syntax.
This guide covers exporting from Turso, converting the SQLite dump to PostgreSQL-compatible SQL, importing into DB9, and updating your application.
Key Differences
Section titled “Key Differences”| Area | Turso (libSQL/SQLite) | DB9 (PostgreSQL) |
|---|---|---|
| SQL dialect | SQLite | PostgreSQL |
| Connection | HTTP (libSQL protocol) or embedded | pgwire (TCP) — standard PostgreSQL drivers |
| Type system | Dynamic typing (type affinity) | Strict static typing |
| Data types | TEXT, INTEGER, REAL, BLOB, NULL | Full PostgreSQL type system (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors, etc.) |
| Auto-increment | INTEGER PRIMARY KEY (implicit ROWID) | GENERATED ALWAYS AS IDENTITY or SERIAL |
| Boolean | Stored as 0/1 (INTEGER) | Native BOOLEAN (true/false) |
| Date/time | Stored as TEXT or INTEGER (no native type) | Native TIMESTAMPTZ, DATE, TIME |
| JSON | json() / json_extract() functions | JSONB type with operators (->, ->>, @>) |
| Concurrent writes | Single-writer (WAL mode) | Full multi-writer MVCC |
| Joins | Supported | Full JOIN support with more advanced options (lateral, full outer) |
| Transactions | SERIALIZABLE (default) | REPEATABLE READ (SERIALIZABLE not supported) |
| Replication | Embedded replicas (edge) | Not supported |
| Foreign keys | Supported (must be enabled per-connection) | Always enforced |
| Connection pooling | Not applicable (HTTP) | Application-side pooling |
Prerequisites
Section titled “Prerequisites”- Access to your Turso database (Turso CLI or connection URL and auth token)
- Turso CLI installed (
curl -sSfL https://get.tur.so/install.sh | bash) - DB9 CLI installed:
curl -fsSL https://db9.ai/install | sh - A DB9 account:
db9 create --name my-appto create your target database
-
Export from Turso
Option A: Turso CLI dump
Terminal turso db shell your-database .dump > export.sqlThis produces a SQLite-format SQL dump.
Option B: Export as SQL via the shell
Terminal turso db shell your-database <<'EOF'.mode insert.output users.sqlSELECT * FROM users;.output posts.sqlSELECT * FROM posts;EOFOption C: Download the database file
Terminal # Create a local copyturso db shell your-database .dump > dump.sqlsqlite3 local-copy.db < dump.sql -
Convert SQLite to PostgreSQL
SQLite and PostgreSQL have different SQL dialects. Convert the dump:
Common SQLite → PostgreSQL conversions:
SQLite → PostgreSQL──────────────────────────────────────────────────────────INTEGER PRIMARY KEY (autoincrement) → BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEYREAL → DOUBLE PRECISIONBLOB → BYTEA0 / 1 (boolean) → FALSE / TRUEdatetime('now') → now()strftime(...) → to_char(...)json_extract(col, '$.key') → col->>'key'GROUP_CONCAT(col) → string_agg(col, ',')IFNULL(a, b) → COALESCE(a, b)|| (string concat) → || (same in PostgreSQL)AUTOINCREMENT → GENERATED ALWAYS AS IDENTITYExample table conversion:
SQL -- SQLite (Turso)CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL,email TEXT NOT NULL UNIQUE,active INTEGER DEFAULT 1,metadata TEXT, -- JSON stored as textcreated_at TEXT DEFAULT (datetime('now')));-- PostgreSQL (DB9)CREATE TABLE users (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,name TEXT NOT NULL,email TEXT NOT NULL UNIQUE,active BOOLEAN DEFAULT true,metadata JSONB,created_at TIMESTAMPTZ DEFAULT now());Conversion script for the dump:
Terminal cat export.sql \| sed 's/INTEGER PRIMARY KEY AUTOINCREMENT/BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY/g' \| sed 's/INTEGER PRIMARY KEY/BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY/g' \| sed "s/datetime('now')/now()/g" \| sed 's/REAL/DOUBLE PRECISION/g' \| sed 's/BLOB/BYTEA/g' \| grep -v "^BEGIN TRANSACTION" \| grep -v "^COMMIT" \| grep -v "^CREATE INDEX.*sqlite" \> import.sqlReview the output manually — automated conversion misses edge cases, especially around boolean values and date handling.
Convert boolean data:
SQLite stores booleans as
0/1. After importing, convert to proper booleans:SQL -- If you kept the column as BOOLEAN, SQLite's 0/1 may need conversion-- PostgreSQL accepts 0/1 as boolean in most contexts, but verify your dataConvert date/time data:
SQLite stores dates as TEXT (e.g.,
"2026-01-15 10:30:00"). If you changed the column type to TIMESTAMPTZ, PostgreSQL will parse ISO 8601 strings automatically. Verify timezone handling:SQL -- SQLite dates are typically UTC with no timezone info-- Ensure your application interprets them correctly as UTC in DB9 -
Create the DB9 Database
Terminal db9 create --name my-app --show-connection-string -
Import into DB9
Terminal # For small to medium databasesdb9 db sql my-app -f import.sql# For larger databasespsql "$(db9 db status my-app --json | jq -r .connection_string)" -f import.sql -
Update Your Application
Replace the Turso/libSQL driver with a PostgreSQL driver
Diff import { createClient } from '@libsql/client';const db = createClient({ url: process.env.TURSO_URL, authToken: process.env.TURSO_TOKEN });const result = await db.execute('SELECT * FROM users WHERE id = ?', [userId]);import pg from 'pg';const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);Key query syntax changes:
SQLite (Turso) PostgreSQL (DB9) ?parameter placeholders$1, $2, $3numbered placeholdersIFNULL(col, default)COALESCE(col, default)GROUP_CONCAT(col, ',')string_agg(col, ',')json_extract(col, '$.key')col->>'key'datetime('now')now()strftime('%Y-%m-%d', col)to_char(col, 'YYYY-MM-DD')typeof(col)pg_typeof(col)LIMIT count OFFSET offsetLIMIT count OFFSET offset(same)Update your ORM
If you use an ORM, switch the database provider:
Diff // Prismaprovider = "sqlite"provider = "postgresql"// Drizzleimport { sqliteTable } from 'drizzle-orm/sqlite-core';import { pgTable } from 'drizzle-orm/pg-core';For ORM-specific setup, see: Prisma, Drizzle, SQLAlchemy.
Connection string
Diff TURSO_URL=libsql://your-db-name-your-org.turso.ioTURSO_TOKEN=your-auth-tokenDATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=requireRemove embedded replica logic
If you use Turso’s embedded replicas for edge reads, remove that logic. DB9 uses a single connection endpoint:
Diff const db = createClient({url: process.env.TURSO_URL,authToken: process.env.TURSO_TOKEN,syncUrl: process.env.TURSO_SYNC_URL,});const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); -
Validate
Check schema
Terminal db9 db dump my-app --ddl-onlyVerify tables, columns, types, and constraints match your expectations.
Check row counts
Terminal db9 db sql my-app -q "SELECT count(*) FROM users"db9 db sql my-app -q "SELECT count(*) FROM posts"Compare against the source Turso database.
Run your test suite
Terminal DATABASE_URL="$(db9 db status my-app --json | jq -r .connection_string)" npm testCommon test failures
- Parameter placeholders —
?must become$1, $2, ... - Boolean values —
0/1vstrue/false - Date handling — text dates vs native TIMESTAMPTZ
- Dynamic typing — SQLite allows any value in any column; PostgreSQL enforces types strictly
- SERIALIZABLE transactions — Turso defaults to SERIALIZABLE; DB9 does not support it. Use REPEATABLE READ.
- Parameter placeholders —
Rollback Plan
Section titled “Rollback Plan”Your Turso database is unchanged. To revert:
- Switch back to the Turso connection URL and auth token, and revert driver/ORM changes.
- If you need to export data created in DB9 back to Turso, export from DB9 and convert PostgreSQL SQL back to SQLite-compatible format.
Caveats
Section titled “Caveats”- Full SQL dialect migration — Every raw SQL query must be converted from SQLite to PostgreSQL syntax. ORMs reduce this effort significantly.
- Driver change required — The libSQL client (
@libsql/client) must be replaced with a PostgreSQL driver (pg,psycopg,pgx). - Type system change — SQLite’s dynamic typing means any column can hold any type. PostgreSQL enforces types strictly. Review your data for type mismatches before importing.
- No embedded replicas — Turso’s embedded replicas for edge reads have no DB9 equivalent. All reads go through the single DB9 endpoint.
- Boolean and date conversion — SQLite stores booleans as integers and dates as text. These need explicit type conversion during migration.
- Transaction isolation change — Turso/SQLite defaults to SERIALIZABLE. DB9 does not support SERIALIZABLE — use REPEATABLE READ. Applications that depend on SERIALIZABLE behavior may need logic changes.
- No HTTP protocol — Turso uses HTTP (libSQL protocol). DB9 uses pgwire (TCP). This affects environments like Cloudflare Workers that only support HTTP — move database queries to a Node.js runtime.
Next Pages
Section titled “Next Pages”- Compatibility Matrix — full list of supported and unsupported PostgreSQL features
- Connect — connection string format and authentication options
- Prisma Guide — type-safe ORM (handles dialect differences)
- Drizzle Guide — lightweight TypeScript ORM
- Production Checklist — deployment readiness