A persistent SQLite for the Web

Photo by Alexander Sinn on Unsplash

WebSQL left the Web a while ago, but WebAssembly brought it back, as nothing on the Web is as simple, yet powerful, as this wonderful Open Source project.

This post summarizes my idea behind sqlite-worker, a module that brings sql.js in Web pages and/or workers, and persists through the only option we have these days: IndexedDB.

A quick recap

”, said nobody ever, and the reason is simple:

  • the API is everything but straight forward (low-level is the excuse)
  • querying it makes little sense, as it cannot do much, compared to
  • but hey, it can store blobs (not everywhere) and buffers (bingo!)

The community, myself included, has never been too happy about the choice made, and while reasons for dropping made sense, nothing ever filled that gap … until recent days, where or programs could target , and could run everywhere, most importantly, on the Web.

Connecting the dots

So, there is this project, which brings to the Web, and there is IndexedDB, which can store the buffer exported by .

What are we missing then? Why are not all on board?

  • legacy browsers won’t work (but personally I wouldn’t care for PWAs, as legacy browsers don’t run PWAs neither)
  • is not persistent: it runs on RAM, it stores the DB on RAM too, but then it vanishes on each refresh. Amazing for a forever-running browser/runtime that never crashes, but pointless as persistent DB
  • (IMHO) is fun, but dealing with “” is less fun, so that projects such as sqlit-tag becomes the strawberry on top to keep it simple

Web or Workers

import {init, SQLiteWorker} from 'sqlite-worker';// direct bootstrap
// init({name: 'my-db'});
// Web Worker bootstrap
SQLiteWorker({name: 'my-db'}).then(async ({all, get, query}) => {
await query`CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY, value TEXT)`;
const {total} = await get`SELECT COUNT(id) as total FROM todos`;
if (total < 1) {
console.log('Inserting some value');
await query`INSERT INTO todos (value) VALUES (${'a'})`;
await query`INSERT INTO todos (value) VALUES (${'b'})`;
await query`INSERT INTO todos (value) VALUES (${'c'})`;
}
console.log(await all`SELECT * FROM todos`);
});

With a few optional bootstrap properties, this project offers an extremely simple interface to deal with the database and perform these operations:

  • get a single row, instead of all of them
  • retrieve all rows, if any
  • query the db to , , or entries

All these helpers are that result into real bindings, so that all values passed to the query are always safely, and natively, escaped.

// the following operation
for (let i = 0; i < 3; i++)
query`INSERT INTO table VALUES (${i})`;
// will be the equivalent of this one
db.run('INSERT INTO table VALUES (?)', [0]);
db.run('INSERT INTO table VALUES (?)', [1]);
db.run('INSERT INTO table VALUES (?)', [2]);

Keep It Simple

The project runs on RAM, is very fast by default, and if it fails during a valid operation it’s likely due the device being out of RAM, so that while transactions are a must know and use, when it comes to databases, a fully virtual, memory based, database that works on a single client and uses such client’s resources, won’t likely blow, unless it’s too late (browser crashed, and so on).

Since these cases should be more than rare/edge, and a crashed browser tab won’t ever restore a transaction, I think we can start “” with this module, and see how many things we could do … here a few:

  • MATCH AGAINST queries, for FTS5 tables, to be able to search text content without needing exact input match
  • indexed, ordered, filtered, searches, without having on our way to manually do what was born to do
  • scale up to the allowed storage space, which is something WebSQL never offered, or better, it was limited by default to 5MB, scaling up to maximum 50MB on user permission and no more …

Here there is simplicity, performance, and freedom to use more space, respecting our users first, of course, but doing nothing different than what an IndexedDB could do already, in terms of storage, except, it’s much better.

How does it persist?

Well, as mentioned, , , and queries, performed via the query tag, schedule an update timer that tries to not bee too greedy on the IndexedDB side, but it eventually update the unique entry the storage uses, with the exported uint8 buffer.

Through the update callback, it is also possible to store remotely the latest version of the DB, so that users might erase its phone and have it back from where they’ve left, and everything else should be explained in the .

P.S. this module should work on Web Extensions too, but I might provide a database hook in the future to make it portable for literally any kind of storage, not just , but hey … I need you help me testing, and confirming, everything is fine ♥

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store