From 173ab39644a6439971c3a69f4ca8e2d531eccab2 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Mon, 13 Jan 2025 23:03:24 -0800 Subject: [PATCH] bind helper --- api/pkgs/@duckdb/node-api/README.md | 16 ++++ api/src/DuckDBPreparedStatement.ts | 15 ++++ api/src/DuckDBVector.ts | 1 - api/src/typeForValue.ts | 112 ++++++++++++++++++++++++++++ api/test/api.test.ts | 80 ++++++++++++++++++++ 5 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 api/src/typeForValue.ts diff --git a/api/pkgs/@duckdb/node-api/README.md b/api/pkgs/@duckdb/node-api/README.md index 4a26d4d..923d3b4 100644 --- a/api/pkgs/@duckdb/node-api/README.md +++ b/api/pkgs/@duckdb/node-api/README.md @@ -110,6 +110,22 @@ prepared.bindList(3, listValue([10, 11, 12]), LIST(INTEGER)); const result = await prepared.run(); ``` +or: + +```ts +const prepared = await connection.prepare('select $a, $b, $c'); +prepared.bind({ + 'a': 'duck', + 'b': 42, + 'c': listValue([10, 11, 12]), +}, { + 'a': VARCHAR, + 'b': INTEGER, + 'c': LIST(INTEGER), +}); +const result = await prepared.run(); +``` + ### Stream Results Streaming results evaluate lazily when rows are read. diff --git a/api/src/DuckDBPreparedStatement.ts b/api/src/DuckDBPreparedStatement.ts index 9f0afbf..8f70dcc 100644 --- a/api/src/DuckDBPreparedStatement.ts +++ b/api/src/DuckDBPreparedStatement.ts @@ -14,6 +14,7 @@ import { } from './DuckDBType'; import { DuckDBTypeId } from './DuckDBTypeId'; import { StatementType } from './enums'; +import { typeForValue } from './typeForValue'; import { DuckDBArrayValue, DuckDBDateValue, @@ -163,6 +164,20 @@ export class DuckDBPreparedStatement { createValue(type, value) ); } + public bind(values: DuckDBValue[] | Record, types?: DuckDBType[] | Record) { + if (Array.isArray(values)) { + const typesIsArray = Array.isArray(types); + for (let i = 0; i < values.length; i++) { + this.bindValue(i + 1, values[i], typesIsArray ? types[i] : typeForValue(values[i])); + } + } else { + const typesIsRecord = types && !Array.isArray(types); + for (const key in values) { + const index = this.parameterIndex(key); + this.bindValue(index, values[key], typesIsRecord ? types[key] : typeForValue(values[key])); + } + } + } public async run(): Promise { return new DuckDBMaterializedResult( await duckdb.execute_prepared(this.prepared_statement) diff --git a/api/src/DuckDBVector.ts b/api/src/DuckDBVector.ts index 9170742..f9279cb 100644 --- a/api/src/DuckDBVector.ts +++ b/api/src/DuckDBVector.ts @@ -3283,7 +3283,6 @@ export class DuckDBUnionVector extends DuckDBVector { public override setItem(itemIndex: number, value: DuckDBUnionValue | null) { if (value != null) { const memberIndex = this.unionType.memberIndexForTag(value.tag); - console.log({ value, memberIndex }); this.structVector.setItemValue(itemIndex, 0, memberIndex); const entryIndex = memberIndex + 1; this.structVector.setItemValue(itemIndex, entryIndex, value.value); diff --git a/api/src/typeForValue.ts b/api/src/typeForValue.ts new file mode 100644 index 0000000..f9d2f2a --- /dev/null +++ b/api/src/typeForValue.ts @@ -0,0 +1,112 @@ +import { + ANY, + ARRAY, + BIT, + BLOB, + BOOLEAN, + DATE, + DECIMAL, + DOUBLE, + DuckDBType, + HUGEINT, + INTERVAL, + LIST, + MAP, + SQLNULL, + STRUCT, + TIME, + TIMESTAMP, + TIMESTAMP_MS, + TIMESTAMP_NS, + TIMESTAMP_S, + TIMESTAMPTZ, + TIMETZ, + UNION, + UUID, + VARCHAR, +} from './DuckDBType'; +import { + DuckDBArrayValue, + DuckDBBitValue, + DuckDBBlobValue, + DuckDBDateValue, + DuckDBDecimalValue, + DuckDBIntervalValue, + DuckDBListValue, + DuckDBMapValue, + DuckDBStructValue, + DuckDBTimestampMillisecondsValue, + DuckDBTimestampNanosecondsValue, + DuckDBTimestampSecondsValue, + DuckDBTimestampTZValue, + DuckDBTimestampValue, + DuckDBTimeTZValue, + DuckDBTimeValue, + DuckDBUnionValue, + DuckDBUUIDValue, + DuckDBValue, +} from './values'; + +export function typeForValue(value: DuckDBValue): DuckDBType { + if (value === null) { + return SQLNULL; + } else { + switch (typeof value) { + case 'boolean': + return BOOLEAN; + case 'number': + return DOUBLE; + case 'bigint': + return HUGEINT; + case 'string': + return VARCHAR; + case 'object': + if (value instanceof DuckDBArrayValue) { + return ARRAY(typeForValue(value.items[0]), value.items.length); + } else if (value instanceof DuckDBBitValue) { + return BIT; + } else if (value instanceof DuckDBBlobValue) { + return BLOB; + } else if (value instanceof DuckDBDateValue) { + return DATE; + } else if (value instanceof DuckDBDecimalValue) { + return DECIMAL(value.width, value.scale); + } else if (value instanceof DuckDBIntervalValue) { + return INTERVAL; + } else if (value instanceof DuckDBListValue) { + return LIST(typeForValue(value.items[0])); + } else if (value instanceof DuckDBMapValue) { + return MAP( + typeForValue(value.entries[0].key), + typeForValue(value.entries[0].value) + ); + } else if (value instanceof DuckDBStructValue) { + const entryTypes: Record = {}; + for (const key in value.entries) { + entryTypes[key] = typeForValue(value.entries[key]); + } + return STRUCT(entryTypes); + } else if (value instanceof DuckDBTimestampMillisecondsValue) { + return TIMESTAMP_MS; + } else if (value instanceof DuckDBTimestampNanosecondsValue) { + return TIMESTAMP_NS; + } else if (value instanceof DuckDBTimestampSecondsValue) { + return TIMESTAMP_S; + } else if (value instanceof DuckDBTimestampTZValue) { + return TIMESTAMPTZ; + } else if (value instanceof DuckDBTimestampValue) { + return TIMESTAMP; + } else if (value instanceof DuckDBTimeTZValue) { + return TIMETZ; + } else if (value instanceof DuckDBTimeValue) { + return TIME; + } else if (value instanceof DuckDBUnionValue) { + return UNION({ [value.tag]: typeForValue(value.value) }); + } else if (value instanceof DuckDBUUIDValue) { + return UUID; + } + break; + } + } + return ANY; +} diff --git a/api/test/api.test.ts b/api/test/api.test.ts index bf031c4..4e8a7ba 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -477,6 +477,86 @@ describe('api', () => { } }); }); + test('should support prepare statement bind with list', async () => { + await withConnection(async (connection) => { + const prepared = await connection.prepare( + 'select $1 as a, $2 as b, $3 as c' + ); + prepared.bind([42, 'duck', listValue([10, 11, 12])]); + const result = await prepared.run(); + assertColumns(result, [ + { name: 'a', type: DOUBLE }, + { name: 'b', type: VARCHAR }, + { name: 'c', type: LIST(DOUBLE) }, + ]); + const chunk = await result.fetchChunk(); + assert.isDefined(chunk); + if (chunk) { + assert.strictEqual(chunk.columnCount, 3); + assert.strictEqual(chunk.rowCount, 1); + assertValues( + chunk, + 0, + DuckDBDoubleVector, + [42] + ); + assertValues( + chunk, + 1, + DuckDBVarCharVector, + ['duck'] + ); + assertValues( + chunk, + 2, + DuckDBListVector, + [listValue([10, 11, 12])] + ); + } + }); + }); + test('should support prepare statement bind with object', async () => { + await withConnection(async (connection) => { + const prepared = await connection.prepare( + 'select $a as a, $b as b, $c as c' + ); + prepared.bind({ + a: 42, + b: 'duck', + c: listValue([10, 11, 12]), + }); + const result = await prepared.run(); + assertColumns(result, [ + { name: 'a', type: DOUBLE }, + { name: 'b', type: VARCHAR }, + { name: 'c', type: LIST(DOUBLE) }, + ]); + const chunk = await result.fetchChunk(); + assert.isDefined(chunk); + if (chunk) { + assert.strictEqual(chunk.columnCount, 3); + assert.strictEqual(chunk.rowCount, 1); + assertValues( + chunk, + 0, + DuckDBDoubleVector, + [42] + ); + assertValues( + chunk, + 1, + DuckDBVarCharVector, + ['duck'] + ); + assertValues( + chunk, + 2, + DuckDBListVector, + [listValue([10, 11, 12])] + ); + } + }); + }); test('should support starting prepared statements and running them incrementally', async () => { await withConnection(async (connection) => { const prepared = await connection.prepare(