Deno SQLite Shell

This is a SQLite module for Deno, an alternative to deno-sqlite or deno-sqlite-plugin.

Instead of compiling SQLite to WASM or implementing a wrapper in Rust, this module has a simpler implementation: It spawns a long-running child SQLite CLI process and communicates with it via stdin/stdout.

Usage

API is similar to that of deno-sqlite, but mostly async since this module is delegating actual SQLite library usage to a child process. Also it doesn't have prepared statements, so performance can be hurt.

import { connect } from "https://deno.land/x/sqlite_shell/mod.ts";

// Connect to a database
const db = await connect("test.db");
await db.execute(
  "CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);

const names = ["Peter Parker", "Clark Kent", "Bruce Wayne"];

// Run a simple query
for (const name of names) {
  await db.execute("INSERT INTO people (name) VALUES (?)", [name]);
}

// Print out data in table
for await (const { name } of db.query("SELECT name FROM people")) {
  console.log(name);
}

// Close connection
await db.close();

How it works

Recent versions of SQLite CLI support JSON output mode, which makes it possible to deterministically parse and split query results. This module depends on this feature and will fail when used with an sqlite version that doesn't support the -json command line flag.

The connect function imported from the main module will automatically download the proper sqlite3 binary for the host platform and store in the user's cache directory. This is done to simplify using this module, as it doesn't depend on the sqlite3 installed on the host system which can have a different version.

To automatically download sqlite3 some extra dependencies are required (JSZip). If this behavior is not desired, it is possible to manually import Shell from the shell.ts module:

import { Shell } from "https://deno.land/x/sqlite_shell/shell.ts";
// Connect to a database
const db = await Shell.create({ databasePath: "test.db" });

In the above example the "sqlite3" binary installed on the PATH will be used. It is also possible to manually specify a path to "sqlite3" like so:

import { Shell } from "https://deno.land/x/sqlite_shell/shell.ts";
// Connect to a database
const db = await Shell.create({
  sqliteProgram: "/path/to/sqlite3",
  databasePath: "test.db",
});

Since JSON output is parsed, binary data cannot be read transparently like in other SQLite wrapper modules. If you SELECT a column that might have binary data, it must be encoded to hex and then decoded on JS side:

import { connect } from "https://deno.land/x/sqlite_shell/mod.ts";
import { decodeString } from "https://deno.land/std/encoding/hex.ts";

const db = await connect("test.db");
await db.execute(
  "CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
await db.execute("INSERT INTO people (name) VALUES (?)", [
  new Uint8Array([1, 2, 3]),
]);
for await (
  const { hexName } of db.query("SELECT hex(name) as hexName FROM people")
) {
  console.log(decodeString(hexName as string));
}
await db.close();

Each Shell instance owns a single SQLite CLI process, so if you need concurrent access, multiple Shell instances are required. In a server environment, you probably want to have a pool that assigns Shell instances to incoming connections.

Note that this module will not block special commands. Anything passed to query/execute will be forwarded to SQLite CLI. For example, it is possible to invoke .mode list to change the output format, which will completely break parsing.

Why?

I wrote this module for three reasons:

  • I/O performance on deno-sqlite is really hurt by WASM. In fact, even though this module communicates with subprocesses and has no prepared statements, you might find it will outperform deno-sqlite on some real world usage scenarios.
  • deno-sqlite-plugin is depending on --unstable, which might break on Deno updates.
  • But the main reason is that I'm new to Deno and wanted an exercise to get a bit more familiar with its standard I/O API, which is different than Streams API used in Node.js.