From e67c9442849e38d9d81b51f56ba00ea2dadd12d9 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Mon, 13 Jan 2025 19:11:40 -0800 Subject: [PATCH 1/4] support more binds --- api/src/DuckDBPreparedStatement.ts | 17 ++-- api/src/createValue.ts | 155 +++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 api/src/createValue.ts diff --git a/api/src/DuckDBPreparedStatement.ts b/api/src/DuckDBPreparedStatement.ts index c29a0dfa..93adf3d2 100644 --- a/api/src/DuckDBPreparedStatement.ts +++ b/api/src/DuckDBPreparedStatement.ts @@ -1,15 +1,19 @@ import duckdb from '@duckdb/node-bindings'; +import { createValue } from './createValue'; import { DuckDBMaterializedResult } from './DuckDBMaterializedResult'; import { DuckDBPendingResult } from './DuckDBPendingResult'; import { DuckDBResult } from './DuckDBResult'; import { DuckDBResultReader } from './DuckDBResultReader'; +import { DuckDBTimestampTZType, DuckDBTimeTZType } from './DuckDBType'; import { DuckDBTypeId } from './DuckDBTypeId'; import { StatementType } from './enums'; import { DuckDBDateValue, DuckDBDecimalValue, DuckDBIntervalValue, + DuckDBTimestampTZValue, DuckDBTimestampValue, + DuckDBTimeTZValue, DuckDBTimeValue, } from './values'; @@ -87,11 +91,16 @@ export class DuckDBPreparedStatement { public bindTime(parameterIndex: number, value: DuckDBTimeValue) { duckdb.bind_time(this.prepared_statement, parameterIndex, value); } + public bindTimeTZ(parameterIndex: number, value: DuckDBTimeTZValue) { + duckdb.bind_value(this.prepared_statement, parameterIndex, createValue(DuckDBTimeTZType.instance, value)); + } public bindTimestamp(parameterIndex: number, value: DuckDBTimestampValue) { duckdb.bind_timestamp(this.prepared_statement, parameterIndex, value); } - // TODO: bind TIMESTAMPS_S/_MS/_NS? - // TODO: bind TIME_TZ/TIMESTAMP_TZ? + public bindTimestampTZ(parameterIndex: number, value: DuckDBTimestampTZValue) { + duckdb.bind_value(this.prepared_statement, parameterIndex, createValue(DuckDBTimestampTZType.instance, value)); + } + // TODO: bind TIMESTAMPS_S/_MS/_NS? public bindInterval(parameterIndex: number, value: DuckDBIntervalValue) { duckdb.bind_interval(this.prepared_statement, parameterIndex, value); } @@ -108,10 +117,6 @@ export class DuckDBPreparedStatement { public bindNull(parameterIndex: number) { duckdb.bind_null(this.prepared_statement, parameterIndex); } - // TODO: expose bindValue, or implement bindList, bindStruct, etc.? - // public bindValue(parameterIndex: number, value: Value) { - // duckdb.bind_value(this.prepared_statement, parameterIndex, value); - // } public async run(): Promise { return new DuckDBMaterializedResult(await duckdb.execute_prepared(this.prepared_statement)); } diff --git a/api/src/createValue.ts b/api/src/createValue.ts new file mode 100644 index 00000000..f64e62b3 --- /dev/null +++ b/api/src/createValue.ts @@ -0,0 +1,155 @@ +import duckdb, { Value } from '@duckdb/node-bindings'; +import { DuckDBType } from './DuckDBType'; +import { DuckDBTypeId } from './DuckDBTypeId'; +import { + DuckDBBlobValue, + DuckDBDateValue, + DuckDBIntervalValue, + DuckDBTimestampTZValue, + DuckDBTimestampValue, + DuckDBTimeTZValue, + DuckDBTimeValue, + DuckDBValue, +} from './values'; + +export function createValue(type: DuckDBType, input: DuckDBValue): Value { + switch (type.typeId) { + case DuckDBTypeId.BOOLEAN: + if (typeof input === 'boolean') { + return duckdb.create_bool(input); + } + throw new Error(`input is not a boolean`); + case DuckDBTypeId.TINYINT: + if (typeof input === 'number') { + return duckdb.create_int8(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.SMALLINT: + if (typeof input === 'number') { + return duckdb.create_int16(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.INTEGER: + if (typeof input === 'number') { + return duckdb.create_int32(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.BIGINT: + if (typeof input === 'bigint') { + return duckdb.create_int64(input); + } + throw new Error(`input is not a bigint`); + case DuckDBTypeId.UTINYINT: + if (typeof input === 'number') { + return duckdb.create_uint8(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.USMALLINT: + if (typeof input === 'number') { + return duckdb.create_uint16(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.UINTEGER: + if (typeof input === 'number') { + return duckdb.create_uint32(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.UBIGINT: + if (typeof input === 'bigint') { + return duckdb.create_uint64(input); + } + throw new Error(`input is not a bigint`); + case DuckDBTypeId.FLOAT: + if (typeof input === 'number') { + return duckdb.create_float(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.DOUBLE: + if (typeof input === 'number') { + return duckdb.create_double(input); + } + throw new Error(`input is not a number`); + case DuckDBTypeId.TIMESTAMP: + if (input instanceof DuckDBTimestampValue) { + return duckdb.create_timestamp(input); + } + throw new Error(`input is not a DuckDBTimestampValue`); + case DuckDBTypeId.DATE: + if (input instanceof DuckDBDateValue) { + return duckdb.create_date(input); + } + throw new Error(`input is not a DuckDBDateValue`); + case DuckDBTypeId.TIME: + if (input instanceof DuckDBTimeValue) { + return duckdb.create_time(input); + } + throw new Error(`input is not a DuckDBTimeValue`); + case DuckDBTypeId.INTERVAL: + if (input instanceof DuckDBIntervalValue) { + return duckdb.create_interval(input); + } + throw new Error(`input is not a DuckDBIntervalValue`); + case DuckDBTypeId.HUGEINT: + if (typeof input === 'bigint') { + return duckdb.create_hugeint(input); + } + throw new Error(`input is not a bigint`); + case DuckDBTypeId.UHUGEINT: + if (typeof input === 'bigint') { + return duckdb.create_uhugeint(input); + } + throw new Error(`input is not a bigint`); + case DuckDBTypeId.VARCHAR: + if (typeof input === 'string') { + return duckdb.create_varchar(input); + } + throw new Error(`input is not a string`); + case DuckDBTypeId.BLOB: + if (input instanceof DuckDBBlobValue) { + return duckdb.create_blob(input.bytes); + } + throw new Error(`input is not a DuckDBBlobValue`); + case DuckDBTypeId.DECIMAL: + throw new Error(`not yet implemented for DECIMAL`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.TIMESTAMP_S: + throw new Error(`not yet implemented for TIMESTAMP_S`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.TIMESTAMP_MS: + throw new Error(`not yet implemented for TIMESTAMP_MS`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.TIMESTAMP_NS: + throw new Error(`not yet implemented for TIMESTAMP_NS`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.ENUM: + throw new Error(`not yet implemented for ENUM`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.LIST: + throw new Error(`not yet implemented for LIST`); // TODO (need toLogicalType) + case DuckDBTypeId.STRUCT: + throw new Error(`not yet implemented for STRUCT`); // TODO (need toLogicalType) + case DuckDBTypeId.MAP: + throw new Error(`not yet implemented for MAP`); // TODO: implement when available, hopefully in 1.2.0 + case DuckDBTypeId.ARRAY: + throw new Error(`not yet implemented for ARRAY`); // TODO (need toLogicalType) + case DuckDBTypeId.UUID: + throw new Error(`not yet implemented for UUID`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.UNION: + throw new Error(`not yet implemented for UNION`); // TODO: implement when available, hopefully in 1.2.0 + case DuckDBTypeId.UNION: + throw new Error(`not yet implemented for BIT`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.TIME_TZ: + if (input instanceof DuckDBTimeTZValue) { + return duckdb.create_time_tz_value(input); + } + throw new Error(`input is not a DuckDBTimeTZValue`); + case DuckDBTypeId.TIMESTAMP_TZ: + if (input instanceof DuckDBTimestampTZValue) { + return duckdb.create_timestamp(input); // TODO: change to create_timestamp_tz when available in 1.2.0 + } + throw new Error(`input is not a DuckDBTimestampTZValue`); + case DuckDBTypeId.ANY: + throw new Error(`cannot create values of type ANY`); + case DuckDBTypeId.VARINT: + throw new Error(`not yet implemented for VARINT`); // TODO: implement when available in 1.2.0 + case DuckDBTypeId.SQLNULL: + throw new Error(`not yet implemented for SQLNUll`); // TODO: implement when available in 1.2.0 + default: + throw new Error(`unrecognized type id ${type.typeId}`); + } +} From 48daee4bd570ca819a51b0ae4a6c1cbb22257a78 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Mon, 13 Jan 2025 22:08:18 -0800 Subject: [PATCH 2/4] bind LIST, STRUCT, ARRAY, plus tests --- api/src/DuckDBPreparedStatement.ts | 80 +++++++++++++++++++++++++----- api/src/createValue.ts | 29 +++++++++-- api/test/api.test.ts | 52 ++++++++++++++++--- 3 files changed, 137 insertions(+), 24 deletions(-) diff --git a/api/src/DuckDBPreparedStatement.ts b/api/src/DuckDBPreparedStatement.ts index 93adf3d2..9f0afbff 100644 --- a/api/src/DuckDBPreparedStatement.ts +++ b/api/src/DuckDBPreparedStatement.ts @@ -4,17 +4,28 @@ import { DuckDBMaterializedResult } from './DuckDBMaterializedResult'; import { DuckDBPendingResult } from './DuckDBPendingResult'; import { DuckDBResult } from './DuckDBResult'; import { DuckDBResultReader } from './DuckDBResultReader'; -import { DuckDBTimestampTZType, DuckDBTimeTZType } from './DuckDBType'; +import { + DuckDBArrayType, + DuckDBListType, + DuckDBStructType, + DuckDBType, + TIMESTAMPTZ, + TIMETZ, +} from './DuckDBType'; import { DuckDBTypeId } from './DuckDBTypeId'; import { StatementType } from './enums'; import { + DuckDBArrayValue, DuckDBDateValue, DuckDBDecimalValue, DuckDBIntervalValue, + DuckDBListValue, + DuckDBStructValue, DuckDBTimestampTZValue, DuckDBTimestampValue, DuckDBTimeTZValue, DuckDBTimeValue, + DuckDBValue, } from './values'; export class DuckDBPreparedStatement { @@ -92,15 +103,18 @@ export class DuckDBPreparedStatement { duckdb.bind_time(this.prepared_statement, parameterIndex, value); } public bindTimeTZ(parameterIndex: number, value: DuckDBTimeTZValue) { - duckdb.bind_value(this.prepared_statement, parameterIndex, createValue(DuckDBTimeTZType.instance, value)); + this.bindValue(parameterIndex, value, TIMETZ); } public bindTimestamp(parameterIndex: number, value: DuckDBTimestampValue) { duckdb.bind_timestamp(this.prepared_statement, parameterIndex, value); } - public bindTimestampTZ(parameterIndex: number, value: DuckDBTimestampTZValue) { - duckdb.bind_value(this.prepared_statement, parameterIndex, createValue(DuckDBTimestampTZType.instance, value)); + public bindTimestampTZ( + parameterIndex: number, + value: DuckDBTimestampTZValue + ) { + this.bindValue(parameterIndex, value, TIMESTAMPTZ); } - // TODO: bind TIMESTAMPS_S/_MS/_NS? + // TODO: bind TIMESTAMPS_S/_MS/_NS public bindInterval(parameterIndex: number, value: DuckDBIntervalValue) { duckdb.bind_interval(this.prepared_statement, parameterIndex, value); } @@ -110,15 +124,49 @@ export class DuckDBPreparedStatement { public bindBlob(parameterIndex: number, value: Uint8Array) { duckdb.bind_blob(this.prepared_statement, parameterIndex, value); } - // TODO: bind ENUM? - // TODO: bind nested types? (ARRAY, LIST, STRUCT, MAP, UNION) (using bindValue?) - // TODO: bind UUID? - // TODO: bind BIT? + // TODO: bind ENUM + public bindArray( + parameterIndex: number, + value: DuckDBArrayValue, + type: DuckDBArrayType + ) { + this.bindValue(parameterIndex, value, type); + } + public bindList( + parameterIndex: number, + value: DuckDBListValue, + type: DuckDBListType + ) { + this.bindValue(parameterIndex, value, type); + } + public bindStruct( + parameterIndex: number, + value: DuckDBStructValue, + type: DuckDBStructType + ) { + this.bindValue(parameterIndex, value, type); + } + // TODO: bind MAP, UNION + // TODO: bind UUID + // TODO: bind BIT public bindNull(parameterIndex: number) { duckdb.bind_null(this.prepared_statement, parameterIndex); } + public bindValue( + parameterIndex: number, + value: DuckDBValue, + type: DuckDBType + ) { + duckdb.bind_value( + this.prepared_statement, + parameterIndex, + createValue(type, value) + ); + } public async run(): Promise { - return new DuckDBMaterializedResult(await duckdb.execute_prepared(this.prepared_statement)); + return new DuckDBMaterializedResult( + await duckdb.execute_prepared(this.prepared_statement) + ); } public async runAndRead(): Promise { return new DuckDBResultReader(await this.run()); @@ -128,13 +176,17 @@ export class DuckDBPreparedStatement { await reader.readAll(); return reader; } - public async runAndReadUntil(targetRowCount: number): Promise { + public async runAndReadUntil( + targetRowCount: number + ): Promise { const reader = new DuckDBResultReader(await this.run()); await reader.readUntil(targetRowCount); return reader; } public async stream(): Promise { - return new DuckDBResult(await duckdb.execute_prepared_streaming(this.prepared_statement)); + return new DuckDBResult( + await duckdb.execute_prepared_streaming(this.prepared_statement) + ); } public async streamAndRead(): Promise { return new DuckDBResultReader(await this.stream()); @@ -144,7 +196,9 @@ export class DuckDBPreparedStatement { await reader.readAll(); return reader; } - public async streamAndReadUntil(targetRowCount: number): Promise { + public async streamAndReadUntil( + targetRowCount: number + ): Promise { const reader = new DuckDBResultReader(await this.stream()); await reader.readUntil(targetRowCount); return reader; diff --git a/api/src/createValue.ts b/api/src/createValue.ts index f64e62b3..630a76ab 100644 --- a/api/src/createValue.ts +++ b/api/src/createValue.ts @@ -2,9 +2,12 @@ import duckdb, { Value } from '@duckdb/node-bindings'; import { DuckDBType } from './DuckDBType'; import { DuckDBTypeId } from './DuckDBTypeId'; import { + DuckDBArrayValue, DuckDBBlobValue, DuckDBDateValue, DuckDBIntervalValue, + DuckDBListValue, + DuckDBStructValue, DuckDBTimestampTZValue, DuckDBTimestampValue, DuckDBTimeTZValue, @@ -120,13 +123,33 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value { case DuckDBTypeId.ENUM: throw new Error(`not yet implemented for ENUM`); // TODO: implement when available in 1.2.0 case DuckDBTypeId.LIST: - throw new Error(`not yet implemented for LIST`); // TODO (need toLogicalType) + if (input instanceof DuckDBListValue) { + return duckdb.create_list_value( + type.valueType.toLogicalType().logical_type, + input.items.map((item) => createValue(type.valueType, item)) + ); + } + throw new Error(`input is not a DuckDBListValue`); case DuckDBTypeId.STRUCT: - throw new Error(`not yet implemented for STRUCT`); // TODO (need toLogicalType) + if (input instanceof DuckDBStructValue) { + return duckdb.create_struct_value( + type.toLogicalType().logical_type, + Object.values(input.entries).map((value, i) => + createValue(type.entryTypes[i], value) + ) + ); + } + throw new Error(`input is not a DuckDBStructValue`); case DuckDBTypeId.MAP: throw new Error(`not yet implemented for MAP`); // TODO: implement when available, hopefully in 1.2.0 case DuckDBTypeId.ARRAY: - throw new Error(`not yet implemented for ARRAY`); // TODO (need toLogicalType) + if (input instanceof DuckDBArrayValue) { + return duckdb.create_array_value( + type.valueType.toLogicalType().logical_type, + input.items.map((item) => createValue(type.valueType, item)) + ); + } + throw new Error(`input is not a DuckDBArrayValue`); case DuckDBTypeId.UUID: throw new Error(`not yet implemented for UUID`); // TODO: implement when available in 1.2.0 case DuckDBTypeId.UNION: diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 6f66bf04..3add574b 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -388,31 +388,43 @@ describe('api', () => { // ensure double-disconnect doesn't break anything connection.disconnect(); }); - test('should support running prepared statements', async () => { + test.only('should support running prepared statements', async () => { await withConnection(async (connection) => { const prepared = await connection.prepare( - 'select $num as a, $str as b, $bool as c, $null as d' + 'select $num as a, $str as b, $bool as c, $timetz as d, $list as e, $struct as f, $array as g, $null as h' ); - assert.strictEqual(prepared.parameterCount, 4); + assert.strictEqual(prepared.parameterCount, 8); assert.strictEqual(prepared.parameterName(1), 'num'); assert.strictEqual(prepared.parameterName(2), 'str'); assert.strictEqual(prepared.parameterName(3), 'bool'); - assert.strictEqual(prepared.parameterName(4), 'null'); + assert.strictEqual(prepared.parameterName(4), 'timetz'); + assert.strictEqual(prepared.parameterName(5), 'list'); + assert.strictEqual(prepared.parameterName(6), 'struct'); + assert.strictEqual(prepared.parameterName(7), 'array'); + assert.strictEqual(prepared.parameterName(8), 'null'); prepared.bindInteger(1, 10); prepared.bindVarchar(2, 'abc'); prepared.bindBoolean(3, true); - prepared.bindNull(4); + prepared.bindTimeTZ(4, TIMETZ.max); + prepared.bindList(5, listValue([100, 200, 300]), LIST(INTEGER)); + prepared.bindStruct(6, structValue({ 'a': 42, 'b': 'duck' }), STRUCT({ 'a': INTEGER, 'b': VARCHAR })); + prepared.bindArray(7, arrayValue([100, 200, 300]), ARRAY(INTEGER, 3)); + prepared.bindNull(8); const result = await prepared.run(); assertColumns(result, [ { name: 'a', type: INTEGER }, { name: 'b', type: VARCHAR }, { name: 'c', type: BOOLEAN }, - { name: 'd', type: INTEGER }, + { name: 'd', type: TIMETZ }, + { name: 'e', type: LIST(INTEGER) }, + { name: 'f', type: STRUCT({ 'a': INTEGER, 'b': VARCHAR }) }, + { name: 'g', type: ARRAY(INTEGER, 3) }, + { name: 'h', type: INTEGER }, ]); const chunk = await result.fetchChunk(); assert.isDefined(chunk); if (chunk) { - assert.strictEqual(chunk.columnCount, 4); + assert.strictEqual(chunk.columnCount, 8); assert.strictEqual(chunk.rowCount, 1); assertValues( chunk, @@ -432,9 +444,33 @@ describe('api', () => { DuckDBBooleanVector, [true] ); - assertValues( + assertValues( chunk, 3, + DuckDBTimeTZVector, + [TIMETZ.max] + ); + assertValues( + chunk, + 4, + DuckDBListVector, + [listValue([100, 200, 300])] + ); + assertValues( + chunk, + 5, + DuckDBStructVector, + [structValue({ 'a': 42, 'b': 'duck' })] + ); + assertValues( + chunk, + 6, + DuckDBArrayVector, + [arrayValue([100, 200, 300])] + ); + assertValues( + chunk, + 7, DuckDBIntegerVector, [null] ); From d525e71cbcde3a5a46cd7554572f627b87381a0e Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Mon, 13 Jan 2025 22:12:08 -0800 Subject: [PATCH 3/4] remove test.only --- api/test/api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 3add574b..bf031c40 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -388,7 +388,7 @@ describe('api', () => { // ensure double-disconnect doesn't break anything connection.disconnect(); }); - test.only('should support running prepared statements', async () => { + test('should support running prepared statements', async () => { await withConnection(async (connection) => { const prepared = await connection.prepare( 'select $num as a, $str as b, $bool as c, $timetz as d, $list as e, $struct as f, $array as g, $null as h' From 67eb7928c90ef8b95d4719caea957a5f85d47380 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Mon, 13 Jan 2025 22:18:39 -0800 Subject: [PATCH 4/4] add list binding example to README --- api/pkgs/@duckdb/node-api/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/pkgs/@duckdb/node-api/README.md b/api/pkgs/@duckdb/node-api/README.md index b3ea0f9e..4a26d4d9 100644 --- a/api/pkgs/@duckdb/node-api/README.md +++ b/api/pkgs/@duckdb/node-api/README.md @@ -103,9 +103,10 @@ const result = await connection.run('from test_all_types()'); ### Parameterize SQL ```ts -const prepared = await connection.prepare('select $1, $2'); +const prepared = await connection.prepare('select $1, $2, $3'); prepared.bindVarchar(1, 'duck'); prepared.bindInteger(2, 42); +prepared.bindList(3, listValue([10, 11, 12]), LIST(INTEGER)); const result = await prepared.run(); ```