diff --git a/api/src/DuckDBPreparedStatement.ts b/api/src/DuckDBPreparedStatement.ts index 93adf3d..9f0afbf 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 f64e62b..630a76a 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 6f66bf0..3add574 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] );