Merge pull request 'multi-result-sets' (#1) from multi-result-sets into main

This commit is contained in:
Mahlon E. Smith 2025-07-17 16:50:34 -07:00
commit 02894cf8fe
12 changed files with 141 additions and 1284 deletions

1
.gitignore vendored
View file

@ -6,4 +6,5 @@ kuzu
nimcache/* nimcache/*
tests/* tests/*
testresults/* testresults/*
testresults.html

View file

@ -1,5 +1,19 @@
# Release History for nim-kuzu # Release History for nim-kuzu
---
## v0.6.0 [2025-07-17] Mahlon E. Smith <mahlon@martini.nu>
Enhancement:
- Provide functions for retreiving multiple result sets from a
single primary query object.
Minutiae:
- Add a KuzuInterationError, to disambiguate iteration errors from
IndexErrors.
--- ---
## v0.5.0 [2025-07-13] Mahlon E. Smith <mahlon@martini.nu> ## v0.5.0 [2025-07-13] Mahlon E. Smith <mahlon@martini.nu>
@ -17,7 +31,7 @@ Bump for kuzu 0.10.0. No practical changes.
Enhancement: Enhancement:
- Add `toBlob` for quickly fetching a sequence of opaque bytes. - Add `toBlob` for quickly fetching a sequence of opaque bytes.
--- ---
@ -34,4 +48,3 @@ so update version to reflect the 0.9.0 release.
## v0.1.0 [2025-03-31] Mahlon E. Smith <mahlon@martini.nu> ## v0.1.0 [2025-03-31] Mahlon E. Smith <mahlon@martini.nu>
Initial public release. Initial public release.

View file

@ -308,13 +308,13 @@ for pair in res.column_names.zip( res.column_types ):
# ("e.activity", KUZU_TIMESTAMP) # ("e.activity", KUZU_TIMESTAMP)
``` ```
## Reading Result Sets ## Reading Results
So far we've just been showing values by converting the entire `KuzuQueryResult` So far we've just been showing values by converting the entire `KuzuQueryResult`
to a string. Convenient for quick examples and debugging, but not much else. to a string. Convenient for quick examples and debugging, but not much else.
A `KuzuQueryResult` is an iterator. You can use regular Nim functions that yield A `KuzuQueryResult` is an iterator. You can use regular Nim functions that yield
each `KuzuFlatTuple` -- essentially, each row that was returned in the set. each `KuzuFlatTuple` -- essentially, each row that was returned in a result set.
```nim ```nim
var res = conn.query """ var res = conn.query """
@ -350,12 +350,52 @@ if res.hasNext:
echo res.getNext #=> 2 echo res.getNext #=> 2
echo res.getNext #=> 3 echo res.getNext #=> 3
echo res.getNext #=> KuzuIndexError exception! echo res.getNext #=> KuzuIterationError exception!
``` ```
Manually rewind the `KuzuQueryResult` via `rewind()`. Manually rewind the `KuzuQueryResult` via `rewind()`.
## Multiple Query Results
A query can potentially return any number of separate statements. Iterate over
linked `KuzuQueryResult` objects with the `sets()` iterator.
```nim
import kuzu
let db = newKuzuDatabase()
let conn = db.connect
let query = conn.query """
UNWIND [1,2,3] as items
RETURN items;
UNWIND [4,5,6] as items
RETURN items;
UNWIND [7,8,9] as items
RETURN items;
"""
for row in query:
echo row
# 1
# 2
# 3
for set in query.sets:
for row in set:
echo row
# 4
# 5
# 6
# 7
# 8
# 9
```
## Working with Values ## Working with Values
A `KuzuFlatTuple` contains the entire row. You can index a value at its column A `KuzuFlatTuple` contains the entire row. You can index a value at its column

View file

@ -19,7 +19,7 @@ import
std/strformat std/strformat
import kuzu import kuzu
const DB = "imdb" const DB = "imdb.kz"
const DOT = "imdb-results.dot" const DOT = "imdb-results.dot"
if not DB.fileExists: if not DB.fileExists:
@ -37,7 +37,7 @@ var res: KuzuQueryResult
var fromActor = paramStr(1) var fromActor = paramStr(1)
var toActor = paramStr(2) var toActor = paramStr(2)
var db = newKuzuDatabase( "imdb", kuzuConfig(read_only=true) ) var db = newKuzuDatabase( DB, kuzuConfig(read_only=true) )
var conn = db.connect var conn = db.connect
echo "Database opened: ", db.path echo "Database opened: ", db.path

View file

@ -20,7 +20,7 @@ import
zip/gzipfiles, zip/gzipfiles,
kuzu kuzu
const DB = "imdb" const DB = "imdb.kz"
const SOURCE = "https://datasets.imdbws.com" const SOURCE = "https://datasets.imdbws.com"
const FILES = @[ "name.basics", "title.basics", "title.principals" ] const FILES = @[ "name.basics", "title.basics", "title.principals" ]

View file

@ -1,6 +1,6 @@
# vim: set et sta sw=4 ts=4 : # vim: set et sta sw=4 ts=4 :
version = "0.5.0" version = "0.6.0"
author = "Mahlon E. Smith" author = "Mahlon E. Smith"
description = "Kuzu is an embedded graph database built for query speed and scalability." description = "Kuzu is an embedded graph database built for query speed and scalability."
license = "BSD-3-Clause" license = "BSD-3-Clause"

View file

@ -1,6 +1,6 @@
# vim: set et sta sw=4 ts=4 : # vim: set et sta sw=4 ts=4 :
const KUZU_VERSION* = "0.5.0" const KUZU_VERSION* = "0.6.0"
const KUZU_EXPECTED_LIBVERSION* = "0.11.0" const KUZU_EXPECTED_LIBVERSION* = "0.11.0"
const BLOB_MAXSIZE = 4096 const BLOB_MAXSIZE = 4096

View file

@ -147,14 +147,14 @@ func hasNext*( query: KuzuQueryResult ): bool =
func getNext*( query: KuzuQueryResult ): KuzuFlatTuple = func getNext*( query: KuzuQueryResult ): KuzuFlatTuple =
## Consume and return the next tuple result, or raise a KuzuIndexError ## Consume and return the next tuple result, or raise a KuzuIterationError
## if at the end of the result set. ## if at the end of the result tuples.
result = new KuzuFlatTuple result = new KuzuFlatTuple
if kuzu_query_result_get_next( addr query.handle, addr result.handle ) == KuzuSuccess: if kuzu_query_result_get_next( addr query.handle, addr result.handle ) == KuzuSuccess:
result.valid = true result.valid = true
result.num_columns = query.num_columns result.num_columns = query.num_columns
else: else:
raise newException( KuzuIndexError, &"Query iteration past end." ) raise newException( KuzuIterationError, &"Query iteration past end of tuples." )
func rewind*( query: KuzuQueryResult ) = func rewind*( query: KuzuQueryResult ) =
@ -162,6 +162,32 @@ func rewind*( query: KuzuQueryResult ) =
kuzu_query_result_reset_iterator( addr query.handle ) kuzu_query_result_reset_iterator( addr query.handle )
func hasNextSet*( query: KuzuQueryResult ): bool =
## Returns +true+ if there are more result sets to be consumed.
result = kuzu_query_result_has_next_query_result( addr query.handle )
# Keeping this private, because it's only safe to call from within
# an iterator -- overwriting a query variable with the next result
# goes boom.
func getNextSet( query: KuzuQueryResult ): KuzuQueryResult =
## Consume and return the next query set result, or raise a KuzuIterationError
## if at the end of sets.
result = new KuzuQueryResult
if kuzu_query_result_get_next_query_result( addr query.handle, addr result.handle ) == KuzuSuccess:
result.valid = true
result.getQueryMetadata()
else:
raise newException( KuzuIterationError, &"Query iteration past end of set." )
iterator sets*( query: KuzuQueryResult ): KuzuQueryResult =
## Iterate available query result sets, yielding to the block.
while query.hasNextSet:
yield query.getNextSet
# NOTE: There is no 'rewind' mechanism for result sets.
iterator items*( query: KuzuQueryResult ): KuzuFlatTuple = iterator items*( query: KuzuQueryResult ): KuzuFlatTuple =
## Iterate available tuples, yielding to the block. ## Iterate available tuples, yielding to the block.
while query.hasNext: while query.hasNext:

View file

@ -51,5 +51,6 @@ type
KuzuException* = object of CatchableError KuzuException* = object of CatchableError
KuzuQueryError* = object of KuzuException KuzuQueryError* = object of KuzuException
KuzuIndexError* = object of KuzuException KuzuIndexError* = object of KuzuException
KuzuIterationError* = object of KuzuException
KuzuTypeError* = object of KuzuException KuzuTypeError* = object of KuzuException

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
# vim: set et sta sw=4 ts=4 :
discard """
output: "Jenny|Lenny\nLenny\nJenny\n"
"""
import kuzu
let db = newKuzuDatabase()
let conn = db.connect
var q = conn.query """
CREATE NODE TABLE User(
id SERIAL PRIMARY KEY,
name STRING
);
CREATE REL TABLE FOLLOWS(
From User To User
);
MERGE (a:User {name: "Lenny"})-[f:Follows]->(b:User {name: "Jenny"});
"""
q = conn.query( "MATCH (u:User) RETURN *" )
assert typeOf( q ) is KuzuQueryResult
assert q.hasNextSet == false
q = conn.query """
MATCH (a:User)<-[f:Follows]-(b:User) RETURN a.name, b.name;
MATCH (u:User) RETURN u.name;
"""
assert typeOf( q ) is KuzuQueryResult
assert q.hasNextSet == true
echo q.getNext
for query_result in q.sets:
for row in query_result.items:
echo row

View file

@ -12,6 +12,6 @@ q = conn.query( "MATCH (d:Doop) RETURN d.thing" )
try: try:
discard q.getNext discard q.getNext
except KuzuIndexError as err: except KuzuIterationError as err:
assert err.msg.contains( re"""Query iteration past end.""" ) assert err.msg.contains( re"""Query iteration past end.""" )