Nucleus Client

View as Markdown

Neutron 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-live CI 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.

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