bindings/go: Add error propagation from bindings lib

This commit is contained in:
PThorpe92
2025-02-01 14:16:42 -05:00
parent 593febd9a4
commit 1493d499e5
8 changed files with 307 additions and 66 deletions

View File

@@ -29,17 +29,20 @@ var (
dbOpen func(string) uintptr
dbClose func(uintptr) uintptr
connPrepare func(uintptr, string) uintptr
connGetError func(uintptr) uintptr
freeBlobFunc func(uintptr)
freeStringFunc func(uintptr)
rowsGetColumns func(uintptr) int32
rowsGetColumnName func(uintptr, int32) uintptr
rowsGetValue func(uintptr, int32) uintptr
rowsGetError func(uintptr) uintptr
closeRows func(uintptr) uintptr
rowsNext func(uintptr) uintptr
stmtQuery func(stmtPtr uintptr, argsPtr uintptr, argCount uint64) uintptr
stmtExec func(stmtPtr uintptr, argsPtr uintptr, argCount uint64, changes uintptr) int32
stmtParamCount func(uintptr) int32
closeStmt func(uintptr) int32
stmtGetError func(uintptr) uintptr
stmtClose func(uintptr) int32
)
// Register all the symbols on library load
@@ -52,6 +55,7 @@ func ensureLibLoaded() error {
purego.RegisterLibFunc(&dbOpen, limboLib, FfiDbOpen)
purego.RegisterLibFunc(&dbClose, limboLib, FfiDbClose)
purego.RegisterLibFunc(&connPrepare, limboLib, FfiDbPrepare)
purego.RegisterLibFunc(&connGetError, limboLib, FfiDbGetError)
purego.RegisterLibFunc(&freeBlobFunc, limboLib, FfiFreeBlob)
purego.RegisterLibFunc(&freeStringFunc, limboLib, FfiFreeCString)
purego.RegisterLibFunc(&rowsGetColumns, limboLib, FfiRowsGetColumns)
@@ -59,10 +63,12 @@ func ensureLibLoaded() error {
purego.RegisterLibFunc(&rowsGetValue, limboLib, FfiRowsGetValue)
purego.RegisterLibFunc(&closeRows, limboLib, FfiRowsClose)
purego.RegisterLibFunc(&rowsNext, limboLib, FfiRowsNext)
purego.RegisterLibFunc(&rowsGetError, limboLib, FfiDbGetError)
purego.RegisterLibFunc(&stmtQuery, limboLib, FfiStmtQuery)
purego.RegisterLibFunc(&stmtExec, limboLib, FfiStmtExec)
purego.RegisterLibFunc(&stmtParamCount, limboLib, FfiStmtParameterCount)
purego.RegisterLibFunc(&closeStmt, limboLib, FfiStmtClose)
purego.RegisterLibFunc(&stmtGetError, limboLib, FfiDbGetError)
purego.RegisterLibFunc(&stmtClose, limboLib, FfiStmtClose)
})
return loadErr
}
@@ -104,6 +110,19 @@ func (c *limboConn) Close() error {
return nil
}
func (c *limboConn) getError() error {
if c.ctx == 0 {
return errors.New("connection closed")
}
err := connGetError(c.ctx)
if err == 0 {
return nil
}
defer freeStringFunc(err)
cpy := fmt.Sprintf("%s", GoString(err))
return errors.New(cpy)
}
func (c *limboConn) Prepare(query string) (driver.Stmt, error) {
if c.ctx == 0 {
return nil, errors.New("connection closed")
@@ -112,7 +131,7 @@ func (c *limboConn) Prepare(query string) (driver.Stmt, error) {
defer c.Unlock()
stmtPtr := connPrepare(c.ctx, query)
if stmtPtr == 0 {
return nil, fmt.Errorf("failed to prepare query=%q", query)
return nil, c.getError()
}
return newStmt(stmtPtr, query), nil
}

View File

@@ -205,6 +205,81 @@ func TestDuplicateConnection2(t *testing.T) {
}
}
func TestConnectionError(t *testing.T) {
newConn, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Error opening new connection: %v", err)
}
sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);"
newConn.Exec(sql)
sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, notafunction(?));"
_, err = newConn.Prepare(sql)
if err == nil {
t.Fatalf("Expected error, got nil")
}
expectedErr := "Parse error: unknown function notafunction"
if err.Error() != expectedErr {
t.Fatalf("Error test failed, expected: %s, found: %v", expectedErr, err)
}
fmt.Println("Connection error test passed")
}
func TestStatementError(t *testing.T) {
newConn, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Error opening new connection: %v", err)
}
sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);"
newConn.Exec(sql)
sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, ?);"
stmt, err := newConn.Prepare(sql)
if err != nil {
t.Fatalf("Error preparing statement: %v", err)
}
_, err = stmt.Exec(1, 2)
if err == nil {
t.Fatalf("Expected error, got nil")
}
if err.Error() != "sql: expected 3 arguments, got 2" {
t.Fatalf("Unexpected : %v\n", err)
}
fmt.Println("Statement error test passed")
}
func TestDriverRowsErrorMessages(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer db.Close()
_, err = db.Exec("CREATE TABLE test (id INTEGER, name TEXT)")
if err != nil {
t.Fatalf("failed to create table: %v", err)
}
_, err = db.Exec("INSERT INTO test (id, name) VALUES (?, ?)", 1, "Alice")
if err != nil {
t.Fatalf("failed to insert row: %v", err)
}
rows, err := db.Query("SELECT id, name FROM test")
if err != nil {
t.Fatalf("failed to query table: %v", err)
}
if !rows.Next() {
t.Fatalf("expected at least one row")
}
var id int
var name string
err = rows.Scan(&name, &id)
if err == nil {
t.Fatalf("expected error scanning wrong type: %v", err)
}
t.Log("Rows error behavior test passed")
}
func slicesAreEq(a, b []byte) bool {
if len(a) != len(b) {
fmt.Printf("LENGTHS NOT EQUAL: %d != %d\n", len(a), len(b))

View File

@@ -2,6 +2,7 @@ package limbo
import (
"database/sql/driver"
"errors"
"fmt"
"io"
"sync"
@@ -11,6 +12,7 @@ type limboRows struct {
mu sync.Mutex
ctx uintptr
columns []string
err error
closed bool
}
@@ -18,13 +20,21 @@ func newRows(ctx uintptr) *limboRows {
return &limboRows{
mu: sync.Mutex{},
ctx: ctx,
closed: false,
columns: nil,
err: nil,
closed: false,
}
}
func (r *limboRows) Columns() []string {
func (r *limboRows) isClosed() bool {
if r.ctx == 0 || r.closed {
return true
}
return false
}
func (r *limboRows) Columns() []string {
if r.isClosed() {
return nil
}
if r.columns == nil {
@@ -45,8 +55,9 @@ func (r *limboRows) Columns() []string {
}
func (r *limboRows) Close() error {
if r.closed {
return nil
r.err = errors.New(RowsClosedErr)
if r.isClosed() {
return r.err
}
r.mu.Lock()
r.closed = true
@@ -56,12 +67,21 @@ func (r *limboRows) Close() error {
return nil
}
func (r *limboRows) Next(dest []driver.Value) error {
if r.ctx == 0 || r.closed {
return io.EOF
func (r *limboRows) Err() error {
if r.err == nil {
r.mu.Lock()
defer r.mu.Unlock()
r.getError()
}
return r.err
}
func (r *limboRows) Next(dest []driver.Value) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.isClosed() {
return r.err
}
for {
status := rowsNext(r.ctx)
switch ResultCode(status) {
@@ -69,6 +89,9 @@ func (r *limboRows) Next(dest []driver.Value) error {
for i := range dest {
valPtr := rowsGetValue(r.ctx, int32(i))
val := toGoValue(valPtr)
if val == nil {
r.getError()
}
dest[i] = val
}
return nil
@@ -77,7 +100,22 @@ func (r *limboRows) Next(dest []driver.Value) error {
case Done:
return io.EOF
default:
return fmt.Errorf("unexpected status: %d", status)
return r.getError()
}
}
}
// mutex will already be locked. this is always called after FFI
func (r *limboRows) getError() error {
if r.isClosed() {
return r.err
}
err := rowsGetError(r.ctx)
if err == 0 {
return nil
}
defer freeCString(err)
cpy := fmt.Sprintf("%s", GoString(err))
r.err = errors.New(cpy)
return r.err
}

View File

@@ -42,11 +42,16 @@ pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void {
struct LimboConn {
conn: Rc<Connection>,
io: Arc<dyn limbo_core::IO>,
err: Option<LimboError>,
}
impl<'conn> LimboConn {
fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
LimboConn { conn, io }
LimboConn {
conn,
io,
err: None,
}
}
#[allow(clippy::wrong_self_convention)]
@@ -60,6 +65,28 @@ impl<'conn> LimboConn {
}
unsafe { &mut *(ptr as *mut LimboConn) }
}
fn get_error(&mut self) -> *const c_char {
if let Some(err) = &self.err {
let err = format!("{}", err);
let c_str = std::ffi::CString::new(err).unwrap();
self.err = None;
c_str.into_raw() as *const c_char
} else {
std::ptr::null()
}
}
}
/// Get the error value from the connection, if any, as a null
/// terminated string. The caller is responsible for freeing the
/// memory with `free_string`.
#[no_mangle]
pub extern "C" fn db_get_error(ctx: *mut c_void) -> *const c_char {
if ctx.is_null() {
return std::ptr::null();
}
let conn = LimboConn::from_ptr(ctx);
conn.get_error()
}
/// Close the database connection

View File

@@ -2,21 +2,23 @@ use crate::{
types::{LimboValue, ResultCode},
LimboConn,
};
use limbo_core::{Row, Statement, StepResult};
use limbo_core::{LimboError, Row, Statement, StepResult};
use std::ffi::{c_char, c_void};
pub struct LimboRows<'a> {
pub struct LimboRows<'conn, 'a> {
stmt: Box<Statement>,
conn: &'a LimboConn,
conn: &'conn mut LimboConn,
cursor: Option<Row<'a>>,
err: Option<LimboError>,
}
impl<'a> LimboRows<'a> {
pub fn new(stmt: Statement, conn: &'a LimboConn) -> Self {
impl<'conn, 'a> LimboRows<'conn, 'a> {
pub fn new(stmt: Statement, conn: &'conn mut LimboConn) -> Self {
LimboRows {
stmt: Box::new(stmt),
cursor: None,
conn,
err: None,
}
}
@@ -25,12 +27,23 @@ impl<'a> LimboRows<'a> {
Box::into_raw(Box::new(self)) as *mut c_void
}
pub fn from_ptr(ptr: *mut c_void) -> &'static mut LimboRows<'a> {
pub fn from_ptr(ptr: *mut c_void) -> &'conn mut LimboRows<'conn, 'a> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut LimboRows) }
}
fn get_error(&mut self) -> *const c_char {
if let Some(err) = &self.err {
let err = format!("{}", err);
let c_str = std::ffi::CString::new(err).unwrap();
self.err = None;
c_str.into_raw() as *const c_char
} else {
std::ptr::null()
}
}
}
#[no_mangle]
@@ -52,7 +65,10 @@ pub extern "C" fn rows_next(ctx: *mut c_void) -> ResultCode {
}
Ok(StepResult::Busy) => ResultCode::Busy,
Ok(StepResult::Interrupt) => ResultCode::Interrupt,
Err(_) => ResultCode::Error,
Err(err) => {
ctx.err = Some(err);
ResultCode::Error
}
}
}
@@ -108,18 +124,23 @@ pub extern "C" fn rows_get_column_name(rows_ptr: *mut c_void, idx: i32) -> *cons
}
#[no_mangle]
pub extern "C" fn rows_close(rows_ptr: *mut c_void) {
if !rows_ptr.is_null() {
let _ = unsafe { Box::from_raw(rows_ptr as *mut LimboRows) };
pub extern "C" fn rows_get_error(ctx: *mut c_void) -> *const c_char {
if ctx.is_null() {
return std::ptr::null();
}
let ctx = LimboRows::from_ptr(ctx);
ctx.get_error()
}
#[no_mangle]
pub extern "C" fn free_rows(rows: *mut c_void) {
if rows.is_null() {
return;
pub extern "C" fn rows_close(ctx: *mut c_void) {
if !ctx.is_null() {
let rows = LimboRows::from_ptr(ctx);
rows.stmt.reset();
rows.cursor = None;
rows.err = None;
}
unsafe {
let _ = Box::from_raw(rows as *mut Statement);
let _ = Box::from_raw(ctx.cast::<LimboRows>());
}
}

View File

@@ -1,7 +1,7 @@
use crate::rows::LimboRows;
use crate::types::{AllocPool, LimboValue, ResultCode};
use crate::LimboConn;
use limbo_core::{Statement, StepResult};
use limbo_core::{LimboError, Statement, StepResult};
use std::ffi::{c_char, c_void};
use std::num::NonZero;
@@ -15,8 +15,11 @@ pub extern "C" fn db_prepare(ctx: *mut c_void, query: *const c_char) -> *mut c_v
let db = LimboConn::from_ptr(ctx);
let stmt = db.conn.prepare(query_str);
match stmt {
Ok(stmt) => LimboStatement::new(Some(stmt), LimboConn::from_ptr(ctx)).to_ptr(),
Err(_) => std::ptr::null_mut(),
Ok(stmt) => LimboStatement::new(Some(stmt), db).to_ptr(),
Err(err) => {
db.err = Some(err);
std::ptr::null_mut()
}
}
}
@@ -69,7 +72,8 @@ pub extern "C" fn stmt_execute(
Ok(StepResult::Interrupt) => {
return ResultCode::Interrupt;
}
Err(_) => {
Err(err) => {
stmt.conn.err = Some(err);
return ResultCode::Error;
}
}
@@ -83,6 +87,7 @@ pub extern "C" fn stmt_parameter_count(ctx: *mut c_void) -> i32 {
}
let stmt = LimboStatement::from_ptr(ctx);
let Some(statement) = stmt.statement.as_ref() else {
stmt.err = Some(LimboError::InternalError("Statement is closed".to_string()));
return -1;
};
statement.parameters_count() as i32
@@ -116,11 +121,10 @@ pub extern "C" fn stmt_query(
}
pub struct LimboStatement<'conn> {
/// If 'query' is ran on the statement, ownership is transfered to the LimboRows object,
/// and this is set to true. `stmt_close` should never be called on a statement that has
/// been used to create a LimboRows object.
/// If 'query' is ran on the statement, ownership is transfered to the LimboRows object
pub statement: Option<Statement>,
pub conn: &'conn mut LimboConn,
pub err: Option<LimboError>,
}
#[no_mangle]
@@ -133,9 +137,22 @@ pub extern "C" fn stmt_close(ctx: *mut c_void) -> ResultCode {
ResultCode::Invalid
}
#[no_mangle]
pub extern "C" fn stmt_get_error(ctx: *mut c_void) -> *const c_char {
if ctx.is_null() {
return std::ptr::null();
}
let stmt = LimboStatement::from_ptr(ctx);
stmt.get_error()
}
impl<'conn> LimboStatement<'conn> {
pub fn new(statement: Option<Statement>, conn: &'conn mut LimboConn) -> Self {
LimboStatement { statement, conn }
LimboStatement {
statement,
conn,
err: None,
}
}
#[allow(clippy::wrong_self_convention)]
@@ -149,4 +166,15 @@ impl<'conn> LimboStatement<'conn> {
}
unsafe { &mut *(ptr as *mut LimboStatement) }
}
fn get_error(&mut self) -> *const c_char {
if let Some(err) = &self.err {
let err = format!("{}", err);
let c_str = std::ffi::CString::new(err).unwrap();
self.err = None;
c_str.into_raw() as *const c_char
} else {
std::ptr::null()
}
}
}

View File

@@ -13,29 +13,39 @@ type limboStmt struct {
mu sync.Mutex
ctx uintptr
sql string
err error
}
func newStmt(ctx uintptr, sql string) *limboStmt {
return &limboStmt{
ctx: uintptr(ctx),
sql: sql,
err: nil,
}
}
func (ls *limboStmt) NumInput() int {
ls.mu.Lock()
defer ls.mu.Unlock()
return int(stmtParamCount(ls.ctx))
res := int(stmtParamCount(ls.ctx))
if res < 0 {
// set the error from rust
_ = ls.getError()
}
return res
}
func (ls *limboStmt) Close() error {
ls.mu.Lock()
res := closeStmt(ls.ctx)
ls.mu.Unlock()
defer ls.mu.Unlock()
if ls.ctx == 0 {
return nil
}
res := stmtClose(ls.ctx)
ls.ctx = 0
if ResultCode(res) != Ok {
return fmt.Errorf("error closing statement: %s", ResultCode(res).String())
}
ls.ctx = 0
return nil
}
@@ -52,8 +62,8 @@ func (ls *limboStmt) Exec(args []driver.Value) (driver.Result, error) {
}
var changes uint64
ls.mu.Lock()
defer ls.mu.Unlock()
rc := stmtExec(ls.ctx, argPtr, argCount, uintptr(unsafe.Pointer(&changes)))
ls.mu.Unlock()
switch ResultCode(rc) {
case Ok, Done:
return driver.RowsAffected(changes), nil
@@ -66,7 +76,7 @@ func (ls *limboStmt) Exec(args []driver.Value) (driver.Result, error) {
case Invalid:
return nil, errors.New("invalid statement")
default:
return nil, fmt.Errorf("unexpected status: %d", rc)
return nil, ls.getError()
}
}
@@ -81,10 +91,10 @@ func (ls *limboStmt) Query(args []driver.Value) (driver.Rows, error) {
argPtr = uintptr(unsafe.Pointer(&queryArgs[0]))
}
ls.mu.Lock()
defer ls.mu.Unlock()
rowsPtr := stmtQuery(ls.ctx, argPtr, uint64(len(queryArgs)))
ls.mu.Unlock()
if rowsPtr == 0 {
return nil, fmt.Errorf("query failed for: %q", ls.sql)
return nil, ls.getError()
}
return newRows(rowsPtr), nil
}
@@ -96,27 +106,26 @@ func (ls *limboStmt) ExecContext(ctx context.Context, query string, args []drive
if err != nil {
return nil, err
}
ls.mu.Lock()
select {
case <-ctx.Done():
ls.mu.Unlock()
return nil, ctx.Err()
default:
}
var changes uint64
ls.mu.Lock()
res := stmtExec(ls.ctx, argArray, uint64(len(args)), uintptr(unsafe.Pointer(&changes)))
ls.mu.Unlock()
switch ResultCode(res) {
case Ok, Done:
changes := uint64(changes)
return driver.RowsAffected(changes), nil
case Error:
return nil, errors.New("error executing statement")
case Busy:
return nil, errors.New("busy")
case Interrupt:
return nil, errors.New("interrupted")
default:
return nil, fmt.Errorf("unexpected status: %d", res)
var changes uint64
defer ls.mu.Unlock()
res := stmtExec(ls.ctx, argArray, uint64(len(args)), uintptr(unsafe.Pointer(&changes)))
switch ResultCode(res) {
case Ok, Done:
changes := uint64(changes)
return driver.RowsAffected(changes), nil
case Busy:
return nil, errors.New("Database is Busy")
case Interrupt:
return nil, errors.New("Interrupted")
default:
return nil, ls.getError()
}
}
}
@@ -130,16 +139,38 @@ func (ls *limboStmt) QueryContext(ctx context.Context, args []driver.NamedValue)
if len(queryArgs) > 0 {
argsPtr = uintptr(unsafe.Pointer(&queryArgs[0]))
}
ls.mu.Lock()
select {
case <-ctx.Done():
ls.mu.Unlock()
return nil, ctx.Err()
default:
defer ls.mu.Unlock()
rowsPtr := stmtQuery(ls.ctx, argsPtr, uint64(len(queryArgs)))
if rowsPtr == 0 {
return nil, ls.getError()
}
return newRows(rowsPtr), nil
}
ls.mu.Lock()
rowsPtr := stmtQuery(ls.ctx, argsPtr, uint64(len(queryArgs)))
ls.mu.Unlock()
if rowsPtr == 0 {
return nil, fmt.Errorf("query failed for: %q", ls.sql)
}
return newRows(rowsPtr), nil
}
func (ls *limboStmt) Err() error {
if ls.err == nil {
ls.mu.Lock()
defer ls.mu.Unlock()
ls.getError()
}
return ls.err
}
// mutex should always be locked when calling - always called after FFI
func (ls *limboStmt) getError() error {
err := stmtGetError(ls.ctx)
if err == 0 {
return nil
}
defer freeCString(err)
cpy := fmt.Sprintf("%s", GoString(err))
ls.err = errors.New(cpy)
return ls.err
}

View File

@@ -67,9 +67,11 @@ func (rc ResultCode) String() string {
const (
driverName = "sqlite3"
libName = "lib_limbo_go"
RowsClosedErr = "sql: Rows closed"
FfiDbOpen = "db_open"
FfiDbClose = "db_close"
FfiDbPrepare = "db_prepare"
FfiDbGetError = "db_get_error"
FfiStmtExec = "stmt_execute"
FfiStmtQuery = "stmt_query"
FfiStmtParameterCount = "stmt_parameter_count"