diff --git a/api/src/createValue.ts b/api/src/createValue.ts index 6cd55270..8a836fa8 100644 --- a/api/src/createValue.ts +++ b/api/src/createValue.ts @@ -220,7 +220,7 @@ export function createValue(type: DuckDBType, input: DuckDBValue): Value { } 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 + return duckdb.create_null_value(); default: throw new Error(`unrecognized type id ${typeId}`); } diff --git a/api/test/api.test.ts b/api/test/api.test.ts index f89351ac..85944045 100644 --- a/api/test/api.test.ts +++ b/api/test/api.test.ts @@ -121,6 +121,7 @@ import { createTestAllTypesRowObjectsJson, createTestAllTypesRowsJson, } from './util/testAllTypes'; +import { replaceSqlNullWithInteger } from './util/replaceSqlNullWithInteger'; async function sleep(ms: number): Promise { return new Promise((resolve) => { @@ -409,6 +410,7 @@ describe('api', () => { { name: 'timestamp_ns', type: TIMESTAMP_NS }, { name: 'list_int', type: LIST(INTEGER) }, { name: 'list_dec', type: LIST(DECIMAL(4, 1)) }, + { name: 'list_null', type: LIST(SQLNULL) }, { name: 'struct', type: STRUCT({ 'a': INTEGER, 'b': VARCHAR }) }, { name: 'array', type: ARRAY(INTEGER, 3) }, { name: 'uuid', type: UUID }, @@ -426,7 +428,11 @@ describe('api', () => { assert.strictEqual(prepared.parameterCount, params.length); for (let i = 0; i < params.length; i++) { - assert.strictEqual(prepared.parameterName(i + 1), params[i].name, `param ${i} name mismatch`); + assert.strictEqual( + prepared.parameterName(i + 1), + params[i].name, + `param ${i} name mismatch` + ); } let i = 1; @@ -442,6 +448,7 @@ describe('api', () => { listValue([decimalValue(9876n, 4, 1), decimalValue(5432n, 4, 1)]), LIST(DECIMAL(4, 1)) ); + prepared.bindList(i++, listValue([null]), LIST(SQLNULL)); prepared.bindStruct( i++, structValue({ 'a': 42, 'b': 'duck' }), @@ -461,16 +468,28 @@ describe('api', () => { // VARCHAR type is reported incorrectly; see https://github.com/duckdb/duckdb/issues/16137 continue; } - assert.equal(prepared.parameterTypeId(i + 1), type.typeId, `param ${i} type id mismatch`); - assert.deepEqual(prepared.parameterType(i + 1), type, `param ${i} type mismatch`); + assert.equal( + prepared.parameterTypeId(i + 1), + type.typeId, + `param ${i} type id mismatch` + ); + assert.deepEqual( + prepared.parameterType(i + 1), + type, + `param ${i} type mismatch` + ); } const result = await prepared.run(); // In the result, SQLNULL params get type INTEGER. - const expectedColumns = params.map((p) => - p.type.typeId === DuckDBTypeId.SQLNULL ? { ...p, type: INTEGER } : p - ); + const expectedColumns = params.map((p) => { + const replacedType = replaceSqlNullWithInteger(p.type); + if (replacedType !== p.type) { + return { ...p, type: replacedType }; + } + return p; + }); assertColumns(result, expectedColumns); @@ -514,14 +533,19 @@ describe('api', () => { assertValues(chunk, i++, DuckDBListVector, [ listValue([decimalValue(9876n, 4, 1), decimalValue(5432n, 4, 1)]), ]); + assertValues(chunk, i++, DuckDBListVector, [listValue([null])]); assertValues(chunk, i++, DuckDBStructVector, [ structValue({ 'a': 42, 'b': 'duck' }), ]); assertValues(chunk, i++, DuckDBArrayVector, [ arrayValue([100, 200, 300]), ]); - assertValues(chunk, i++, DuckDBUUIDVector, [uuidValue(0xf0e1d2c3b4a596870123456789abcdefn)]); - assertValues(chunk, i++, DuckDBBitVector, [bitValue('0010001001011100010101011010111')]); + assertValues(chunk, i++, DuckDBUUIDVector, [ + uuidValue(0xf0e1d2c3b4a596870123456789abcdefn), + ]); + assertValues(chunk, i++, DuckDBBitVector, [ + bitValue('0010001001011100010101011010111'), + ]); assertValues(chunk, i++, DuckDBTimeTZVector, [TIMETZ.max]); assertValues(chunk, i++, DuckDBTimestampTZVector, [TIMESTAMPTZ.max]); assertValues(chunk, i++, DuckDBVarIntVector, [VARINT.max]); diff --git a/api/test/util/replaceSqlNullWithInteger.ts b/api/test/util/replaceSqlNullWithInteger.ts new file mode 100644 index 00000000..bf680dbe --- /dev/null +++ b/api/test/util/replaceSqlNullWithInteger.ts @@ -0,0 +1,51 @@ +import { + ARRAY, + DuckDBIntegerType, + DuckDBType, + DuckDBTypeId, + LIST, + MAP, + STRUCT, + UNION, +} from '../../src'; + +export function replaceSqlNullWithInteger(input: DuckDBType): DuckDBType { + switch (input.typeId) { + case DuckDBTypeId.SQLNULL: + return DuckDBIntegerType.create(input.alias); + case DuckDBTypeId.LIST: + return LIST(replaceSqlNullWithInteger(input.valueType), input.alias); + case DuckDBTypeId.STRUCT: { + const entries: Record = {}; + for (let i = 0; i < input.entryCount; i++) { + entries[input.entryNames[i]] = replaceSqlNullWithInteger( + input.entryTypes[i] + ); + } + return STRUCT(entries, input.alias); + } + case DuckDBTypeId.ARRAY: + return ARRAY( + replaceSqlNullWithInteger(input.valueType), + input.length, + input.alias + ); + case DuckDBTypeId.MAP: + return MAP( + replaceSqlNullWithInteger(input.keyType), + replaceSqlNullWithInteger(input.valueType), + input.alias + ); + case DuckDBTypeId.UNION: { + const members: Record = {}; + for (let i = 0; i < input.memberCount; i++) { + members[input.memberTags[i]] = replaceSqlNullWithInteger( + input.memberTypes[i] + ); + } + return UNION(members, input.alias); + } + default: + return input; + } +} diff --git a/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts b/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts index 4a07c36f..0792ddd1 100644 --- a/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts +++ b/bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts @@ -790,7 +790,11 @@ export function get_map_key(value: Value, index: number): Value; export function get_map_value(value: Value, index: number): Value; // DUCKDB_API bool duckdb_is_null_value(duckdb_value value); +export function is_null_value(value: Value): boolean; + // DUCKDB_API duckdb_value duckdb_create_null_value(); +export function create_null_value(): Value; + // DUCKDB_API idx_t duckdb_get_list_size(duckdb_value value); // DUCKDB_API duckdb_value duckdb_get_list_child(duckdb_value value, idx_t index); // DUCKDB_API duckdb_value duckdb_create_enum_value(duckdb_logical_type type, uint64_t value); diff --git a/bindings/src/duckdb_node_bindings.cpp b/bindings/src/duckdb_node_bindings.cpp index c9216315..52ee92b1 100644 --- a/bindings/src/duckdb_node_bindings.cpp +++ b/bindings/src/duckdb_node_bindings.cpp @@ -1211,6 +1211,8 @@ class DuckDBNodeAddon : public Napi::Addon { InstanceMethod("get_map_size", &DuckDBNodeAddon::get_map_size), InstanceMethod("get_map_key", &DuckDBNodeAddon::get_map_key), InstanceMethod("get_map_value", &DuckDBNodeAddon::get_map_value), + InstanceMethod("is_null_value", &DuckDBNodeAddon::is_null_value), + InstanceMethod("create_null_value", &DuckDBNodeAddon::create_null_value), InstanceMethod("create_logical_type", &DuckDBNodeAddon::create_logical_type), InstanceMethod("logical_type_get_alias", &DuckDBNodeAddon::logical_type_get_alias), @@ -2988,7 +2990,22 @@ class DuckDBNodeAddon : public Napi::Addon { } // DUCKDB_API bool duckdb_is_null_value(duckdb_value value); + // function is_null_value(value: Value): boolean + Napi::Value is_null_value(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto value = GetValueFromExternal(env, info[0]); + auto is_null = duckdb_is_null_value(value); + return Napi::Boolean::New(env, is_null); + } + // DUCKDB_API duckdb_value duckdb_create_null_value(); + // function create_null_value(): Value + Napi::Value create_null_value(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto value = duckdb_create_null_value(); + return CreateExternalForValue(env, value); + } + // DUCKDB_API idx_t duckdb_get_list_size(duckdb_value value); // DUCKDB_API duckdb_value duckdb_get_list_child(duckdb_value value, idx_t index); // DUCKDB_API duckdb_value duckdb_create_enum_value(duckdb_logical_type type, uint64_t value); @@ -4104,11 +4121,11 @@ NODE_API_ADDON(DuckDBNodeAddon) --- 411 total functions - 230 instance methods + 232 instance methods 3 unimplemented instance cache functions 1 unimplemented logical type function - 2 unimplemented value creation functions - 5 unimplemented value inspection functions + 1 unimplemented value creation functions + 4 unimplemented value inspection functions 13 unimplemented scalar function functions 4 unimplemented scalar function set functions 12 unimplemented aggregate function functions diff --git a/bindings/test/values.test.ts b/bindings/test/values.test.ts index aef39fee..3756babd 100644 --- a/bindings/test/values.test.ts +++ b/bindings/test/values.test.ts @@ -275,4 +275,10 @@ suite('values', () => { 'Failed to create array value' ); }); + test('null', () => { + const null_value = duckdb.create_null_value(); + expect(duckdb.is_null_value(null_value)).toEqual(true); + const int32_value = duckdb.create_int32(42); + expect(duckdb.is_null_value(int32_value)).toEqual(false); + }); });