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

@knockdata/duckdb

v1.5.5-r.6

Published

DuckDB for querying data files — native addon and wasm from one source

Readme

@knockdata/duckdb

DuckDB built for querying data files — a database file, a parquet, a csv, an iceberg or ducklake table, local or in a bucket. One source produces both a native N-API addon and a wasm module, so the same query(sql) works in Node and in the browser.

native addon   37 MB   9 extensions linked in statically
wasm           26 MB   5 extensions, single threaded
glue           72 KB   wasm/duckdb.js, bundles with your app

Install

npm install @knockdata/duckdb

npm brings the addon for your machine along with it. Each platform's binary is its own package (@knockdata/duckdb-darwin-arm64, -linux-x64, -win32-x64, …) listed under optionalDependencies with os and cpu fields, so the resolver downloads exactly one and skips the rest. Nothing is fetched by an install script, and everything is covered by your lockfile. If no binary matches your platform, the wasm runs instead.

Use

import DuckDB from '@knockdata/duckdb'

const db = await DuckDB('sales.duckdb')            // a path in Node, a File or bytes in the browser

await db.query('SELECT id, name FROM main.users LIMIT 2')
// → [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]

await db.close()

query(sql) returns rows and close() releases the file. exec and run are the same function under other names, for callers that already speak those. A database file is opened READ_ONLY on a single thread, so it is never rewritten and no .wal is left beside it.

Integers and floats come back as JavaScript numbers, booleans as booleans, and everything DuckDB has that JavaScript does not — dates, timestamps, decimals, hugeints, lists, structs — as the string DuckDB itself would print.

Querying files rather than opening one

A parquet or a csv is not a database, so there is nothing to open: start an in-memory database and name the file in the SQL. That needs writing, because reading a file puts temporary state somewhere, and READ_ONLY refuses to open an in-memory database at all:

const db = await DuckDB(':memory:', { readOnly: false })
await db.query("SELECT * FROM 'sales.parquet' LIMIT 100")

readOnly defaults to true for a path and false for ':memory:'.

In the browser there is no filesystem to read from, so hand the bytes over first and query them by the name you gave them:

await db.load('sales.parquet', bytes)
await db.query("SELECT * FROM 'sales.parquet' LIMIT 100")

Listing what is in a database

There is no getEntries here, on purpose. "A schema is a folder, a table looks like that, here is a page of rows" is an application's shape, not DuckDB's — bake one app's answer into the engine package and the next app spends its time fighting it. Ask DuckDB directly:

await db.query(`SELECT schema_name FROM duckdb_schemas()
  WHERE database_name = current_database() ORDER BY schema_name`)

await db.query(`SELECT table_name, table_type FROM information_schema.tables
  WHERE table_catalog = current_database() AND table_schema = 'main' ORDER BY table_name`)

await db.query('SELECT * FROM "main"."users" LIMIT 100 OFFSET 0')

Identifiers cannot be bound as parameters, so a schema or table name is interpolated — double any embedded quote (name.replaceAll('"', '""')) before you do.

In the browser

The wasm binary is 26 MB, so this package does not decide how you get it — serve it, cache it, however suits your app — you just hand over the bytes once:

import { setWasmBinary } from '@knockdata/duckdb/browser.js'

setWasmBinary(await (await fetch('/duckdb/duckdb.wasm')).arrayBuffer())

The file to serve is node_modules/@knockdata/duckdb/wasm/duckdb.wasm. The 72 KB of glue next to it bundles with your app normally. The wasm build is single-threaded, so it needs no SharedArrayBuffer and no COOP/COEP headers.

In a single-file bundle

An SEA or an esbuild bundle has no node_modules to resolve against. Unpack the two files wherever you like and point the package at them:

import { setEngineDir } from '@knockdata/duckdb/engineDir.js'

setEngineDir('/somewhere/duckdb')   // holding duckdb_napi.node and wasm/duckdb.wasm

engineDir.js is a separate entry, not part of the main one, because it reads the filesystem — importing it from the root would pull node:fs into every browser bundle. wasmPath() from the same module answers where the wasm is, which is what a server serving /duckdb/duckdb.wasm needs.

What is linked in

Extension loading is compiled out (DISABLE_EXTENSION_LOAD=1), so the engine can never fetch or LOAD anything at runtime: what is in the binary is all there will ever be. The two sets live in extension/config-native.cmake and extension/config-wasm.cmake.

