tursodatabase/turso · error
Cursor not found: {key:?}
Error message
Cursor not found: {key:?} What it means
During bytecode building, resolve_cursor_id maps a CursorKey (table/index reference) to the CursorID of an earlier OpenRead/OpenIndex. If the key was never opened, it panics - the program references a cursor that translation never allocated. A sibling helper, resolve_cursor_id_safe, returns Option for cases where absence is legal.
Source
Thrown at core/vdbe/builder.rs:1857
// translate [CursorKey] to cursor id
pub fn resolve_cursor_id_safe(&self, key: &CursorKey) -> Option<CursorID> {
// Check cursor overrides first, only apply override for table cursors.
// Index cursor lookups are not overridden because when a cursor override is active,
// the calling code (translate_expr) should skip index logic entirely.
if key.index.is_none() && !key.is_build {
let table_id: usize = key.table_reference_id.into();
if let Some(&cursor_id) = self.cursor_overrides.get(&table_id) {
return Some(cursor_id);
}
}
self.cursor_ref
.iter()
.position(|(k, _)| k.as_ref().is_some_and(|k| k.equals(key)))
}
pub fn resolve_cursor_id(&self, key: &CursorKey) -> CursorID {
self.resolve_cursor_id_safe(key)
.unwrap_or_else(|| panic!("Cursor not found: {key:?}"))
}
/// Resolve the first allocated index cursor for a given table reference.
/// This method exists due to a limitation of our translation system where
/// a subquery that references an outer query table cannot know whether a
/// table cursor, index cursor, or both were opened for that table reference.
/// Hence: currently we first try to resolve a table cursor, and if that fails,
/// we resolve an index cursor via this method.
pub fn resolve_any_index_cursor_id_for_table(&self, table_ref_id: TableInternalId) -> CursorID {
self.resolve_any_index_cursor_id_for_table_safe(table_ref_id)
.unwrap_or_else(|| panic!("No index cursor found for table {table_ref_id}"))
}
pub fn resolve_any_index_cursor_id_for_table_safe(
&self,
table_ref_id: TableInternalId,
) -> Option<CursorID> {
self.cursor_ref.iter().position(|(k, _)| {View on GitHub (pinned to 492c4a71cd)
Solutions
- Emit/register the cursor (OpenRead/OpenIndex or cursor_overrides) before resolving its ID
- Use resolve_cursor_id_safe where absence is expected and handle the None branch
- Add an EXPLAIN-based regression test for the plan shape
Example fix
// before let id = builder.resolve_cursor_id(&key); // panics: cursor never opened // after builder.open_table_cursor(key.clone()); // emit OpenRead first let id = builder.resolve_cursor_id(&key);
Defensive patterns
Strategy: validation
Validate before calling
// prefer the safe variant when the cursor may legitimately be absent
match builder.resolve_cursor_id_safe(&key) {
Some(id) => id,
None => {
// emit OpenRead/OpenIndex for `key` first, then resolve again
builder.open_table_cursor(key.clone());
builder.resolve_cursor_id(&key)
}
} Prevention
- Open then resolve: keep cursor registration adjacent to first use
- Diff EXPLAIN output in tests whenever touching the planner or translator
When it happens
Trigger: Internal: new planner or translation shapes that use a table/index cursor before (or without) emitting the corresponding open instruction; subqueries referencing outer-query cursors resolved against the wrong program; cursor_overrides wiring regressions.
Common situations: Contributor PRs to the optimizer/translator; correlated-subquery changes; refactors of cursor registration order.
Related errors
- No index cursor found for table {table_ref_id}
- Cursor not found: {cursor_id}
- invalid position to read current mvcc row
- Invalid btree seek state in seek_btree_and_set_peek: {:?}
- RowKey::Record requires Index cursor type
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/4e972e379c8e391f.
Report an issue: GitHub.