diff --git a/.gitignore b/.gitignore index 33b6351..c3fcff2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ experiments/imdb/* +!experiments/imdb/*.nim +!experiments/imdb/Makefile nimble.paths config.nims tmp/* kuzu nimcache/* tests/* +!tests/*/t_*.nim testresults/* testresults.html diff --git a/USAGE.md b/USAGE.md index 17f5087..23f7682 100644 --- a/USAGE.md +++ b/USAGE.md @@ -358,8 +358,10 @@ 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. +A query can potentially return any number of separate statements. In the case +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 import kuzu diff --git a/experiments/imdb/imdb-results.pdf b/experiments/imdb/imdb-results.pdf index 5a092e5..a429af5 100644 Binary files a/experiments/imdb/imdb-results.pdf and b/experiments/imdb/imdb-results.pdf differ diff --git a/kuzu.nimble b/kuzu.nimble index b124df0..81770d8 100644 --- a/kuzu.nimble +++ b/kuzu.nimble @@ -18,9 +18,7 @@ task makewrapper, "Generate the C wrapper using Futhark": task test, "Run the test suite.": exec "testament --megatest:off all" exec "testament html" - -task clean, "Remove all non-repository artifacts.": - exec "fossil clean -x" + exec """find tests/ -type f \! -name \*.nim -delete""" task docs, "Generate automated documentation.": exec "nim md2html --project --outdir:docs README.md" diff --git a/src/kuzu.nim b/src/kuzu.nim index cf52a41..36ff91b 100644 --- a/src/kuzu.nim +++ b/src/kuzu.nim @@ -13,6 +13,8 @@ else: include "kuzu/0.11.0.nim" import + std/files, + std/paths, std/strformat, std/strutils diff --git a/src/kuzu/constants.nim b/src/kuzu/constants.nim index 705937e..7cb6a82 100644 --- a/src/kuzu/constants.nim +++ b/src/kuzu/constants.nim @@ -4,5 +4,5 @@ const KUZU_VERSION* = "0.6.0" const KUZU_EXPECTED_LIBVERSION* = "0.11.0" const BLOB_MAXSIZE = 4096 -let KUZU_DEFAULT_CONFIG* = kuzu_default_system_config() +let KUZU_DEFAULT_CONFIG* = kuzu_default_system_config() diff --git a/src/kuzu/database.nim b/src/kuzu/database.nim index 830f552..20c1e84 100644 --- a/src/kuzu/database.nim +++ b/src/kuzu/database.nim @@ -7,18 +7,50 @@ proc `=destroy`*( db: KuzuDatabaseObj ) = 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 ## database by default, but writes to disk if a +path+ is supplied. result = new KuzuDatabase 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() + if result.kind == disk: + result.validateDatabase() + 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 03fe293..71058bb 100644 --- a/src/kuzu/queries.nim +++ b/src/kuzu/queries.nim @@ -7,7 +7,12 @@ proc `=destroy`*( query: KuzuQueryResultObj ) = 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. query.num_columns = kuzu_query_result_get_num_columns( 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 ) 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. query.column_types = @[] query.column_names = @[] @@ -48,13 +59,29 @@ func getQueryMetadata( query: KuzuQueryResult ) = 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 = ## Perform a database +query+ and return the result. result = new KuzuQueryResult if kuzu_connection_query( addr conn.handle, query, addr result.handle ) == KuzuSuccess: result.valid = true - result.getQueryMetadata() + result.getQueryMetadata( getAllQueryResults=true ) else: var err = kuzu_query_result_get_error_message( addr result.handle ) raise newException( KuzuQueryError, &"Error running query: {err}" ) @@ -162,32 +189,6 @@ func rewind*( query: KuzuQueryResult ) = 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 = ## Iterate available tuples, yielding to the block. while query.hasNext: diff --git a/src/kuzu/types.nim b/src/kuzu/types.nim index f5026eb..24feb78 100644 --- a/src/kuzu/types.nim +++ b/src/kuzu/types.nim @@ -1,9 +1,13 @@ # vim: set et sta sw=4 ts=4 : type + KuzuDBType* = enum + disk, memory + KuzuDatabaseObj = object handle: kuzu_database path*: string + kind*: KuzuDBType config*: kuzu_system_config valid = false KuzuDatabase* = ref KuzuDatabaseObj @@ -21,6 +25,7 @@ type execution_time*: cdouble = 0 column_types*: seq[ kuzu_data_type_id ] column_names*: seq[ string ] + sets*: seq[ KuzuQueryResult ] valid = false KuzuQueryResult* = ref KuzuQueryResultObj diff --git a/tests/database/t_can_create_in_memory.nim b/tests/database/t_can_create_in_memory.nim index ca96333..08333c3 100644 --- a/tests/database/t_can_create_in_memory.nim +++ b/tests/database/t_can_create_in_memory.nim @@ -4,4 +4,5 @@ import kuzu var db = newKuzuDatabase() assert db.path == "(in-memory)" +assert db.kind == memory diff --git a/tests/database/t_checks_for_valid_kuzu_file.nim b/tests/database/t_checks_for_valid_kuzu_file.nim new file mode 100644 index 0000000..dd7eb95 --- /dev/null +++ b/tests/database/t_checks_for_valid_kuzu_file.nim @@ -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() + diff --git a/tests/database/t_creates_with_default_config.nim b/tests/database/t_creates_with_default_config.nim index 88c3fd4..3e8cafb 100644 --- a/tests/database/t_creates_with_default_config.nim +++ b/tests/database/t_creates_with_default_config.nim @@ -12,6 +12,7 @@ DATABASE_PATH.removeFile() var db = newKuzuDatabase( $DATABASE_PATH ) assert db.path == $DATABASE_PATH +assert db.kind == disk assert db.config == kuzuConfig() assert db.config.read_only == false diff --git a/tests/queries/t_can_contain_multiple_result_sets.nim b/tests/queries/t_can_contain_multiple_result_sets.nim index d0be2d5..534f871 100644 --- a/tests/queries/t_can_contain_multiple_result_sets.nim +++ b/tests/queries/t_can_contain_multiple_result_sets.nim @@ -1,7 +1,7 @@ # vim: set et sta sw=4 ts=4 : discard """ -output: "Jenny|Lenny\nLenny\nJenny\n" +output: "a\nb\nc\nd\ne\nf\n" """ import kuzu @@ -9,34 +9,26 @@ 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 *" ) +var q = conn.query( "RETURN 'hi'" ) assert typeOf( q ) is KuzuQueryResult -assert q.hasNextSet == false +assert q.sets.len == 0 q = conn.query """ - MATCH (a:User)<-[f:Follows]-(b:User) RETURN a.name, b.name; - MATCH (u:User) RETURN u.name; + RETURN "a"; + RETURN "b"; + RETURN "c"; + RETURN "d"; + RETURN "e"; + RETURN "f"; """ assert typeOf( q ) is KuzuQueryResult -assert q.hasNextSet == true +assert q.sets.len == 5 echo q.getNext -for query_result in q.sets: - for row in query_result.items: +for set in q.sets: + for row in set.items: echo row +