From 1699e45ae605dd890975608deda25d8f0c623dec Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Sat, 15 Feb 2025 12:34:17 -0800 Subject: [PATCH 1/2] create, get, and bind varint --- api/src/DuckDBPreparedStatement.ts | 4 + api/src/createValue.ts | 5 +- api/test/api.test.ts | 77 +++++++++++-------- .../pkgs/@duckdb/node-bindings/duckdb.d.ts | 4 + bindings/src/duckdb_node_bindings.cpp | 54 +++++++++++++ bindings/test/values.test.ts | 9 ++- 6 files changed, 120 insertions(+), 33 deletions(-) diff --git a/api/src/DuckDBPreparedStatement.ts b/api/src/DuckDBPreparedStatement.ts index ea05e795..7eeed8f2 100644 --- a/api/src/DuckDBPreparedStatement.ts +++ b/api/src/DuckDBPreparedStatement.ts @@ -12,6 +12,7 @@ import { DuckDBType, TIMESTAMPTZ, TIMETZ, + VARINT, } from './DuckDBType'; import { DuckDBTypeId } from './DuckDBTypeId'; import { StatementType } from './enums'; @@ -95,6 +96,9 @@ export class DuckDBPreparedStatement { public bindUHugeInt(parameterIndex: number, value: bigint) { duckdb.bind_uhugeint(this.prepared_statement, parameterIndex, value); } + public bindVarInt(parameterIndex: number, value: bigint) { + this.bindValue(parameterIndex, value, VARINT); + } public bindDecimal(parameterIndex: number, value: DuckDBDecimalValue) { duckdb.bind_decimal(this.prepared_statement, parameterIndex, value); } diff --git a/api/src/createValue.ts b/api/src/createValue.ts index 2e8d6f21..f26717f4 100644 --- a/api/src/createValue.ts +++ b/api/src/createValue.ts @@ -186,7 +186,10 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value { `Cannot create values of type ANY. Specify a specific type.` ); case DuckDBTypeId.VARINT: - throw new Error(`not yet implemented for VARINT`); // TODO: implement when available in 1.2.0 + if (typeof input === 'bigint') { + return duckdb.create_varint(input); + } + throw new Error(`input is not a bigint`); case DuckDBTypeId.SQLNULL: throw new Error(`not yet implemented for SQLNUll`); // TODO: implement when available in 1.2.0 default: diff --git a/api/test/api.test.ts b/api/test/api.test.ts index 18c2bb88..ea3ffce1 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -400,29 +400,40 @@ describe('api', () => { 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' + 'select \ + $num as num, \ + $str as str, \ + $bool as bool, \ + $timetz as timetz, \ + $varint as varint, \ + $list as list, \ + $struct as struct, \ + $array as array, \ + $null as null_value' ); - assert.strictEqual(prepared.parameterCount, 8); + assert.strictEqual(prepared.parameterCount, 9); assert.strictEqual(prepared.parameterName(1), 'num'); assert.strictEqual(prepared.parameterName(2), 'str'); assert.strictEqual(prepared.parameterName(3), 'bool'); 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'); + assert.strictEqual(prepared.parameterName(5), 'varint'); + assert.strictEqual(prepared.parameterName(6), 'list'); + assert.strictEqual(prepared.parameterName(7), 'struct'); + assert.strictEqual(prepared.parameterName(8), 'array'); + assert.strictEqual(prepared.parameterName(9), 'null'); prepared.bindInteger(1, 10); prepared.bindVarchar(2, 'abc'); prepared.bindBoolean(3, true); prepared.bindTimeTZ(4, TIMETZ.max); - prepared.bindList(5, listValue([100, 200, 300]), LIST(INTEGER)); + prepared.bindVarInt(5, VARINT.max); + prepared.bindList(6, listValue([100, 200, 300]), LIST(INTEGER)); prepared.bindStruct( - 6, + 7, structValue({ 'a': 42, 'b': 'duck' }), STRUCT({ 'a': INTEGER, 'b': VARCHAR }) ); - prepared.bindArray(7, arrayValue([100, 200, 300]), ARRAY(INTEGER, 3)); - prepared.bindNull(8); + prepared.bindArray(8, arrayValue([100, 200, 300]), ARRAY(INTEGER, 3)); + prepared.bindNull(9); assert.equal(prepared.parameterTypeId(1), DuckDBTypeId.INTEGER); assert.deepEqual(prepared.parameterType(1), INTEGER); // See https://github.com/duckdb/duckdb/issues/16137 @@ -432,29 +443,32 @@ describe('api', () => { assert.deepEqual(prepared.parameterType(3), BOOLEAN); assert.equal(prepared.parameterTypeId(4), DuckDBTypeId.TIME_TZ); assert.deepEqual(prepared.parameterType(4), TIMETZ); - assert.equal(prepared.parameterTypeId(5), DuckDBTypeId.LIST); - assert.deepEqual(prepared.parameterType(5), LIST(INTEGER)); - assert.equal(prepared.parameterTypeId(6), DuckDBTypeId.STRUCT); - assert.deepEqual(prepared.parameterType(6), STRUCT({ 'a': INTEGER, 'b': VARCHAR })); - assert.equal(prepared.parameterTypeId(7), DuckDBTypeId.ARRAY); - assert.deepEqual(prepared.parameterType(7), ARRAY(INTEGER, 3)); - assert.equal(prepared.parameterTypeId(8), DuckDBTypeId.SQLNULL); - assert.deepEqual(prepared.parameterType(8), SQLNULL); + assert.equal(prepared.parameterTypeId(5), DuckDBTypeId.VARINT); + assert.deepEqual(prepared.parameterType(5), VARINT); + assert.equal(prepared.parameterTypeId(6), DuckDBTypeId.LIST); + assert.deepEqual(prepared.parameterType(6), LIST(INTEGER)); + assert.equal(prepared.parameterTypeId(7), DuckDBTypeId.STRUCT); + assert.deepEqual(prepared.parameterType(7), STRUCT({ 'a': INTEGER, 'b': VARCHAR })); + assert.equal(prepared.parameterTypeId(8), DuckDBTypeId.ARRAY); + assert.deepEqual(prepared.parameterType(8), ARRAY(INTEGER, 3)); + assert.equal(prepared.parameterTypeId(9), DuckDBTypeId.SQLNULL); + assert.deepEqual(prepared.parameterType(9), SQLNULL); const result = await prepared.run(); assertColumns(result, [ - { name: 'a', type: INTEGER }, - { name: 'b', type: VARCHAR }, - { name: 'c', type: BOOLEAN }, - { 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 }, + { name: 'num', type: INTEGER }, + { name: 'str', type: VARCHAR }, + { name: 'bool', type: BOOLEAN }, + { name: 'timetz', type: TIMETZ }, + { name: 'varint', type: VARINT }, + { name: 'list', type: LIST(INTEGER) }, + { name: 'struct', type: STRUCT({ 'a': INTEGER, 'b': VARCHAR }) }, + { name: 'array', type: ARRAY(INTEGER, 3) }, + { name: 'null_value', type: INTEGER }, ]); const chunk = await result.fetchChunk(); assert.isDefined(chunk); if (chunk) { - assert.strictEqual(chunk.columnCount, 8); + assert.strictEqual(chunk.columnCount, 9); assert.strictEqual(chunk.rowCount, 1); assertValues( chunk, @@ -475,16 +489,17 @@ describe('api', () => { [true] ); assertValues(chunk, 3, DuckDBTimeTZVector, [TIMETZ.max]); - assertValues(chunk, 4, DuckDBListVector, [listValue([100, 200, 300])]); - assertValues(chunk, 5, DuckDBStructVector, [ + assertValues(chunk, 4, DuckDBVarIntVector, [VARINT.max]); + assertValues(chunk, 5, DuckDBListVector, [listValue([100, 200, 300])]); + assertValues(chunk, 6, DuckDBStructVector, [ structValue({ 'a': 42, 'b': 'duck' }), ]); - assertValues(chunk, 6, DuckDBArrayVector, [ + assertValues(chunk, 7, DuckDBArrayVector, [ arrayValue([100, 200, 300]), ]); assertValues( chunk, - 7, + 8, DuckDBIntegerVector, [null] ); diff --git a/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts b/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts index cdf12133..892bc2b7 100644 --- a/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts +++ b/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts @@ -637,6 +637,8 @@ export function create_hugeint(input: bigint): Value; export function create_uhugeint(input: bigint): Value; // DUCKDB_API duckdb_value duckdb_create_varint(duckdb_varint input); +export function create_varint(input: bigint): Value; + // DUCKDB_API duckdb_value duckdb_create_decimal(duckdb_decimal input); // DUCKDB_API duckdb_value duckdb_create_float(float input); @@ -705,6 +707,8 @@ export function get_hugeint(value: Value): bigint; export function get_uhugeint(value: Value): bigint; // DUCKDB_API duckdb_varint duckdb_get_varint(duckdb_value val); +export function get_varint(value: Value): bigint; + // DUCKDB_API duckdb_decimal duckdb_get_decimal(duckdb_value val); // DUCKDB_API float duckdb_get_float(duckdb_value val); diff --git a/bindings/src/duckdb_node_bindings.cpp b/bindings/src/duckdb_node_bindings.cpp index fb2d1333..4b70da24 100644 --- a/bindings/src/duckdb_node_bindings.cpp +++ b/bindings/src/duckdb_node_bindings.cpp @@ -213,6 +213,38 @@ Napi::BigInt MakeBigIntFromUHugeInt(Napi::Env env, duckdb_uhugeint uhugeint) { return Napi::BigInt::New(env, sign_bit, word_count, words); } +duckdb_varint GetVarIntFromBigInt(Napi::Env env, Napi::BigInt bigint) { + int sign_bit; + size_t word_count = bigint.WordCount(); + size_t byte_count = word_count * 8; + uint64_t *words = static_cast(duckdb_malloc(byte_count)); + bigint.ToWords(&sign_bit, &word_count, words); + uint8_t *data = reinterpret_cast(words); + idx_t size = byte_count; + bool is_negative = bool(sign_bit); + // convert little-endian to big-endian + for (size_t i = 0; i < size/2; i++) { + auto tmp = data[i]; + data[i] = data[size - 1 - i]; + data[size - 1 - i] = tmp; + } + return { data, size, is_negative }; +} + +Napi::BigInt MakeBigIntFromVarInt(Napi::Env env, duckdb_varint varint) { + int sign_bit = varint.is_negative ? 1 : 0; + size_t word_count = varint.size / 8; + uint8_t *data = static_cast(duckdb_malloc(varint.size)); + // convert big-endian to little-endian + for (size_t i = 0; i < varint.size; i++) { + data[i] = varint.data[varint.size - 1 - i]; + } + uint64_t *words = reinterpret_cast(data); + auto bigint = Napi::BigInt::New(env, sign_bit, word_count, words); + duckdb_free(data); + return bigint; +} + Napi::Object MakeDecimalObject(Napi::Env env, duckdb_decimal decimal) { auto decimal_obj = Napi::Object::New(env); decimal_obj.Set("width", Napi::Number::New(env, decimal.width)); @@ -1110,6 +1142,7 @@ class DuckDBNodeAddon : public Napi::Addon { InstanceMethod("create_int64", &DuckDBNodeAddon::create_int64), InstanceMethod("create_hugeint", &DuckDBNodeAddon::create_hugeint), InstanceMethod("create_uhugeint", &DuckDBNodeAddon::create_uhugeint), + InstanceMethod("create_varint", &DuckDBNodeAddon::create_varint), InstanceMethod("create_float", &DuckDBNodeAddon::create_float), InstanceMethod("create_double", &DuckDBNodeAddon::create_double), InstanceMethod("create_date", &DuckDBNodeAddon::create_date), @@ -1129,6 +1162,7 @@ class DuckDBNodeAddon : public Napi::Addon { InstanceMethod("get_uint64", &DuckDBNodeAddon::get_uint64), InstanceMethod("get_hugeint", &DuckDBNodeAddon::get_hugeint), InstanceMethod("get_uhugeint", &DuckDBNodeAddon::get_uhugeint), + InstanceMethod("get_varint", &DuckDBNodeAddon::get_varint), InstanceMethod("get_float", &DuckDBNodeAddon::get_float), InstanceMethod("get_double", &DuckDBNodeAddon::get_double), InstanceMethod("get_date", &DuckDBNodeAddon::get_date), @@ -2416,6 +2450,16 @@ class DuckDBNodeAddon : public Napi::Addon { } // DUCKDB_API duckdb_value duckdb_create_varint(duckdb_varint input); + // function create_varint(input: bigint): Value + Napi::Value create_varint(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto input_bigint = info[0].As(); + auto varint = GetVarIntFromBigInt(env, input_bigint); + auto value = duckdb_create_varint(varint); + duckdb_free(varint.data); + return CreateExternalForValue(env, value); + } + // DUCKDB_API duckdb_value duckdb_create_decimal(duckdb_decimal input); // DUCKDB_API duckdb_value duckdb_create_float(float input); @@ -2600,6 +2644,16 @@ class DuckDBNodeAddon : public Napi::Addon { } // DUCKDB_API duckdb_varint duckdb_get_varint(duckdb_value val); + // function get_varint(value: Value): bigint + Napi::Value get_varint(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto value = GetValueFromExternal(env, info[0]); + auto varint = duckdb_get_varint(value); + auto bigint = MakeBigIntFromVarInt(env, varint); + duckdb_free(varint.data); + return bigint; + } + // DUCKDB_API duckdb_decimal duckdb_get_decimal(duckdb_value val); // DUCKDB_API float duckdb_get_float(duckdb_value val); diff --git a/bindings/test/values.test.ts b/bindings/test/values.test.ts index e66bd963..e9b00334 100644 --- a/bindings/test/values.test.ts +++ b/bindings/test/values.test.ts @@ -25,7 +25,8 @@ import { UINTEGER, USMALLINT, UTINYINT, - VARCHAR + VARCHAR, + VARINT } from './utils/expectedLogicalTypes'; suite('values', () => { @@ -95,6 +96,12 @@ suite('values', () => { expectLogicalType(duckdb.get_value_type(uhugeint_value), UHUGEINT); expect(duckdb.get_uhugeint(uhugeint_value)).toBe(input); }); + test('varint', () => { + const input = -((((2n ** 10n + 11n) * (2n ** 64n) + (2n ** 9n + 7n)) * (2n ** 64n)) + (2n ** 8n + 5n)); + const varint_value = duckdb.create_varint(input); + expectLogicalType(duckdb.get_value_type(varint_value), VARINT); + expect(duckdb.get_varint(varint_value)).toBe(input); + }); test('float', () => { const input = 3.4028234663852886e38; const float_value = duckdb.create_float(input); From df5cc16e24f5fac16883497356b2b463a3619bd6 Mon Sep 17 00:00:00 2001 From: Jeff Raymakers Date: Sat, 15 Feb 2025 12:36:30 -0800 Subject: [PATCH 2/2] update function accounting comment --- bindings/src/duckdb_node_bindings.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/src/duckdb_node_bindings.cpp b/bindings/src/duckdb_node_bindings.cpp index 4b70da24..546c1af3 100644 --- a/bindings/src/duckdb_node_bindings.cpp +++ b/bindings/src/duckdb_node_bindings.cpp @@ -3962,11 +3962,11 @@ NODE_API_ADDON(DuckDBNodeAddon) --- 411 total functions - 214 instance methods + 216 instance methods 3 unimplemented instance cache functions 1 unimplemented logical type function - 10 unimplemented value creation functions - 13 unimplemented value inspection functions + 9 unimplemented value creation functions + 12 unimplemented value inspection functions 13 unimplemented scalar function functions 4 unimplemented scalar function set functions 12 unimplemented aggregate function functions