SDK generation

The SDK plugin generates a typed package from the database's stored definitions. While the database is open, it updates that package in the configured output directory.

The generated code calls the language binding and provides typed access to stored definitions.

Enable sdk in the plugin selection at open, with the output settings below. Calls without it report the unavailable status.

Query results recursively omit fields containing empty objects or arrays. Generated rows keep zero values for absent fields, such as nil for an any field. Null, empty strings, false, zero and array positions stay intact in the returned document.

Native time values

Generated native time fields use time.Time for timestamps and time.Duration for durations. Use stardust.instant_from_ticks, instant_to_ticks, duration_from_ticks and duration_to_ticks for native tick conversion.

The explicit tick conversions refuse precision loss and values outside the target range. These conversions perform no native operation.

A native time schema requires one string member, #utc or #dur, and sets additionalProperties to false. For example, this schema describes a native timestamp:

TRON184 B
Offset (hex)Bytes (hex)Meaning
0054 52 4F 4Emagic TRON
044C 74797065txt "type"
096C 737472696E67txt "string"
100F 0A 04000000 0900 0000map leaf, size 10: @4 → @9
1A4C 23757463txt "#utc"
1F0F 0A 1A000000 1000 0000map leaf, size 10: @1A → @10
294C 23757463txt "#utc"
2E0E 0D 00 0100 010000 00 29000000array leaf, size 13, shift 0, bitmap 0001, count 1 (root): @29
3BAC 70726F70657274 696573txt "properties"
460F 0A 3B000000 1F00 0000map leaf, size 10: @3B → @1F
508C 72657175697265 64txt "required"
590F 0A 50000000 2E00 0000map leaf, size 10: @50 → @2E
6314 14 616464697469 6F6E616C50726F70 657274696573txt "additionalProperties"
7901bit false
7A0F 0A 63000000 7900 0000map leaf, size 10: @63 → @79
844C 74797065txt "type"
896C 6F626A656374txt "object"
900F 0A 84000000 8900 0000map leaf, size 10: @84 → @89
9A07 16 A8040000 4600 0000 59000000 7A00 0000 90000000map branch, size 22, depth 0 (root), slots 3, 5, 7, 10: @46 → @59 → @7A → @90

Use #dur in both places for a duration schema. Schema fields support lists and stored-schema references. Scalar time fields also type query filters and rows. Native time expression results use the same binding types. An ordinary string schema stays text, including strings with a date-time format. Native ticks are signed 64-bit counts of 100 ns. Timestamp ticks count from the Unix epoch in UTC. Duration ticks can be negative.

Settings

Use sdk {odin {dir gen/people}} in the database's plugin settings. debounce defaults to PT0.25S, and max_wait defaults to PT2S. Keep application source outside the output directory.

The Odin packages

The plugin writes schemas, queries, mutations, and expressions packages under odin.dir, with MODEL_FINGERPRINT in each emitted module. Use -collection:sdk=<output-root> to import these packages as sdk:queries or sdk:schemas. Their binding imports require -collection:stardust_bindings=<binding-root>, where the binding root contains odin/ and odin/typed/. They call the generated high-level binding.

Object schemas become structs with exact wire names in stardust tags. Object schema references become pointers, including recursive references. Native time references use the time types described above. Other non-object references and unknown results use any. Native dynamic values decode as bool, i64, f64, string, []any, or map[string]any. Native null decodes as a zero any. Entity variables use typed.Link. The decoder allocates memory for decoded host graphs. Release those graphs with typed.destroy and the allocator passed to decode. Do not destroy borrowed input values as if the decoder allocated them.

Optional schema fields, stored defaults, and clause filters use Maybe(T) with omitempty. An absent variant leaves the input omitted. An active variant supplies false, zero, or an empty value. A supplied null declared parameter reports missing_parameter. Predicate-only variables are required fields, and their operation requires a parameter argument. Aggregate and window outputs are not filters. Native null decoded into Maybe(any) becomes an absent variant. It does not preserve the distinction between omitted and explicit null on re-encoding.

Odin nil slices and nil maps encode null. For an empty array, use a non-nil zero-length slice, or a [0]T array through any. For an empty object, reserve a map before encoding it. These rules also apply to active optional variants.