| | native | wasm | |---------------------------------------------|--------|------| | core_functions, parquet, json, icu, inet | yes | yes | | excel, avro, ducklake | yes | — | | objectfs (ours) | yes | — |

The wasm set stops there for two reasons: the table formats would push a browser download well past 26 MB, and objectfs needs a socket that a wasm build does not have.

Three are absent from both. encodings reads CSVs in legacy code pages and carries a conversion table for each one — a 452 MB static archive against 33 MB for every other extension put together, which roughly doubled the addon.

delta and iceberg are absent for the same reason, which is what they cost to answer one question. delta is a C++ shell over delta-kernel-rs: a rust toolchain on every build machine, a 4.3 GB build tree, a 103 MB static archive, and through rustls the aws-lc-sys BoringSSL fork that never once compiled on windows arm64. iceberg calls find_package(AWSSDK REQUIRED) unconditionally, so it brings the AWS SDK, curl and openssl — about 50 MB of archives and the longest pole in every build — for REST, Glue and S3Tables catalogs.

The question both answer is which parquet files in this folder is the table made of, and both write the answer down: Delta in _delta_log, Iceberg through metadata.json → manifest list → manifests, which are avro and therefore readable by the avro extension above. So the reader that consumes this package walks those itself and queries the live files with read_parquet — schema evolution by field id, partition values, position and equality deletes, deletion vectors in both formats. Dropping the two extensions took the addon from 87 MB to 37 MB, and the vcpkg libraries underneath it from about forty to eight.

No file this engine reads comes off the network by itself, and there is no exception. httpfs, aws and azure are absent, the built-in httplib is compiled out (DISABLE_BUILTIN_HTTPLIB=1), and with iceberg went the last HTTP client of any kind. The one socket left is objectfs, which claims s3://, gs://, gcs://, az://, abfs:// and abfss:// and answers them by asking the explorer's web server, which already holds the sign-in and the cache. It talks to 127.0.0.1:8080 unless the objectfs_server setting or the OBJECTFS_SERVER environment variable says otherwise.

Why this exists

The official @duckdb/node-bindings ships a 117 MB libduckdb.dylib — a fat binary, so about 57 MB for one architecture — beside a 425 KB addon; effectively all of the weight is the engine, and it comes with extension loading and an HTTP client compiled in. Our build is the same engine rebuilt at the pinned upstream tag with the extension set above, DuckDB's own SMALLER_BINARY, -fvisibility=hidden, no shell and no tests. The native artifact is a single self-contained .node with the engine and all nine extensions linked in — no .dylib to ship beside it, nothing downloaded on first use, and no code path that could load an extension we did not build.

The API is the other half of the point. Reading a file through the official client means instances, connections, readers and materialised results; here it is query and close, returning plain objects, with the same shapes from both engines.

Build it yourself

bash build.sh          # native addon → build/Release/duckdb_napi.node
bash build.sh wasm     # wasm module  → wasm/duckdb.js + wasm/duckdb.wasm

build.sh shallow-clones upstream DuckDB at the tag matching this package's version (there is no version= to edit — it reads package.json), so nothing is vendored here. DuckDB has no amalgamation: it is ~1,600 C++17 translation units, cmake is required, and with this extension set a cold build takes well over an hour.

Two things have to be on the machine: cmake, and emcc for the wasm target. vcpkg is bootstrapped by the script itself if VCPKG_TOOLCHAIN_PATH is unset — iceberg, avro and excel declare vcpkg dependencies, and the script runs DuckDB's own configure pass first to merge their manifests into one. TARGET_ARCH cross-builds on macOS, where one Xcode targets both architectures.

What is actually ours is small: napi/duckdb_napi.c and wasm/shim.c are two thin C surfaces over DuckDB's C API, extension/objectfs is the cloud filesystem, plus the JS that picks an engine and hands back a handle.

Releasing

./release.sh tags the version already in package.json and pushes the tag; .github/workflows/release.yml does the rest — six native legs each publish their own platform package, and a seventh builds the wasm and publishes the universal package last, because that one names the platform versions in its optionalDependencies. A rerun skips whatever is already on npm, so a half-published release can be finished without burning a revision.

Versioning

<upstream duckdb version>-r.<revision>1.5.5-r.1 is upstream v1.5.5, our first build of it. A new revision means we changed the build or the JS; a new base version means DuckDB moved. Pin exactly.

License

MIT for the code in this repository. DuckDB itself is MIT, © DuckDB Labs and contributors.