DuckDB Node API

repository·main·Indexed 41 days ago

https://github.com/duckdb/duckdb-node-neo

High-level Node.js bindings for DuckDB (version 1.5.5-r.5) providing native Promise support, lossless data type integrity, and a developer-friendly interface. Features include support for in-memory and file-based databases, prepared statements, lazy streaming of large datasets, and efficient bulk data insertion via the Appender. Supports Linux, macOS, and Windows across arm64 and x64 architectures.

Tokens
27K
Snippets
68
Records
105
Agent score
51%

What's inside DuckDB

  1. Overview of DuckDB Node API

    main

    The DuckDB Node API is a high-level API designed for Node.js applications to use DuckDB. It provides a more ergonomic interface than the low-level @duckdb/node-bindings package, which adheres closely to the DuckDB C API.

    Key advantages over the legacy duckdb-node package include:

    • Native Promise support: No need for external async wrappers like duckdb-async.
    • DuckDB-native design: Unlike sqlite3-based wrappers, this is built specifically for DuckDB's capabilities.
    • Data Integrity: Lossless and efficient support for all DuckDB data types.
    • Binary-based: It wraps released DuckDB binaries rather than requiring a local rebuild of DuckDB.
    • Full Feature Set: Built on the DuckDB C API, exposing more functionality than previous Node implementations.
  2. Overview of DuckDB Node Bindings & API

    main

    This project provides Node.js bindings to the DuckDB C API, along with a high-level, user-friendly API designed for seamless DuckDB integration in Node.js applications. It consists of two primary layers:

    1. @duckdb/node-api: The high-level Node.js API.
    2. @duckdb/node-bindings: The low-level bindings to the DuckDB C API, distributed across various platform-specific packages (Darwin, Linux, and Win32 for both x64 and arm64 architectures).
  3. Comparison of DuckDB C API V1 and V2 features

    main

    The DuckDB C API is transitioning from V1 to V2. This transition involves significant changes in how functions are named, how errors are reported, and how memory ownership is handled.

    Features not yet available in V2

    • Appender: There is no V2 counterpart for the Appender (used for bulk-append scenarios). The project intends to cover bulk-append scenarios using other methods.
    • Conversion Helpers: Functions for converting Date, time, timestamp, hugeint, and decimal (e.g., duckdb_from_date, duckdb_to_timestamp) are absent in V2. Clients are expected to implement these as pure computation over plain structs. In the Node.js binding, these are implemented in TypeScript to avoid native round-trips.
    • Memory Helpers: duckdb_vector_size and various malloc/free helpers are missing.
    • Control Interfaces: Profiling info and Task/threading control (duckdb_execute_tasks) are not exposed in V2.
    • Instance Cache: Subsumed by the new 'environment' concept, though the mapping is not one-to-one (DuckDBInstanceCache remains a public type in the API package).

    Benefits of migrating to V2

    • Typed Error Codes: Replaces message-string matching with structured error codes.
    • Improved Type Safety: Function-info type tags become ordinary externals tags rather than guards against memory corruption.
    • Efficient Data Access: Supports dictionary and constant vector reading without requiring a flatten operation.
    • Async-Friendly Streaming: Uses WAITING-based streaming, which is better suited for async bindings than the V1 pending-result pump.
    • Advanced Features: Provides better support for table function filter/projection pushdown, catalog/table description, and replacement scans.
    • Standardized Identifiers: qname (qualified-name parsing/rendering) and identifier_render_quoted replace hand-rolled quoting logic.
  4. Understand V2 Result Streaming and Materialization

    main

    Unlike V1, the V2 C API does not have a 'materialized result' concept. Consequently, the V2 API layer does not include a DuckDBMaterializedResult class.

    All V2 results are streaming by nature. To work with results, use the DuckDBResultReader which provides helpers to accumulate chunks and access data.

    Key differences in behavior:

    • Streaming: Every V2 result is a stream.
    • Row Count: In V1, rowsChanged was a synchronous property. In V2, because results are streamed, determining the number of rows changed requires an asynchronous, state-changing call via result_drain (exposed through the API layer) to consume the stream.
  5. Control task evaluation for cooperative multithreading

    main

    To prevent long-running DuckDB tasks from blocking the Node.js event loop, you can use low-level methods to control task evaluation. This allows for better cooperative multithreading by splitting work into short tasks.

    Recommended Approach: Use startStreamThenRead methods. These are the best options for cooperative multithreading because they create a streaming result (preventing full materialization) and use pending results to split processing into short tasks.

    Low-level Approach:

    1. Use prepared.startStream() to get a pending result.
    2. Enter a loop checking pending.runTask() !== DuckDBPendingResultState.RESULT_READY.
    3. Use await sleep(1) or similar inside the loop to yield control back to the Node.js event loop.
    4. Once ready, call await pending.getResult().
    import { DuckDBPendingResultState } from '@duckdb/node-api';
    
    async function sleep(ms) {
      return new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    
    const prepared = await connection.prepare('from range(10_000_000)');
    const pending = prepared.startStream();
    while (pending.runTask() !== DuckDBPendingResultState.RESULT_READY) {
      console.log('not ready');
      await sleep(1);
    }
    console.log('ready');
    const result = await pending.getResult();
  6. Use Instance Cache to prevent multiple attachments

    main

    Multiple instances in the same process should not attach to the same database file. Use DuckDBInstance.fromCache() to ensure you reuse an existing instance for a specific database file.

    Using the default cache:

    const instance = await DuckDBInstance.fromCache('my_duckdb.db');

    Using an explicit cache:

    import { DuckDBInstanceCache } from '@duckdb/node-api';
    
    const cache = new DuckDBInstanceCache();
    const instance = await cache.getOrCreateInstance('my_duckdb.db');
  7. Understand Node Neo C API coverage gaps

    main

    A gap in Node Neo refers to a specific DUCKDB_C_API function that is currently unexposed in the Node Neo client, but meets three criteria:

    1. It is not deprecated in the C API.
    2. It has not been deliberately skipped (e.g., functions marked as destroyed in finalizer or consolidated into open).
    3. At least one other C-API-based client exposes it (providing a 'usefulness signal').

    Node Neo maintains a TODO: list for all unexposed functions. The 'gaps' are a subset of this list representing functions that other language clients (like Go, Rust, or C#) have already deemed useful enough to implement.

  8. Handle Errors in DuckDB C API V2

    main
    Unlike V1 which uses multiple error mechanisms, V2 uses a unified error reporting pattern. Functions return a DUCKDB_V2_ERROR code and provide an optional duckdb_v2_error_info_handle via a trailing err out-parameter. The caller is responsible for owning and destroying this error info handle. Error codes are a specific taxonomy (e.g., IO_FILE_NOT_FOUND, TYPE_CONVERSION) rather than simple success/error flags.
  9. Compare DuckDB C API V1 and V2

    main
    The DuckDB C API has transitioned from V1 to V2. V2 introduces significant changes to naming, error reporting, string handling, ownership models, and query execution. While V1 and V2 can coexist in the same translation unit due to distinct naming conventions, V2 is designed to be more robust, type-safe, and better suited for modern environments like N-API.
  10. Handling VARIANT columns in V2 bulk appends

    main

    Unlike V1, the DuckDB C API V2 supports VARIANT logical types (type ID 41) within data chunks and column data collections.

    In V1, attempting to create a data chunk with a VARIANT column would return a null handle, which could lead to native crashes in Node.js if not checked. In V2, duckdb_v2_data_chunk_create and duckdb_v2_column_data_collection_create_with_connection correctly handle VARIANT columns.

    Note for Node.js developers: While the underlying C API supports it, the current Node.js implementation of DuckDBVariantVector may have limitations on writing non-null values via setItem. Check the current implementation status if you require high-performance bulk appends with VARIANT types.

  11. Manage DuckDB API versioning and deprecation

    main

    In DuckDB 2.0, deprecation of the V1 API is managed via compile-time switches rather than compiler warnings. This means that using deprecated V1 functions will not trigger warnings unless specific macros are defined.

    Controlling Deprecation

    • V1 Deprecation: Controlled by DUCKDB_API_ALLOW_DEPRECATED. It defaults to 1. If you want to ensure you are not using deprecated V1 features, you can define DUCKDB_API_NO_DEPRECATED.
    • V2 Deprecation: Controlled by DUCKDB_V2_API_ALLOW_DEPRECATED, which also defaults to 1.
    • Node Neo Behavior: By default, Node Neo does not define these switches, so the defaults apply and all V1/V2 calls compile without warnings.

    Pinning API Versions

    Every declaration in the V1 header is now wrapped in a DUCKDB_API_VERSION_AT_LEAST(major, minor, patch) guard. To pin your application to an older surface, you can define the following macros:

    • DUCKDB_API_VERSION_MAJOR
    • DUCKDB_API_VERSION_MINOR
    • DUCKDB_API_VERSION_PATCH

    If not defined, the targeted version defaults to 1.5.6.