564 lines
14 KiB
Markdown
564 lines
14 KiB
Markdown
|
|
# 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 -- unfortunately, with all the C wrappers, there'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 while Kuzu
|
|
is pre 1.0, and in practice, mismatched versions *might* work. Don't count too
|
|
heavily on it. :-) Once there's a 1.0, this should be less chaotic.
|
|
|
|
| Kuzu Library Version | Nim Kuzu Minimum Version |
|
|
| -------------------- | ------------------------ |
|
|
| v0.8.2 | v0.1.0 |
|
|
| v0.9.0 | v0.2.0 |
|
|
| v0.10.0 | v0.4.0 |
|
|
| v0.11.0 | v0.5.0 |
|
|
|
|
You can use the `kuzuVersionCompatible()` function (along with the
|
|
`kuzuGetVersion()` and the `KUZU_VERSION` constant) to quickly check if things
|
|
are looking right.
|
|
|
|
```nim
|
|
import kuzu
|
|
|
|
echo KUZU_VERSION #=> "0.1.0"
|
|
echo kuzuGetVersion() #=> "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
|
|
"""
|
|
|
|
assert res.num_columns == 3
|
|
assert res.num_tuples == 1
|
|
echo res.compile_time #=> 14.028
|
|
echo res.execution_time #=> 1.624
|
|
|
|
# Return the column names as a sequence.
|
|
assert res.column_names == @["hi", "pin", "list"]
|
|
|
|
# Return the column data types as a sequence.
|
|
assert 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) )
|
|
|
|
assert 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) )
|
|
assert res.column_types[0] == KUZU_UINT64
|
|
|
|
res = stmt.execute( (num: 12.float) )
|
|
assert 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 Results
|
|
|
|
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 a result 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 #=> KuzuIterationError exception!
|
|
```
|
|
|
|
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
|
|
|
|
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]
|
|
|
|
assert 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!
|
|
|
|
assert $value == "2560"
|
|
assert 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 an 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 more elaborate example, following a node path:
|
|
|
|
```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}.""" #=>
|
|
# Bob has known Bruce since 2003.
|
|
# Bob has known Alice since 2009.
|
|
# Alice has known Bob since 2010.
|
|
```
|
|
|
|
|
|
### Blobs
|
|
|
|
Kuzu can store small chunks of opaque binary data. For these BLOB columns,
|
|
using `toBlob` will return the raw sequence of bytes.
|
|
|
|
```nim
|
|
var q = conn.query """
|
|
CREATE NODE TABLE Doot ( id SERIAL, data BLOB, PRIMARY KEY(id) )
|
|
"""
|
|
|
|
var stmt = conn.prepare( "CREATE (d:Doot {data: encode($str)})" )
|
|
q = stmt.execute( (str: "Hello!") )
|
|
q = conn.query( "MATCH (d:Doot) RETURN d.data" )
|
|
|
|
var blob = q.getNext[0].toBlob #=> @[72, 101, 108, 108, 111, 33]
|
|
```
|