Nucleus Client
View as MarkdownNeutron for Julia accesses all 14 Nucleus data models through typed accessors, using multiple dispatch so each model exposes an idiomatic Julia API. Connect once, then reach any model.
Status: not covered by the live conformance suite. The
sdk-liveCI matrix runs python, go, typescript and elixir against a running engine; julia is not in it. The signatures below are taken from the SDK source, not from executed calls against Nucleus. When that suite was first pointed at an SDK it found breakage in eight methods on day one — and a source audit of this page on 2026-08-15 found four documented calls that could not have worked. Checked against source 2026-08-15.
client = NeutronJulia.connect(url)
sql(client) # SQL queries
kv(client) # Key-Value (TTL, lists, hashes, sets, sorted sets, HyperLogLog)
vector(client) # Vector search (HNSW, IVFFlat)
timeseries(client) # Time series (Gorilla compression)
document(client) # Document store (JSONB)
graph(client) # Graph (Cypher, adjacency)
fts(client) # Full-text search (BM25)
geo(client) # Geospatial (R-tree)
blob(client) # Blob storage (BLAKE3)
streams(client) # Streams (consumer groups)
columnar(client) # Columnar analytics
datalog(client) # Datalog queries
cdc(client) # Change Data Capture
pubsub(client) # PubSub (LISTEN/NOTIFY)
SQL
Parameters are varargs, not an array. query(s, sql, [true]) binds a
one-element array to $1; query(s, sql, true) binds the boolean:
s = sql(client)
rows = query(s, "SELECT id, name, email FROM users WHERE active = \$1", true)
row = query_one(s, "SELECT * FROM users WHERE id = \$1", 42)
n = execute!(s, "INSERT INTO users (name, email) VALUES (\$1, \$2)",
"Alice", "alice@example.com")
query returns a column table (Tables.columntable), so it drops straight into
DataFrame(rows). query_one returns a single NamedTuple and throws a
NucleusError with status 404 when there are no rows — it does not return
nothing, so a lookup that may legitimately miss belongs in a try or should
go through query and check for empty. execute! returns the affected-row
count.
Key-Value
k = kv(client)
kv_set!(k, "key", "value") # or the alias set!
kv_get(k, "key") # "value", or nothing when absent
kv_delete!(k, "key")
kv_set!(k, "session", "abc", ttl=3600) # ttl in seconds, keyword argument
# Collections
lpush!(k, "queue", "item1")
hset!(k, "user:1", "name", "Alice")
sadd!(k, "tags", "rust", "julia")
zadd!(k, "leaderboard", 100.0, "player1")
pfadd!(k, "visitors", "user1", "user2") # HyperLogLog
On the kv_ prefix. Every scalar KV operation is exported twice: as
kv_get / kv_set! / kv_delete! / kv_exists / kv_incr! / kv_ttl /
kv_expire!, and — where the short name does not collide with Julia's Base —
as an alias (set!, setnx!, exists, incr!, ttl, expire!, dbsize,
flushdb!).
There is deliberately no get and no delete! alias: those would clash with
Base.get and Base.delete! and silently change dispatch for the whole
session. Reach for the kv_ form when in doubt — it always exists. The
collection operations (lpush!, hset!, sadd!, zadd!, pfadd!, …) have no
prefix, because none of them collide.
Vector Search
v = vector(client)
# The dimension is taken from the column, not passed in. `column` defaults to
# "embedding"; `ef` and `m_param` tune HNSW build quality.
create_index!(v, "embeddings"; column="embedding", metric=Cosine, ef=200, m_param=16)
results = search(v, "embeddings", query_vec; k=10, metric=Cosine)
d = dims(v, "embeddings")
metric takes the exported DistanceMetric values L2, Cosine and
InnerProduct — not strings. There is no index_type keyword: create_index!
builds an HNSW index, and IvfFlat is created through SQL directly.
TimeSeries
ts = timeseries(client)
insert!(ts, "cpu_usage", [now_ms, now_ms + 1000], [0.75, 0.82])
last = last_value(ts, "cpu_usage")
count = ts_count(ts, "cpu_usage")
avg = range_avg(ts, "cpu_usage", start_ms, end_ms)
Graph
g = graph(client)
# `properties` is a KEYWORD argument — passing the Dict positionally is a
# MethodError, not a silently ignored extra.
alice = add_node!(g, "Person"; properties=Dict("name" => "Alice"))
bob = add_node!(g, "Person"; properties=Dict("name" => "Bob"))
add_edge!(g, alice, bob, "KNOWS"; properties=Dict("since" => 2024))
neighbors(g, alice) # Vector{Int64}
path = shortest_path(g, alice, bob) # Vector{Int64}
Node and edge ids are Int64 and are what every other graph call takes —
add_edge!, neighbors, shortest_path, delete_node! and delete_edge! all
address nodes by the id add_node! returned. Cypher is available too, through
graph_query(g, "MATCH ...").
Transactions
transaction takes a function, so it is used as a do block. COMMIT and
ROLLBACK are automatic — the block committing on normal return and rolling back
on any exception, which is then rethrown:
transaction(client) do tx
execute!(sql(tx), "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
execute!(sql(tx), "UPDATE accounts SET balance = balance + 100 WHERE id = 2")
end
The block receives a NucleusTransaction, and every model accessor works on
it — sql(tx), kv(tx), vector(tx), and so on — so a transaction can span
models. Use tx rather than client inside the block; reaching for the outer
client issues statements on the same connection but outside the reader's
intent, which is the kind of thing that reads correct and is not.
There is no commit! or rollback! to call: neither is exported, and the
lifecycle is the block's.
The return value of the block is the return value of transaction:
balance = transaction(client) do tx
query_one(sql(tx), "SELECT balance FROM accounts WHERE id = \$1", 1)
end