From 6f6ab2f85a2006d02eba3925e24c5c2091fe1826 Mon Sep 17 00:00:00 2001 From: mahlon <> Date: Sun, 30 Mar 2025 10:03:39 +0000 Subject: [PATCH] Add first round of kuzu -> nim type conversions. FossilOrigin-Name: 7435f8dcdff8f0a22eb1a07b4e19f379dd731e6557eeefb5dbf75f5b32cb82ed --- src/kuzu/tuple.nim | 3 + src/kuzu/types.nim | 6 + src/kuzu/value.nim | 190 +++++++++++++++++- .../values/t_can_convert_to_native_types.nim | 96 +++++++++ tests/values/t_can_return_a_list_object.nim | 39 ++++ tests/values/t_can_return_a_struct_object.nim | 22 ++ .../t_raise_error_on_invalid_conversion.nim | 24 +++ .../t_raises_error_on_struct_missing_key.nim | 21 ++ 8 files changed, 392 insertions(+), 9 deletions(-) create mode 100644 tests/values/t_can_convert_to_native_types.nim create mode 100644 tests/values/t_can_return_a_list_object.nim create mode 100644 tests/values/t_can_return_a_struct_object.nim create mode 100644 tests/values/t_raise_error_on_invalid_conversion.nim create mode 100644 tests/values/t_raises_error_on_struct_missing_key.nim diff --git a/src/kuzu/tuple.nim b/src/kuzu/tuple.nim index b16917a..ac8fb61 100644 --- a/src/kuzu/tuple.nim +++ b/src/kuzu/tuple.nim @@ -1,5 +1,7 @@ # vim: set et sta sw=4 ts=4 : +# NOTE: Constructor in queries.nim, #getNext + proc `=destroy`*( tpl: KuzuFlatTupleObj ) = ## Graceful cleanup for out of scope tuples. if tpl.valid: @@ -25,3 +27,4 @@ func `[]`*( tpl: KuzuFlatTuple, idx: int ): KuzuValue = raise newException( KuzuIndexError, &"Unable to fetch tuple value at idx {idx}. ({tpl.num_columns} column(s).)" ) + diff --git a/src/kuzu/types.nim b/src/kuzu/types.nim index aec9eec..59e5c5a 100644 --- a/src/kuzu/types.nim +++ b/src/kuzu/types.nim @@ -42,6 +42,12 @@ type kind*: kuzu_data_type_id KuzuValue* = ref KuzuValueObj + KuzuStructValueObj = object + value: KuzuValue + len*: uint64 + keys*: seq[ string ] + KuzuStructValue* = ref KuzuStructValueObj + KuzuException* = object of CatchableError KuzuQueryError* = object of KuzuException KuzuIndexError* = object of KuzuException diff --git a/src/kuzu/value.nim b/src/kuzu/value.nim index 1e32514..8f5675b 100644 --- a/src/kuzu/value.nim +++ b/src/kuzu/value.nim @@ -1,5 +1,7 @@ # vim: set et sta sw=4 ts=4 : +# NOTE: Constructor in tuples.nim, #[] + proc `=destroy`*( value: KuzuValueObj ) = ## Graceful cleanup for out of scope values. if value.valid: @@ -7,11 +9,6 @@ proc `=destroy`*( value: KuzuValueObj ) = kuzu_value_destroy( addr value.handle ) -func `$`*( value: KuzuValue ): string = - ## Stringify a value. - result = $kuzu_value_to_string( addr value.handle ) - - func getType( value: KuzuValue ) = ## Find and set the native Kuzu type of this value. var logical_type: kuzu_logical_type @@ -20,11 +17,186 @@ func getType( value: KuzuValue ) = kuzu_data_type_destroy( addr logical_type ) +func `$`*( value: KuzuValue ): string = + ## Stringify a value. + result = $kuzu_value_to_string( addr value.handle ) + + +func toBool*( value: KuzuValue ): bool = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_BOOL: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != bool" ) + assert( kuzu_value_get_bool( addr value.handle, addr result ) == KuzuSuccess ) + + func toInt8*( value: KuzuValue ): int8 = + ## Conversion from Kuzu type to Nim. if value.kind != KUZU_INT8: raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != int8" ) - assert( - kuzu_value_get_int8( addr value.handle, addr result ) == - KuzuSuccess - ) + assert( kuzu_value_get_int8( addr value.handle, addr result ) == KuzuSuccess ) + + +func toInt16*( value: KuzuValue ): int16 = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_INT16: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != int16" ) + assert( kuzu_value_get_int16( addr value.handle, addr result ) == KuzuSuccess ) + + +func toInt32*( value: KuzuValue ): int32 = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_INT32: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != int32" ) + assert( kuzu_value_get_int32( addr value.handle, addr result ) == KuzuSuccess ) + + +func toInt64*( value: KuzuValue ): int64 = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_INT64: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != int64" ) + assert( kuzu_value_get_int64( addr value.handle, addr result ) == KuzuSuccess ) + + +func toUint8*( value: KuzuValue ): uint8 = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_UINT8: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != uint8" ) + assert( kuzu_value_get_uint8( addr value.handle, addr result ) == KuzuSuccess ) + + +func toUint16*( value: KuzuValue ): uint16 = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_UINT16: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != uint16" ) + assert( kuzu_value_get_uint16( addr value.handle, addr result ) == KuzuSuccess ) + + +func toUint32*( value: KuzuValue ): uint32 = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_UINT32: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != uint32" ) + assert( kuzu_value_get_uint32( addr value.handle, addr result ) == KuzuSuccess ) + + +func toUint64*( value: KuzuValue ): uint64 = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_UINT64: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != uint64" ) + assert( kuzu_value_get_uint64( addr value.handle, addr result ) == KuzuSuccess ) + + +func toDouble*( value: KuzuValue ): float = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_DOUBLE: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != double" ) + assert( kuzu_value_get_double( addr value.handle, addr result ) == KuzuSuccess ) + + +func toFloat*( value: KuzuValue ): float = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_FLOAT: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != float" ) + var rv: cfloat + assert( kuzu_value_get_float( addr value.handle, addr rv ) == KuzuSuccess ) + result = rv + + +func toTimestamp*( value: KuzuValue ): int = + ## Conversion from Kuzu type to Nim. + if value.kind != KUZU_TIMESTAMP: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != timestamp" ) + var rv: kuzu_timestamp_t + assert( kuzu_value_get_timestamp( addr value.handle, addr rv ) == KuzuSuccess ) + result = rv.value + + +func toList*( value: KuzuValue ): seq[ KuzuValue ] = + ## Return a sequence from KUZU_LIST values. + if value.kind != KUZU_LIST: + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != list" ) + result = @[] + var size: uint64 + assert( kuzu_value_get_list_size( addr value.handle, addr size ) == KuzuSuccess ) + if size == 0: return + + for i in ( 0 .. size-1 ): + var kval = new KuzuValue + assert( + kuzu_value_get_list_element( + addr value.handle, i.uint64, addr kval.handle + ) == KuzuSuccess ) + kval.getType() + result.add( kval ) + +const toSeq* = toList + + +func toStruct*( value: KuzuValue ): KuzuStructValue = + ## Create a convenience class for struct-like KuzuValues. + if not [ + KUZU_STRUCT, + KUZU_NODE, + KUZU_REL, + KUZU_RECURSIVE_REL, + KUZU_UNION + ].contains( value.kind ): + raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != struct" ) + result = new KuzuStructValue + result.value = value + + discard kuzu_value_get_struct_num_fields( addr value.handle, addr result.len ) + if result.len == 0: return + + # Build keys + for idx in ( 0 .. result.len - 1 ): + var keyname: cstring + assert( + kuzu_value_get_struct_field_name( + addr value.handle, idx.uint64, addr keyname + ) == KuzuSuccess ) + result.keys.add( $keyname ) + +const toNode* = toStruct +const toRel* = toStruct + + +func `[]`*( struct: KuzuStructValue, key: string ): KuzuValue = + ## Return a KuzuValue for the struct *key*. + var idx: uint64 + var found = false + for i in ( 0 .. struct.len-1 ): + if struct.keys[i] == key: + found = true + idx = i + break + if not found: + raise newException( KuzuIndexError, + &"""No such struct key "{key}".""" ) + + result = new KuzuValue + assert( + kuzu_value_get_struct_field_value( + addr struct.value.handle, idx.uint64, addr result.handle + ) == KuzuSuccess ) + result.getType() + + +func `$`*( struct: KuzuStructValue ): string = + ## Stringify a struct value. + result = $kuzu_value_to_string( addr struct.value.handle ) + + +# func toBlob*( value: KuzuValue ): array[ 4096, byte ] = +# ## Conversion from Kuzu type to Nim. +# if value.kind != KUZU_BLOB: +# raise newException( KuzuTypeError, &"Mismatched types: {value.kind} != blob" ) +# var rv: array[ 4096, byte ] +# assert( kuzu_value_get_blob( addr value.handle, addr rv[0] ) == KuzuSuccess ) +# return rv + + +#[ + BLOB + MAP +]# diff --git a/tests/values/t_can_convert_to_native_types.nim b/tests/values/t_can_convert_to_native_types.nim new file mode 100644 index 0000000..b8c0c6b --- /dev/null +++ b/tests/values/t_can_convert_to_native_types.nim @@ -0,0 +1,96 @@ +# vim: set et sta sw=4 ts=4 : + +import kuzu + +let db = newKuzuDatabase() +let conn = db.connect + +var q = conn.query( """CREATE NODE TABLE Doop ( + id SERIAL, + test1 int8, + test2 int16, + test3 int32, + test4 int64, + test5 uint8, + test6 uint16, + test7 uint32, + test8 uint64, + test9 float, + test10 double, + test11 timestamp, + test12 string, + PRIMARY KEY(id))""" ) + +q = conn.query( """CREATE (d:Doop { + test1: 12, + test2: 144, + test3: -2201, + test4: 123550332, + test5: 2, + test6: 82, + test7: 50922, + test8: 294050066922312345, + test9: 10.5, + test10: 2.333021, + test11: TIMESTAMP("2025-03-29"), + test12: "Well hello, there." })""" ) + +q = conn.query( "MATCH (d:Doop) RETURN d.*" ) +var tup = q.getNext + +var id = tup[0].toInt64 +assert typeOf( id ) is int64 +assert id == 0 + +var test1 = tup[1].toInt8 +assert typeOf( test1 ) is int8 +assert test1 == 12 + +var test2 = tup[2].toInt16 +assert typeOf( test2 ) is int16 +assert test2 == 144 + +var test3 = tup[3].toInt32 +assert typeOf( test3 ) is int32 +assert test3 == -2201 + +var test4 = tup[4].toInt64 +assert typeOf( test4 ) is int64 +assert test4 == 123550332 + +var test5 = tup[5].toUint8 +assert typeOf( test5 ) is uint8 +assert test5 == 2 + +var test6 = tup[6].toUint16 +assert typeOf( test6 ) is uint16 +assert test6 == 82 + +var test7 = tup[7].toUint32 +assert typeOf( test7 ) is uint32 +assert test7 == 50922 + +var test8 = tup[8].toUint64 +assert typeOf( test8 ) is uint64 +assert test8 == 294050066922312345.uint64 + +var test9 = tup[9].toFloat +assert typeOf( test9 ) is float +assert test9 == 10.5 + +var test10 = tup[10].toDouble +assert typeOf( test10 ) is float +assert test10 == 2.333021 + +var test11 = tup[11].toTimestamp +assert typeOf( test11 ) is int +assert test11 == 1743206400000000 + +var test11s = $tup[11] +assert typeOf( test11s ) is string +assert test11s == "2025-03-29 00:00:00" + +var test12 = $tup[12] +assert typeOf( test12 ) is string +assert test12 == "Well hello, there." + diff --git a/tests/values/t_can_return_a_list_object.nim b/tests/values/t_can_return_a_list_object.nim new file mode 100644 index 0000000..db32e63 --- /dev/null +++ b/tests/values/t_can_return_a_list_object.nim @@ -0,0 +1,39 @@ +# vim: set et sta sw=4 ts=4 : + +import kuzu + +let db = newKuzuDatabase() +let conn = db.connect + +var q = conn.query( "RETURN [1,2,3,4,5] AS list" ) +var list = q.getNext[0] +assert list.kind == KUZU_LIST + +var items = list.toSeq +assert items.len == 5 +assert typeOf( items ) is seq[KuzuValue] + +for i in items: + assert( i.kind == KUZU_INT64 ) + + +q = conn.query( """RETURN ["woo", "hoo"] AS list""" ) +list = q.getNext[0] +assert list.kind == KUZU_LIST + +items = list.toSeq +assert items.len == 2 +assert typeOf( items ) is seq[KuzuValue] + +for i in items: + assert( i.kind == KUZU_STRING ) + + +q = conn.query( """RETURN [] AS list""" ) +list = q.getNext[0] +assert list.kind == KUZU_LIST + +items = list.toList +assert items.len == 0 +assert typeOf( items ) is seq[KuzuValue] + diff --git a/tests/values/t_can_return_a_struct_object.nim b/tests/values/t_can_return_a_struct_object.nim new file mode 100644 index 0000000..c8361f1 --- /dev/null +++ b/tests/values/t_can_return_a_struct_object.nim @@ -0,0 +1,22 @@ +# vim: set et sta sw=4 ts=4 : + +import kuzu + +let db = newKuzuDatabase() +let conn = db.connect + +var q = conn.query( """RETURN 12""" ) +try: + discard q.getNext[0].toStruct +except KuzuTypeError: + discard + + +q = conn.query( """RETURN {test1: 1, test2: "bewts"} AS struct""" ) +var struct = q.getNext[0].toStruct + +assert struct.len == 2 +assert struct.keys == @["test1", "test2"] +assert struct["test1"].toInt64 == 1 +assert $struct["test2"] == "bewts" + diff --git a/tests/values/t_raise_error_on_invalid_conversion.nim b/tests/values/t_raise_error_on_invalid_conversion.nim new file mode 100644 index 0000000..b1fba6d --- /dev/null +++ b/tests/values/t_raise_error_on_invalid_conversion.nim @@ -0,0 +1,24 @@ +# vim: set et sta sw=4 ts=4 : + +import + std/re +import kuzu + +let db = newKuzuDatabase() +let conn = db.connect + +var q = conn.query( "CREATE NODE TABLE Doop ( id SERIAL, thing STRING, PRIMARY KEY(id) )" ) + +q = conn.query( "CREATE (d:Doop {thing: 'okay!'})" ) +q = conn.query( "MATCH (d:Doop) RETURN d" ) + +var tup = q.getNext +var val = tup[0] +assert val.kind == KUZU_NODE + +try: + discard val.toInt32 +except KuzuTypeError as err: + assert err.msg.contains( re"""Mismatched types: KUZU_NODE != int32""" ) + + diff --git a/tests/values/t_raises_error_on_struct_missing_key.nim b/tests/values/t_raises_error_on_struct_missing_key.nim new file mode 100644 index 0000000..63c77a3 --- /dev/null +++ b/tests/values/t_raises_error_on_struct_missing_key.nim @@ -0,0 +1,21 @@ +# vim: set et sta sw=4 ts=4 : + +import + std/re +import kuzu + +let db = newKuzuDatabase() +let conn = db.connect + +var q = conn.query( """RETURN {test1: 1, test2: "bewts"} AS struct""" ) +var struct = q.getNext[0].toStruct + +assert struct.len == 2 +assert struct.keys == @["test1", "test2"] + +try: + discard struct["nope"] +except KuzuIndexError as err: + assert err.msg.contains( re"""No such struct key "nope"""" ) + +