{"record":{"id":"d8b48183b4f23f6c","repo":"tursodatabase/turso","slug":"function-should-only-be-called-after-sqlite-row","errorCode":null,"errorMessage":"Function should only be called after `SQLITE_ROW`","messagePattern":"Function should only be called after `SQLITE_ROW`","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"bindings/c/src/lib.rs","lineNumber":2327,"sourceCode":"        return SQLITE_MISUSE;\n    }\n\n    let stmt_ref = &mut *stmt;\n    stmt_ref.stmt.clear_bindings();\n\n    SQLITE_OK\n}\n\n#[no_mangle]\npub unsafe extern \"C\" fn sqlite3_column_type(\n    stmt: *mut sqlite3_stmt,\n    idx: ffi::c_int,\n) -> ffi::c_int {\n    let stmt = &mut *stmt;\n    let row = stmt\n        .stmt\n        .row()\n        .expect(\"Function should only be called after `SQLITE_ROW`\");\n\n    match row.get::<&Value>(idx as usize) {\n        Ok(turso_core::Value::Numeric(turso_core::Numeric::Integer(_))) => SQLITE_INTEGER,\n        Ok(turso_core::Value::Text(_)) => SQLITE_TEXT,\n        Ok(turso_core::Value::Numeric(turso_core::Numeric::Float(_))) => SQLITE_FLOAT,\n        Ok(turso_core::Value::Blob(_)) => SQLITE_BLOB,\n        _ => SQLITE_NULL,\n    }\n}\n\n#[no_mangle]\npub unsafe extern \"C\" fn sqlite3_column_count(stmt: *mut sqlite3_stmt) -> ffi::c_int {\n    let stmt = &mut *stmt;\n    stmt.stmt.num_columns() as ffi::c_int\n}\n\n#[no_mangle]\npub unsafe extern \"C\" fn sqlite3_column_decltype(","sourceCodeStart":2309,"sourceCodeEnd":2345,"githubUrl":"https://github.com/tursodatabase/turso/blob/492c4a71cd7c2649e7df83da1471b74f4b1c7aa9/bindings/c/src/lib.rs#L2309-L2345","documentation":"In the C compatibility layer, sqlite3_column_type calls stmt.row().expect(\"Function should only be called after SQLITE_ROW\"). If no row is current - never stepped, stepped to SQLITE_DONE, or after reset - the expect panics, and a panic across the C FFI boundary aborts the process. This is stricter than upstream sqlite3, which tolerates column access after DONE.","triggerScenarios":"Calling sqlite3_column_type (or sibling column accessors routed the same way) before the first successful sqlite3_step, after a step returned SQLITE_DONE/ERROR/BUSY, or after sqlite3_reset - i.e. without a fresh SQLITE_ROW.","commonSituations":"Ported C code that forgets to check the step return code, do-while loop structures that call column functions unconditionally, or code relying on sqlite3's lenient post-DONE behavior.","solutions":["Gate every column access on `sqlite3_step(stmt) == SQLITE_ROW`","Check every sqlite3_step return code (ROW, DONE, BUSY, ERROR) before touching columns","After sqlite3_reset, always step again before any column call","Refactor code that relies on sqlite3's post-DONE leniency - this binding deliberately aborts instead"],"exampleFix":"// before\nsqlite3_step(stmt);\nint t = sqlite3_column_type(stmt, 0); // aborts when step returned SQLITE_DONE\n// after\nif (sqlite3_step(stmt) == SQLITE_ROW) {\n    int t = sqlite3_column_type(stmt, 0);\n}","handlingStrategy":"validation","validationCode":"int rc = sqlite3_step(stmt);\nif (rc == SQLITE_ROW) {\n    int t = sqlite3_column_type(stmt, 0);\n    /* safe */\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never call sqlite3_column_* without a fresh SQLITE_ROW","Check every sqlite3_step return code (ROW, DONE, BUSY, ERROR)","Follow the order reset, step, then column access - every time"],"tags":["c-api","ffi","sqlite-compat","panic","statement-lifecycle"],"backgroundTag":"sqlite-column-access-before-row","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"}