Skip to content
Discord Get Started

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.

AreaTurso (libSQL/SQLite)DB9 (PostgreSQL)
SQL dialectSQLitePostgreSQL
ConnectionHTTP (libSQL protocol) or embeddedpgwire (TCP) — standard PostgreSQL drivers
Type systemDynamic typing (type affinity)Strict static typing
Data typesTEXT, INTEGER, REAL, BLOB, NULLFull PostgreSQL type system (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors, etc.)
Auto-incrementINTEGER PRIMARY KEY (implicit ROWID)GENERATED ALWAYS AS IDENTITY or SERIAL
BooleanStored as 0/1 (INTEGER)Native BOOLEAN (true/false)
Date/timeStored as TEXT or INTEGER (no native type)Native TIMESTAMPTZ, DATE, TIME
JSONjson() / json_extract() functionsJSONB type with operators (->, ->>, @>)
Concurrent writesSingle-writer (WAL mode)Full multi-writer MVCC
JoinsSupportedFull JOIN support with more advanced options (lateral, full outer)
TransactionsSERIALIZABLE (default)REPEATABLE READ (SERIALIZABLE not supported)
ReplicationEmbedded replicas (edge)Not supported
Foreign keysSupported (must be enabled per-connection)Always enforced
Connection poolingNot applicable (HTTP)Application-side pooling
  • 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-app to create your target database
  1. Export from Turso

    Option A: Turso CLI dump

    Terminal
    turso db shell your-database .dump > export.sql

    This 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.sql
    SELECT * FROM users;
    .output posts.sql
    SELECT * FROM posts;
    EOF

    Option C: Download the database file

    Terminal
    # Create a local copy
    turso db shell your-database .dump > dump.sql
    sqlite3 local-copy.db < dump.sql
  2. 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 KEY
    REAL → DOUBLE PRECISION
    BLOB → BYTEA
    0 / 1 (boolean) → FALSE / TRUE
    datetime('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 IDENTITY

    Example 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 text
    created_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.sql

    Review 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 data

    Convert 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
  3. Create the DB9 Database

    Terminal
    db9 create --name my-app --show-connection-string
  4. Import into DB9

    Terminal
    # For small to medium databases
    db9 db sql my-app -f import.sql
    # For larger databases
    psql "$(db9 db status my-app --json | jq -r .connection_string)" -f import.sql
  5. 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, $3 numbered placeholders
    IFNULL(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
    // Prisma
    provider = "sqlite"
    provider = "postgresql"
    // Drizzle
    import { 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.io
    TURSO_TOKEN=your-auth-token
    DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require

    Remove 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 });
  6. Validate

    Check schema

    Terminal
    db9 db dump my-app --ddl-only

    Verify 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 test

    Common test failures

    • Parameter placeholders? must become $1, $2, ...
    • Boolean values0/1 vs true/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.

Your Turso database is unchanged. To revert:

  1. Switch back to the Turso connection URL and auth token, and revert driver/ORM changes.
  2. If you need to export data created in DB9 back to Turso, export from DB9 and convert PostgreSQL SQL back to SQLite-compatible format.
  • 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.