Queries and mutations
The example stores the people in a database and queries their names. It collects query pages, updates Ada's age, and reads the changed age.
The optional adaptive query planner learns from runs and compares alternative plans for stored queries.
Enable it through the database's plugins open option.
Example
The example compares these results:
- a committed mutation
- a repeated mutation
- a query that matches no entity
Give Ada her birthday and read her age
birthday_text holds this document:
{:query {:find [?e]
:where [[?e "name" "Ada"]]}
:patch {:?e {:age 37}}}query_text holds this document:
{:find [?age]
:where [
[?e "name" "Ada"]
[?e "age" ?age]]}birthday_source := stardust.parse(birthday_text, .EDN) or_return
defer stardust.close(&birthday_source)
birthday := stardust.mutation(db, birthday_source) or_return
defer stardust.close(&birthday)
commit := stardust.run(birthday) or_return
defer stardust.close(&commit)
definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query) or_return
defer stardust.close(&rows)
result := stardust.all_pages(rows) or_return
defer stardust.close(&result){:value [{:age 37}]}Query pages
Each query page is an owned document.
- You can keep a page after you release the rows handle, query, or database that produced it.
- Release each page when you finish with its values. The document ownership rule applies.
Cached joins
The database can keep completed native fact joins as owned binding rows before aggregation. When you run the same query and plan again, the cache can supply those rows at the same database snapshot. Aggregates and output expressions still run for each call.
The cache uses the database working-memory budget and gives memory back when queries need it. The cache discards rows from joins that do not complete. After a write, new queries use a new snapshot. An active reader keeps the rows from its original snapshot.
Parameterized, temporal and overlay queries use their normal execution paths. Queries with predicates, derivations, member clauses or transaction terms also use those paths. Background planner trials and sampled foreground measurements execute joins directly.
Exact indexes
A query or mutation definition that compares a field with a value makes the database build that field's exact index.
The comparison is a fact clause with a constant or a parameter value, or a =, <, <=, >, >= or hasPrefix predicate on a fact clause's value.
With the index, a hasPrefix predicate reads only the texts that start with the prefix, and no value of another type.
- Storing the definition starts the build. An inline query or mutation that compares a field the same way also starts one.
- The database builds the index in the background, including the values committed before the definition. The call that started it does not wait.
- Queries scan the field until its index is ready. The results are the same; only the time differs.
- Use indexes_sync to wait until every index that earlier definitions and inline runs ask for is ready, for example before you measure a query.
- An index that no definition uses any more stays until the database next opens.
Operations
| Operation | Returns | Summary |
|---|---|---|
| read_entity | document | database.read_entity(entity, max_depth, max_entities) atomically returns an owned {document links revisions truncated} document, expanding links up to max_depth hops. |
| query | query | Open an owned query from a path or an inline definition. |
| mutation | mutation | Open an owned mutation from a path or an inline definition. |
| run | rows | Run the query and return an owned row cursor. |
| close | none | Release the query handle. |
| next | document | Read the next page as an owned document. |
| page_iterator | pages | rows.pages() iterates the non-empty pages of the cursor. |
| all_pages | document | rows.all_pages() reads every remaining page into one owned array document. |
| close | none | Release the rows handle. |
| subscribe | subscription | query.subscribe(parameters, options) opens an owned live subscription. |
| next | event | Wait for the next live event. |
| events | Unavailable | subscription.events() delivers events in order until the subscription is cancelled, closes or fails. |
| cancel | none | subscription.cancel() ends the subscription. |
| close | none | Release the subscription handle. |
| run | commit | Run the mutation and return commit information. |
| close | none | Release the mutation handle. |
| merge_patch | commit | database.merge_patch(patch) applies an entity-keyed patch document in one transaction and returns commit information. |
| indexes_sync | none | database.indexes_sync(max_wait) waits until the database built every exact index that earlier definition commits and inline runs ask for. |
| bulk_begin | none | database.bulk_begin() starts a bulk build of data merge patches held by the database. |
| bulk_add | none | database.bulk_add(patch) copies and queues one merge-patch document in the bulk build. |
| bulk_finish | document | database.bulk_finish() publishes the bulk build with one commit, ends it and returns an owned summary document. |
| bulk_abort | none | database.bulk_abort() drops the bulk build in progress without writing it. |
| subscription_set_notify | Error | Wake a host scheduler when a subscription may be ready. |
read_entity
read_entity :: proc(
db: Database,
entity: builtin.i64,
max_depth: builtin.i64 = 0,
max_entities: builtin.i64 = 0,
allocator := context.allocator,
) -> (Document, Error)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
entity | builtin.i64 | Value | Required | The decimal user entity ID. |
max_depth | builtin.i64 | Value | 0 | Link hops to expand along each path, 0 through 32. 0, the default, leaves every link a {#link id} reference. |
max_entities | builtin.i64 | Value | 0 | The most entity documents the result holds, counting the root and every expanded or absent target each time it is placed. 0 selects the default of 1000. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Argument | the entity ID is outside the user entity range, max_depth is negative or above 32, or max_entities is negative |
.Closed | the database is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
revisions maps each placed entity's decimal ID to its last-touch transaction for #_tx.stardust/checkAndSet. Empty object and array fields are omitted recursively. See entity reads for result fields, JSON Pointer links and expansion rules.
Examples
Read Bob with his mentor expanded
result := stardust.read_entity(db, 2, 1) or_return
defer stardust.close(&result){:value {:document {:age 41
:mentor {:age 36
:name "Ada"
:role "engineer"}
:name "Bob"
:role "engineer"}
:links {"/mentor" {:#link 1}}
:revisions {:1 4
:2 4}
:truncated false}}C exports: stardust_db_read_entity, stardust_result_take_document, stardust_result_error, stardust_result_release
query
query :: proc(db: Database, path: string, allocator := context.allocator) -> (Query, Error)query :: proc(
db: Database,
definition: Document,
allocator := context.allocator,
) -> (
Query,
Error,
)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
path | string | Value | Required | The stored definition path selected at run time. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
definition | Document | Borrowed for the call | Required | The inline native definition copied into the opened handle. |
Errors
| Status | When |
|---|---|
.Not_Found | no definition is stored at the path |
.Invalid_Definition | the definition is invalid |
.Closed | the database is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Inline definitions are copied. Stored paths select their definition at run time.
Examples
Open an inline query and read its first page
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query) or_return
defer stardust.close(&rows)
result := stardust.next(rows) or_return
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}Refuse an invalid query definition
query_text holds this document:
{:find [?x]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query, result := stardust.query(db, definition)
defer stardust.close(&query)
defer stardust.close(&result){:value {:code "unbound_variable"
:message "Unbound_Variable"
:path nil
:field nil
:entity nil
:offset nil
:status "invalid_definition"}}Report a missing stored query
query, result := stardust.query(db, "people/missing")
defer stardust.close(&query)
defer stardust.close(&result){:value {:code "not_found"
:message "no definition at this path"
:path "people/missing"
:field nil
:entity nil
:offset nil
:status "not_found"}}C exports: stardust_db_query_open, stardust_db_query_error
mutation
mutation :: proc(db: Database, path: string, allocator := context.allocator) -> (Mutation, Error)mutation :: proc(
db: Database,
definition: Document,
allocator := context.allocator,
) -> (
Mutation,
Error,
)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
path | string | Value | Required | The stored definition path selected at run time. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
definition | Document | Borrowed for the call | Required | The inline native definition copied into the opened handle. |
Errors
| Status | When |
|---|---|
.Not_Found | no definition is stored at the path |
.Invalid_Definition | the definition is invalid |
.Closed | the database is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Inline definitions are copied. Stored paths select their definition at run time.
Examples
database/mutation
birthday_text holds this document:
{:query {:find [?e]
:where [[?e "name" "Ada"]]}
:patch {:?e {:age 37}}}source := stardust.parse(birthday_text, .EDN) or_return
defer stardust.close(&source)
mutation := stardust.mutation(db, source) or_return
defer stardust.close(&mutation)
commit := stardust.run(mutation) or_return
defer stardust.close(&commit)
result := commit{:value {:status "committed"
:transaction 5
:facts 1
:committed_at 17923367877040951
:ids {}}}C exports: stardust_db_mutation_open, stardust_db_mutation_error
run
run :: proc(
query: Query,
parameters: Document = {},
options: Run_Options = {},
explain: ^Query_Explain = nil,
allocator := context.allocator,
) -> (
Rows,
Error,
)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
query | Query | Borrowed for the call | Required | The query owner for this operation. Keep it alive during the call. |
parameters | Document | Borrowed for the call | {} | An optional object of parameter values that merge-patches the definition's declared parameters. A parameter matched in the entity position of a fact clause takes an entity reference {#link id}, never the bare ID number: a commit returns IDs as numbers, so wrap one as {#link id}, or as the typed Link value where the binding has one, before passing it. A query run fails with entity_reference_expected when a bare number reaches that position. Projected and found entities also read back as {#link id}, so a typed field that receives one uses Link rather than a plain integer. |
options | Run_Options | Value | {} | The operation's options record. See its field defaults. |
explain | ^Query_Explain | Caller-owned output | nil | Optional explain output. This record contains explain output for query run. The compile, plan, execute, group and order fields divide the wall time of run into stages. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Not_Found | the stored query definition was deleted |
.Invalid_Definition | the loaded query definition is invalid |
.Invalid_Argument | the parameters or run options are invalid |
.Evaluation_Failed | the query fails or exceeds max_cost |
.Closed | the query is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Examples
Open an inline query and read its first page
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query) or_return
defer stardust.close(&rows)
result := stardust.next(rows) or_return
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}Capture the host duration boundary
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}boundary_text holds this document:
9223372036854775807definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
boundary := stardust.parse(boundary_text, .EDN) or_return
defer stardust.close(&boundary)
nanoseconds := stardust.i64(boundary, stardust.root(boundary) or_return) or_return
options := stardust.Run_Options{page_size = 2, max_cost = time.Duration(nanoseconds)}
rows := stardust.run(query, options = options) or_return
defer stardust.close(&rows)
result := options{:value {:page_size 2
:max_cost 92233720368547758
:as_of 0}}Truncate a positive sub-tick duration
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}boundary_text holds this document:
99definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
boundary := stardust.parse(boundary_text, .EDN) or_return
defer stardust.close(&boundary)
nanoseconds := stardust.i64(boundary, stardust.root(boundary) or_return) or_return
options := stardust.Run_Options{page_size = 2, max_cost = time.Duration(nanoseconds)}
rows := stardust.run(query, options = options) or_return
defer stardust.close(&rows)
result := options{:value {:page_size 2
:max_cost 0
:as_of 0}}Explain query execution
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
explain: stardust.Query_Explain
rows := stardust.run(query, explain = &explain) or_return
defer stardust.close(&rows)
result := explain{:value {:load_definition {:cpu 550
:wall 555
:arena_bytes 1680}
:parameters {:cpu 7
:wall 9
:arena_bytes 0}
:run {:cpu 8737
:wall 8761
:arena_bytes 7424}
:total {:cpu 9318
:wall 9348
:arena_bytes 9104}
:arena_capacity 4194304
:work 5773
:rows 3
:compile 232
:plan 426
:execute 0
:group 0
:order 0
:memory_bytes 5995
:memory_refusals 0
:budget_bytes 10489856512
:budget_used_bytes 10194
:budget_peak_bytes 14267
:budget_refusals 0
:retry 0
:stages_completed 3
:retries 0}}A negative Odin duration keeps its sign at the ABI boundary
result := stardust.Run_Options{max_cost = -time.Nanosecond}{:value {:page_size 0
:max_cost -1
:as_of 0}}Run with typed query options
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
options := stardust.Run_Options{page_size = 2, max_cost = time.Second}
rows := stardust.run(query, options = options) or_return
defer stardust.close(&rows)
result := options{:value {:page_size 2
:max_cost 10000000
:as_of 0}}C exports: stardust_db_query_run, stardust_db_rows_error
close
close :: proc(query: ^Query)Consumes the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
query | ^Query | Consumed owner | Required | The query owner to release. Do not use it after this call. |
Examples
Open an inline query and read its first page
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query) or_return
defer stardust.close(&rows)
result := stardust.next(rows) or_return
defer stardust.close(&result)
stardust.close(&query){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}C exports: stardust_db_query_release
next
next :: proc(rows: Rows, allocator := context.allocator) -> (Document, Error)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
rows | Rows | Borrowed for the call | Required | The rows owner for this operation. Keep it alive during the call. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Evaluation_Failed | the page does not render |
.Closed | the rows is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
An empty array ends the cursor. Every page reads the snapshot selected by the run. Returned pages survive cursor and database closure.
Examples
Open an inline query and read its first page
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query) or_return
defer stardust.close(&rows)
result := stardust.next(rows) or_return
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}C exports: stardust_db_rows_next, stardust_result_take_document, stardust_result_error, stardust_result_release
page_iterator
page_iterator :: proc(rows: Rows, allocator := context.allocator) -> Page_IteratorParameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
rows | Rows | Borrowed for the call | Required | The rows owner for this operation. Keep it alive during the call. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Evaluation_Failed | a page does not render |
.Closed | the rows is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Pages are read lazily from the run's snapshot. Rust callers own each page. In other bindings, a page closes when iteration advances. Use next to retain an owned page. Close the cursor separately.
Examples
Iterate the pages of a query run
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query, options = stardust.Run_Options{page_size = 1}) or_return
defer stardust.close(&rows)
it := stardust.page_iterator(rows)
defer stardust.page_iterator_destroy(&it)
result: i64 = 0
for _ in stardust.iterate_pages(&it) {
result += 1
}
if it.err != nil {return it.err}{:value 3}C exports: stardust_db_rows_next, stardust_result_take_document, stardust_result_error, stardust_result_release, stardust_document_root, stardust_document_read_many, stardust_document_release
all_pages
all_pages :: proc(rows: Rows, allocator := context.allocator) -> (result: Document, error: Error)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
rows | Rows | Borrowed for the call | Required | The rows owner for this operation. Keep it alive during the call. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Evaluation_Failed | a page does not render |
.Closed | the rows is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
The array contains rows, not page documents. Previously read pages are excluded. Memory grows with the remaining result. Use pages for bounded page-by-page processing. Close the cursor separately.
Examples
Read all remaining rows into one document
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query, options = stardust.Run_Options{page_size = 1}) or_return
defer stardust.close(&rows)
result := stardust.all_pages(rows) or_return
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}C exports: stardust_db_rows_next, stardust_result_take_document, stardust_result_error, stardust_result_release, stardust_document_count, stardust_document_read_many, stardust_document_i64, stardust_document_f64, stardust_document_release, stardust_document_builder_create, stardust_document_builder_add_many, stardust_document_builder_seal, stardust_document_builder_release
close
close :: proc(rows: ^Rows)Consumes the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
rows | ^Rows | Consumed owner | Required | The rows owner to release. Do not use it after this call. |
Examples
Open an inline query and read its first page
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
rows := stardust.run(query) or_return
defer stardust.close(&rows)
result := stardust.next(rows) or_return
defer stardust.close(&result)
stardust.close(&rows){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}C exports: stardust_db_rows_release
subscribe
subscribe :: proc(
query: Query,
parameters: Document = {},
options: Run_Options = {},
allocator := context.allocator,
) -> (
Subscription,
Error,
)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
query | Query | Borrowed for the call | Required | The query owner for this operation. Keep it alive during the call. |
parameters | Document | Borrowed for the call | {} | An optional object of parameter values that merge-patches the definition's declared parameters. A parameter matched in the entity position of a fact clause takes an entity reference {#link id}, never the bare ID number: a commit returns IDs as numbers, so wrap one as {#link id}, or as the typed Link value where the binding has one, before passing it. A query run fails with entity_reference_expected when a bare number reaches that position. Projected and found entities also read back as {#link id}, so a typed field that receives one uses Link rather than a plain integer. |
options | Run_Options | Value | {} | The operation's options record. See its field defaults. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Definition | the query or its callable definitions are invalid |
.Invalid_Argument | the parameters or subscription options are invalid |
.Closed | the query is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
The subscription copies its definition and parameters. Later definition edits do not change it. The page_size option is ignored.
Blocking next works without registration. To wake your scheduler, see Readiness callbacks for registration, waiting and context lifetime.
Examples
query/subscribe
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}source := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&source)
query := stardust.query(db, source) or_return
defer stardust.close(&query)
subscription := stardust.subscribe(query) or_return
defer stardust.close(&subscription)
event := stardust.next(subscription, 0) or_return
result := event.result
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}Read the complete initial subscription event
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}source := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&source)
query := stardust.query(db, source) or_return
defer stardust.close(&query)
subscription := stardust.subscribe(query) or_return
defer stardust.close(&subscription)
result := stardust.next(subscription, 0) or_return
defer stardust.close(&result){:value {:sequence 1
:superseded 0
:transaction 4
:result [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}}C exports: stardust_db_query_subscribe, stardust_db_subscription_set_notify, stardust_db_subscription_error
next
next :: proc(
subscription: Subscription,
max_wait: time.Duration,
allocator := context.allocator,
) -> (
Event,
Error,
)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
subscription | Subscription | Borrowed for the call | Required | The subscription owner for this operation. Keep it alive during the call. |
max_wait | time.Duration | Value | Required | The maximum wait. Zero checks once. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Argument | max_wait is negative |
.Cancelled | the subscription is cancelled or closed |
.Evaluation_Failed | a rerun of the query failed. Every later call reports this failure |
.Closed | the subscription is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Each event replaces the entire result. superseded counts skipped computed results, not commits. Cancel, close and database close wake blocked waits. Windows rounds deadlines to milliseconds.
Examples
subscription/next
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}source := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&source)
query := stardust.query(db, source) or_return
defer stardust.close(&query)
subscription := stardust.subscribe(query) or_return
defer stardust.close(&subscription)
event := stardust.next(subscription, 0) or_return
result := event.result
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}C exports: stardust_db_subscription_next, stardust_db_subscription_error, stardust_document_release
events
Odin callers loop over next directly
C exports: stardust_db_subscription_next, stardust_db_subscription_error
cancel
subscription_cancel :: proc(subscription: Subscription)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
subscription | Subscription | Borrowed for the call | Required | The subscription owner for this operation. Keep it alive during the call. |
Notes
Repeated cancellation is safe. Close the subscription separately to release its handle.
Examples
subscription/cancel
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}source := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&source)
query := stardust.query(db, source) or_return
defer stardust.close(&query)
subscription := stardust.subscribe(query) or_return
defer stardust.close(&subscription)
event := stardust.next(subscription, 0) or_return
stardust.cancel(subscription)
result := event.result
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}C exports: stardust_db_subscription_cancel
close
close :: proc(subscription: ^Subscription)Consumes the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
subscription | ^Subscription | Consumed owner | Required | The subscription owner to release. Do not use it after this call. |
Examples
subscription/close
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}source := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&source)
query := stardust.query(db, source) or_return
defer stardust.close(&query)
subscription := stardust.subscribe(query) or_return
defer stardust.close(&subscription)
event := stardust.next(subscription, 0) or_return
result := event.result
defer stardust.close(&result)
stardust.close(&subscription){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}C exports: stardust_db_subscription_release
run
run :: proc(
mutation: Mutation,
parameters: Document = {},
options: Run_Options = {},
explain: ^Mutation_Explain = nil,
allocator := context.allocator,
) -> (
Commit,
Error,
)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
mutation | Mutation | Borrowed for the call | Required | The mutation owner for this operation. Keep it alive during the call. |
parameters | Document | Borrowed for the call | {} | An optional object of parameter values that merge-patches the definition's declared parameters. A parameter matched in the entity position of a fact clause takes an entity reference {#link id}, never the bare ID number: a commit returns IDs as numbers, so wrap one as {#link id}, or as the typed Link value where the binding has one, before passing it. A query run fails with entity_reference_expected when a bare number reaches that position. Projected and found entities also read back as {#link id}, so a typed field that receives one uses Link rather than a plain integer. |
options | Run_Options | Value | {} | The operation's options record. See its field defaults. |
explain | ^Mutation_Explain | Caller-owned output | nil | Optional explain output. This record contains explain output for mutation run. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Definition | the mutation selection or definition is invalid |
.Invalid_Argument | the parameters or run options are invalid |
.Schema_Violation | the write breaks a schema |
.Conflict | concurrent writes prevent the commit |
.Rejected | a mutation target or write guard refuses the write |
.Unavailable | the patch sets stardust/history and no enabled plugin purges history |
.Evaluation_Failed | the mutation fails |
.Closed | the mutation is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Examples
mutation/run
birthday_text holds this document:
{:query {:find [?e]
:where [[?e "name" "Ada"]]}
:patch {:?e {:age 37}}}source := stardust.parse(birthday_text, .EDN) or_return
defer stardust.close(&source)
mutation := stardust.mutation(db, source) or_return
defer stardust.close(&mutation)
commit := stardust.run(mutation) or_return
defer stardust.close(&commit)
result := commit{:value {:status "committed"
:transaction 5
:facts 1
:committed_at 17923367881229875
:ids {}}}Explain mutation execution
birthday_text holds this document:
{:query {:find [?e]
:where [[?e "name" "Ada"]]}
:patch {:?e {:age 37}}}source := stardust.parse(birthday_text, .EDN) or_return
defer stardust.close(&source)
mutation := stardust.mutation(db, source) or_return
defer stardust.close(&mutation)
explain: stardust.Mutation_Explain
commit := stardust.run(mutation, explain = &explain) or_return
defer stardust.close(&commit)
result := explain{:value {:load_definition {:cpu 1204
:wall 1211
:arena_bytes 2320}
:parameters {:cpu 13
:wall 14
:arena_bytes 0}
:run {:cpu 8062
:wall 53287
:arena_bytes 43856}
:total {:cpu 9377
:wall 54616
:arena_bytes 46176}
:arena_capacity 4194304
:stages_completed 3
:attempts 1}}C exports: stardust_db_mutation_run, stardust_db_result_commit_info, stardust_result_error, stardust_result_release
close
close :: proc(mutation: ^Mutation)Consumes the receiver handle.
Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
mutation | ^Mutation | Consumed owner | Required | The mutation owner to release. Do not use it after this call. |
Examples
mutation/close
birthday_text holds this document:
{:query {:find [?e]
:where [[?e "name" "Ada"]]}
:patch {:?e {:age 37}}}source := stardust.parse(birthday_text, .EDN) or_return
defer stardust.close(&source)
mutation := stardust.mutation(db, source) or_return
defer stardust.close(&mutation)
commit := stardust.run(mutation) or_return
defer stardust.close(&commit)
result := commit
stardust.close(&mutation){:value {:status "committed"
:transaction 5
:facts 1
:committed_at 17923367880327807
:ids {}}}C exports: stardust_db_mutation_release
merge_patch
merge_patch :: proc(
db: Database,
patch: Document,
explain: ^Patch_Explain = nil,
allocator := context.allocator,
) -> (
Commit,
Error,
)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
patch | Document | Borrowed for the call | Required | An entity-keyed native patch. Temporary names use #_name. #_tx holds metadata and an optional stardust/checkAndSet map from entity IDs to expected transactions. Positive expectations match exact last-touch transactions; zero requires no current facts. Conditions are consumed and checked atomically, including for no-ops. |
explain | ^Patch_Explain | Caller-owned output | nil | Optional explain output. This record contains explain output for merge_patch. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Commit_Time | the #_tx commit time is not later than the last commit |
.Conflict | an entity revision check fails or a concurrent writer prevents the commit; a revision mismatch reports the entity and expected and actual revisions |
.Rejected | the database refuses the patch; the code and diagnostic path name the reason and key |
.Unavailable | the patch sets stardust/history and no enabled plugin purges history |
.Invalid_Argument | the patch is not an entity-keyed object, or a link names an ID outside the user entity range |
.Closed | the database is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Examples
database/merge_patch
seed_text holds this document:
{:#_ada {:name "Ada"
:role "engineer"
:age 36}
:#_bob {:name "Bob"
:role "engineer"
:age 41
:mentor {:#link "#_ada"}}
:#_cy {:name "Cy"
:role "designer"
:age 29}
:#_tx {:author "smoke"
:reason "seed"}}patch := stardust.parse(seed_text, .EDN) or_return
defer stardust.close(&patch)
commit := stardust.merge_patch(db, patch) or_return
defer stardust.close(&commit)
result := commit{:value {:status "committed"
:transaction 4
:facts 12
:committed_at 17923367874985475
:ids {:#_ada 1
:#_bob 2
:#_cy 3}}}Explain patch validation and commit
seed_text holds this document:
{:#_ada {:name "Ada"
:role "engineer"
:age 36}
:#_bob {:name "Bob"
:role "engineer"
:age 41
:mentor {:#link "#_ada"}}
:#_cy {:name "Cy"
:role "designer"
:age 29}
:#_tx {:author "smoke"
:reason "seed"}}patch := stardust.parse(seed_text, .EDN) or_return
defer stardust.close(&patch)
explain: stardust.Patch_Explain
commit := stardust.merge_patch(db, patch, &explain) or_return
defer stardust.close(&commit)
result := explain{:value {:copy_patch {:cpu 1217
:wall 1221
:arena_bytes 3096}
:commit {:cpu 404
:wall 47083
:arena_bytes 8008}
:total {:cpu 1754
:wall 48443
:arena_bytes 11118}
:arena_capacity 4194304
:stages_completed 2}}Refuse a metadata-only transaction
patch_text holds this document:
{:#_tx {:author "x"}}patch := stardust.parse(patch_text, .EDN) or_return
defer stardust.close(&patch)
commit, result := stardust.merge_patch(db, patch)
defer stardust.close(&commit)
defer stardust.close(&result){:value {:code "transaction_only"
:message "a patch of only #_tx is refused: #_tx annotates a write"
:path "#_tx"
:field nil
:entity nil
:offset nil
:status "rejected"}}Refuse a repeated commit time
future_text holds this document:
{:#_d {:x 1}
:#_tx {:stardust/committedAt #inst "2200-01-01T00:00:00Z"}}older_text holds this document:
{:#_e {:x 2}
:#_tx {:stardust/committedAt #inst "2200-01-01T00:00:00Z"}}future := stardust.parse(future_text, .EDN) or_return
defer stardust.close(&future)
committed := stardust.merge_patch(db, future) or_return
defer stardust.close(&committed)
patch := stardust.parse(older_text, .EDN) or_return
defer stardust.close(&patch)
commit, result := stardust.merge_patch(db, patch)
defer stardust.close(&commit)
defer stardust.close(&result){:value {:code "invalid_commit_time"
:message "stardust/committedAt must be strictly newer than the latest commit time"
:path nil
:field nil
:entity nil
:offset nil
:status "invalid_commit_time"}}C exports: stardust_db_merge_patch, stardust_db_result_commit_info, stardust_result_error, stardust_result_release
indexes_sync
indexes_sync :: proc(db: Database, max_wait: time.Duration, allocator := context.allocator) -> ErrorParameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
max_wait | time.Duration | Value | Required | The maximum wait. Zero checks once. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Argument | max_wait is negative |
.Timeout | max_wait passed first |
.Closed | the database is closed |
Notes
An index that cannot be built does not hold this wait.
Examples
Wait for the exact indexes of the stored queries
stardust.indexes_sync(db, 5 * time.Second) or_return
result := stardust.definitions(db, "queries", "people/.*") or_return
defer stardust.close(&result){:value ["people/by_role"]}C exports: stardust_db_indexes_sync
bulk_begin
bulk_begin :: proc(db: Database, allocator := context.allocator) -> ErrorParameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Argument | a bulk build is already in progress on the database |
.Closed | the database is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
On a database with existing facts, each added patch commits immediately and the summary reports fallback true. Closing the database drops an unfinished build.
Examples
Start a bulk build and drop it
stardust.bulk_begin(db) or_return
stardust.bulk_abort(db)C exports: stardust_db_bulk_begin
bulk_add
bulk_add :: proc(db: Database, patch: Document, allocator := context.allocator) -> ErrorParameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
patch | Document | Borrowed for the call | Required | One borrowed entity-keyed merge-patch document, as merge_patch takes it. Temporary names use #_name and are scoped to this patch. Transaction metadata uses #_tx. The document is borrowed only until the call returns. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Argument | no bulk build is in progress |
.Rejected | the database refuses the patch; the build has failed and later adds report the same status |
.Closed | the database is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Patches retain add order. Entity and field order within a patch does not matter. Temporary names resolve within one patch. The call waits for space within the build's memory budget. After failure, use finish for its diagnostic or abort for cleanup.
Examples
Queue patches in commit order
stardust.bulk_begin(db) or_return
defer stardust.bulk_abort(db)
// The first entity of an empty database is 1, so the second patch updates it.
patch1 := stardust.parse(`{"#_ada":{"name":"Ada"}}`, .JSON) or_return
defer stardust.close(&patch1)
stardust.bulk_add(db, patch1) or_return
patch2 := stardust.parse(`{"1":{"title":"Countess"}}`, .JSON) or_return
defer stardust.close(&patch2)
stardust.bulk_add(db, patch2) or_return
summary := stardust.bulk_finish(db) or_return
stardust.close(&summary)C exports: stardust_db_bulk_add
bulk_finish
bulk_finish :: proc(db: Database, allocator := context.allocator) -> (Document, Error)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
allocator | mem.Allocator | Value | context.allocator | Allocator for copied return values and diagnostics. The caller releases them with the same allocator. |
Errors
| Status | When |
|---|---|
.Invalid_Argument | no bulk build is in progress |
.Rejected | the database refuses a patch; an initial build publishes nothing and fallback keeps earlier commits |
.Conflict | another write committed to the database during the build |
.Closed | the database is closed |
.Out_Of_Memory | native memory is exhausted |
Notes
Finish ends the build even on failure. An initial build publishes all patches or none. Fallback keeps patches already committed.
The summary contains:
| Field | Meaning |
|---|---|
| status | committed, or no_change when nothing was written |
| patches, transactions, facts, entities | Load counts |
| firstTransaction, lastTransaction | Assigned transaction range |
| fallback | Whether patches committed individually |
| threads | Worker count |
| tempBytes | Temporary run bytes |
| memoryBytes, memoryPeakBytes | Reserved memory share and its peak use |
| phases | parse, assign, rows, updates, spill, build and commit timings, each with wallNs and cpuNs |
The memory budget covers build allocations. It excludes operating-system file cache and the process baseline.
Examples
Load patches with one commit and read the summary
stardust.bulk_begin(db) or_return
defer stardust.bulk_abort(db)
patch := stardust.parse(`{"#_ada":{"name":"Ada","title":"Countess"}}`, .JSON) or_return
defer stardust.close(&patch)
stardust.bulk_add(db, patch) or_return
// Finish ends the build also when it fails.
summary := stardust.bulk_finish(db) or_return
defer stardust.close(&summary)
// summary holds status, patches, transactions, facts, entities and phase timings.C exports: stardust_db_bulk_finish, stardust_result_take_document, stardust_result_error, stardust_result_release
bulk_abort
bulk_abort :: proc(db: Database)Parameters
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
db | Database | Borrowed for the call | Required | The database owner for this operation. Keep it alive during the call. |
Notes
Abort preserves patches already committed in fallback mode. It is safe without a build or after database closure.
Examples
Drop a bulk build without writing it
stardust.bulk_begin(db) or_return
// Abort writes nothing; without a build it does nothing.
defer stardust.bulk_abort(db)
patch := stardust.parse(`{"#_ada":{"name":"Ada"}}`, .JSON) or_return
defer stardust.close(&patch)
stardust.bulk_add(db, patch) or_returnC exports: stardust_db_bulk_abort
subscription_set_notify
Notify :: #type proc "c"(ctx: rawptr)subscription_set_notify :: proc(
subscription: Subscription,
notify: Notify,
ctx: rawptr,
allocator := context.allocator,
) -> ErrorParameters
| Parameter | Requirement | Meaning |
|---|---|---|
subscription | Required | Subscription receiving the callback. Use the handle exclusively during registration. |
notify | Required | Readiness callback. |
ctx | nil is valid | Caller-owned state, passed unchanged to the callback. Stardust does not read or free it. |
allocator | context.allocator | Allocator for copied error diagnostics. |
Notes
Use this optional callback to wake a host scheduler without polling or a dedicated waiting thread. Blocking next works without registration.
Register a borrowed signal-only C callback once, before any next call. NULL context is valid. Native registration allocates nothing. Install the application waiter before next with zero wait; TIMEOUT arms a coalesced readiness hint. The callback may run on any thread, must return promptly, and must not call Stardust or run application work. Keep callback code and context valid until close returns; no callback is running or can run afterward. Concurrent next calls race for events. A null subscription or callback, duplicate registration, or registration after next reports invalid_argument. A failed subscription preserves its saved failure. A rejected registration retains nothing.
Examples
Wait through a readiness callback
query_text holds this document:
{:find [?name]
:where [[?e "name" ?name]]
:orderBy [?name]}definition := stardust.parse(query_text, .EDN) or_return
defer stardust.close(&definition)
query := stardust.query(db, definition) or_return
defer stardust.close(&query)
Waiter :: struct {
mutex: sync.Mutex,
changed: sync.Cond,
generation: i64,
}
waiter: Waiter
notify :: proc "c" (ctx: rawptr) {
state := (^Waiter)(ctx)
sync.mutex_lock(&state.mutex)
state.generation += 1
sync.cond_broadcast(&state.changed)
sync.mutex_unlock(&state.mutex)
}
subscription := stardust.subscribe(query) or_return
// Deferred close joins callbacks before this stack context leaves scope.
defer stardust.close(&subscription)
stardust.subscription_set_notify(subscription, notify, &waiter) or_return
started := time.tick_now()
result: stardust.Document
for {
sync.mutex_lock(&waiter.mutex)
observed := waiter.generation
sync.mutex_unlock(&waiter.mutex)
event, error := stardust.next(subscription, 0)
if error == nil {result = event.result; break}
failure := error.(stardust.Failure)
if failure.status != .Timeout {return error}
sync.mutex_lock(&waiter.mutex)
for waiter.generation == observed {
remaining := 5*time.Second-time.tick_since(started)
if remaining <= 0 {break}
_ = sync.cond_wait_with_timeout(&waiter.changed, &waiter.mutex, remaining)
}
timed_out := waiter.generation == observed
sync.mutex_unlock(&waiter.mutex)
if timed_out {return error}
stardust.close(&error)
}
defer stardust.close(&result){:value [{:name "Ada"} {:name "Bob"} {:name "Cy"}]}run_options
This record supplies options for query and mutation run. Zero selects each default. Mutations use DB_Write_Options, whose fields are a subset.
| Field | Type | Unit | Range | Meaning |
|---|---|---|---|---|
page_size | u32 | Count | Host type range | Rows per page. 0 selects the default page size. Queries only. |
max_cost | time.Duration | Host duration (100 ns ticks at the ABI) | Tick range | Thread CPU duration limit. 0 selects the engine’s dynamic budget. |
as_of | builtin.i64 | None | Host type range | A positive transaction runs a stored definition as of that transaction. |
commit
This record contains the result of mutation run, merge_patch and patch_definition.
| Field | Type | Unit | Range | Absent | Meaning |
|---|---|---|---|---|---|
status | Commit_Status | None | Host type range | — | — |
transaction | builtin.i64 | None | Host type range | — | Zero when nothing was written. |
facts | builtin.i64 | Count | Host type range | — | — |
committed_at | time.Time | UTC instant (100 ns ticks since the Unix epoch at the ABI) | Tick range | The zero time.Time | Recorded commit time as a UTC instant. The call measures this value. Independent calls can differ. |
ids | map[string]builtin.i64 | None | Host type range | — | Maps each #_name of a merge patch to its entity ID number. Wrap an ID as {#link id}, or as the typed Link value where the binding has one, to pass it as an entity reference. |
event
This record contains the result of subscription next.
| Field | Type | Unit | Meaning |
|---|---|---|---|
sequence | builtin.i64 | Count | Event number. Event 1 is the initial result. |
superseded | builtin.i64 | Count | Intermediate computed results skipped by this subscription since its previous event. Zero for its first event. The delivered result replaces them in full. This value counts neither commits nor changed rows. |
transaction | builtin.i64 | None | Transaction the result was computed at. |
result | Document | None | The full query result. The event owns it and the consumer closes it. |