tursodatabase/turso · error

register holds unexpected value: {self:?}

Error message

register holds unexpected value: {self:?}

What it means

Register::get_value() only understands the generic register representations: Register::Value, and Register::Record returned as a blob value. The Register enum also has Register::Aggregate(AggContext) — an in-flight aggregate accumulator — and reading a register that still holds one through the generic accessor panics. It means the bytecode wrote a representation the reading opcode does not support: the register's writer and reader disagree (typically a register reused for both accumulator and scalar roles).

Source

Thrown at core/vdbe/mod.rs:1748

    pub fn get_value(&self) -> &Value {
        match self {
            Register::Value(v) => v,
            _ => self.get_value_of_other(),
        }
    }

    /// The value of a register that holds no plain value: a record reads as
    /// its blob, anything else is a bug. Kept out of line so that
    /// `get_value` inlines as a tag check.
    #[cold]
    #[inline(never)]
    fn get_value_of_other(&self) -> &Value {
        match self {
            Register::Record(r) => {
                turso_assert!(!r.is_invalidated());
                r.as_blob_value()
            }
            _ => panic!("register holds unexpected value: {self:?}"),
        }
    }
}

#[macro_export]
macro_rules! must_be_btree_cursor {
    ($cursor_id:expr, $cursor_ref:expr, $state:expr, $insn_name:expr) => {{
        let (_, cursor_type) = $cursor_ref.get($cursor_id).unwrap();
        if matches!(
            cursor_type,
            CursorType::BTreeTable(_)
                | CursorType::BTreeIndex(_)
                | CursorType::MaterializedView(_, _)
        ) {
            $crate::get_cursor!($state, $cursor_id)
        } else {
            panic!("{} on unexpected cursor", $insn_name)
        }

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report with SQL and EXPLAIN — the register holding the AggContext was read as a plain value
  2. Rewrite so the aggregate lives in its own query level (GROUP BY subquery) instead of being flattened
  3. Check for column/register index mix-ups if you maintain a fork and recently touched emit_column or aggregate finalization
  4. Upgrade

Example fix

// contributor fix: read aggregates through the aggregate path, not get_value()
let value = match &state.registers[target] {
    Register::Value(v) => v.clone(),
    Register::Record(r) => r.as_blob_value().clone(),
    Register::Aggregate(agg) => agg.final_value().clone(), // instead of get_value() panic
};
Defensive patterns

Strategy: type-guard

Validate before calling

// before reading a register that may hold an aggregate, narrow the variant
if matches!(state.registers[target], Register::Aggregate(_)) {
    crate::bail_parse_error!("register {target} still holds an aggregate context");
}

Type guard

fn as_scalar_value(reg: &Register) -> Option<&Value> {
    match reg {
        Register::Value(v) => Some(v),
        Register::Record(r) => Some(r.as_blob_value()),
        Register::Aggregate(_) => None,
    }
}

Prevention

When it happens

Trigger: An opcode reads a register via get_value() while that register still holds the Register::Aggregate context from an AggStep-family instruction — e.g. aggregate accumulator registers reused across query levels (flattened subqueries, window functions), or codegen writing the wrong register index for a finalize.

Common situations: Aggregates and window functions in flattened or co-routine subqueries; register-allocation refactors in translate/; upgrades changing how accumulators are stored.

Related errors


AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20). Data as JSON: /api/errors/a9e7a568a2731043. Report an issue: GitHub.