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

@perryts/tursodb

v0.2.0

Published

Tursodb (pure-Rust SQLite-compatible engine) bindings for the Perry TypeScript-to-native compiler.

Readme

@perryts/tursodb

Native bindings for Tursodb — a pure-Rust SQLite-compatible engine — for the Perry TypeScript-to-native compiler.

Closes PerryTS/perry#424.

What this is

A Perry "native library" package: a Rust crate exporting extern "C" symbols that the Perry compiler links into your TypeScript program. From your TypeScript code you import tursodb like any npm package; under the hood every method call resolves to a direct call into the bundled staticlib — no Node addon, no IPC, no JSON marshalling.

This package contains:

  • src/lib.rs — the Rust crate that wraps turso and exposes js_turso_* extern "C" symbols
  • src/index.d.ts — the TypeScript surface (tursodb module declaration) Perry resolves at compile time
  • Cargo.toml — staticlib build config consumed by the Perry linker
  • package.json — includes the perry.nativeLibrary manifest block

Install

bun add @perryts/tursodb
# or
npm install @perryts/tursodb

The package's package.json declares a perry.nativeLibrary block (see the manifest spec) which Perry's compiler reads at link time to discover the staticlib + extern "C" symbols. No post-install build step — Perry compiles the Rust crate as part of your project's build.

Quick start

import * as tursodb from "tursodb";

const db = await tursodb.open(":memory:");

await tursodb.execBatch(db, `
  CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);
  INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
  INSERT INTO users (name, email) VALUES ('Bob',   '[email protected]');
`);

const rows = await tursodb.queryAll(db, "SELECT * FROM users ORDER BY id");
for (const row of rows) {
  console.log(row.id, row.name, row.email);
}

await tursodb.close(db);

API reference

open(path)

function open(path: string): Promise<Database>

Open a database connection. Use ":memory:" for an in-memory database, otherwise pass a filesystem path (created if missing). Resolves with an opaque Database handle. Reject reasons surface the underlying turso error.

const memory = await tursodb.open(":memory:");
const onDisk  = await tursodb.open("./app.db");

exec(db, sql)

function exec(db: Database, sql: string): Promise<number>

Execute a single non-query statement. Resolves with the number of rows affected. Use this for INSERT / UPDATE / DELETE and DDL.

const inserted = await tursodb.exec(db, "INSERT INTO users (name) VALUES ('Carol')");
const removed  = await tursodb.exec(db, "DELETE FROM users WHERE id = 2");

execBatch(db, sql)

function execBatch(db: Database, sql: string): Promise<void>

Execute multiple ;-separated statements in one round-trip. Useful for migrations, schema bootstrap, and seed scripts. Does not return row counts.

await tursodb.execBatch(db, `
  CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, body TEXT);
  CREATE INDEX idx_posts_title ON posts(title);
`);

queryAll(db, sql)

function queryAll(db: Database, sql: string): Promise<Row[]>

Run a SELECT and resolve with every row as an object keyed by column name. Equivalent to better-sqlite3's stmt.all().

const rows = await tursodb.queryAll(db, "SELECT id, name FROM users ORDER BY id");
// rows: [{ id: 1, name: "Alice" }, { id: 3, name: "Carol" }]

queryOne(db, sql)

function queryOne(db: Database, sql: string): Promise<Row | null>

Run a query and resolve with the first row, or null if the result set is empty. Equivalent to better-sqlite3's stmt.get().

const alice = await tursodb.queryOne(db, "SELECT * FROM users WHERE name = 'Alice'");
console.log(alice?.email);

Parameterized variants — execWith / queryAllWith / queryOneWith (v0.2.0)

Use ? placeholders in the SQL and bind from a params array. Use these whenever any value comes from untrusted input — the unparameterized variants only accept static SQL.

function execWith(db: Database, sql: string, params: Param[]): Promise<number>;
function queryAllWith(db: Database, sql: string, params: Param[]): Promise<Row[]>;
function queryOneWith(db: Database, sql: string, params: Param[]): Promise<Row | null>;

