2024-12-18

DuckDB Node Neo Client

Jeff Raymakers

Lernen Sie die neueste DuckDB-Client-API kennen: DuckDB Node „Neo“!

Vielleicht kennen Sie DuckDBs alten Node-Client. Er hat der Community über die Jahre gut gedient; „Neo“ will von ihm lernen und ihn verbessern. Es bietet eine freundlichere API, unterstützt mehr Features und nutzt eine robustere, wartbarere Architektur. Es liefert sowohl High-Level-Bequemlichkeiten als auch Low-Level-Zugriff. Schauen wir uns um!

Was bietet es?

Freundliche, moderne API

Die API des alten Node-Clients basiert auf der von SQLite. Vielen vertraut, nutzt sie aber einen unbeholfenen, veralteten Callback-Stil. Neo nutzt Promises nativ.

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

Außerdem ist Neo von Grund auf in TypeScript gebaut. Sorgfältig gewählte Namen und Typen minimieren den Bedarf, in die Dokumentation zu schauen.

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

Neo bietet außerdem bequeme Helfer, um nur so viele Zeilen zu lesen, wie nötig, und sie im spalten- oder zeilenorientierten Format zurückzugeben.

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

Volle Datentypunterstützung

DuckDB unterstützt eine reiche Vielfalt an Datentypen. Neo unterstützt jeden eingebauten Typ sowie Custom Types wie JSON. Zum Beispiel 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;
}

Und JSON:

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

Typspezifische Hilfen erleichtern gängige Konvertierungen, etwa menschenlesbare Strings aus TIMESTAMPs oder DECIMALs, und bewahren gleichzeitig Zugriff auf die Rohwerte für verlustfreie Verarbeitung.

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();
}

Fortgeschrittene Features

Müssen Sie bestimmte Werttypen an Prepared Statements binden oder die SQL-Ausführung präzise steuern? Vielleicht wollen Sie DuckDBs Parser nutzen, um Statements zu extrahieren, oder effizient Daten an eine Tabelle anhängen. Neo deckt das ab und bietet vollen Zugriff auf diese leistungsfähigen DuckDB-Features.

Werte an Prepared Statements binden

Beim Binden von Werten an Parameter von Prepared Statements können Sie den SQL-Datentyp wählen. Das ist nützlich für Typen, die in JavaScript kein natürliches Pendant haben.

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();

Task-Ausführung steuern

Mit Pending Results können Sie die SQL-Ausführung jederzeit anhalten oder stoppen, auch bevor das Ergebnis bereit ist.

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();
// ...

Statements extrahieren und mit Parametern ausführen

Sie können mehrteiliges SQL mit Parametern über die Extract-Statements-API ausführen.

// 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();
// ...
}

Daten an eine Tabelle anhängen

Die Appender-API ist der effizienteste Weg, Daten in großen Mengen in eine Tabelle einzufügen.

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();

Wie ist es gebaut?

Abhängigkeiten

Neo nutzt einen anderen Implementierungsansatz als die meisten anderen DuckDB-Client-APIs, einschließlich des alten Node-Clients. Es bindet an DuckDBs C-API statt an die C++-API.

Warum sollte Sie das interessieren? DuckDBs C++-API zu nutzen bedeutet, ganz DuckDB von Grund auf zu bauen. Jede Client-API mit diesem Ansatz liefert einen leicht anderen DuckDB-Build. Das kann für Maintainer und Nutzer Kopfschmerzen bereiten.

Maintainer müssen den gesamten DuckDB-Quellcode einziehen. Das erhöht Kosten und Komplexität des Builds und damit die Kosten von Codeänderungen und vor allem DuckDB-Versionsupdates. Diese Kosten führen oft zu erheblichen Verzögerungen beim Beheben von Bugs oder der Unterstützung neuer Versionen.

Nutzer spüren diese Verzögerungen. Es gibt außerdem die Möglichkeit subtiler Verhaltensunterschiede zwischen den Builds in jedem Client, vielleicht durch unterschiedliche Compile-Time-Konfiguration.

Einige Client-APIs liegen im Haupt-DuckDB-Repository. Das löst einige der obigen Probleme, erhöht aber Kosten und Komplexität der Wartung von DuckDB selbst.

DuckDBs C-API zu nutzen bedeutet dagegen, nur von veröffentlichten Binaries abzuhängen. Das vereinfacht die Wartung erheblich, beschleunigt Builds und minimiert die Kosten von Updates. Es nimmt die Unsicherheit und das Risiko, DuckDB neu zu bauen.

Pakete

DuckDB braucht unterschiedliche Binaries für jede Plattform. Plattformspezifische Binaries in Node-Paketen zu verteilen, ist berüchtigt schwierig. Oft führt das zu undurchsichtigen Fehlern bei der Installation, wenn der Package Manager versucht, eine Komponente aus dem Quellcode neu zu bauen – mit welchen Build- und Konfigurationswerkzeugen gerade da sind.

Neo nutzt ein Paketdesign, das diese Probleme vermeiden soll. Inspiriert von ESBuild packt Neo vorgebaute Binaries für jede unterstützte Plattform in ein eigenes Paket. Jedes dieser Pakete deklariert die jeweilige Plattform (z. B. os und cpu). Das Hauptpaket hängt dann von all diesen plattformspezifischen Paketen über optionalDependencies ab.

Wird das Hauptpaket installiert, installiert der Package Manager nur optionalDependencies für unterstützte Plattformen. Sie bekommen also genau die Binaries, die Sie brauchen, nicht mehr. Auf einer nicht unterstützten Plattform werden keine Binaries installiert. Zu keinem Zeitpunkt wird bei der Installation versucht, aus dem Quellcode zu bauen.

Schichten

Der DuckDB-Node-Neo-Client hat mehrere Schichten. Die meisten wollen Neos Hauptpaket „api“ nutzen, @duckdb/node-api. Es enthält die freundliche API mit bequemen Helfern. Für fortgeschrittene Fälle legt Neo aber auch das niedrigere „bindings“-Paket offen, @duckdb/node-bindings, das eine direktere Übersetzung von DuckDBs C-API nach Node implementiert.

Diese API hat TypeScript-Definitionen, folgt aber den Konventionen von C und kann von Node aus unbeholfen sein. Sie bietet aber einen relativ unopinionated Zugang zu DuckDB, der den Bau spezialisierter Anwendungen oder alternativer höherer APIs unterstützt.

Wohin geht es?

Neo ist derzeit als „alpha“ markiert. Das zeigt Vollständigkeit und Reife, nicht Robustheit. Der Großteil der Funktionalität von DuckDBs C-API ist offengelegt, und was offengelegt ist, hat umfangreiche Tests. Es ist aber relativ neu und kann unentdeckte Bugs enthalten.

Einige Funktionsbereiche sind noch nicht fertig:

Neue DuckDB-Versionen bringen Ergänzungen der C-API. Da Neo die gesamte Funktionalität der C-API abdecken will, kommen diese Ergänzungen auf die Roadmap, sobald sie erscheinen.

Haben Sie einen Feature-Wunsch oder anderes Feedback, lassen Sie es uns wissen! Pull Requests sind ebenfalls willkommen.

Und jetzt?

DuckDB Node Neo bietet einen freundlichen und leistungsfähigen Weg, DuckDB mit Node zu nutzen. Durch die Nutzung von DuckDBs C-API zeigt es einen neuen, wartbareren Weg, auf DuckDB aufzubauen – mit Vorteilen für Maintainer und Nutzer. Es ist noch jung, wächst aber schnell. Probieren Sie es selbst!