Queries return an owned cursor with generated *_next and *_close functions. Defer close immediately. Each yielded row belongs to the caller and survives cursor close. Keep the allocator passed to the query for row cleanup. Exhaustion and errors release the native query and rows. The cursor keeps its error until close. Inspect cursor.values.err before close and do not close a copy of that error. Keep the database open while advancing the cursor.

odin417 B
cursor, error := queries.people_adults(database, {}, allocator = allocator)
if error != nil {return error}
defer queries.people_adults_close(&cursor)
for {
    row, _, ok := queries.people_adults_next(&cursor)
    if !ok {break}
    defer typed.destroy(&row, allocator)
    fmt.println(row.name)
}
if terminal_error := cursor.values.err; terminal_error != nil {
    cursor.values.err = nil
    return terminal_error
}

This example transfers a terminal error to the caller before deferred cursor close. The caller closes the returned error with the query allocator.

The pinned Odin compiler converts scalar constants passed to any into a zero any. String constants can also cause a compiler assertion. Use typed local variables before boxing inputs into any:

odin107 B
age: i64 = 36
name := "Ada"
parameters := mutations.People_Add_Parameters{name = any(name), age = any(age)}

Mutations return binding commits. Close each commit with its allocator. Stored expressions build with the current stored macros and decode their result before closing the program and result document. Destroy returned host allocations with the allocator passed to the expression function.

Names use Parts_Address for types and parts_address for functions. Each definition type has an independent namespace. Definition, field, derived helper, and reserved-name collisions report Model_Errors before publication. The typed binding's tag grammar cannot represent an empty wire name, -, or a wire name with a comma. These names report Invalid_Field. Quoted names, backticks, and control characters keep their authored wire names.

Runs

The plugin generates once at open, and again after commits that change definitions. Data commits do not start a run. A run reads all definitions from one committed snapshot.

Each successful render replaces the output directory's contents, including the startup run. See file ownership before choosing an output directory.

Call status to inspect these fields:

FieldContents
enabledWhether the plugin is enabled
pendingWhether generation remains
runsRun count
fingerprintGeneration model fingerprint
outputsOutputs, each with language, directory and file counts written and removed
diagnosticsDiagnostics with severity, code, definition type and path, pointer, member, language and message
failureLast run failure, with code and message; absent after a successful run

When to call sync

Generation runs automatically at open and after definition commits, according to the debounce settings. Opening the database or committing definitions can finish before the generated files are ready. Call sync before compiling or reading files that must include those definitions. Applications that can use the files after automatic generation finishes do not need an explicit sync.

The call requests a render immediately, bypassing debounce. It also rebuilds files when definitions have not changed. Its options accept only the common maxWait option. A timeout leaves the requested run active. Repeated waits after a timeout join that run. Inspect status when generation reports failed. Without an output language, sync validates the model inline and writes no files.

File ownership

The plugin owns the entire configured output directory. Every successful render removes its previous contents, including edited files, unrelated files and subdirectories. Keep application source, database files and other data outside this directory. Do not edit generated files: the next run replaces your changes. The plugin retains only its internal .stardust-sdk.lock file between runs.

The plugin renders and validates the package before it removes any files. Rendering errors leave the previous directory contents unchanged. A filesystem error during cleanup or writing can leave the directory empty or partly written. The next successful run clears that partial output and writes a fresh package. Use sync before a build and check its result.

Each file is written through an atomic rename. Rebuilding the whole directory is not atomic. A destination lock prevents concurrent publication and reports Destination_Busy when another publisher holds it. Use a separate output directory for each database, with no directory nested inside another output directory. Cleanup removes symlinks inside the output directory without following them. The output directory itself and the lock file must not be symlinks. Directory inspection is limited to 65,536 entries, 64 levels and 16 MiB of stored paths. An inspection failure leaves the previous contents unchanged.

Operations

OperationReturnsSummary
sdk_statusdocumentsdk.status() returns the SDK generator's owned status document.
sdk_syncnonesdk.sync(options) waits until the SDK generator published the definitions committed before the call.

sdk_status

odin95 B
sdk_status :: proc(
	db: Database,
	allocator := context.allocator,
) -> (
	Document,
	Error,
)

Parameters

ParameterTypeOwnershipDefaultMeaning
dbDatabaseBorrowed for the callRequiredThe sdk owner for this operation. Keep it alive during the call.
allocatormem.AllocatorValuecontext.allocatorAllocator for copied return values and diagnostics. The caller releases them with the same allocator.

