A Nushell plugin that embeds the Stardust database. Open a database, write to it, run queries as streaming nu tables, and follow live query results, all from the nu prompt.
This is a prototype against Stardust's Rust binding (release
alpha-2610240014). It doesn't include Stardust itself: you need your own
release download and license token.
-
Nushell 0.116.0. Plugins must match nu's version exactly; change the
nu-plugin/nu-protocolpins inCargo.tomlfor another nu. -
Rust 1.85 or newer.
-
A Stardust release for your platform, extracted next to this repo:
~/stardust-alpha-2610240014/ # the release ~/nu_plugin_stardust/ # this repoCargo.tomlpoints at../stardust-alpha-2610240014/abi/bindings/rust. Edit that path for another release or location. -
A Stardust license token (the
v4.public.…text from your account page).
cargo build --release
plugin add target/release/nu_plugin_stardust
plugin use stardustThe plugin links Stardust's static archive, so the binary is self-contained and
nu can launch it without LD_LIBRARY_PATH.
Stardust reads no environment variable or file itself, so the plugin loads the token for it, from the first of:
$env.STARDUST_LICENSE~/.config/stardust/license
mkdir ~/.config/stardust
"v4.public.…" | save -f ~/.config/stardust/license
chmod 600 ~/.config/stardust/license
stardust license # holder, tier and dates; never the tokenlet db = stardust open ~/people.db
# keys like #_ada name new entities
'{
#_ada {name Ada role engineer age 36}
#_bob {name Bob role engineer age 41}
#_cy {name Cy role designer age 29}
}' | stardust patch $db
# => {status: committed, transaction: 4, facts: 9, committed_at: …, ids: {#_ada: 1, #_bob: 2, #_cy: 3}}Anywhere a command takes a document (a patch, a query or mutation, a
definition, --params, --input), it accepts:
- a record or list: nu data
- a string: DUST text, or JSON or EDN with
--format json/--format edn - binary: TRON
So the patch above can also be a record. Quote the #_ keys there, since #
starts a nu comment:
{'#_ada': {name: Ada role: engineer age: 36}} | stardust patch $dbA string that holds only a name, like people/by_age, is a stored
definition's path.
Rows stream in one page at a time, so they work with every nu table command,
and | first n stops reading early:
stardust query $db {find: [?name ?age] where: [[?e name ?name] [?e age ?age]]}
| where age > 30
| sort-by ageOr let Stardust filter and sort, which can use its indexes:
stardust query $db '{
find [?name ?age]
where [[?e name ?name] [?e age ?age] [> ?age 30]]
orderBy [?age]
}'stardust define $db queries people/by_age '{
find [?name]
parameters {min 18}
where [[?e name ?name] [?e age ?age] [>= ?age ?min]]
}'
stardust query $db people/by_age --params {min: 40}
stardust definitions $db queries # => [people/by_age]
stardust definition $db queries people/by_age
stardust define $db queries people/by_age null # deletestardust mutate $db '{query {find [?e] where [[?e name Ada]]} patch {?e {age 37}}}'
# => committed, then no_change if you run it againstardust subscribe streams the query's result now, and again after every
commit that changes it. Each event is the whole result; superseded counts
intermediate results skipped during a burst of commits.
stardust subscribe $db people/by_age | each { $in.result } # Ctrl-C to stopTo keep the prompt free, run the subscription in a background job and have it send events to the main thread's mailbox:
let sub = job spawn {
stardust subscribe $db people/by_age | each {|ev| $ev | job send 0 --tag 7 }
}
job recv --tag 7 # the current result
'{#_dee {name Dee age 52}}' | stardust patch $db
job recv --tag 7 --timeout 5sec # the result after that commit
job kill $substardust eval '[* ?n 2]' --input {n: 21} # => 42 (a string is DUST text)
stardust eval [+ ?x 1] --input {x: 41} # => 42 (or nu data)from and to for Stardust's DUST, EDN and TRON. DUST and EDN are text
(--width wraps lines); TRON is binary. open and save use them by file
extension.
open people.dust
{a: 1 b: [x y]} | to edn # EDN text
{a: 1} | save data.tron # `save` runs `to tron`
open data.tron # => {a: 1}Stardust's own plugins (reactors, backups, objects, ...) are off unless named at open.
stardust plugins | select name description
let db = stardust open ~/people.db --plugins {reactors: {}}
stardust plugin-call $db reactors statusThe objects plugin stores payloads once per SHA-256 and names them with a
tree of files and directories. stardust objects <operation> mirrors the
SDK's typed view; upload and open stream bytes in and out.
let db = stardust open ~/files.db --plugins {objects: {}}
let hello = 'Hello, objects' | stardust objects upload $db
# => {digest: {#sha256: 95e84d…}, size: 14, deduplicated: false}
[{name: notes} {name: hello.txt under: 1 digest: $hello.digest}] | stardust objects create $db
stardust objects open $db $hello.digest # => Hello, objects
open --raw big.iso | stardust objects upload $db # streams; never held whole
stardust objects children $db 1 | select name size| nu | Stardust |
|---|---|
| nothing, bool, int, float, string | null, boolean, i64, f64, string |
| list, record | array, object (member order kept) |
| date | {#utc "2026-10-07T12:34:56Z"} (100 ns resolution) |
| duration | {#dur "PT1.5S"} |
| filesize | i64 bytes |
Entity references stay as {#link id} records.
- Handles.
stardust openreturns a custom value holding an ID. The plugin keeps the openDatabasein a table; nu tells the plugin when the last copy of a handle is dropped, and the database closes then.stardust close $dbcloses it immediately. - Staying alive. nu stops idle plugins, which would close every database.
While any handle is open the plugin disables that with
engine.set_gc_disabled(true), and re-enables it when the last one closes. - Threads. In this release the database and its handles are
Send + Sync, so one plugin process serves every call. All jobs in a nu session share it. Every call into Stardust runs on a small fixed pool of threads (src/pool.rs): Stardust keeps memory for each thread that runs a query, even after the thread exits (see the TodoMVC notes), and nu runs plugin calls on new threads. - Subscriptions. The binding's
nextis async; nu streams are iterators. So the stream pollsnext().awaiton its own thread, parking until Stardust wakes it.- Ctrl-C: a signal handler (
engine.register_signal_handler) calls aCanceller, which wakes the waitingnextat once. - nu exiting: nothing tells a waiting iterator that nu has gone, and the
plugin can't exit while that thread runs. So between events the stream
waits at most 1 s, pings nu, and ends when the ping fails. A
nextfuture dropped mid-wait loses no event.
- Ctrl-C: a signal handler (
examples/todomvc is the Datastar TodoMVC served by
http-nu, which loads this plugin with --plugin. Writes are patches, and
each viewer streams a live query over SSE.
- One process per database file. Stardust takes an exclusive lock, so a
second nu session can't open a database that another has open (
locked). Use jobs within one session, as above. Within one session, opening a path that's already open (with the same--plugins) returns another handle to the same database, so http-nu's-whot reload, which runs the script'sstardust openagain while the old handler still holds it, works. - Ctrl-C cancels every subscription in the plugin. nu sends the interrupt to the whole plugin without saying which call it was for, so interrupting any foreground command also ends subscriptions running in background jobs.
job killdoesn't reach the plugin. A killed job's subscription keeps running until its next event, then stops.- Abandoned streams linger. After
stardust subscribe … | first n, the subscription ends at its next event, not immediately.
The last three come from the same gap: nu doesn't tell a plugin's iterator that its reader has gone.
smoke.nu drives every command with the release's example data
(abi/examples/data) and checks each result against the file's expected
values, including a subscription receiving a commit from a background job:
nu smoke.nu
# restart: ok, eval: ok, patch: ok, query: ok, mutate: committed then no_change,
# definitions: ok, documents: ok, subscribe: ok, time: ok, formats: ok, plugins: ok,
# objects: ok, shared open: ok, all oksrc/main.rs serve_plugin
src/plugin.rs open databases, handles, license loading
src/commands.rs the commands
src/objects.rs stardust objects: the objects plugin, streams included
src/pool.rs the threads every Stardust call runs on
src/convert.rs nu values <-> Stardust documents (with unit tests)
smoke.nu end-to-end test
examples/todomvc Datastar TodoMVC on http-nu, state in Stardust