// `Param` types: string | number | boolean | null | number[] (Uint8 → Blob)
type Param = string | number | boolean | null | number[];
const rows = await tursodb.execWith(
  db,
  "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
  ["Eve", "[email protected]", 31],
);

const user = await tursodb.queryOneWith(
  db,
  "SELECT * FROM users WHERE email = ? AND active = ?",
  ["[email protected]", true],
);

// Blob: pass a Uint8Array as a number[] (each element 0..=255)
await tursodb.execWith(
  db,
  "INSERT INTO files (sha256, body) VALUES (?, ?)",
  ["abc123", [0x89, 0x50, 0x4e, 0x47]],  // PNG header
);

Object params (e.g. { foo: "bar" }) are rejected at runtime since SQLite has no native JSON column type — JSON.stringify first if you want to store a structured value.

lastInsertRowid(db)

function lastInsertRowid(db: Database): number

Synchronous accessor for the rowid of the most recent successful INSERT on this connection. Returns 0 if no insert has happened yet.

await tursodb.exec(db, "INSERT INTO users (name) VALUES ('Dan')");
const newId = tursodb.lastInsertRowid(db);

isAutocommit(db)

function isAutocommit(db: Database): boolean

Synchronous. Returns true when the connection is outside an explicit transaction, false while a BEGIN block is open.

await tursodb.exec(db, "BEGIN");
tursodb.isAutocommit(db); // false
await tursodb.exec(db, "COMMIT");
tursodb.isAutocommit(db); // true

close(db)

function close(db: Database): boolean

Synchronous. Drops the connection and frees the underlying handle. Returns true on success, false if the handle was already closed or invalid. Calling any other function on a closed handle rejects with "tursodb: invalid handle".

tursodb.close(db);

Types

Exported from the tursodb module declaration in src/index.d.ts:

type Database = number & { readonly __tursodb: unique symbol };
type Value    = string | number | null;
type Row      = Record<string, Value>;

Column-value mapping (from the Rust turso_value_to_js):

| SQLite type | TypeScript value | |---|---| | NULL | null | | INTEGER | number | | REAL | number | | TEXT | string | | BLOB | string (lowercase hex) |

BLOB columns are rendered as a lowercase hex string to keep the wrapper dependency-free; decode them in user code if you need raw bytes.

Transactions

Use BEGIN / COMMIT / ROLLBACK via exec. There is no high-level transaction wrapper yet.

await tursodb.exec(db, "BEGIN");
try {
  await tursodb.exec(db, "INSERT INTO users (name) VALUES ('Eve')");
  await tursodb.exec(db, "INSERT INTO users (name) VALUES ('Frank')");
  await tursodb.exec(db, "COMMIT");
} catch (err) {
  await tursodb.exec(db, "ROLLBACK");
  throw err;
}

Error handling

Async functions reject with an Error whose message is prefixed by the operation, e.g. tursodb queryAll: <upstream message>. An invalid or already-closed handle rejects with "tursodb: invalid handle". Sync functions (lastInsertRowid, isAutocommit, close) do not throw — lastInsertRowid returns 0 on a bad handle, isAutocommit returns false, close returns false.

Status & roadmap

MVP. What's there:

  • open / close
  • exec / execBatch
  • queryAll / queryOne (rows materialized as objects keyed by column name)
  • lastInsertRowid / isAutocommit

Known gaps, tracked in PerryTS/perry:

  • Parameter binding (? placeholders) — needs a JS Array<JsValue> reader on the perry-ffi side that maps onto turso's IntoParams. Until then, embed values directly in SQL — but only for trusted input.
  • Prepared-statement objects (stmt.all() / stmt.get() / stmt.run() style)
  • Streaming / iterator-based row reads
  • Native BLOB (Uint8Array) values instead of hex strings
  • High-level transaction helper

Versioning

Pre-1.0. The perry.nativeLibrary.abiVersion (currently 0.5) is a hard pin against Perry's perry-ffi ABI — bump it in lockstep with the Perry release that the bindings target.

License

MIT — see LICENSE.