From 7850a79372cf77012a19e6305e55011420325cfe Mon Sep 17 00:00:00 2001 From: mahlon <> Date: Sat, 22 Mar 2025 22:28:22 +0000 Subject: [PATCH] Add basic tuple and value fetching from queries. Add safeties for =destroy hooks. FossilOrigin-Name: 2fae5297a0d0598cc3580777688b4f4307de008d4f379d2fb224c8a74cb9b708 --- src/kuzu.nim | 6 +++- src/kuzu/connection.nim | 10 +++--- src/kuzu/database.nim | 14 ++++---- src/kuzu/queries.nim | 33 ++++++++++++++++--- src/kuzu/tuple.nim | 23 +++++++++++++ src/kuzu/types.nim | 19 +++++++++-- src/kuzu/value.nim | 13 ++++++++ tests/queries/t_can_iterate_tuples.nim | 19 +++++++++++ tests/queries/t_can_stringify_results.nim | 17 ++++++++++ .../t_knows_if_there_are_waiting_tuples.nim | 24 ++++++++++++++ tests/tuples/t_can_be_stringified.nim | 19 +++++++++++ .../t_throws_exception_fetching_at_end.nim | 17 ++++++++++ .../t_throws_exception_invalid_index.nim | 20 +++++++++++ tests/values/t_can_be_stringified.nim | 20 +++++++++++ 14 files changed, 237 insertions(+), 17 deletions(-) create mode 100644 src/kuzu/tuple.nim create mode 100644 src/kuzu/value.nim create mode 100644 tests/queries/t_can_iterate_tuples.nim create mode 100644 tests/queries/t_can_stringify_results.nim create mode 100644 tests/queries/t_knows_if_there_are_waiting_tuples.nim create mode 100644 tests/tuples/t_can_be_stringified.nim create mode 100644 tests/tuples/t_throws_exception_fetching_at_end.nim create mode 100644 tests/tuples/t_throws_exception_invalid_index.nim create mode 100644 tests/values/t_can_be_stringified.nim diff --git a/src/kuzu.nim b/src/kuzu.nim index b41779e..e8a100d 100644 --- a/src/kuzu.nim +++ b/src/kuzu.nim @@ -13,14 +13,18 @@ else: include "kuzu/0.8.2.nim" import - std/strformat + std/strformat, + std/strutils +# Order very much matters here pre Nim 3.0 multi-pass compiling. include "kuzu/constants.nim", "kuzu/types.nim", "kuzu/config.nim", "kuzu/database.nim", "kuzu/connection.nim", + "kuzu/value.nim", + "kuzu/tuple.nim", "kuzu/queries.nim" diff --git a/src/kuzu/connection.nim b/src/kuzu/connection.nim index f97e4b3..42a16e6 100644 --- a/src/kuzu/connection.nim +++ b/src/kuzu/connection.nim @@ -2,14 +2,16 @@ proc `=destroy`*( conn: KuzuConnectionObj ) = ## Graceful cleanup for open connection handles. - kuzu_connection_destroy( addr conn.handle ) + if conn.valid: + kuzu_connection_destroy( addr conn.handle ) -proc connect*( db: KuzuDB ): KuzuConnection = +proc connect*( db: KuzuDatabase ): KuzuConnection = ## Connect to a database. result = new KuzuConnection - var rv = kuzu_connection_init( addr db.handle, addr result.handle ) - if rv != KuzuSuccess: + if kuzu_connection_init( addr db.handle, addr result.handle ) == KuzuSuccess: + result.valid = true + else: raise newException( KuzuException, "Unable to connect to the database." ) diff --git a/src/kuzu/database.nim b/src/kuzu/database.nim index bd3e24f..4cfa97d 100644 --- a/src/kuzu/database.nim +++ b/src/kuzu/database.nim @@ -1,21 +1,23 @@ # vim: set et sta sw=4 ts=4 : -proc `=destroy`*( db: KuzuDBObj ) = +proc `=destroy`*( db: KuzuDatabaseObj ) = ## Graceful cleanup for an open DB handle when it goes out of scope. - kuzu_database_destroy( addr db.handle ) + if db.valid: + kuzu_database_destroy( addr db.handle ) -proc newKuzuDatabase*( path="", config=kuzuConfig() ): KuzuDB = +proc newKuzuDatabase*( path="", config=kuzuConfig() ): KuzuDatabase = ## Create a new Kuzu database handle. Creates an in-memory ## database by default, but writes to disk if a +path+ is supplied. - result = new KuzuDB + result = new KuzuDatabase result.config = config result.path = if path != "" and path != ":memory:": path else: "(in-memory)" result.handle = kuzu_database() - var rv = kuzu_database_init( path, config, addr result.handle ) - if rv != KuzuSuccess: + if kuzu_database_init( path, config, addr result.handle ) == KuzuSuccess: + result.valid = true + else: raise newException( KuzuException, "Unable to open database." ) diff --git a/src/kuzu/queries.nim b/src/kuzu/queries.nim index 189918a..2a89f80 100644 --- a/src/kuzu/queries.nim +++ b/src/kuzu/queries.nim @@ -2,22 +2,47 @@ proc `=destroy`*( query: KuzuQueryResultObj ) = ## Graceful cleanup for out of scope query objects. - kuzu_query_result_destroy( addr query.handle ) - kuzu_query_summary_destroy( addr query.summary ) + if query.valid: + kuzu_query_result_destroy( addr query.handle ) + kuzu_query_summary_destroy( addr query.summary ) proc query*( conn: KuzuConnection, query: string ): KuzuQueryResult = ## Perform a database +query+ and return the result. result = new KuzuQueryResult - var rv = kuzu_connection_query( addr conn.handle, query, addr result.handle ) - if rv == KuzuSuccess: + if kuzu_connection_query( addr conn.handle, query, addr result.handle ) == KuzuSuccess: discard kuzu_query_result_get_query_summary( addr result.handle, addr result.summary ) result.num_columns = kuzu_query_result_get_num_columns( addr result.handle ) result.num_tuples = kuzu_query_result_get_num_tuples( addr result.handle ) result.compile_time = kuzu_query_summary_get_compiling_time( addr result.summary ) result.execution_time = kuzu_query_summary_get_execution_time( addr result.summary ) + result.valid = true else: var err = kuzu_query_result_get_error_message( addr result.handle ) raise newException( KuzuQueryException, &"Error running query: {err}" ) +proc `$`*( query: KuzuQueryResult ): string = + ## Return the entire result set as a string. + result = $kuzu_query_result_to_string( addr query.handle ) + + +proc hasNext*( query: KuzuQueryResult ): bool = + ## Returns +true+ if there are more tuples to be consumed. + result = kuzu_query_result_has_next( addr query.handle ) + + +proc getNext*( query: KuzuQueryResult ): KuzuFlatTuple = + result = new KuzuFlatTuple + if kuzu_query_result_get_next( addr query.handle, addr result.handle ) == KuzuSuccess: + result.valid = true + result.num_columns = query.num_columns + else: + raise newException( KuzuQueryException, &"Unable to fetch next tuple." ) + + +iterator items*( query: KuzuQueryResult ): KuzuFlatTuple = + ## Iterate available tuples, yielding to the block. + while query.hasNext: + yield query.getNext + diff --git a/src/kuzu/tuple.nim b/src/kuzu/tuple.nim new file mode 100644 index 0000000..8c23ed3 --- /dev/null +++ b/src/kuzu/tuple.nim @@ -0,0 +1,23 @@ +# vim: set et sta sw=4 ts=4 : + +proc `=destroy`*( tpl: KuzuFlatTupleObj ) = + ## Graceful cleanup for out of scope tuples. + if tpl.valid: + kuzu_flat_tuple_destroy( addr tpl.handle ) + + +proc `$`*( tpl: KuzuFlatTuple ): string = + ## Stringify a tuple. + result = $kuzu_flat_tuple_to_string( addr tpl.handle ) + result.removeSuffix( "\n" ) + + +proc `[]`*( tpl: KuzuFlatTuple, idx: int ): KuzuValue = + ## Returns a KuzuValue at the given +idx+. + result = new KuzuValue + if kuzu_flat_tuple_get_value( addr tpl.handle, idx.uint64, addr result.handle ) == KuzuSuccess: + result.valid = true + else: + raise newException( KuzuIndexException, + &"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 428f1d0..754881f 100644 --- a/src/kuzu/types.nim +++ b/src/kuzu/types.nim @@ -1,14 +1,16 @@ # vim: set et sta sw=4 ts=4 : type - KuzuDBObj = object + KuzuDatabaseObj = object handle*: kuzu_database path*: string config*: kuzu_system_config - KuzuDB* = ref KuzuDBObj + valid = false + KuzuDatabase* = ref KuzuDatabaseObj KuzuConnectionObj = object handle*: kuzu_connection + valid = false KuzuConnection* = ref KuzuConnectionObj KuzuQueryResultObj = object @@ -18,8 +20,21 @@ type num_tuples*: uint64 = 0 compile_time*: cdouble = 0 execution_time*: cdouble = 0 + valid = false KuzuQueryResult* = ref KuzuQueryResultObj + KuzuFlatTupleObj = object + handle*: kuzu_flat_tuple + num_columns: uint64 = 0 + valid = false + KuzuFlatTuple* = ref KuzuFlatTupleObj + + KuzuValueObj = object + handle*: kuzu_value + valid = false + KuzuValue* = ref KuzuValueObj + KuzuException* = object of CatchableError KuzuQueryException* = object of KuzuException + KuzuIndexException* = object of KuzuException diff --git a/src/kuzu/value.nim b/src/kuzu/value.nim new file mode 100644 index 0000000..ba2fe97 --- /dev/null +++ b/src/kuzu/value.nim @@ -0,0 +1,13 @@ +# vim: set et sta sw=4 ts=4 : + +proc `=destroy`*( value: KuzuValueObj ) = + ## Graceful cleanup for out of scope values. + if value.valid: + kuzu_value_destroy( addr value.handle ) + + +proc `$`*( value: KuzuValue ): string = + ## Stringify a value. + result = $kuzu_value_to_string( addr value.handle ) + + diff --git a/tests/queries/t_can_iterate_tuples.nim b/tests/queries/t_can_iterate_tuples.nim new file mode 100644 index 0000000..e4fb45f --- /dev/null +++ b/tests/queries/t_can_iterate_tuples.nim @@ -0,0 +1,19 @@ +# vim: set et sta sw=4 ts=4 : + +discard """ +output: "Camel\nLampshade\nDelicious Cake\n" +""" + +import kuzu + +let db = newKuzuDatabase() +let conn = db.connect + +var q = conn.query( "CREATE NODE TABLE Doop ( id SERIAL, thing STRING, PRIMARY KEY(id) )" ) + +for thing in @[ "Camel", "Lampshade", "Delicious Cake" ]: + q = conn.query( "CREATE (d:Doop {thing: '" & thing & "'})" ) + +for tpl in conn.query( "MATCH (d:Doop) RETURN d.thing" ): + echo $tpl + diff --git a/tests/queries/t_can_stringify_results.nim b/tests/queries/t_can_stringify_results.nim new file mode 100644 index 0000000..eea07f6 --- /dev/null +++ b/tests/queries/t_can_stringify_results.nim @@ -0,0 +1,17 @@ +# vim: set et sta sw=4 ts=4 : + +discard """ +output: "d.thing\nokay!\n\n" +""" + +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.thing" ) + +echo $q + diff --git a/tests/queries/t_knows_if_there_are_waiting_tuples.nim b/tests/queries/t_knows_if_there_are_waiting_tuples.nim new file mode 100644 index 0000000..6a2e76f --- /dev/null +++ b/tests/queries/t_knows_if_there_are_waiting_tuples.nim @@ -0,0 +1,24 @@ +# 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, thing STRING, PRIMARY KEY(id) )" ) +assert typeOf( q ) is KuzuQueryResult + +q = conn.query( "MATCH (d:Doop) RETURN d.thing" ) +assert q.num_tuples == 0 +assert q.hasNext == false + +q = conn.query( "CREATE (d:Doop {thing: 'okay!'})" ) +q = conn.query( "MATCH (d:Doop) RETURN d.thing" ) +assert q.num_tuples == 1 +assert q.hasNext == true + +discard $q # consume the tuple result + +assert q.num_tuples == 1 +assert q.hasNext == false + diff --git a/tests/tuples/t_can_be_stringified.nim b/tests/tuples/t_can_be_stringified.nim new file mode 100644 index 0000000..e255b6d --- /dev/null +++ b/tests/tuples/t_can_be_stringified.nim @@ -0,0 +1,19 @@ +# vim: set et sta sw=4 ts=4 : + +discard """ +output: "okay!" +""" + +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.thing" ) + +var tup = q.getNext + +echo $tup + diff --git a/tests/tuples/t_throws_exception_fetching_at_end.nim b/tests/tuples/t_throws_exception_fetching_at_end.nim new file mode 100644 index 0000000..d771c04 --- /dev/null +++ b/tests/tuples/t_throws_exception_fetching_at_end.nim @@ -0,0 +1,17 @@ +# 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( "MATCH (d:Doop) RETURN d.thing" ) + +try: + discard q.getNext +except KuzuQueryException as err: + assert err.msg.contains( re"""Unable to fetch next tuple.""" ) + diff --git a/tests/tuples/t_throws_exception_invalid_index.nim b/tests/tuples/t_throws_exception_invalid_index.nim new file mode 100644 index 0000000..78fe6d8 --- /dev/null +++ b/tests/tuples/t_throws_exception_invalid_index.nim @@ -0,0 +1,20 @@ +# 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.thing" ) + +let tup = q.getNext + +try: + echo tup[22] +except KuzuIndexException as err: + assert err.msg.contains( re"""Unable to fetch tuple value at idx 22.""" ) + diff --git a/tests/values/t_can_be_stringified.nim b/tests/values/t_can_be_stringified.nim new file mode 100644 index 0000000..d14ad69 --- /dev/null +++ b/tests/values/t_can_be_stringified.nim @@ -0,0 +1,20 @@ +# vim: set et sta sw=4 ts=4 : + +discard """ +output: "okay!" +""" + +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.thing" ) + +var tup = q.getNext +var val = tup[0] + +echo $val +