From 6d34b081bb4e89c5fb9299a77255d06d41696d5a Mon Sep 17 00:00:00 2001 From: mahlon <> Date: Sun, 23 Mar 2025 21:21:05 +0000 Subject: [PATCH] Work on prepared statements. (Still not working 100%, but getting close.) Additionally, start on the README, fix some type member visibility, add some additional tests, tag some FIXMEs for where type conversions will take place, and add `#rewind` for the query iterator. FossilOrigin-Name: 490f27a4792d5243d82d90dcb12be1074c945c74d7fa63dd5baaf942ac42d7c9 --- README.md | 110 +++++++++++++++- kuzu.nimble | 10 +- src/kuzu/queries.nim | 120 +++++++++++++++++- src/kuzu/tuple.nim | 8 ++ src/kuzu/types.nim | 20 ++- tests/queries/t_can_prepare_a_statement.nim | 23 ++++ tests/queries/t_can_rewind_iteration.nim | 23 ++++ tests/queries/t_raises_on_unknown_varbind.nim | 20 +++ ...raises_with_invalid_prepared_statement.nim | 20 +++ .../t_throws_exception_fetching_at_end.nim | 4 +- 10 files changed, 344 insertions(+), 14 deletions(-) create mode 100644 tests/queries/t_can_prepare_a_statement.nim create mode 100644 tests/queries/t_can_rewind_iteration.nim create mode 100644 tests/queries/t_raises_on_unknown_varbind.nim create mode 100644 tests/queries/t_raises_with_invalid_prepared_statement.nim diff --git a/README.md b/README.md index e9c7476..7802664 100644 --- a/README.md +++ b/README.md @@ -1 +1,109 @@ -**TBD** + +# Nim Kuzu + +home +: https://code.martini.nu/fossil/nim-kuzu + +github_mirror +: https://github.com/mahlonsmith/nim-kuzu + + +## Description + +This is a Nim binding for the Kuzu graph database library. + +Kuzu is an embedded graph database built for query speed and scalability. It is +optimized for handling complex join-heavy analytical workloads on very large +graphs, with the following core feature set: + +- Property Graph data model and Cypher query language +- Embedded (in-process) integration with applications +- Columnar disk-based storage +- Columnar, compressed sparse row-based (CSR) adjacency list/join indices +- Vectorized and factorized query processor +- Novel and very fast join algorithms +- Multi-core query parallelism +- Serializable ACID transactions + +For more information about Kuzu itself, see its +[documentation](https://docs.kuzudb.com/). + + +## Prerequisites + +* A functioning Nim >= 2 installation +- [KuzuDB](https://kuzudb.com) + + +## Installation + + $ nimble install kuzu + + +## Usage + + +> [!TODO]- Human readable usage docs! +> +> ... The nim generated source isn't great when pulling in +> the C wrapper auto-gen stuff. +> +> If you're here and reading this before I have proper docs written, see the +> tests/ for some working examples. + + + +## Contributing + +You can check out the current development source with Fossil via its [home +repo](https://code.martini.nu/fossil/nim-kuzu), or with Git/Jujutsu at its +[project mirror](https://github.com/mahlonsmith/nim-kuzu) + +After checking out the source, uncomment the development dependencies +from the `kuzu.nimble` file, and run: + + $ nimble setup + +This will install dependencies, and do any other necessary setup for +development. + + + +## Authors + +- Mahlon E. Smith + +A note of thanks to @mantielero on Github, who has a Kuzu binding for an early +KuzuDB (0.4.x) that I found after starting this project. + + +## License + +Copyright (c) 2025 Mahlon E. Smith +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author/s, nor the names of the project's + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/kuzu.nimble b/kuzu.nimble index 3c7dc77..e06a162 100644 --- a/kuzu.nimble +++ b/kuzu.nimble @@ -16,9 +16,13 @@ task makewrapper, "Generate the C wrapper using Futhark": exec "nim c -d:futharkWrap --outdir=. src/kuzu.nim" task test, "Run the test suite.": - exec "testament all" + exec "testament --megatest:off all" exec "testament html" -task clean, "Remove all non-critical artifacts.": - exec "fossil clean --disable-undo --dotfiles --emptydirs -f -v" +task clean, "Remove all non-repository artifacts.": + exec "fossil clean -x" + +task docs, "Generate automated documentation.": + exec "nim doc --project --outdir:docs src/kuzu.nim" + exec "nim md2html --project --outdir:docs README.md" diff --git a/src/kuzu/queries.nim b/src/kuzu/queries.nim index 2a89f80..24f3215 100644 --- a/src/kuzu/queries.nim +++ b/src/kuzu/queries.nim @@ -22,6 +22,117 @@ proc query*( conn: KuzuConnection, query: string ): KuzuQueryResult = raise newException( KuzuQueryException, &"Error running query: {err}" ) +proc `=destroy`*( prepared: KuzuPreparedStatementObj ) = + ## Graceful cleanup for out of scope prepared objects. + if prepared.valid: + kuzu_prepared_statement_destroy( addr prepared.handle ) + + +proc prepare*( conn: KuzuConnection, query: string ): KuzuPreparedStatement = + ## Return a prepared statement that can avoid planning for repeat calls, + ## with optional variable binding via #execute. + result = new KuzuPreparedStatement + if kuzu_connection_prepare( addr conn.handle, query, addr result.handle ) == KuzuSuccess: + result.conn = conn + result.valid = true + else: + var err = kuzu_prepared_statement_get_error_message( addr result.handle ) + raise newException( KuzuQueryException, &"Error preparing statement: {err}" ) + + +proc execute*( + prepared: KuzuPreparedStatement, + params: tuple = () +): KuzuQueryResult = + ## Bind variables in *params* to the statement, and return + ## a KuzuQueryResult. + + result = new KuzuQueryResult + + for key, val in params.fieldPairs: + # + # FIXME: type checks and conversions for all bound variables + # from nim types to supported Kuzu types. + # + discard kuzu_prepared_statement_bind_string( addr prepared.handle, key.cstring, val.cstring ) + + #[ + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_bool (kuzu_prepared_statement *prepared_statement, const char *param_name, bool value) + Binds the given boolean value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_int64 (kuzu_prepared_statement *prepared_statement, const char *param_name, int64_t value) + Binds the given int64_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_int32 (kuzu_prepared_statement *prepared_statement, const char *param_name, int32_t value) + Binds the given int32_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_int16 (kuzu_prepared_statement *prepared_statement, const char *param_name, int16_t value) + Binds the given int16_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_int8 (kuzu_prepared_statement *prepared_statement, const char *param_name, int8_t value) + Binds the given int8_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_uint64 (kuzu_prepared_statement *prepared_statement, const char *param_name, uint64_t value) + Binds the given uint64_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_uint32 (kuzu_prepared_statement *prepared_statement, const char *param_name, uint32_t value) + Binds the given uint32_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_uint16 (kuzu_prepared_statement *prepared_statement, const char *param_name, uint16_t value) + Binds the given uint16_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_uint8 (kuzu_prepared_statement *prepared_statement, const char *param_name, uint8_t value) + Binds the given int8_t value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_double (kuzu_prepared_statement *prepared_statement, const char *param_name, double value) + Binds the given double value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_float (kuzu_prepared_statement *prepared_statement, const char *param_name, float value) + Binds the given float value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_date (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_date_t value) + Binds the given date value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_timestamp_ns (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_timestamp_ns_t value) + Binds the given timestamp_ns value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_timestamp_sec (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_timestamp_sec_t value) + Binds the given timestamp_sec value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_timestamp_tz (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_timestamp_tz_t value) + Binds the given timestamp_tz value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_timestamp_ms (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_timestamp_ms_t value) + Binds the given timestamp_ms value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_timestamp (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_timestamp_t value) + Binds the given timestamp value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_interval (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_interval_t value) + Binds the given interval value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_string (kuzu_prepared_statement *prepared_statement, const char *param_name, const char *value) + Binds the given string value to the given parameter name in the prepared statement. + + KUZU_C_API kuzu_state kuzu_prepared_statement_bind_value (kuzu_prepared_statement *prepared_statement, const char *param_name, kuzu_value *value) + ]# + + if kuzu_connection_execute( + addr prepared.conn.handle, + addr prepared.handle, + 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 executing prepared statement: {err}" ) + + proc `$`*( query: KuzuQueryResult ): string = ## Return the entire result set as a string. result = $kuzu_query_result_to_string( addr query.handle ) @@ -33,12 +144,19 @@ proc hasNext*( query: KuzuQueryResult ): bool = proc getNext*( query: KuzuQueryResult ): KuzuFlatTuple = + ## Consume and return the next tuple result, or raise a KuzuIndexException + ## if at the end of the result set. 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." ) + raise newException( KuzuIndexException, &"Query iteration past end." ) + + +proc rewind*( query: KuzuQueryResult ) = + ## Reset query iteration back to the beginning. + kuzu_query_result_reset_iterator( addr query.handle ) iterator items*( query: KuzuQueryResult ): KuzuFlatTuple = diff --git a/src/kuzu/tuple.nim b/src/kuzu/tuple.nim index 8c23ed3..bc3751a 100644 --- a/src/kuzu/tuple.nim +++ b/src/kuzu/tuple.nim @@ -17,6 +17,14 @@ proc `[]`*( tpl: KuzuFlatTuple, idx: int ): KuzuValue = result = new KuzuValue if kuzu_flat_tuple_get_value( addr tpl.handle, idx.uint64, addr result.handle ) == KuzuSuccess: result.valid = true + + # + # FIXME: type checks and conversions from supported kuzu + # types to supported Nim types. + # + # Currently the value can only be stringified via `$`. + # + 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 754881f..6bb9f9f 100644 --- a/src/kuzu/types.nim +++ b/src/kuzu/types.nim @@ -2,19 +2,19 @@ type KuzuDatabaseObj = object - handle*: kuzu_database - path*: string + handle: kuzu_database + path*: string config*: kuzu_system_config valid = false KuzuDatabase* = ref KuzuDatabaseObj KuzuConnectionObj = object - handle*: kuzu_connection + handle: kuzu_connection valid = false KuzuConnection* = ref KuzuConnectionObj KuzuQueryResultObj = object - handle*: kuzu_query_result + handle: kuzu_query_result summary: kuzu_query_summary num_columns*: uint64 = 0 num_tuples*: uint64 = 0 @@ -23,18 +23,24 @@ type valid = false KuzuQueryResult* = ref KuzuQueryResultObj + KuzuPreparedStatementObj = object + handle: kuzu_prepared_statement + conn: KuzuConnection + valid = false + KuzuPreparedStatement* = ref KuzuPreparedStatementObj + KuzuFlatTupleObj = object - handle*: kuzu_flat_tuple + handle: kuzu_flat_tuple num_columns: uint64 = 0 valid = false KuzuFlatTuple* = ref KuzuFlatTupleObj KuzuValueObj = object - handle*: kuzu_value + handle: kuzu_value valid = false KuzuValue* = ref KuzuValueObj - KuzuException* = object of CatchableError + KuzuException* = object of CatchableError KuzuQueryException* = object of KuzuException KuzuIndexException* = object of KuzuException diff --git a/tests/queries/t_can_prepare_a_statement.nim b/tests/queries/t_can_prepare_a_statement.nim new file mode 100644 index 0000000..4646377 --- /dev/null +++ b/tests/queries/t_can_prepare_a_statement.nim @@ -0,0 +1,23 @@ +# vim: set et sta sw=4 ts=4 : + +discard """ +output: "d.thing\nCamel\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) )" ) + +var p = conn.prepare( "CREATE (d:Doop {thing: $thing})" ) +assert typeOf( p ) is KuzuPreparedStatement + +for thing in @[ "Camel", "Lampshade", "Delicious Cake" ]: + q = p.execute( (thing: thing) ) + assert typeOf( q ) is KuzuQueryResult + +q = conn.query( "MATCH (d:Doop) RETURN d.thing" ) +echo $q + diff --git a/tests/queries/t_can_rewind_iteration.nim b/tests/queries/t_can_rewind_iteration.nim new file mode 100644 index 0000000..b22b1a5 --- /dev/null +++ b/tests/queries/t_can_rewind_iteration.nim @@ -0,0 +1,23 @@ +# vim: set et sta sw=4 ts=4 : + +discard """ +output: "Camel\nLampshade\nCamel\nLampshade\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" ]: + q = conn.query( "CREATE (d:Doop {thing: '" & thing & "'})" ) + +for tpl in conn.query( "MATCH (d:Doop) RETURN d.thing" ): + echo $tpl + +q.rewind + +for tpl in conn.query( "MATCH (d:Doop) RETURN d.thing" ): + echo $tpl diff --git a/tests/queries/t_raises_on_unknown_varbind.nim b/tests/queries/t_raises_on_unknown_varbind.nim new file mode 100644 index 0000000..cc23724 --- /dev/null +++ b/tests/queries/t_raises_on_unknown_varbind.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) )" ) + +var p = conn.prepare( "CREATE (d:Doop {thing: $thing})" ) +assert typeOf( p ) is KuzuPreparedStatement + +try: + discard p.execute( (nope: "undefined var in statement!") ) +except KuzuQueryException as err: + assert err.msg.contains( re"""Parameter nope not found.""" ) + + diff --git a/tests/queries/t_raises_with_invalid_prepared_statement.nim b/tests/queries/t_raises_with_invalid_prepared_statement.nim new file mode 100644 index 0000000..64b7ebd --- /dev/null +++ b/tests/queries/t_raises_with_invalid_prepared_statement.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) )" ) + +var p = conn.prepare( "CREAET (d:Doop {thing: $thing})" ) +assert typeOf( p ) is KuzuPreparedStatement + +try: + discard p.execute +except KuzuQueryException as err: + assert err.msg.contains( re""".*Error executing prepared statement:.*CREAET""" ) + + diff --git a/tests/tuples/t_throws_exception_fetching_at_end.nim b/tests/tuples/t_throws_exception_fetching_at_end.nim index d771c04..ca4cc55 100644 --- a/tests/tuples/t_throws_exception_fetching_at_end.nim +++ b/tests/tuples/t_throws_exception_fetching_at_end.nim @@ -12,6 +12,6 @@ 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.""" ) +except KuzuIndexException as err: + assert err.msg.contains( re"""Query iteration past end.""" )