Merge query iterator and file magic branches.

This commit is contained in:
Mahlon E. Smith 2025-07-19 13:34:51 -07:00
commit ebd0d3dc45
Signed by: mahlon
SSH key fingerprint: SHA256:dP84sRGKZRpOOiPD/+GuOq+SHSxEw9qi5BWLQobaHm0
13 changed files with 118 additions and 58 deletions

3
.gitignore vendored
View file

@ -1,10 +1,13 @@
experiments/imdb/* experiments/imdb/*
!experiments/imdb/*.nim
!experiments/imdb/Makefile
nimble.paths nimble.paths
config.nims config.nims
tmp/* tmp/*
kuzu kuzu
nimcache/* nimcache/*
tests/* tests/*
!tests/*/t_*.nim
testresults/* testresults/*
testresults.html testresults.html

View file

@ -358,8 +358,10 @@ Manually rewind the `KuzuQueryResult` via `rewind()`.
## Multiple Query Results ## Multiple Query Results
A query can potentially return any number of separate statements. Iterate over A query can potentially return any number of separate statements. In the case
linked `KuzuQueryResult` objects with the `sets()` iterator. of more potential `RETURN`s, the query will only contain the first. Iterate
over linked `KuzuQueryResult` objects with the `sets()` iterator to retreive the
remaining:
```nim ```nim
import kuzu import kuzu

Binary file not shown.

View file

@ -18,9 +18,7 @@ task makewrapper, "Generate the C wrapper using Futhark":
task test, "Run the test suite.": task test, "Run the test suite.":
exec "testament --megatest:off all" exec "testament --megatest:off all"
exec "testament html" exec "testament html"
exec """find tests/ -type f \! -name \*.nim -delete"""
task clean, "Remove all non-repository artifacts.":
exec "fossil clean -x"
task docs, "Generate automated documentation.": task docs, "Generate automated documentation.":
exec "nim md2html --project --outdir:docs README.md" exec "nim md2html --project --outdir:docs README.md"

View file

@ -13,6 +13,8 @@ else:
include "kuzu/0.11.0.nim" include "kuzu/0.11.0.nim"
import import
std/files,
std/paths,
std/strformat, std/strformat,
std/strutils std/strutils

View file

@ -4,5 +4,5 @@ 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
let KUZU_DEFAULT_CONFIG* = kuzu_default_system_config() let KUZU_DEFAULT_CONFIG* = kuzu_default_system_config()

View file

@ -7,18 +7,50 @@ proc `=destroy`*( db: KuzuDatabaseObj ) =
kuzu_database_destroy( addr db.handle ) kuzu_database_destroy( addr db.handle )
func newKuzuDatabase*( path="", config=kuzuConfig() ): KuzuDatabase = proc validateDatabase( db: KuzuDatabase ): void =
## Perform basic validity checks against an existing on disk database
## for better error messaging.
if not Path( db.path ).fileExists: return
var buf = newSeq[char]( 5 )
let f = open( db.path )
discard f.readChars( buf )
f.close
let magic = buf[0..3].join
let storage_version = buf[4].uint
if magic != "KUZU":
raise newException( KuzuException, "Unable to open database: " &
&""""{db.path}" Doesn't appear to be a Kuzu file.""" )
if storageVersion != kuzuGetStorageVersion():
raise newException( KuzuException, "Unable to open database: " &
&" mismatched storage versions - file is {storageVersion}, expected {kuzuGetStorageVersion()}." )
proc newKuzuDatabase*( path="", config=kuzuConfig() ): KuzuDatabase =
## Create a new Kuzu database handle. Creates an in-memory ## Create a new Kuzu database handle. Creates an in-memory
## database by default, but writes to disk if a +path+ is supplied. ## database by default, but writes to disk if a +path+ is supplied.
result = new KuzuDatabase result = new KuzuDatabase
result.config = config result.config = config
result.path = if path != "" and path != ":memory:": path else: "(in-memory)"
if path != "" and path != ":memory:":
result.path = path
result.kind = disk
else:
result.path = "(in-memory)"
result.kind = memory
result.handle = kuzu_database() result.handle = kuzu_database()
if result.kind == disk:
result.validateDatabase()
if kuzu_database_init( path, config, addr result.handle ) == KuzuSuccess: if kuzu_database_init( path, config, addr result.handle ) == KuzuSuccess:
result.valid = true result.valid = true
else: else:
raise newException( KuzuException, "Unable to open database." ) raise newException( KuzuException, "Unable to open database." )

View file

@ -7,7 +7,12 @@ proc `=destroy`*( query: KuzuQueryResultObj ) =
kuzu_query_result_destroy( addr query.handle ) kuzu_query_result_destroy( addr query.handle )
func getQueryMetadata( query: KuzuQueryResult ) = # Forward declarations.
func hasNextSet( query: KuzuQueryResult ): bool
func getNextSet( query: KuzuQueryResult ): KuzuQueryResult
func getQueryMetadata( query: KuzuQueryResult, getAllQueryResults=false ) =
## Find and retain additional data for the query. ## Find and retain additional data for the query.
query.num_columns = kuzu_query_result_get_num_columns( addr query.handle ) query.num_columns = kuzu_query_result_get_num_columns( addr query.handle )
query.num_tuples = kuzu_query_result_get_num_tuples( addr query.handle ) query.num_tuples = kuzu_query_result_get_num_tuples( addr query.handle )
@ -19,6 +24,12 @@ func getQueryMetadata( query: KuzuQueryResult ) =
query.execution_time = kuzu_query_summary_get_execution_time( addr summary ) query.execution_time = kuzu_query_summary_get_execution_time( addr summary )
kuzu_query_summary_destroy( addr summary ) kuzu_query_summary_destroy( addr summary )
# Pull any additional query results.
query.sets = @[]
if getAllQueryResults:
while query.hasNextSet:
query.sets.add( query.getNextSet )
# Column information. # Column information.
query.column_types = @[] query.column_types = @[]
query.column_names = @[] query.column_names = @[]
@ -48,13 +59,29 @@ func getQueryMetadata( query: KuzuQueryResult ) =
kuzu_destroy_string( name ) kuzu_destroy_string( name )
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 )
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." )
func query*( conn: KuzuConnection, query: string ): KuzuQueryResult = func query*( conn: KuzuConnection, query: string ): KuzuQueryResult =
## Perform a database +query+ and return the result. ## Perform a database +query+ and return the result.
result = new KuzuQueryResult result = new KuzuQueryResult
if kuzu_connection_query( addr conn.handle, query, addr result.handle ) == KuzuSuccess: if kuzu_connection_query( addr conn.handle, query, addr result.handle ) == KuzuSuccess:
result.valid = true result.valid = true
result.getQueryMetadata() result.getQueryMetadata( getAllQueryResults=true )
else: else:
var err = kuzu_query_result_get_error_message( addr result.handle ) var err = kuzu_query_result_get_error_message( addr result.handle )
raise newException( KuzuQueryError, &"Error running query: {err}" ) raise newException( KuzuQueryError, &"Error running query: {err}" )
@ -162,32 +189,6 @@ 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

@ -1,9 +1,13 @@
# vim: set et sta sw=4 ts=4 : # vim: set et sta sw=4 ts=4 :
type type
KuzuDBType* = enum
disk, memory
KuzuDatabaseObj = object KuzuDatabaseObj = object
handle: kuzu_database handle: kuzu_database
path*: string path*: string
kind*: KuzuDBType
config*: kuzu_system_config config*: kuzu_system_config
valid = false valid = false
KuzuDatabase* = ref KuzuDatabaseObj KuzuDatabase* = ref KuzuDatabaseObj
@ -21,6 +25,7 @@ type
execution_time*: cdouble = 0 execution_time*: cdouble = 0
column_types*: seq[ kuzu_data_type_id ] column_types*: seq[ kuzu_data_type_id ]
column_names*: seq[ string ] column_names*: seq[ string ]
sets*: seq[ KuzuQueryResult ]
valid = false valid = false
KuzuQueryResult* = ref KuzuQueryResultObj KuzuQueryResult* = ref KuzuQueryResultObj

View file

@ -4,4 +4,5 @@ import kuzu
var db = newKuzuDatabase() var db = newKuzuDatabase()
assert db.path == "(in-memory)" assert db.path == "(in-memory)"
assert db.kind == memory

View file

@ -0,0 +1,23 @@
# vim: set et sta sw=4 ts=4 :
import
std/files,
std/paths,
std/re
import kuzu
const NOT_A_DATABASE_PATH = Path( "tmp/not-a-db" )
NOT_A_DATABASE_PATH.removeFile()
var fh = NOT_A_DATABASE_PATH.string.open( fmWrite )
fh.write( "Hi." )
fh.close
try:
discard newKuzuDatabase( $NOT_A_DATABASE_PATH )
except KuzuException as err:
assert err.msg.contains( re"""Unable to open database: "tmp/not-a-db" Doesn't appear to be a Kuzu file""" )
NOT_A_DATABASE_PATH.removeFile()

View file

@ -12,6 +12,7 @@ DATABASE_PATH.removeFile()
var db = newKuzuDatabase( $DATABASE_PATH ) var db = newKuzuDatabase( $DATABASE_PATH )
assert db.path == $DATABASE_PATH assert db.path == $DATABASE_PATH
assert db.kind == disk
assert db.config == kuzuConfig() assert db.config == kuzuConfig()
assert db.config.read_only == false assert db.config.read_only == false

View file

@ -1,7 +1,7 @@
# vim: set et sta sw=4 ts=4 : # vim: set et sta sw=4 ts=4 :
discard """ discard """
output: "Jenny|Lenny\nLenny\nJenny\n" output: "a\nb\nc\nd\ne\nf\n"
""" """
import kuzu import kuzu
@ -9,34 +9,26 @@ import kuzu
let db = newKuzuDatabase() let db = newKuzuDatabase()
let conn = db.connect let conn = db.connect
var q = conn.query """ var q = conn.query( "RETURN 'hi'" )
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 typeOf( q ) is KuzuQueryResult
assert q.hasNextSet == false assert q.sets.len == 0
q = conn.query """ q = conn.query """
MATCH (a:User)<-[f:Follows]-(b:User) RETURN a.name, b.name; RETURN "a";
MATCH (u:User) RETURN u.name; RETURN "b";
RETURN "c";
RETURN "d";
RETURN "e";
RETURN "f";
""" """
assert typeOf( q ) is KuzuQueryResult assert typeOf( q ) is KuzuQueryResult
assert q.hasNextSet == true assert q.sets.len == 5
echo q.getNext echo q.getNext
for query_result in q.sets: for set in q.sets:
for row in query_result.items: for row in set.items:
echo row echo row