ReferenceExperimentalFor developerFor agency

Decode native database responses in Rust

Reuse a typed decoder for native reads, mutation receipts and uncertain outcomes.

Last updated 2026-10-31

What you'll achieve

  • Decode exact typed values and positional rows
  • Validate database and operation identity
  • Preserve commitment and unknown outcomes without automatic retries

Typed native database responses in Rust

Copy the complete module below into src/native_db_response.rs and add mod native_db_response; to your application. It uses the existing blinkhost-app-support 0.1.0 package. Keep your application manifest and complete lockfile; see Rust application support for configuration. No new package or compiler version is required.

This example decodes strings returned directly by blinkhost_sdk::database::{query,write,transaction,status}. It does not handle the older top-level query API. A native DEFAULT binding and database permission remain required. The native database reference covers parameterized SQL, scope, operation IDs and recovery.

Use the decoder

These snippets run inside application code. raw_read comes from a native query; raw_mutation comes from a write, transaction or status call. Handle SDK errors before decoding.

let read = native_db_response::read(&raw_read, statement_count)?;
let new_scope = read.primary_scope()?.clone();
// Map read.results only after checking expected columns, row count and cell types.
// Persist new_scope with a NEW operation ID and exact payload before first send.

let receipt = native_db_response::mutation(
    &raw_mutation, &saved_scope, &saved_operation_id, saved_statement_count,
)?;
match receipt.outcome {
    native_db_response::Outcome::Committed => { /* commitment is proven */ }
    native_db_response::Outcome::NotCommitted => { /* this attempt rolled back */ }
    native_db_response::Outcome::Unknown => { /* retain the original operation */ }
}

saved_scope is the original persisted native_db_response::Scope, including on status calls. Check receipt.error even after commitment: expired or policy-restricted result bodies can be unavailable while the write remains committed. Empty successful SELECT rows differ from a failed read. Integer cells are exact i64 values; blobs are bytes; duplicate column names retain their positions. Application mapping must still validate the expected schema and record ownership.

Safety and limits

A transport or decoding error after a mutation leaves commitment unknown. Do not replay it with a new scope, ID or payload. The decoder never retries, performs I/O, authorizes users or persists operations. Reusing an operation ID with changed inputs can be rejected even if the original operation committed.

Only decode unmodified native host responses, never customer-supplied receipts or HTTP bodies. The underlying JSON parser uses ordinary last-value handling of duplicate object keys. Additional envelope/result metadata is accepted; tagged SQL cells are strict. This is an application example, not a new SDK API.

Responses are limited to 128 KiB plus 1 KiB of envelope allowance, 32 statements, 128 columns and 1,000 total rows; text and decoded blobs are limited to 64 KiB. Module policy may be lower. JSON and typed results coexist briefly in guest memory: paginate large queries. Local parser tests do not attest live database availability.

Complete reusable module

//! Copy into an app using blinkhost-app-support 0.1.0. Only decode native host
//! responses here, never arbitrary request bodies. This performs no I/O/retries.
use blinkhost_app_support::{parse_json, Value};

const MAX_RESPONSE: usize = 128 * 1024 + 1024;
const MAX_VALUE: usize = 64 * 1024;

#[derive(Debug, PartialEq, Eq)]
pub enum Error {
    InvalidJson,
    InvalidResponse,
    InvalidValue,
    LimitExceeded,
    IdentityMismatch,
    PrimaryRequired,
    Database(String),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Scope {
    pub database_id: String,
    pub generation: String,
}

#[derive(Debug, PartialEq)]
pub enum SqlValue {
    Null,
    Integer(i64),
    Real(f64),
    Text(String),
    Blob(Vec<u8>),
}

#[derive(Debug, PartialEq)]
pub struct StatementResult {
    pub columns: Vec<String>,
    pub rows: Vec<Vec<SqlValue>>,
    pub affected_rows: u64,
}

#[derive(Debug, PartialEq)]
pub struct ReadResponse {
    pub scope: Scope,
    pub served_by: String,
    pub replica_sync_age_ms: Option<u64>,
    pub results: Vec<StatementResult>,
}

impl ReadResponse {
    /// Only a successful primary read can supply a new mutation's scope.
    pub fn primary_scope(&self) -> Result<&Scope, Error> {
        if self.served_by != "primary" {
            return Err(Error::PrimaryRequired);
        }
        Ok(&self.scope)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
    Committed,
    NotCommitted,
    Unknown,
}

#[derive(Debug, PartialEq)]
pub struct MutationResponse {
    pub scope: Option<Scope>,
    pub operation_id: String,
    pub outcome: Outcome,
    pub results: Vec<StatementResult>,
    /// Commitment can be proven even when the result body is no longer available.
    pub error: Option<String>,
}

fn text(value: &Value) -> Result<&str, Error> {
    value.as_str().ok_or(Error::InvalidResponse)
}

fn field<'a>(value: &'a Value, name: &str) -> Result<&'a Value, Error> {
    value.get(name).ok_or(Error::InvalidResponse)
}

