{"record":{"id":"aad9e9d4fecc9778","repo":"tursodatabase/turso","slug":"no-index-cursor-found-for-table-table-ref-id","errorCode":null,"errorMessage":"No index cursor found for table {table_ref_id}","messagePattern":"No index cursor found for table (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/vdbe/builder.rs","lineNumber":1868,"sourceCode":"        self.cursor_ref\n            .iter()\n            .position(|(k, _)| k.as_ref().is_some_and(|k| k.equals(key)))\n    }\n\n    pub fn resolve_cursor_id(&self, key: &CursorKey) -> CursorID {\n        self.resolve_cursor_id_safe(key)\n            .unwrap_or_else(|| panic!(\"Cursor not found: {key:?}\"))\n    }\n\n    /// Resolve the first allocated index cursor for a given table reference.\n    /// This method exists due to a limitation of our translation system where\n    /// a subquery that references an outer query table cannot know whether a\n    /// table cursor, index cursor, or both were opened for that table reference.\n    /// Hence: currently we first try to resolve a table cursor, and if that fails,\n    /// we resolve an index cursor via this method.\n    pub fn resolve_any_index_cursor_id_for_table(&self, table_ref_id: TableInternalId) -> CursorID {\n        self.resolve_any_index_cursor_id_for_table_safe(table_ref_id)\n            .unwrap_or_else(|| panic!(\"No index cursor found for table {table_ref_id}\"))\n    }\n\n    pub fn resolve_any_index_cursor_id_for_table_safe(\n        &self,\n        table_ref_id: TableInternalId,\n    ) -> Option<CursorID> {\n        self.cursor_ref.iter().position(|(k, _)| {\n            k.as_ref()\n                .is_some_and(|k| k.table_reference_id == table_ref_id && k.index.is_some())\n        })\n    }\n\n    /// Resolve the [Index] that a given cursor is associated with.\n    pub fn resolve_index_for_cursor_id(&self, cursor_id: CursorID) -> Arc<Index> {\n        let cursor_ref = &self\n            .cursor_ref\n            .get(cursor_id)\n            .unwrap_or_else(|| panic!(\"Cursor not found: {cursor_id}\"))","sourceCodeStart":1850,"sourceCodeEnd":1886,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/core/vdbe/builder.rs#L1850-L1886","documentation":"Raised in the SQL-to-bytecode builder when resolve_any_index_cursor_id_for_table() finds no index cursor allocated for a table reference. The method exists because a correlated subquery that references an outer query table cannot know whether the outer plan opened a table cursor, an index cursor, or both, so translation resolves whichever exists. The panic means translation demanded an index cursor the planner never allocated — an internal codegen/plan inconsistency, not bad user data.","triggerScenarios":"Preparing a query with a correlated subquery (SELECT-list scalar subquery, EXISTS, IN) where the outer table reference has only a table cursor (full scan chosen, NOT INDEXED, or no matching index) while the subquery translation path unconditionally resolves an index cursor via resolve_any_index_cursor_id_for_table. Surfaces at statement preparation time, often after optimizer or subquery-decorrelation changes.","commonSituations":"Version upgrades that changed cursor allocation for correlated references; queries mixing indexed and non-indexed outer columns; development branches touching core/translate/ subquery flattening or co-routines; reproduces deterministically for a given SQL string.","solutions":["File a bug with the exact SQL — the resolver must not assume an index cursor exists; prepare-time reproduction makes it cheap to fix","Rewrite the correlated subquery as a JOIN or grouped derived table so no outer-reference cursor resolution is needed","Change the plan: drop/disable the relevant index or use +col / NOT INDEXED so the index-cursor code path is not taken","Upgrade to a release where subquery cursor resolution is fixed"],"exampleFix":"-- before: correlated subquery resolves an outer index cursor\nSELECT a, (SELECT SUM(b) FROM t2 WHERE t2.k = t1.indexed_k) FROM t1;\n\n-- after: join form avoids outer-reference cursor resolution\nSELECT t1.a, agg.s FROM t1\nLEFT JOIN (SELECT k, SUM(b) AS s FROM t2 GROUP BY k) AS agg\n  ON agg.k = t1.indexed_k;","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Treat statement preparation as fallible: a builder panic is a codegen bug,\n// not something to unwind through your embedding layer.\nlet prepared = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    conn.prepare(sql)\n}))\n.map_err(|_| ApiError::QueryUnsupported { sql: sql.to_string() })??;","preventionTips":["Prepare every statement at app startup so codegen panics surface in CI, not production","Keep a differential harness comparing Turso vs sqlite3 output (scripts/diff.sh) over your query corpus","Pin the engine version and smoke-test planner-heavy queries (correlated subqueries, EXISTS, IN) after upgrades"],"tags":["vdbe","codegen","cursor","correlated-subquery","query-planner","panic","prepare-time"],"backgroundTag":"vdbe-cursor-resolution-failure","analyzedSha":"492c4a71cd7c2649e7df83da1471b74f4b1c7aa9","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}