npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zukhruf/sqlite-snapshot

v0.8.0

Published

A read-only snapshot of a SQLite database that another process owns and writes, held by one read transaction so the tables agree, or by a checked copy when the owner holds the exclusive lock. Checks the columns a reader needs.

Readme

@zukhruf/sqlite-snapshot

Some programs keep their data in a SQLite database, and write it while they run. Another program can read that database, but the reader must not see half of a change. For example, it must not read a row from one table before a commit and a row from another table after it. Also, the newest rows can be in the WAL file, not in the database file. This package opens such a database read-only, and holds one moment with a read transaction. Thus each query of the snapshot sees the same moment, and the tables agree. This is also known as snapshot isolation of a WAL reader.

Some owners let no reader in. Chromium holds its History with SQLite's exclusive locking mode, so a read fails with "database is locked". Snapshot.ofExclusiveOwner reads such a database from a checked copy at the last commit of the owner.

The words in these documents have one meaning each. See the glossary in CONTEXT.md.

Use it

This project is an experiment. It uses only node:sqlite.

npm install @zukhruf/sqlite-snapshot
import { mkdtempDisposable } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';

import { Snapshot, UnavailableError } from '@zukhruf/sqlite-snapshot';

await using scratch = await mkdtempDisposable(join(tmpdir(), 'snapshot-'));
const path = join(scratch.path, 'notes.sqlite');

// The owner: another connection that writes the database through a WAL.
using owner = new DatabaseSync(path);
owner.exec(`PRAGMA journal_mode = WAL;
  CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT);
  INSERT INTO notes (title) VALUES ('first')`);

{
  using snapshot = new Snapshot(path);
  snapshot.requireColumns({ notes: ['id', 'title'] });
  owner.exec("INSERT INTO notes (title) VALUES ('second')");
  console.log(snapshot.all('SELECT title FROM notes'));
}

using next = new Snapshot(path);
console.log(next.missingColumns({ notes: ['title', 'body'] }));
console.log(next.all('SELECT count(*) AS notes FROM notes'));

try {
  new Snapshot(join(scratch.path, 'missing', 'notes.sqlite'));
} catch (error) {
  console.log(error instanceof UnavailableError);
}

Output:

[ [Object: null prototype] { title: 'first' } ]
[ 'notes.body' ]
[ [Object: null prototype] { notes: 2 } ]
true

The rules

  • new Snapshot(path) opens the database read-only. It runs BEGIN and a first read at once, so the snapshot holds the moment when it opens, not the moment of the first query of the caller.
  • The snapshot never opens the file with immutable. The owner keeps a persistent WAL, and the newest rows are in it. ADR 0001 tells why.
  • Hold a snapshot only while you read. An open read transaction stops the checkpoint of the owner, and then the WAL of the owner grows. Dispose the snapshot with using, or call [Symbol.dispose]().
  • all(sql, ...parameters) gives all rows. iterate(sql, ...parameters) gives the rows one at a time, for a table that is too large to hold in memory.
  • requireColumns(columns) checks the columns that the reader needs, as { table: [column, ...] }. When one is missing, it disposes the snapshot and throws MissingColumnsError.
  • missingColumns(columns) gives the missing columns as table.column, and keeps the snapshot open. A reader whose queries use different tables checks each query with it.
  • The package throws its own errors: UnavailableError and MissingColumnsError. UnavailableError is the class of @zukhruf/sqlite-open, which opens the connection. The package exports it, so a catch with instanceof works with either package. A host that needs its own error translates the error of the package at its boundary, and keeps the error of the package as cause. ADR 0002 tells why.
  • Dispose commits the read transaction and closes the connection. A second dispose does nothing.

An owner that lets no reader in

An exclusive owner holds SQLite's exclusive locking mode. It takes the lock at its first write and keeps it until it closes the database. Chrome, Edge, Brave, Arc and Dia hold their History this way. new Snapshot(path) then fails with SQLITE_BUSY (5). Read the database with Snapshot.ofExclusiveOwner(path):

import { DatabaseChangingError, Snapshot } from '@zukhruf/sqlite-snapshot';

try {
  using history = await Snapshot.ofExclusiveOwner(path);
  history.requireColumns({ urls: ['id', 'url', 'title'] });
  for (const row of history.iterate('SELECT id, url, title FROM urls'))
    console.log(row.url);
} catch (error) {
  if (!(error instanceof DatabaseChangingError)) throw error;
  console.log('The owner kept writing. Read it again later.');
}
  • The snapshot reads an owner copy: a clone of the database and its rollback journal in a private folder. APFS and Btrfs clone a file with no time and no space. Other file systems copy the bytes.
  • The file stamps of the database and the journal must be the same before and after the copy. When the owner wrote during the copy, the snapshot tries again at once, up to 5 times. Then it throws DatabaseChangingError.
  • The snapshot opens the copy for writing one time, so that SQLite rolls back a hot journal in the copy. The snapshot holds the last commit of the owner, and no page of a transaction that the owner did not commit.
  • The snapshot never writes a file of the owner. A journal that a dead owner left stays as it is.
  • Dispose deletes the copy. Dispose the snapshot as soon as the read ends.
  • MissingColumnsError and UnavailableError name the path of the owner, not the copy.

ADR 0003 tells why.

Errors

| Error | When | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | UnavailableError | SQLite cannot open the file: SQLITE_CANTOPEN (14), for a missing file or a file that the system denies, a privacy denial too. path is the path. cause is the error of SQLite. ofExclusiveOwner throws it when it cannot copy the file (ENOENT, EACCES, EPERM); then cause is the error of the file system. | | MissingColumnsError | requireColumns finds a missing column. path is the path, and missing gives each missing column as table.column. The snapshot is closed. | | DatabaseChangingError | ofExclusiveOwner found a write of the owner during each of 5 copies. path is the path, and attempts is the number of copies. No copy stays. | | The error of SQLite | All other failures, for example a file that is not a database. The constructor closes the connection before it throws. |

The same concepts in other systems

| System | What it does | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | SQLite WAL readers | A read transaction sees the database as it was at its first read, also while a writer commits. An open read stops the checkpoint at its end mark. See "Write-Ahead Logging" in the SQLite documentation. | | Datasette immutable mode | It opens a file with immutable=1, for a file that nobody writes. SQLite then skips the locks and the WAL. This package is the opposite case: the owner writes the file. | | sqlite3 -readonly | It opens a database read-only. Each statement outside a transaction can see a different moment. | | Chromium sql::Database | It opens each database with the exclusive locking mode, as exclusive_locking_ = true in sql/database.h. No other process can read the database while the browser runs. | | yt-dlp _open_database_copy | It copies the cookie database of a running Chromium and reads the copy. It copies the database file only, with no check. |

Documentation

Development

This package is part of the zukhruf workspace. Run the commands from the workspace root.

npm install
npx nx run sqlite-snapshot:test        # builds, then runs the tests in src/
npx nx run sqlite-snapshot:typecheck   # formats, lints, then type checks
npx nx run sqlite-snapshot:build       # compiles src/ to dist/
src/
  index.ts                    the exports
  snapshot.ts                 Snapshot and SnapshotColumns
  owner-copy.ts               the checked copy of an exclusive owner
  missing-columns-error.ts    MissingColumnsError
  database-changing-error.ts  DatabaseChangingError
  snapshot.test.ts            the tests, on real SQLite files
  exclusive-owner.test.ts     the tests, with an owner in another process