DuckDB Node 客户端 - Neo

DuckDB

概要:全新的DuckDB Node客户端“Neo”,为使用你最喜爱的数据库提供了一种强大且友好的方式。

你可能对DuckDB旧版的Node客户端比较熟悉。多年来,它为社区提供了良好的服务,而“Neo”旨在借鉴其经验并加以改进。它提供了更友好的API,支持更多功能,并采用了更健壮、易维护的架构。它既提供高级便捷功能,也支持底层访问。让我们来一探究竟!

基本特性

友好、现代的API

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

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();
// 或者: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);
}

特定类型的实用工具简化了常见的转换,比如从时间戳(TIMESTAMP)或十进制数(DECIMAL)生成人类可读的字符串,同时保留对原始值的访问,以便进行无损处理。

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';

// 用于演示在任务之间执行其他工作的占位符
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();
// 运行任务,直到结果准备好
// 这允许根据需要暂停和恢复执行
// 可以在任务之间执行其他工作
while (pending.runTask()!== DuckDBPendingResultState.RESULT_READY) {
    console.log('not ready');
    await sleep(1);
}
console.log('ready');
const result = await pending.getResult();
//...

提取语句并使用参数运行

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

// 将此多语句输入解析为单独的语句
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;
// 运行每个语句,并根据需要绑定值
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();
Publish on 2025-01-05,Update on 2025-02-10