DuckDB Node Neo 客户端

Jeff Raymakers
2024-12-18 · 7 分钟

TL;DR: 全新的 DuckDB Node 客户端 “Neo” 提供了一种强大且友好的方式来使用您喜爱的数据库。

隆重推出最新的 DuckDB 客户端 API:DuckDB Node “Neo”

您可能熟悉 DuckDB 的旧版 Node 客户端。虽然它多年来为社区提供了良好的服务,但 “Neo” 旨在吸取其前身的经验并进行改进。它提供了更友好的 API,支持更多功能,并采用了更健壮、更易于维护的架构。它既提供了高级便利性,也提供了低级访问。让我们来一探究竟!

它提供了什么?

友好、现代的 API

旧版 Node 客户端的 API 基于 SQLite。虽然许多人对此很熟悉,但它使用的是一种笨拙、过时的基于回调的风格。Neo 原生使用 Promises

const result = await connection.run(`SELECT 'Hello, Neo!'`);

此外,Neo 完全由 TypeScript 构建。精心选择的名称和类型最大限度地减少了查阅文档的需要。

const columnNames = result.columnNames();
const columnTypes = result.columnTypes();

Neo 还提供了方便的辅助功能,可以根据需要读取任意数量的行,并以列主序或行主序格式返回它们。

const reader = await connection.runAndReadUtil('FROM range(5000)',
    1000);
const rows = reader.getRows();
// OR: const columns = reader.getColumns();

完整的数据类型支持

DuckDB 支持丰富的数据类型。Neo 支持所有内置类型以及自定义类型,例如 JSON。例如,ARRAY

if (columnType.typeId === DuckDBTypeId.ARRAY) {
  const arrayValueType = columnType.valueType;
  const arrayLength = columnType.length;
}

DECIMAL:

if (columnType.typeId === DuckDBTypeId.DECIMAL) {
  const decimalWidth = columnType.width;
  const decimalScale = columnType.scale;
}

JSON

if (columnType.alias === 'JSON') {
  const json = JSON.parse(columnValue);
}

类型特有的实用工具简化了常见的转换,例如从 TIMESTAMPDECIMAL 生成人类可读的字符串,同时保留对原始值的访问以实现无损处理。

if (columnType.typeId === DuckDBTypeId.TIMESTAMP) {
  const timestampMicros = columnValue.micros; // bigint
  const timestampString = columnValue.toString();
  const {
    date: { year, month, day },
    time: { hour, min, sec, micros },
  } = columnValue.toParts();
}

高级功能

需要将特定类型的值绑定到预处理语句,或者精确地控制 SQL 执行?也许您想利用 DuckDB 的解析器来提取语句,或者高效地将数据追加到表中。Neo 涵盖了这些需求,提供了对 DuckDB 这些强大功能的完全访问。

将值绑定到预处理语句

将值绑定到预处理语句的参数时,您可以选择 SQL 数据类型。这对于在 JavaScript 中没有自然等价类型的类型很有用。

const prepared = await connection.prepare('SELECT $1, $2');
prepared.bindTimestamp(1, new DuckDBTimestampValue(micros));
prepared.bindDecimal(2, new DuckDBDecimalValue(value, width, scale));
const result = await prepared.run();

控制任务执行

使用挂起的结果允许在任何时候暂停或停止 SQL 执行,甚至在结果准备好之前。

import { DuckDBPendingResultState } from '@duckdb/node-api';

// Placeholder to demonstrate doing other work between tasks.
async function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

const prepared = await connection.prepare('FROM range(10_000_000)');
const pending = prepared.start();
// Run tasks until the result is ready.
// This allows execution to be paused and resumed as needed.
// Other work can be done between tasks.
while (pending.runTask() !== DuckDBPendingResultState.RESULT_READY) {
  console.log('not ready');
  await sleep(1);
}
console.log('ready');
const result = await pending.getResult();
// ...

提取语句并带参数运行

您可以使用提取语句 API 运行包含参数的多语句 SQL。

// Parse this multi-statement input into separate statements.
const extractedStatements = await connection.extractStatements(`
  CREATE OR REPLACE TABLE numbers AS FROM range(?);
  FROM numbers WHERE range < ?;
  DROP TABLE numbers;
`);
const parameterValues = [10, 7];
const stmtCount = extractedStatements.count;
// Run each statement, binding values as needed.
for (let stmtIndex = 0; stmtIndex < stmtCount; stmtIndex++) {
  const prepared = await extractedStatements.prepare(stmtIndex);
  const paramCount = prepared.parameterCount;
  for (let paramIndex = 1; paramIndex <= paramCount; paramIndex++) {
    prepared.bindInteger(paramIndex, parameterValues.shift());
  }
  const result = await prepared.run();
  // ...
}

将数据追加到表中

