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
This commit is contained in:
mahlon 2025-03-23 21:21:05 +00:00
parent 7850a79372
commit 6d34b081bb
10 changed files with 344 additions and 14 deletions

110
README.md
View file

@ -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 <mahlon@martini.nu>
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.

View file

@ -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"

View file

@ -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 =

View file

@ -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).)" )

View file

@ -2,19 +2,19 @@
type
KuzuDatabaseObj = object
handle*: kuzu_database
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,14 +23,20 @@ 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

View file

@ -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

View file

@ -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

View file

@ -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.""" )

View file

@ -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""" )

View file

@ -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.""" )