fn optional_error(value: &Value) -> Result<Option<String>, Error> {
    value
        .get("error")
        .map(|v| {
            let code = text(v)?;
            if code.is_empty() || code.len() > 128 {
                return Err(Error::InvalidResponse);
            }
            Ok(code.to_owned())
        })
        .transpose()
}

fn uuid(value: &str) -> bool {
    value.len() == 36
        && value.bytes().enumerate().all(|(i, b)| {
            if matches!(i, 8 | 13 | 18 | 23) {
                b == b'-'
            } else {
                b.is_ascii_digit() || (b'a'..=b'f').contains(&b)
            }
        })
}

fn scope(value: &Value) -> Result<Option<Scope>, Error> {
    let id = field(value, "database_id")?;
    let generation = field(value, "generation")?;
    if id.is_null() && generation.is_null() {
        return Ok(None);
    }
    let id = text(id)?;
    let generation = text(generation)?;
    if !uuid(id) || !uuid(generation) {
        return Err(Error::InvalidResponse);
    }
    Ok(Some(Scope {
        database_id: id.into(),
        generation: generation.into(),
    }))
}

fn document(raw: &str) -> Result<Value, Error> {
    if raw.len() > MAX_RESPONSE {
        return Err(Error::LimitExceeded);
    }
    let value = parse_json(raw.as_bytes()).map_err(|_| Error::InvalidJson)?;
    if !value.is_object() || !field(&value, "response")?.is_object() {
        return Err(Error::InvalidResponse);
    }
    Ok(value)
}

fn sextet(b: u8) -> Result<u8, Error> {
    match b {
        b'A'..=b'Z' => Ok(b - b'A'),
        b'a'..=b'z' => Ok(b - b'a' + 26),
        b'0'..=b'9' => Ok(b - b'0' + 52),
        b'+' => Ok(62),
        b'/' => Ok(63),
        _ => Err(Error::InvalidValue),
    }
}

fn blob(encoded: &str) -> Result<Vec<u8>, Error> {
    let bytes = encoded.as_bytes();
    if bytes.len() > MAX_VALUE.div_ceil(3) * 4 {
        return Err(Error::LimitExceeded);
    }
    if bytes.len() % 4 != 0 {
        return Err(Error::InvalidValue);
    }
    let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
    for (i, chunk) in bytes.chunks_exact(4).enumerate() {
        let a = sextet(chunk[0])?;
        let b = sextet(chunk[1])?;
        out.push((a << 2) | (b >> 4));
        if chunk[2] == b'=' {
            if chunk[3] != b'=' || b & 15 != 0 || (i + 1) * 4 != bytes.len() {
                return Err(Error::InvalidValue);
            }
        } else {
            let c = sextet(chunk[2])?;
            out.push((b << 4) | (c >> 2));
            if chunk[3] == b'=' {
                if c & 3 != 0 || (i + 1) * 4 != bytes.len() {
                    return Err(Error::InvalidValue);
                }
            } else {
                out.push((c << 6) | sextet(chunk[3])?);
            }
        }
    }
    if out.len() > MAX_VALUE {
        return Err(Error::LimitExceeded);
    }
    Ok(out)
}

fn cell(value: &Value) -> Result<SqlValue, Error> {
    let object = value.as_object().ok_or(Error::InvalidValue)?;
    let tag = text(field(value, "type")?)?;
    if tag == "null" && object.len() == 1 {
        return Ok(SqlValue::Null);
    }
    if object.len() != 2 {
        return Err(Error::InvalidValue);
    }
    let data = field(value, "value")?;
    match tag {
        "integer" => {
            let raw = text(data)?;
            let number: i64 = raw.parse().map_err(|_| Error::InvalidValue)?;
            if number.to_string() != raw {
                return Err(Error::InvalidValue);
            }
            Ok(SqlValue::Integer(number))
        }
        "real" => data
            .as_f64()
            .filter(|v| v.is_finite())
            .map(SqlValue::Real)
            .ok_or(Error::InvalidValue),
        "text" if text(data)?.len() <= MAX_VALUE => Ok(SqlValue::Text(text(data)?.into())),
        "blob" => blob(text(data)?).map(SqlValue::Blob),
        _ => Err(Error::InvalidValue),
    }
}

