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

libSQL is a fork of SQLite that reads and writes ordinary SQLite files and adds features on top, like vector search. Not all of those features survive when the file is opened in stock SQLite. In this article I’ll walk you through what libSQL adds, with tested examples, and what still works back in plain SQLite.

TLDR

  • libSQL is an open-source fork of SQLite maintained by Turso. It uses the SQLite file format.
  • Use it from Node.js through @libsql/client, with url: "file:local.db" for a local file.
  • It adds vector columns and indexes, plus a wider ALTER TABLE, among other features.
  • Once a table has a libSQL vector index, stock SQLite miscounts its rows and can’t insert or delete in it.
  • The README now points new projects to Turso Database, a separate engine, so check the project status before you start.

What is libSQL, and what will we build?

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, such as vector indexes and a wider ALTER TABLE. The README also says it “inherits SQLite’s fundamental limitations such as the single-writer model”.

The README’s status note says: “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.” That link goes to Turso Database, a different project: “an in-process SQL database written in Rust, compatible with SQLite” (Turso Database README).

To show you how libSQL behaves, I will build a small notes table with 3-dimensional vectors in a local file. Then I’ll search it and change a column definition, and open the file with the stock sqlite3 shell.

Prerequisites

You need Node.js. These are the versions I tested with. The client bundles its own engine, which reports an older SQLite version than the shell:

v24.18.0
@libsql/client 0.18.0
libSQL engine reports SQLite 3.45.1
stock sqlite3 3.51.0

Building a vector-enabled notes table with libSQL

The whole example is one script, libsql_demo.mjs.

Step 1: Create the project

In an empty folder, install the client, pinned to the version on this page:

$ npm install @libsql/client@0.18.0

Step 2: Store vectors, search them, and change a column

createClient({ url: "file:local.db" }) opens a local database file (@libsql/client). The same client can also connect to a remote Turso database. Vector columns use F32_BLOB(3). Values are built with vector32() and compared with vector_distance_cos(). You index them with libsql_vector_idx() and search with vector_top_k(idx_name, q_vector, k) (Turso: AI and embeddings). The script also runs ALTER TABLE … ALTER COLUMN, a libSQL extension that replaces a column’s whole definition (libSQL extensions):

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))

The last line is the table’s schema after ALTER COLUMN. It dropped NOT NULL from body, because the new definition didn’t repeat it.

Running it in stock SQLite

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

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

The file with the vector index is different. Stock SQLite still reads rows, but it gives a wrong count for that table. It also 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
$ sqlite3 local.db ".tables"
libsql_vector_meta_shadow  notes_vec_idx_shadow
notes                      other

The query plan shows why. Stock SQLite answers count(*) from the vector index, whose entries libSQL keeps in a separate table, notes_vec_idx_shadow in the .tables list. So it sees 0 entries. Writes fail because stock SQLite doesn’t have the libsql_vector_idx() function.

Common problems

These are the issues to plan for before you commit to libSQL:

  • Other tools need to write the file. Avoid libSQL vector indexes on those tables, or use an extension that stock SQLite can load, such as sqlite-vec.
  • ALTER COLUMN loses constraints. It replaces the whole column definition, so repeat NOT NULL and any other constraint you want to keep.
  • Version differences. The client’s bundled engine reported SQLite 3.45.1 here, older than the stock shell, so newer SQLite features may be missing.

Conclusion

libSQL is SQLite-compatible until you use its own features. Use it when you want one client for a local file and a remote Turso database, or libSQL’s own features. Make sure libSQL is the only engine writing the file. For a local database with no extra dependency, stock SQLite through your language’s built-in driver is enough.

Further reading

Tested with Node.js 24.18.0, @libsql/client 0.18.0 (engine SQLite 3.45.1) and the sqlite3 shell 3.51.0 on 7 October 2026.