From 6197b8ab3fd0b1049b979b3a137d4d4de7b1c8bd Mon Sep 17 00:00:00 2001 From: mahlon <> Date: Mon, 31 Mar 2025 19:35:15 +0000 Subject: [PATCH] First round of USAGE docs. Also, have the Result iterator auto-rewind when complete. FossilOrigin-Name: d10c2e7dd8dd447cc33f1cfb6fbbd94946f56e8da912e1619673338d9c8a968d --- README.md | 12 +- USAGE.md | 501 ++++++++++++++++++ experiments/imdb/Makefile | 8 + experiments/imdb/imdb-results.pdf | Bin 0 -> 26215 bytes experiments/imdb/imdb_find_actor_path.nim | 159 ++++++ .../imdb/{imdbimport.nim => imdb_import.nim} | 10 +- kuzu.nimble | 4 +- src/kuzu/queries.nim | 1 + src/kuzu/tuple.nim | 2 +- tests/queries/t_auto_rewinds_the_iterator.nim | 16 + 10 files changed, 698 insertions(+), 15 deletions(-) create mode 100644 USAGE.md create mode 100644 experiments/imdb/Makefile create mode 100644 experiments/imdb/imdb-results.pdf create mode 100644 experiments/imdb/imdb_find_actor_path.nim rename experiments/imdb/{imdbimport.nim => imdb_import.nim} (95%) create mode 100644 tests/queries/t_auto_rewinds_the_iterator.nim diff --git a/README.md b/README.md index f10a122..1914ff6 100644 --- a/README.md +++ b/README.md @@ -39,18 +39,14 @@ For more information about Kuzu itself, see its $ nimble install kuzu +The current version of this library is built for Kuzu v0.8.2. + ## Usage +See the [Usage documentation](USAGE.md). -> [!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. - +You can also find a bunch of working examples in the tests. ## Contributing diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..12e7c95 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,501 @@ + +# Usage + +This document is a quick guide for how to use this library. If you've cloned +this repository, you can: + +> % nimble docs + +To auto-generate API docs -- with the C wrappers, it's a lot and it's hard to +know where to start. + + +## Prior Reading + +If you're just starting with Kuzu or graph databases, it's probably a good idea +to familiarize yourself with the [Kuzu Documentation](https://docs.kuzudb.com/) +and the [Cypher Language](https://docs.kuzudb.com/tutorials/cypher/). This +library won't do much for you by itself without a basic understanding of Kuzu usage. + + +## Checking Compatibility + +This is a wrapper (with some additional niceties) for the system-installed Kuzu +shared library. As such, the version of this library might not match with what +you currently have installed. + +Check the [README](README.md), the [History](History.md), and the following +table to ensure you're using the correct version for your Kuzu +installation. I'll make a modest effort for backwards compatibility, and other +versions might work. Don't count too heavily on it. :-) + +| Kuzu Library Version | Nim Kuzu Version | +| -------------------- | ---------------- | +| v0.8.2 | v0.1.0 | + +You can use the `kuzuVersionCompatible()` function (along with the +`KUZU_VERSION` and `KUZU_LIBVERSION` constants) to quickly check if things are +looking right. + +```nim +import kuzu + +echo KUZU_VERSION #=> "0.1.0" +echo KUZU_LIBVERSION #=> "0.8.2" +echo kuzuVersionCompatible() #=> true +``` + + +## Connecting to a Database + +Just call `newKuzuDatabase()`. Without an argument (or with an empty string), +the database is in-memory. Any other argument is considered a filesystem path +-- it will create an empty database if the path is currently non-existent, or +open an existing database otherwise. + +```nim +# "db" is in-memory and will evaporate when the process ends. +var db = newKuzuDatabase() +``` + +```nim +# "db" is persistent, stored in the directory "data". +var db = newKuzuDatabase("data") +``` +The database path is retained, and can be recalled via `db.path`. + +```nim +db.path #=> "data" +``` + +### Database Configuration + +The database is configured with default options by default. You can see them +via: + +```nim +echo $db.config +#=> (buffer_pool_size: 23371415552, max_num_threads: 16, ... + +# Is compression enabled? +if db.config.enable_compression: + echo "Yes!" +``` + +You can alter configuration options when connecting by passing a `kuzuConfig` +object as the second argument to `newKuzuDatabase()`: + +```nim +# Open a readonly handle. +var db = newKuzuDatabase( "data", kuzuConfig( read_only=true ) ) +``` + +### The Connection + +All interaction with the database is performed via a connection object. There +are limitations to database handles and connection objects -- see the +[Kuzu Concurrency](https://docs.kuzudb.com/concurrency/) docs for details! + +Call `connect` on an open database handle to create a new connection: + +```nim +var conn = db.connect +``` + +You can set a maximum query lifetime, and interrupt any running queries (thread +shutdown, ctrl-c, etc): + +```nim +# Set a maximum ceiling on how long a query can run, in milliseconds. +conn.queryTimeout( 10 * 1000 ) # 10 seconds + +# Cancel a running query. +conn.queryInterrupt() +``` + +## Performing Queries + +You can perform a basic query via the appropriately named `query()` function on +the connection. Via this method, queries are run immediately. A +`KuzuQueryResult` is returned - this is the object you'll be interacting with to +see results. + +A `KuzuQueryResult` can be turned into a string to quickly see the column +headers and all tuple results: + +```nim +var res = conn.query( """RETURN "Hello world", 1234, [1,2,3]""" ) + +echo $res #=> +# Hello world|1234|LIST_CREATION(1,2,3) +# Hello world|1234|[1,2,3] +``` + +Additionally, various query metadata is available for introspection: + +```nim +var res = conn.query( """ +RETURN + "Hello world" AS hi, + 1234 AS pin, + [1,2,3] AS list +""" ) + +echo res.num_columns #=> 3 +echo res.num_tuples #=> 1 +echo res.compile_time #=> 14.028 +echo res.execution_time #=> 1.624 + +# Return the column names as a sequence. +echo res.column_names #=> @["hi", "pin", "list"] + +# Return the column data types as a sequence. +echo res.column_types #=> @[KUZU_STRING, KUZU_INT64, KUZU_LIST] +``` + +### Prepared Statements + +If you're supplying an argument to a query, or you're running a query +repeatedly, it's safer and faster to create a prepared statement via `prepare()` +on the connection. These statements are only compiled once, and execution is +deferred until you call `execute()`. + +```nim +var stmt = conn.prepare( """ +RETURN + "Hello world" AS hi, + 1234 AS pin, + [1,2,3] AS list +""" ) + +# This returns a KuzuQueryResult, just like `conn.query()`. +var res = stmt.execute() +``` + +Arguments are labeled variables (prefixed with `$`) within the query. +Parameters are matched by providing a Nim tuple argument to `execute()` - a +simple round trip example: + +```nim +var stmt = conn.prepare( """ +RETURN + $message AS message, + $digits AS digits, + LIST_CREATION($list) AS list +""" ) + +var res = stmt.execute( (message: "Hello", digits: 1234, list: "1,2,3") ) + +echo $res #=> +# message|digits|list +# Hello|1234|[1,2,3] +``` + +#### Type Conversion + +When binding variables to a prepared statement, most Nim types are automatically +converted to their respective Kuzu types. + +```nim +var stmt = conn.prepare( """RETURN $num AS num""" ) +var res = stmt.execute( (num: 12) ) + +echo res.column_types[0] #=> KUZU_INT32 +``` + +This might not necessarily be what you want - sometimes you'd rather be strict +with typing, and you might be inserting into a column that has a different type +than the default. + +You can use [integer type suffixes](https://nim-lang.org/docs/manual.html#lexical-analysis-numeric-literals), or casting to be explicit as usual: + +```nim +var stmt = conn.prepare( """RETURN $num AS num""" ) +var res: KuzuQueryResult + +res = stmt.execute( (num: 12'u64) ) +echo res.column_types[0] #=> KUZU_UINT32 + +res = stmt.execute( (num: 12.float) ) +echo res.column_types[0] #=> KUZU_DOUBLE +``` + +#### Kuzu Specific Types + +In the example above, you may have noticed the `LIST_CREATION($list)` in the +prepared query, and that we passed a string `1,2,3` as the `$list` parameter. + +This is a useful way to easily use most Kuzu types without needing corresponding +Nim ones -- if you're inserting into a table that is using a custom type, you +can cast it using the query itself during insertion! + +This has the additional advantage of letting Kuzu error check the validity of +the content, and it works with the majority of types. + +An extended example: + +```nim +import std/sequtils +import kuzu + +var db = newKuzuDatabase() +var conn = db.connect + +var res: KuzuQueryResult + +# Create a node table. +# +res = conn.query """ +CREATE NODE TABLE Example ( + id SERIAL, + num UINT8, + done BOOL, + comment STRING, + karma DOUBLE, + thing UUID, + created DATE, + activity TIMESTAMP, + PRIMARY KEY(id) +) +""" + +# Prepare a statement for adding a node. +# +var stmt = conn.prepare """ +CREATE (e:Example { + num: $num, + done: $done, + comment: $comment, + karma: $karma, + thing: UUID($thing), + created: DATE($created), + activity: TIMESTAMP($activity) +}) +""" + +# Add a node row that contains specific Kuzu types. +# +res = stmt.execute(( + num: 2, + done: true, + comment: "Types!", + karma: 16.7, + thing: "e0e7232e-bec9-4625-9822-9d1a31ea6f93", + created: "2025-03-29", + activity: "2025-03-29" +)) + +# Show the current contents. +res = conn.query( """MATCH (e:Example) RETURN e.*""" ) +echo $res #=> +# e.id|e.num|e.done|e.comment|e.karma|e.thing|e.created|e.activity +# 0|2|True|Types!|16.700000|e0e7232e-bec9-4625-9822-9d1a31ea6f93|2025-03-29|2025-03-29 00:00:00 + +# Show column names and their Kuzu types. +for pair in res.column_names.zip( res.column_types ): + echo pair #=> + # ("e.id", KUZU_SERIAL) + # ("e.num", KUZU_UINT8) + # ("e.done", KUZU_BOOL) + # ("e.comment", KUZU_STRING) + # ("e.karma", KUZU_DOUBLE) + # ("e.thing", KUZU_UUID) + # ("e.created", KUZU_DATE) + # ("e.activity", KUZU_TIMESTAMP) +``` + +## Reading Result Sets + +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. + +A `KuzuQueryResult` is an iterator. You can use regular Nim functions that yield +each `KuzuFlatTuple` -- essentially, each row that was returned in the set. + +```nim +var res = conn.query """ + UNWIND [1,2,3] AS items + UNWIND ["thing"] AS thing + RETURN items, thing +""" + +# KuzuFlatTuple can be stringified just like the result set. +for row in res: + echo row #=> + # 1|thing + # 2|thing + # 3|thing +``` + +Once iteration has reached the end, it is automatically rewound for reuse. + +You can manually get the next `KuzuFlatTuple` via `getNext()`. Calling +`getNext()` after the last row results in an error. Use `hasNext()` to check +before calling. + +```nim +var res = conn.query """ + UNWIND [1,2,3] AS items + RETURN items +""" + +# Get the first row. +if res.hasNext: + var row = res.getNext + echo row #=> 1 + +echo res.getNext #=> 2 +echo res.getNext #=> 3 +echo res.getNext #=> KuzuIndexError exception! +``` + +Manually rewind the `KuzuQueryResult` via `rewind()`. + + +## Working with Values + +A `KuzuFlatTuple` contains the entire row. You can index a value at its column +position, returning a `KuzuValue`. + +```nim +var res = conn.query """ +RETURN + 1 AS num, + true AS done, + "A comment" AS comment, + 12.84 AS karma, + UUID("b41deae0-dddf-430b-981d-3fb93823e495") AS thing, + DATE("2025-03-29") AS created +""" + +var row = res.getNext + +for idx in ( 0 .. res.num_columns-1 ): + var value = row[idx] + echo res.column_names[idx], ": ", value, " (", value.kind, ")" #=> + # num: 1 (KUZU_INT64) + # done: True (KUZU_BOOL) + # comment: A comment (KUZU_STRING) + # karma: 12.840000 (KUZU_DOUBLE) + # thing: b41deae0-dddf-430b-981d-3fb93823e495 (KUZU_UUID) + # created: 2025-03-29 (KUZU_DATE) +``` + +### Types + +A `KuzuValue` can always be stringified, irrespective of its Kuzu type. You can +check what type it is via the 'kind' property. + +```nim +var res = conn.query """RETURN "hello"""" +var value = res.getNext[0] + +echo value.kind #=> KUZU_STRING +``` + +A `KuzuValue` has conversion methods for Nim base types. You'll likely want to +convert it for regular Nim usage: + +```nim +var res = conn.query( "RETURN 2560" ) +var value = res.getNext[0] + +echo value + 1 #=> Type error! + +echo $value #=> "2560" +echo value.toInt64 + 1 #=> 2561 +``` + + +### Lists + +A `KuzuValue` of type `KUZU_LIST` can be converted to a Nim sequence of +`KuzuValues` with the `toList()` function: + +```nim +import std/sequtils +import kuzu + +var res = conn.query """ +RETURN [10, 20, 30] +""" + +var value = res.getNext[0] + +var list = value.toList +echo list #=> @[10,20,30] + +echo list.map( func(v:KuzuValue): int = v.toInt64 * 10 ) #=> @[100,200,300] +``` + + +### Struct-like Objects + +Various Kuzu types can act like a struct - this includes `KUZU_NODE`, +`KUZU_REL`, and of course explicit `KUZU_STRUCT` itself, among others. + +Convert a `KuzuValue` to a `KuzuStructValue` with `toStruct()`. For +convenience, this is also aliased to `toNode()` and `toRel()`. + +Once converted, you can access struct values by passing the key name to `[]`: + +```nim +var res = conn.query """ +RETURN {movie: "The Fifth Element", year: 1997} +""" + +var value = res.getNext[0] + +var struct = value.toStruct +echo struct["movie"], " was released in ", struct["year"], "." #=> +# "The Fifth Element was released in 1997." +``` + +Here's a much more complicated example, following a node paths: + +```nim +import + std/sequtils, + std/strformat +import kuzu + +var db = newKuzuDatabase() +var conn = db.connect + +var res = conn.query """ + CREATE NODE TABLE Person ( + id SERIAL, + name STRING, PRIMARY KEY (id) + ); + CREATE REL TABLE Knows ( + FROM Person TO Person, + since INT + ); + + CREATE (p:Person {name: "Bob"}); + CREATE (p:Person {name: "Alice"}); + CREATE (p:Person {name: "Bruce"}); + CREATE (p:Person {name: "Tom"}); + + CREATE (a:Person {name: "Bruce"})-[r:Knows {since: 1997}]->(b:Person {name: "Tom"}); + CREATE (a:Person {name: "Bob"})-[r:Knows {since: 2009}]->(b:Person {name: "Alice"}); + CREATE (a:Person {name: "Alice"})-[r:Knows {since: 2010}]->(b:Person {name: "Bob"}); + CREATE (a:Person {name: "Bob"})-[r:Knows {since: 2003}]->(b:Person {name: "Bruce"}); +""" + +res = conn.query """ + MATCH path = (a:Person)-[r:Knows]->(b:Person) + WHERE r.since > 2000 + RETURN r.since as Since, nodes(path) as People + ORDER BY r.since +""" + +# Who knows who since when? +# +for row in res: + var since = row[0] + var people = row[1].toList.map( proc(p:KuzuValue):KuzuStructValue = p.toNode ) + echo &"""{people[0]["name"]} has known {people[1]["name"]} since {since}.""" + +``` + diff --git a/experiments/imdb/Makefile b/experiments/imdb/Makefile new file mode 100644 index 0000000..f96c207 --- /dev/null +++ b/experiments/imdb/Makefile @@ -0,0 +1,8 @@ +build: + nim c -d:release imdb_import.nim + nim c -d:release imdb_find_actor_path.nim + +clean: + rm -f *.csv + rm -f *.tsv.gz + diff --git a/experiments/imdb/imdb-results.pdf b/experiments/imdb/imdb-results.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5a092e5fc3ddf9f471bb3f4df48640c2a44bfadd GIT binary patch literal 26215 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 NzzEdTKb 1: + echo res.num_tuples, " tied ", fastestPath, " hop paths:" +for row in res.items: + var output: string + var pathLen = row[1].toInt64 + var pathStep = 0 + + for rawNode in row[0].toList: + pathStep += 1 + var node = rawNode.toNode + if $node["_LABEL"] == "Actor": + output.add $node["name"] + dotFile.write &""""{$node["name"]}"""" + if pathStep == 1: + output.add " was in " + dotFile.write " -> " + elif pathStep < pathLen: + output.add " who was in " + dotFile.write " -> " + dotFile.write "\n" + else: + dotFile.write ";\n" + elif $node["_LABEL"] == "Movie": + output.add &""""{$node["title"]}"""" + output.add " with " + dotFile.write &""""{$node["title"]}"""" + dotFile.write " -> \n" + echo &"{output}." + +dotFile.write "}\n" +dotFile.close +echo "\n\nYou can run 'dot -Tpdf < imdb-results.dot > imdb-results.pdf' if you have graphviz installed." + + diff --git a/experiments/imdb/imdbimport.nim b/experiments/imdb/imdb_import.nim similarity index 95% rename from experiments/imdb/imdbimport.nim rename to experiments/imdb/imdb_import.nim index 788873b..13a9166 100644 --- a/experiments/imdb/imdbimport.nim +++ b/experiments/imdb/imdb_import.nim @@ -7,7 +7,7 @@ # directors, and TV shows are intentionally omitted. # # Compile: -# % nim c -d:release imdbdata.nim +# % nim c -d:release imdb_import.nim # # Sourced from: https://datasets.imdbws.com/ # See: https://developer.imdb.com/non-commercial-datasets/ @@ -124,8 +124,8 @@ if not DB.dirExists: """CREATE NODE TABLE Movie (movieId INT64, title STRING, year UINT16, durationMins INT, PRIMARY KEY (movieId))""", """CREATE REL TABLE ActedIn (FROM Actor TO Movie)""" ]: - var result = conn.query( schema ) - duration += result.execution_time.int + var q = conn.query( schema ) + duration += q.execution_time.int echo &"Created database schema in {duration}ms." duration = 0 @@ -136,8 +136,8 @@ if not DB.dirExists: """COPY ActedIn FROM "./title.principals.csv" (header=true, ignore_errors=true)""" ]: echo dataload - var result = conn.query( dataload ) - duration += result.execution_time.int + var q = conn.query( dataload ) + duration += q.execution_time.int echo &"Imported data in {duration / 1000}s." echo "Done!" diff --git a/kuzu.nimble b/kuzu.nimble index e06a162..744746c 100644 --- a/kuzu.nimble +++ b/kuzu.nimble @@ -23,6 +23,8 @@ 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" + exec "nim md2html --project --outdir:docs History.md" + exec "nim md2html --project --outdir:docs USAGE.md" + exec "nim doc --project --outdir:docs src/kuzu.nim" diff --git a/src/kuzu/queries.nim b/src/kuzu/queries.nim index 2aac566..76d32a3 100644 --- a/src/kuzu/queries.nim +++ b/src/kuzu/queries.nim @@ -166,4 +166,5 @@ iterator items*( query: KuzuQueryResult ): KuzuFlatTuple = ## Iterate available tuples, yielding to the block. while query.hasNext: yield query.getNext + query.rewind diff --git a/src/kuzu/tuple.nim b/src/kuzu/tuple.nim index ac8fb61..54ad857 100644 --- a/src/kuzu/tuple.nim +++ b/src/kuzu/tuple.nim @@ -15,7 +15,7 @@ func `$`*( tpl: KuzuFlatTuple ): string = result.removeSuffix( "\n" ) -func `[]`*( tpl: KuzuFlatTuple, idx: int ): KuzuValue = +func `[]`*( tpl: KuzuFlatTuple, idx: int|uint64 ): KuzuValue = ## Returns a KuzuValue at the given *idx*. result = new KuzuValue diff --git a/tests/queries/t_auto_rewinds_the_iterator.nim b/tests/queries/t_auto_rewinds_the_iterator.nim new file mode 100644 index 0000000..6bca303 --- /dev/null +++ b/tests/queries/t_auto_rewinds_the_iterator.nim @@ -0,0 +1,16 @@ +# vim: set et sta sw=4 ts=4 : + +import kuzu + +let db = newKuzuDatabase() +let conn = db.connect + +var q = conn.query """ +UNWIND [1,2,3] AS items +RETURN items +""" + +for row in q: discard + +assert q.hasNext == true +