fn results(response: &Value) -> Result<Vec<StatementResult>, Error> {
    let items = field(response, "results")?
        .as_array()
        .ok_or(Error::InvalidResponse)?;
    if items.len() > 32 {
        return Err(Error::LimitExceeded);
    }
    let mut total_rows = 0;
    items
        .iter()
        .map(|item| {
            let names = field(item, "columns")?
                .as_array()
                .ok_or(Error::InvalidResponse)?;
            if names.len() > 128 {
                return Err(Error::LimitExceeded);
            }
            let columns = names
                .iter()
                .map(|v| text(v).map(str::to_owned))
                .collect::<Result<Vec<_>, _>>()?;
            let input = field(item, "rows")?
                .as_array()
                .ok_or(Error::InvalidResponse)?;
            total_rows += input.len();
            if total_rows > 1000 {
                return Err(Error::LimitExceeded);
            }
            let rows = input
                .iter()
                .map(|row| {
                    let cells = row.as_array().ok_or(Error::InvalidResponse)?;
                    if cells.len() != columns.len() {
                        return Err(Error::InvalidResponse);
                    }
                    cells.iter().map(cell).collect()
                })
                .collect::<Result<Vec<_>, Error>>()?;
            let affected_rows = field(item, "affected_rows")?
                .as_u64()
                .ok_or(Error::InvalidResponse)?;
            Ok(StatementResult {
                columns,
                rows,
                affected_rows,
            })
        })
        .collect()
}

fn statement_count(count: usize) -> Result<(), Error> {
    if !(1..=32).contains(&count) {
        return Err(Error::InvalidResponse);
    }
    Ok(())
}

/// Decode the host response to a read of exactly expected_statements statements.
/// An empty SELECT is a success; a failed read never becomes an empty success.
pub fn read(raw: &str, expected_statements: usize) -> Result<ReadResponse, Error> {
    statement_count(expected_statements)?;
    let doc = document(raw)?;
    let outer_scope = scope(&doc)?;
    let response = field(&doc, "response")?;
    let results = results(response)?;
    if let Some(code) = optional_error(response)? {
        if !results.is_empty() {
            return Err(Error::InvalidResponse);
        }
        return Err(Error::Database(code));
    }
    let scope = outer_scope.ok_or(Error::InvalidResponse)?;
    if self::scope(response)?.as_ref() != Some(&scope) {
        return Err(Error::IdentityMismatch);
    }
    let served_by = text(field(response, "served_by")?)?;
    if !matches!(served_by, "primary" | "replica")
        || results.len() != expected_statements
        || results.iter().any(|r| r.affected_rows != 0)
    {
        return Err(Error::InvalidResponse);
    }
    let replica_sync_age_ms = response
        .get("replica_sync_age_ms")
        .map(|v| v.as_u64().ok_or(Error::InvalidResponse))
        .transpose()?;
    Ok(ReadResponse {
        scope,
        served_by: served_by.into(),
        replica_sync_age_ms,
        results,
    })
}

/// Decode write/transaction/status using the ORIGINAL stored scope, operation ID
/// and statement count. Any decoding/transport failure leaves commitment unknown.
pub fn mutation(
    raw: &str,
    expected: &Scope,
    operation_id: &str,
    expected_statements: usize,
) -> Result<MutationResponse, Error> {
    statement_count(expected_statements)?;
    if !uuid(&expected.database_id)
        || !uuid(&expected.generation)
        || !(16..=128).contains(&operation_id.len())
        || !operation_id
            .bytes()
            .all(|b| b.is_ascii_alphanumeric() || b"-_:.".contains(&b))
    {
        return Err(Error::InvalidResponse);
    }
    let doc = document(raw)?;
    let scope = scope(&doc)?;
    if scope.as_ref().is_some_and(|actual| actual != expected) {
        return Err(Error::IdentityMismatch);
    }
    let response = field(&doc, "response")?;
    if text(field(response, "operation_id")?)? != operation_id {
        return Err(Error::IdentityMismatch);
    }
    let outcome = match text(field(response, "outcome")?)? {
        "committed" => Outcome::Committed,
        "not_committed" => Outcome::NotCommitted,
        "unknown" => Outcome::Unknown,
        _ => return Err(Error::InvalidResponse),
    };
    let results = results(response)?;
    let error = optional_error(response)?;
    if scope.is_none() && outcome != Outcome::Unknown {
        return Err(Error::InvalidResponse);
    }
    if error.is_some() {
        if !results.is_empty() {
            return Err(Error::InvalidResponse);
        }
    } else if outcome != Outcome::Committed || results.len() != expected_statements {
        return Err(Error::InvalidResponse);
    }
    Ok(MutationResponse {
        scope,
        operation_id: operation_id.into(),
        outcome,
        results,
        error,
    })
}

Help improve this page

Sign in to send page-specific feedback. For account-specific help, email support@blinkhost.me.