libSQL: What It Adds to SQLite, With Tested Examples and Compatibility Notes

libSQL “is an open source, open contribution fork of SQLite, created and maintained by Turso” (libSQL README). It reads and writes the SQLite file format and adds features SQLite doesn’t have: remote access, embedded replicas, a native vector type with vector indexes, and a wider ALTER TABLE. Its README also says it “inherits SQLite’s fundamental limitations such as the single-writer model”.

Status note from the libSQL README: “If you’re starting a new project, you probably want to look into Turso. libSQL is actively maintained, but new features are being developed in Turso.”

The examples were run on 6 October 2026 with Node.js 24.18.0 and @libsql/client 0.18.0, whose bundled engine reports SQLite 3.45.1, against a local file. The compatibility checks used the stock sqlite3 shell 3.51.0. Outputs are pasted from the run.

Local file, SQL, vectors and ALTER COLUMN

import { createClient } from "@libsql/client";

const db = createClient({ url: "file:local.db" }); // a local file; no server needed

await db.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL, embedding F32_BLOB(3))");
await db.batch([
  { sql: "INSERT INTO notes (body, embedding) VALUES (?, vector32(?))", args: ["sqlite tips", "[0.9, 0.1, 0.0]"] },
  { sql: "INSERT INTO notes (body, embedding) VALUES (?, vector32(?))", args: ["postgres tips", "[0.1, 0.9, 0.0]"] },
  { sql: "INSERT INTO notes (body, embedding) VALUES (?, vector32(?))", args: ["sqlite wal", "[0.8, 0.2, 0.1]"] },
], "write");

const rs = await db.execute({ sql: "SELECT id, body FROM notes WHERE body LIKE ?", args: ["sqlite%"] });
console.log(rs.columns, rs.rows.map((r) => [r.id, r.body]));

// Exact nearest neighbours with a distance function
const near = await db.execute({
  sql: "SELECT body, round(vector_distance_cos(embedding, vector32(?)), 3) AS distance FROM notes ORDER BY distance LIMIT 2",
  args: ["[1, 0, 0]"],
});
console.log(near.rows.map((r) => [r.body, r.distance]));

// Indexed nearest neighbours
await db.execute("CREATE INDEX IF NOT EXISTS notes_vec_idx ON notes (libsql_vector_idx(embedding))");
const top = await db.execute({
  sql: "SELECT notes.body FROM vector_top_k('notes_vec_idx', vector32(?), 2) AS v JOIN notes ON notes.rowid = v.id",
  args: ["[1, 0, 0]"],
});
console.log(top.rows.map((r) => r.body));

// libSQL's ALTER COLUMN extension
await db.execute("ALTER TABLE notes ALTER COLUMN body TO body TEXT");
console.log((await db.execute("SELECT sql FROM sqlite_schema WHERE name = 'notes'")).rows[0].sql);
db.close();
$ node libsql_demo.mjs
[ 'id', 'body' ] [ [ 1, 'sqlite tips' ], [ 3, 'sqlite wal' ] ]
[ [ 'sqlite tips', 0.006 ], [ 'sqlite wal', 0.037 ] ]
[ 'sqlite tips', 'sqlite wal' ]
CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT, embedding F32_BLOB(3))

What the script uses, from the docs:

  • url: "file:local.db" for a local database file (@libsql/client).
  • Vector columns use F32_BLOB(3), values are built with vector32(), compared with vector_distance_cos(), indexed with libsql_vector_idx() and searched with vector_top_k(idx_name, q_vector, k) (Turso: AI and embeddings).
  • ALTER TABLE … ALTER COLUMN x TO x … replaces the column’s whole definition (libSQL extensions). Here that dropped NOT NULL from body, because the new definition didn’t repeat it.

Compatibility with stock SQLite

A libSQL file without libSQL-only features is an ordinary SQLite database. One created through @libsql/client with a plain table opens and checks clean in the stock shell:

$ sqlite3 plain.db "SELECT count(*) FROM t; PRAGMA integrity_check;"
2
ok

Once a libSQL vector index exists, stock SQLite still reads rows, but it gives a wrong count for that table, can’t insert or delete rows in it, and its quick_check reports the index as broken. An UPDATE that doesn’t touch the vector column still worked, and so did writes to other tables in the same file:

$ sqlite3 local.db "SELECT body FROM notes;"
sqlite tips
postgres tips
sqlite wal
$ sqlite3 local.db "SELECT count(*) FROM notes;"
0
$ sqlite3 local.db "EXPLAIN QUERY PLAN SELECT count(*) FROM notes;"
QUERY PLAN
`--SCAN notes USING COVERING INDEX notes_vec_idx
$ sqlite3 local.db "INSERT INTO notes (body) VALUES ('from sqlite3');"
Error: in prepare, unknown function: libsql_vector_idx()
  INSERT INTO notes (body) VALUES ('from sqlite3');
                         error here ---^
$ sqlite3 local.db "UPDATE notes SET body = 'x' WHERE id = 1;"
$ sqlite3 local.db "DELETE FROM notes WHERE id = 1;"
Error: in prepare, unknown function: libsql_vector_idx()
$ sqlite3 local.db "CREATE TABLE other (x); INSERT INTO other VALUES (1); SELECT count(*) FROM other;"
1
$ sqlite3 local.db "PRAGMA quick_check;"
wrong # of entries in index notes_vec_idx

The query plan shows why: stock SQLite answers count(*) from the vector index, which libSQL keeps elsewhere (the _shadow tables), so it sees 0 entries. Writes fail because stock SQLite doesn’t have the libsql_vector_idx() function. If other tools must read or write that table, avoid libSQL vector indexes, or use an extension that stock SQLite can load, such as sqlite-vec.

When to use libSQL

  • You want a hosted or replicated SQLite-compatible database (Turso), and the same client for local and remote.
  • You want vector search and are happy for libSQL to be the only engine writing the file.
  • You need to change column types or constraints in place.

If you only need a local database, stock SQLite through Python’s sqlite3, node:sqlite or better-sqlite3 has no extra dependency. See Python sqlite3 and SQLite in Node.js.

sqlite-vec, SQLite JSON, and the official sources: libSQL README, libSQL extensions, @libsql/client, Turso: AI and embeddings.