Documentation
Application Stored Procedures
Define typed application operations that SQLStyle runs with the current database primary.
An Application Stored Procedure (ASP) is a named, typed database operation whose handler is application code and whose execution follows the database primary.
The logic remains ordinary Go or Rust. Nothing is installed into SQLite or deployed as a separate service.
Why use an ASP?
A request can arrive at any application instance, while a database write must run against the current primary. Without an ASP, the application needs its own primary discovery and RPC path, or a separate service that all writes pass through.
An ASP makes that boundary part of the database SDK. The application defines the operation by giving the SDK its name, payload and result types, and handler. The Go SDK returns a typed function; the Rust SDK returns a typed Procedure. Application code invokes the Go function or calls Procedure.call in Rust.
How a call runs
When the application invokes a generated ASP call, the SDK reads its current view of primary authority and chooses one of two paths.
Local primary
The SDK passes the typed payload directly to the handler. No payload or result serialization is involved.
Replica
The SDK serializes the payload and sends a named invocation over SQLStyle's primary stream. At the primary, the receiving SDK resolves the name to the handler, runs it, and serializes the result back.
Serialization
The payload and result are application-defined native types. Go and Rust infer them from the handler and use named-map MessagePack by default; domain types do not implement SQLStyle-specific serialization methods.
Named-map MessagePack is the recommended general-purpose format in the managed Go and Rust SDKs. Go uses msgpack field tags when present and otherwise reuses json field tags; Rust uses ordinary Serde traits. Stable field names plus optional or defaulted new fields support additive compatibility across mixed application versions. Rust also offers Postcard for fixed schemas. Both SDKs retain JSON for readable payloads. Go's JSON selection follows encoding/json tags and custom marshaling interfaces. Its options-bearing definition chooses one encoding for both the payload and result.
Go's Binary selection calls the standard encoding.BinaryAppender and encoding.BinaryUnmarshaler interfaces directly. CustomEncoding retains the typed serializer contract for generated and handwritten formats. Remote append encoders receive a reusable SDK-owned destination.
Select JSON for a procedure
addTodo, err := sqlstyle.DefineIASPWithOptions(
config,
"todos.add-json",
sqlstyle.ProcedureOptions{Encoding: sqlstyle.JSON},
handler,
)let options = sqlstyle::ProcedureOptions::new(
sqlstyle::JsonSerializer,
sqlstyle::JsonSerializer,
);
let add_todo = sqlstyle::define_iasp_with_options(
config,
"todos.add-json",
options,
handler,
)?;When no primary is available
If no writable primary can accept the invocation, the SDK waits for authority to change instead of failing immediately. One wait budget covers primary discovery, write admission, and retries after a transient busy or unavailable response. The default budget is eight seconds.
If a primary becomes ready within the budget, the invocation continues. Otherwise the SDK returns its primary-unavailable error. A zero budget fails immediately; in Go, context cancellation or an earlier deadline ends the wait sooner.
Set the budget in the SDK open configuration. It is not a shared JSON5 configuration field.
Primary wait timeout configuration
wait := 3 * time.Second
// wait := time.Duration(0) // Fail immediately.
config.Options.PrimaryWaitTimeout = &waituse std::time::Duration;
config.options.primary_wait_timeout = Duration::from_secs(3);
// config.options.primary_wait_timeout = Duration::ZERO; // Fail immediately.Example
Define and call todos.add
type AddTodo struct {
Title string `json:"title"`
}
type AddTodoResult struct {
ID int64 `json:"id"`
}
addTodo, err := sqlstyle.DefineIASP(
config,
"todos.add",
func(ctx context.Context, tx *sql.Tx, payload AddTodo) (AddTodoResult, error) {
result, err := tx.ExecContext(ctx,
"INSERT INTO todos(title) VALUES (?)", payload.Title)
if err != nil { return AddTodoResult{}, err }
id, err := result.LastInsertId()
return AddTodoResult{ID: id}, err
},
)
if err != nil { return err }
result, err := addTodo(ctx, AddTodo{Title: "ship it"})
id := result.ID#[derive(serde::Serialize, serde::Deserialize)]
struct AddTodo { title: String }
#[derive(serde::Serialize, serde::Deserialize)]
struct AddTodoResult { id: i64 }
let add_todo = sqlstyle::define_iasp(
config.clone(),
"todos.add",
|transaction, payload: &AddTodo| -> sqlstyle::Result<AddTodoResult> {
transaction.execute(
"INSERT INTO todos(title) VALUES (?1)",
[&payload.title],
)?;
Ok(AddTodoResult { id: transaction.last_insert_rowid() })
},
)?;
let result = add_todo.call(&AddTodo {
title: "ship it".into(),
})?;
let id = result.id;ASP or IASP
Choose based on who owns the transaction and duplicate handling.
IASPSQLStyle owns the transaction. It makes remote execution idempotent; the handler must not commit or roll back.
ASPYour application owns the transaction and duplicate handling. Use it when the operation already has an idempotency key or duplicate policy.