Installation
Plugins add behavior to the embedded library. Install the core library before selecting plugins.
Included plugins
| Plugin | Name | Purpose |
|---|---|---|
| Adaptive query planner | adaptive_query_planner | Learn from query runs and compare alternative plans |
| Reactors | reactors | Run live mutations after commits |
| History retention | history | Purge history according to retention settings |
| Object store | objects | Store binary payloads as named files and directories |
| Backups | backups | Copy history and object payloads to local folders and S3-compatible storage, and recover from them |
| SDK generation | sdk | Generate typed language APIs from stored definitions |
The library's plugins operation lists the plugins it can use and their requirements.
The coming soon list describes prototype features that are not available in this library.
| Form | Plugin set | Files | Change the set |
|---|---|---|---|
| Plugin library | One shared library per plugin beside the core library | The plugin libraries in lib/<os>_<arch>/ | Use the plugin libraries from the same release as the core library |
| Compiled into the core | Chosen when the core library is built | None | Rebuild the core library |
| Static archive | Every plugin in the release, linked into your program | None | Relink with the static archive of another release |
Release packages ship the adaptive planner, Reactors, SDK generation, the object store and backups as plugin libraries. History retention is compiled into the core library. The static archive in the same package contains all these plugins. In all three forms, a database enables plugins when it opens, as described in Enable plugins.
Plugin libraries
A release package keeps each platform's shared core library and plugin libraries together in that platform's directory, lib/<os>_<arch>/:
| File | Linux | macOS | Windows |
|---|---|---|---|
| Core library | libstardust.so | libstardust.dylib | stardust.dll |
| Plugin library, one per plugin | libstardust_<name>.so | libstardust_<name>.dylib | stardust_<name>.dll |
Keep these files together, in the directory of the core library, and use them only with the release they came with. The core library looks for plugin libraries only in its own directory, the first time a call needs the plugin list. Opening a database, listing plugins and checking a license can trigger this scan. The bindings need no separate discovery setting.
Verification
The release build records each finished plugin library's SHA-256 in the core library. The core library accepts only the plugin libraries it was built with:
- It carries a SHA-256 for the plugin named by the file.
- The file's SHA-256 equals that value.
- The plugin library is from the same build and exports the plugin entry under its own name.
The core library reads the file once, checks its SHA-256, and loads the bytes that it checked. On Linux it loads the file through its open descriptor. On Windows it keeps the file open without write sharing until it loads. On macOS it keeps the file open and relies on the library's code signature.
Check the release download with minisign, as described in Verify the download. Each binding checks the core library's SHA-256. The core then checks its plugin libraries. See Load the core for the binding check. These checks detect a mismatched, corrupted, or replaced plugin library.
Available and unavailable plugins
A plugin library is available only after it passes every check. Missing libraries and failed checks make the plugin unavailable. Selecting an unavailable plugin causes database open to fail.
The plugins operation lists each available plugin with its name, description, version, requirements, willNotWorkWith, definition types, hooks, index providers, and database hooks.
After them, it lists each unavailable plugin library as an object with name, file, and unavailable, which states the reason:
| Reason | Cause |
|---|---|
| This core library was not built with the plugin library | The file is not part of this release |
| The plugin library differs from the one this core library was built with | The file changed after the release was built |
| The plugin library cannot be loaded | The platform loader refused the file |
| The plugin library has no plugin entry | The file is not a Stardust plugin library |
| The plugin library is from another build | The file came with a different release |
| The plugin library's name differs from its plugin name | The file was renamed |
| A plugin with this name is already included | The core library already contains that plugin |
| The plugin library's plugin is invalid | The plugin description is not usable |
Development builds
Contributors who build the library from source can compile a development core library that loads plugin libraries without checking their SHA-256. This switch exists only at compile time, for development. Release packaging refuses a library built with it. Release libraries always check their plugin libraries.
Plugins compiled into the core
A plugin compiled into the core library is part of that library file. It needs no plugin library file or SHA-256 check, and it is always available to databases. The plugins that a core library contains are chosen when it is built, so changing them requires rebuilding the core library.
Plugins in a static link
Go, Rust, Odin and C programs can link the static archive (libstardust_full.a, or stardust_full.lib on Windows) instead of the shared core library. Linking gives the commands.
The archive contains the core and every plugin of its release, compiled in. The plugin set is fixed when you link your program.
The backups plugin in the archive carries its own Zstandard copy under stardust_-prefixed symbol names, so your program can also link its own libzstd.
- Ship only your executable. A static program uses no core library, no plugin libraries and no SHA-256 checks.
- A static program does not look for plugin libraries. Plugin library files beside the executable have no effect.
- To use the plugins of another release, link your program again with that release's static archive.
- Each database still enables plugins when it opens, as described in Enable plugins. The license
pluginspermission applies as it does for the shared library.
Check the download with minisign before you link the archive, as described in Verify the download. The bindings do not check the SHA-256 of a statically linked core: it is part of your executable.
Enable plugins
Use the plugins database option when opening a database. It works the same way for plugin libraries, plugins compiled into the core, and static links.
Selection applies until the database closes. Only named plugins are enabled.
| Selection | Result |
|---|---|
Omit plugins | Enable no plugins |
Empty object, {} | Enable no plugins |
Object, such as {reactors {}} | Enable the named plugins with their load settings |
| An unknown name | The open fails with invalid_argument |
Each object key is a plugin name. Its value is a settings object. An empty settings object uses the plugin's defaults.
Select only available plugins. Include each selected plugin's required dependencies in the object.
The willNotWorkWith list names plugins that cannot be enabled together.
If either plugin lists the other, opening the database fails with invalid_argument before configuration or storage changes.
A plugin in this list can be absent from the library or disabled.
Unknown settings and invalid values fail before the database opens.
The adaptive planner accepts memoryShare.
The history plugin accepts scheduling settings, backups accepts its targets, and SDK generation accepts language output settings.
The reactors and objects plugins accept only empty settings.
A disabled plugin does not install its database hooks, definition types, or index providers.
A write that sets stardust/history reports unavailable when the database does not enable history.
A write that sets stardust/object/digest reports unavailable when the database does not enable objects.
Plugin operations
A plugin serves named operations through two calls:
| Call | Input | Result |
|---|---|---|
| plugin_call | Request document | Result document |
| plugin_stream | Request document | Byte stream, such as an object upload |
Bindings provide typed wrappers for the supported operations. Each wrapper builds a request and calls the same underlying operation.
A disabled plugin reports unavailable. An unknown operation reports invalid_argument with code unknown_operation.
Plugins name their runtime description status and their wait for committed work sync, when they offer them.
Plugin boundary
All plugins are first-party plugins from the Stardust project. The plugin entry that a plugin library exports is internal to Stardust: it is not part of the C ABI, and the bindings do not call it. There is no interface for third-party plugins.
Operations
| Operation | Returns | Summary |
|---|---|---|
| Database.plugin_call | document | database.plugin_call(plugin, operation, request) runs a named operation of an enabled plugin and returns its owned result document. |
| Database.plugin_stream | plugin_stream | database.plugin_stream(plugin, operation, request) opens an owned plugin stream of a named stream operation. |
| PluginStream.write | count | plugin_stream.write(data) appends bytes to the stream and returns the count accepted. |
| PluginStream.read | text | plugin_stream.read(length) returns up to length bytes from the stream's position and advances it. |
| PluginStream.seek | integer | plugin_stream.seek(offset, whence) moves the stream's position and returns it. |
| PluginStream.finish | document | plugin_stream.finish() ends the stream and returns its owned result document. |
| PluginStream.close | none | Release the plugin_stream handle, ending an unfinished stream without finishing it. |
Database.plugin_call
def plugin_call(self, plugin: str, operation: str, request: Document | None = None) -> DocumentParameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
self | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
plugin | str | Value | Required | The enabled plugin's name. |
operation | str | Value | Required | The plugin's call operation. |
request | Document | None | Borrowed for the call | None | The borrowed request document. Its absence reads as an empty object. |
Errors
| Status | When |
|---|---|
"invalid_argument" | a name is empty, the plugin has no call operation with the name, or the plugin refuses the request |
"unavailable" | the plugin is unknown or the database does not enable it |
"not_found" | the plugin reports that the request names nothing it holds |
"conflict" | the plugin's write lost to a concurrent change, or the database holds writes |
"evaluation_failed" | the plugin cannot read its stored state |
"failed" | the plugin's work failed |
"timeout" | the plugin's wait passed its limit |
"closed" | the database is closed |
"out_of_memory" | native memory is exhausted |
Notes
The plugin decodes the request and builds the result. A result is null when the operation returns nothing. Returned documents omit empty object and array members recursively; empty roots, array elements and scalar values remain. Each plugin documents its operations: object store, reactors and SDK generation.
Examples
Wait for live mutations, then describe them
async with await stardust.parse("{maxWait {#dur PT5S}}", "dust") as options:
# sync returns a null document; db.reactors().sync wraps the same call.
await (await db.plugin_call("reactors", "sync", options)).close()
# No request reads as an empty object.
result = await db.plugin_call("reactors", "status")| Offset (hex) | Bytes (hex) | Meaning |
|---|---|---|
| 00 | 54 52 4F 4E | magic TRON |
| 04 | 4C 68656164 | txt "head" |
| 09 | 02 04000000000000 00 | i64 4 |
| 12 | 0F 0A 04000000 0900 0000 | map leaf, size 10: @4 → @9 |
| 1C | 6C 637572736F72 | txt "cursor" |
| 23 | 02 04000000000000 00 | i64 4 |
| 2C | 0F 0A 1C000000 2300 0000 | map leaf, size 10: @1C → @23 |
| 36 | 07 0E 04200000 1200 0000 2C000000 | map branch, size 14, depth 1, slots 2, 13: @12 → @2C |
| 44 | 7C 656E61626C6564 | txt "enabled" |
| 4C | 09 | bit true |
| 4D | 0F 0A 44000000 4C00 0000 | map leaf, size 10: @44 → @4C |
| 57 | 7C 70656E64696E67 | txt "pending" |
| 5F | 01 | bit false |
| 60 | 0F 0A 57000000 5F00 0000 | map leaf, size 10: @57 → @5F |
| 6A | 07 12 28020000 3600 0000 4D000000 6000 0000 | map branch, size 18, depth 0 (root), slots 3, 5, 9: @36 → @4D → @60 |
| 7C | 6A000000 00000000 | root → @6A; previous: none |
C exports: stardust_db_plugin_call, stardust_result_take_document, stardust_result_error, stardust_result_release
Database.plugin_stream
def plugin_stream(self, plugin: str, operation: str, request: Document | None = None) -> PluginStreamParameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
self | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
plugin | str | Value | Required | The enabled plugin's name. |
operation | str | Value | Required | The plugin's stream operation. |
request | Document | None | Borrowed for the call | None | The borrowed request document. Its absence reads as an empty object. |
Errors
| Status | When |
|---|---|
"invalid_argument" | a name is empty, the plugin has no stream operation with the name, or the plugin refuses the request |
"unavailable" | the plugin is unknown or the database does not enable it |
"not_found" | the plugin reports that the request names nothing it holds |
"conflict" | the database holds writes |
"evaluation_failed" | the plugin cannot read its stored state |
"closed" | the database is closed |
"out_of_memory" | native memory is exhausted |
Notes
A stream supports the steps its operation documents. Each plugin documents its stream operations: object store.
Examples
Upload a payload and publish it
# Close abandons the upload unless it finished.
async with await db.plugin_stream("objects", "upload") as upload:
await upload.write(b"Hello, ")
await upload.write(b"objects")
result = await upload.finish()| Offset (hex) | Bytes (hex) | Meaning |
|---|---|---|
| 00 | 54 52 4F 4E | magic TRON |
| 04 | 7C 23736861323536 | txt "#sha256" |
| 0C | 14 40 393565383464 6133386362656664 6633663966366566 3032323031303734 6337333132346637 6366323032356433 6531643639653539 3161343130383536 6662 | txt "95e84da38cbefdf3f9f6ef02…" |
| 4E | 0F 0A 04000000 0C00 0000 | map leaf, size 10: @4 → @C |
| 58 | 4C 73697A65 | txt "size" |
| 5D | 02 0E000000000000 00 | i64 14 |
| 66 | 0F 0A 58000000 5D00 0000 | map leaf, size 10: @58 → @5D |
| 70 | 6C 646967657374 | txt "digest" |
| 77 | 0F 0A 70000000 4E00 0000 | map leaf, size 10: @70 → @4E |
| 81 | CC 64656475706C69 6361746564 | txt "deduplicated" |
| 8E | 01 | bit false |
| 8F | 0F 0A 81000000 8E00 0000 | map leaf, size 10: @81 → @8E |
| 99 | 07 0E 00440000 7700 0000 8F000000 | map branch, size 14, depth 1, slots 10, 14: @77 → @8F |
| A7 | 07 0E 00030000 6600 0000 99000000 | map branch, size 14, depth 0 (root), slots 8, 9: @66 → @99 |
| B5 | A7000000 00000000 | root → @A7; previous: none |
C exports: stardust_db_plugin_stream_open, stardust_result_error, stardust_result_release
PluginStream.write
def write(self, data: bytes) -> intRequires exclusive use of the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
self | PluginStream | Borrowed for the call | Required | The plugin_stream owner for this operation. Keep it alive during the call. |
data | bytes | Value | Required | The bytes to append, borrowed for the call. |
Errors
| Status | When |
|---|---|
"invalid_argument" | the stream has finished or does not accept writes |
"conflict" | the database holds writes while the plugin stores the bytes |
"closed" | the plugin_stream is closed |
Notes
A failure also reports the count accepted before it, returned together with the error. Those bytes stay part of the stream; continue with the remaining bytes.
Examples
Append bytes to an upload
async with await db.objects().upload() as upload:
result = await upload.write(b"Hello, objects")| Offset (hex) | Bytes (hex) | Meaning |
|---|---|---|
| 00 | 54 52 4F 4E | magic TRON |
| 04 | 02 0E000000000000 00 | i64 14 |
| 0D | 04000000 00000000 | root → @4; previous: none |
C exports: stardust_db_plugin_stream_write
PluginStream.read
def read(self, size: int = -1) -> bytesRequires exclusive use of the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
self | PluginStream | Borrowed for the call | Required | The plugin_stream owner for this operation. Keep it alive during the call. |
size | int | Value | -1 | The most bytes to read. |
Errors
| Status | When |
|---|---|
"invalid_argument" | the stream does not support reads, or length is negative |
"evaluation_failed" | stored bytes do not match their digest |
"closed" | the plugin_stream is closed |
Notes
A read is shorter than length only at the end, and empty at or past it.
Examples
Read the start of a payload
async with await db.objects().upload() as upload:
await upload.write(b"Hello, objects")
await (await upload.finish()).close()
async with await db.objects().open("95e84da38cbefdf3f9f6ef02201074c73124f7cf2025d3e1d69e591a410856fb") as reader:
# read() without a size reads to the end.
data = await reader.read(5)
# data holds b"Hello".
result = len(data)| Offset (hex) | Bytes (hex) | Meaning |
|---|---|---|
| 00 | 54 52 4F 4E | magic TRON |
| 04 | 02 05000000000000 00 | i64 5 |
| 0D | 04000000 00000000 | root → @4; previous: none |
C exports: stardust_db_plugin_stream_read
PluginStream.seek
def seek(self, offset: int, whence: int = 0) -> intRequires exclusive use of the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
self | PluginStream | Borrowed for the call | Required | The plugin_stream owner for this operation. Keep it alive during the call. |
offset | int | Value | Required | The distance from whence, negative toward the start. |
whence | int | Value | 0 | 0 for the start, 1 for the current position, 2 for the end. |
Errors
| Status | When |
|---|---|
"invalid_argument" | the stream does not support seeks, whence is unknown, or the position is negative or past the i64 range |
"closed" | the plugin_stream is closed |
Notes
whence 0 counts offset from the start, 1 from the current position and 2 from the end. A refused seek keeps the position.
Examples
Seek from the end of a payload
async with await db.objects().upload() as upload:
await upload.write(b"Hello, objects")
await (await upload.finish()).close()
async with await db.objects().open("95e84da38cbefdf3f9f6ef02201074c73124f7cf2025d3e1d69e591a410856fb") as reader:
# whence 2 counts from the end, so -7 is the start of "objects".
result = await reader.seek(-7, 2)| Offset (hex) | Bytes (hex) | Meaning |
|---|---|---|
| 00 | 54 52 4F 4E | magic TRON |
| 04 | 02 07000000000000 00 | i64 7 |
| 0D | 04000000 00000000 | root → @4; previous: none |
C exports: stardust_db_plugin_stream_seek
PluginStream.finish
def finish(self) -> DocumentRequires exclusive use of the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
self | PluginStream | Borrowed for the call | Required | The plugin_stream owner for this operation. Keep it alive during the call. |
Errors
| Status | When |
|---|---|
"invalid_argument" | the stream has finished or has no finish step |
"conflict" | the database holds writes |
"closed" | the plugin_stream is closed |
"out_of_memory" | native memory is exhausted |
Notes
A failed finish keeps the stream, so finish can be retried. Close the stream separately.
Examples
Publish the same payload twice
# The second finish reports deduplicated true and stores nothing new.
result = None
for _ in range(2):
async with await db.objects().upload() as upload:
await upload.write(b"Hello, objects")
if result is not None:
await result.close()
result = await upload.finish()| Offset (hex) | Bytes (hex) | Meaning |
|---|---|---|
| 00 | 54 52 4F 4E | magic TRON |
| 04 | 7C 23736861323536 | txt "#sha256" |
| 0C | 14 40 393565383464 6133386362656664 6633663966366566 3032323031303734 6337333132346637 6366323032356433 6531643639653539 3161343130383536 6662 | txt "95e84da38cbefdf3f9f6ef02…" |
| 4E | 0F 0A 04000000 0C00 0000 | map leaf, size 10: @4 → @C |
| 58 | 4C 73697A65 | txt "size" |
| 5D | 02 0E000000000000 00 | i64 14 |
| 66 | 0F 0A 58000000 5D00 0000 | map leaf, size 10: @58 → @5D |
| 70 | 6C 646967657374 | txt "digest" |
| 77 | 0F 0A 70000000 4E00 0000 | map leaf, size 10: @70 → @4E |
| 81 | CC 64656475706C69 6361746564 | txt "deduplicated" |
| 8E | 09 | bit true |
| 8F | 0F 0A 81000000 8E00 0000 | map leaf, size 10: @81 → @8E |
| 99 | 07 0E 00440000 7700 0000 8F000000 | map branch, size 14, depth 1, slots 10, 14: @77 → @8F |
| A7 | 07 0E 00030000 6600 0000 99000000 | map branch, size 14, depth 0 (root), slots 8, 9: @66 → @99 |
| B5 | A7000000 00000000 | root → @A7; previous: none |
C exports: stardust_db_plugin_stream_finish, stardust_result_take_document, stardust_result_error, stardust_result_release
PluginStream.close
def close(self) -> NoneConsumes the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
self | PluginStream | Consumed owner | Required | The plugin_stream owner to release. Do not use it after this call. |
Notes
Close waits only for a call already in progress on the stream; start no other call once close begins. It is valid after the database closes. A plugin documents what close abandons; the object store reclaims an upload stage at the next open when close could not abandon it.
Examples
Abandon an unfinished upload
upload = await db.objects().upload()
await upload.write(b"draft")
# Closing before finish publishes nothing.
await upload.close()C exports: stardust_db_plugin_stream_release