追加器 API 是将数据批量插入表中最有效的方法。

await connection.run(
  `CREATE OR REPLACE TABLE target_table(i INTEGER, v VARCHAR)`
);

const appender = await connection.createAppender('main', 'target_table');

appender.appendInteger(100);
appender.appendVarchar('walk');
appender.endRow();

appender.appendInteger(200);
appender.appendVarchar('swim');
appender.endRow();

appender.appendInteger(300);
appender.appendVarchar('fly');
appender.endRow();

appender.close();

它是如何构建的?

依赖项

Neo 使用与大多数其他 DuckDB 客户端 API(包括旧版 Node 客户端)不同的实现方法。它绑定到 DuckDB 的 C API 而不是 C++ API。

您为什么要关心?使用 DuckDB 的 C++ API 意味着从头开始构建整个 DuckDB。每个使用这种方法的客户端 API 都附带了 DuckDB 的略有不同的构建版本。这可能会给库维护者和使用者带来麻烦。

维护者需要拉取整个 DuckDB 源代码。这增加了构建的成本和复杂性,从而增加了代码更改(尤其是 DuckDB 版本更新)的成本。这些成本通常会导致修复 bug 或支持新版本时出现重大延迟。

使用者会受到这些延迟的影响。每个客户端的构建之间也可能存在细微的行为差异,这可能是由不同的编译时配置引入的。

一些客户端 API 位于主 DuckDB 存储库中。这解决了一些上述问题,但增加了维护 DuckDB 本身的成本和复杂性。

另一方面,要使用 DuckDB 的 C API,只需要依赖已发布的二进制文件。这大大简化了所需的维护工作,加快了构建速度,并最大限度地降低了更新成本。它消除了重新构建 DuckDB 的不确定性和风险。

软件包

DuckDB 需要针对每个平台的不同二进制文件。在 Node 软件包中分发平台特定的二进制文件是出了名的挑战。当包管理器尝试使用其碰巧存在的任何构建和配置工具从源代码重新构建某些组件时,这通常会导致难以理解的安装错误。

Neo 采用了一种旨在避免这些问题的包设计。受 ESBuild 的启发,Neo 为每个支持的平台将预构建的二进制文件打包到单独的包中。每个包都声明其支持的特定平台(例如 oscpu)。然后,主包使用 optionalDependencies 依赖于所有这些特定于平台的包。

安装主包时,包管理器将只为支持的平台安装 optionalDependencies。因此,您只获得您需要的确切二进制文件,不多不少。如果安装在不支持的平台上,则不会安装任何二进制文件。在安装过程中,绝不会尝试从源代码构建。

DuckDB Node Neo 客户端具有多层。大多数人会希望使用 Neo 的主“api”包,@duckdb/node-api。它包含带方便辅助函数的友好 API。但是,对于高级用例,Neo 还公开了较低层的“bindings”包,@duckdb/node-bindings,它实现了 DuckDB C API 到 Node 的更直接转换。

这个 API 有 TypeScript 定义,但是,由于它遵循 C 的约定,从 Node 使用起来可能很笨拙。然而,它提供了一种相对没有主见的访问 DuckDB 的方式,支持构建专用应用程序或替代的高级 API。

未来的发展方向?

Neo 目前标记为“alpha”。这表示其完整性和成熟度,而不是健壮性。DuckDB C API 的大部分功能都已公开,并且公开的功能都经过了广泛的测试。但它相对较新,因此可能包含未发现的 bug。

此外,一些功能区域尚未完成

  • 追加和绑定高级数据类型。这需要在 DuckDB 的 C API 中添加额外的函数。目标是在 DuckDB 1.2 的下一个版本(目前计划于 2025 年 1 月发布)中添加这些功能。

  • 写入数据块向量。以原生层可见的方式修改二进制缓冲区在 Node 环境中提出了特殊的挑战。这是近期工作的重中之重。

  • 用户定义类型和函数。必要的功能和类型是在 v1.1.0 中相对较新地添加到 DuckDB C API 的。这在近期路线图上。

  • 分析信息。这在 v1.1.0 中添加。它在路线图上。

  • 表描述。这也在 v1.1.0 中添加。它在路线图上。

新版本的 DuckDB 将包含对 C API 的新增内容。由于 Neo 旨在涵盖 C API 的所有功能,这些新增内容将在发布后添加到路线图。

如果您有功能请求或其他反馈,请告知我们!也欢迎提交拉取请求

下一步是什么?

DuckDB Node Neo 提供了一种友好且强大的方式,将 DuckDB 与 Node 结合使用。通过利用 DuckDB 的 C API,它展示了一种新的、更易于维护的 DuckDB 构建方式,为维护者和使用者都带来了好处。它还很年轻,但发展迅速。亲自尝试一下