Errors

StatusWhen
.Unavailablethe database does not enable the sdk plugin
.Closedthe database is closed
.Out_Of_Memorynative memory is exhausted

Notes

The document describes generation outputs, diagnostics and the last run: see runs.

Examples

Describe the SDK generator

odin228 B
sync_options := stardust.parse(`{maxWait {#dur PT5S}}`, .DUST) or_return
defer stardust.close(&sync_options)
stardust.sdk_sync(db, sync_options) or_return
result := stardust.sdk_status(db) or_return
defer stardust.close(&result)
TRON191 B
Offset (hex)Bytes (hex)Meaning
0054 52 4F 4Emagic TRON
044C 72756E73txt "runs"
0902 01000000000000 00i64 1
120F 0A 04000000 0900 0000map leaf, size 10: @4 → @9
1C7C 656E61626C6564txt "enabled"
2409bit true
250F 0A 1C000000 2400 0000map leaf, size 10: @1C → @24
2F7C 70656E64696E67txt "pending"
3701bit false
380F 0A 2F000000 3700 0000map leaf, size 10: @2F → @37
42BC 66696E67657270 72696E74txt "fingerprint"
4E14 47 736861323536 3A66306531373061 6335346236376230 6639613865343739 3439323261353836 6332316461323931 3062303736366463 6433666562343130 6234313662363165 38txt "sha256:f0e170ac54b67b0f9…"
970F 0A 42000000 4E00 0000map leaf, size 10: @42 → @4E
A107 16 280A0000 1200 0000 25000000 3800 0000 97000000map branch, size 22, depth 0 (root), slots 3, 5, 9, 11: @12 → @25 → @38 → @97

C exports: stardust_db_plugin_call, stardust_result_take_document, stardust_result_error, stardust_result_release

sdk_sync

odin101 B
sdk_sync :: proc(
	db: Database,
	options: Document = {},
	allocator := context.allocator,
) -> Error

Parameters

ParameterTypeOwnershipDefaultMeaning
dbDatabaseBorrowed for the callRequiredThe sdk owner for this operation. Keep it alive during the call.
optionsDocumentBorrowed for the call{}The borrowed call options, or none. Optional maxWait is a nonnegative native duration, such as {maxWait {#dur PT5S}}. Without it, or with zero, the call checks once.
allocatormem.AllocatorValuecontext.allocatorAllocator for copied return values and diagnostics. The caller releases them with the same allocator.

Errors

StatusWhen
.Invalid_Argumentoptions is not an object, or an option is invalid, duplicated or unsupported
.Failedthe plugin's work failed; status describes the failure
.TimeoutmaxWait passed before the work finished
.Unavailablethe database does not enable the sdk plugin
.Closedthe database is closed

Notes

Database close interrupts an active wait. See when to call sync.

Examples

Run the SDK generator and wait for it

odin228 B
sync_options := stardust.parse(`{maxWait {#dur PT5S}}`, .DUST) or_return
defer stardust.close(&sync_options)
stardust.sdk_sync(db, sync_options) or_return
result := stardust.sdk_status(db) or_return
defer stardust.close(&result)
TRON191 B
Offset (hex)Bytes (hex)Meaning
0054 52 4F 4Emagic TRON
044C 72756E73txt "runs"
0902 01000000000000 00i64 1
120F 0A 04000000 0900 0000map leaf, size 10: @4 → @9
1C7C 656E61626C6564txt "enabled"
2409bit true
250F 0A 1C000000 2400 0000map leaf, size 10: @1C → @24
2F7C 70656E64696E67txt "pending"
3701bit false
380F 0A 2F000000 3700 0000map leaf, size 10: @2F → @37
42BC 66696E67657270 72696E74txt "fingerprint"
4E14 47 736861323536 3A66306531373061 6335346236376230 6639613865343739 3439323261353836 6332316461323931 3062303736366463 6433666562343130 6234313662363165 38txt "sha256:f0e170ac54b67b0f9…"
970F 0A 42000000 4E00 0000map leaf, size 10: @42 → @4E
A107 16 280A0000 1200 0000 25000000 3800 0000 97000000map branch, size 22, depth 0 (root), slots 3, 5, 9, 11: @12 → @25 → @38 → @97

C exports: stardust_db_plugin_call, stardust_result_take_document, stardust_result_error, stardust_result_release