Progress on Go bindings, add prepare + query statement

This commit is contained in:
PThorpe92 2025-01-24 22:19:37 -05:00
parent 4be1f9c3cc
commit 32c985f9a8
No known key found for this signature in database
GPG key ID: 66DB3FBACBDD05CC
11 changed files with 947 additions and 317 deletions

View file

@ -1,18 +0,0 @@
// package main
//
// import (
// "fmt"
// )
//
// func main() {
// conn, err := lc.Open("new.db")
// if err != nil {
// panic(err)
// }
// fmt.Println("Connected to database")
// sql := "select c from t;"
// conn.Query(sql)
//
// conn.Close()
// fmt.Println("Connection closed")
// }

View file

@ -2,4 +2,7 @@ module turso
go 1.23.4
require github.com/ebitengine/purego v0.8.2 // indirect
require (
github.com/ebitengine/purego v0.8.2
golang.org/x/sys/windows v0.29.0
)

View file

@ -1,2 +1,4 @@
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View file

@ -1,6 +1,8 @@
mod rows;
#[allow(dead_code)]
mod statement;
mod types;
use limbo_core::{Connection, Database, LimboError, Value};
use limbo_core::{Connection, Database, LimboError};
use std::{
ffi::{c_char, c_void},
rc::Rc,
@ -8,6 +10,9 @@ use std::{
sync::Arc,
};
/// # Safety
/// Safe to be called from Go with null terminated DSN string.
/// performs null check on the path.
#[no_mangle]
pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void {
if path.is_null() {
@ -34,27 +39,22 @@ pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void {
std::ptr::null_mut()
}
struct TursoConn<'a> {
#[allow(dead_code)]
struct TursoConn {
conn: Rc<Connection>,
io: Arc<dyn limbo_core::IO>,
cursor_idx: usize,
cursor: Option<Vec<Value<'a>>>,
}
impl<'a> TursoConn<'_> {
impl TursoConn {
fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
TursoConn {
conn,
io,
cursor_idx: 0,
cursor: None,
}
TursoConn { conn, io }
}
#[allow(clippy::wrong_self_convention)]
fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoConn<'a> {
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoConn {
if ptr.is_null() {
panic!("Null pointer");
}
@ -68,7 +68,7 @@ impl<'a> TursoConn<'_> {
#[no_mangle]
pub unsafe extern "C" fn db_close(db: *mut c_void) {
if !db.is_null() {
let _ = unsafe { Box::from_raw(db) };
let _ = unsafe { Box::from_raw(db as *mut TursoConn) };
}
}
@ -77,19 +77,12 @@ fn get_io(db_location: &DbType) -> Result<Arc<dyn limbo_core::IO>, LimboError> {
Ok(match db_location {
DbType::Memory => Arc::new(limbo_core::MemoryIO::new()?),
_ => {
#[cfg(target_family = "unix")]
if cfg!(all(target_os = "linux", feature = "io_uring")) {
Arc::new(limbo_core::UringIO::new()?)
} else {
Arc::new(limbo_core::UnixIO::new()?)
}
#[cfg(target_family = "windows")]
Arc::new(limbo_core::WindowsIO::new()?);
return Ok(Arc::new(limbo_core::PlatformIO::new()?));
}
})
}
#[allow(dead_code)]
struct DbOptions {
path: DbType,
params: Parameters,

138
bindings/go/rs_src/rows.rs Normal file
View file

@ -0,0 +1,138 @@
use crate::{
statement::TursoStatement,
types::{ResultCode, TursoValue},
};
use limbo_core::{Rows, StepResult, Value};
use std::ffi::{c_char, c_void};
pub struct TursoRows<'a> {
rows: Rows,
cursor: Option<Vec<Value<'a>>>,
stmt: Box<TursoStatement<'a>>,
}
impl<'a> TursoRows<'a> {
pub fn new(rows: Rows, stmt: Box<TursoStatement<'a>>) -> Self {
TursoRows {
rows,
stmt,
cursor: None,
}
}
#[allow(clippy::wrong_self_convention)]
pub fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}
pub fn from_ptr(ptr: *mut c_void) -> &'static mut TursoRows<'a> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoRows) }
}
}
#[no_mangle]
pub extern "C" fn rows_next(ctx: *mut c_void) -> ResultCode {
if ctx.is_null() {
return ResultCode::Error;
}
let ctx = TursoRows::from_ptr(ctx);
match ctx.rows.next_row() {
Ok(StepResult::Row(row)) => {
ctx.cursor = Some(row.values);
ResultCode::Row
}
Ok(StepResult::Done) => ResultCode::Done,
Ok(StepResult::IO) => {
let _ = ctx.stmt.conn.io.run_once();
ResultCode::Io
}
Ok(StepResult::Busy) => ResultCode::Busy,
Ok(StepResult::Interrupt) => ResultCode::Interrupt,
Err(_) => ResultCode::Error,
}
}
#[no_mangle]
pub extern "C" fn rows_get_value(ctx: *mut c_void, col_idx: usize) -> *const c_void {
if ctx.is_null() {
return std::ptr::null();
}
let ctx = TursoRows::from_ptr(ctx);
if let Some(ref cursor) = ctx.cursor {
if let Some(value) = cursor.get(col_idx) {
let val = TursoValue::from_value(value);
return val.to_ptr();
}
}
std::ptr::null()
}
#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
if !s.is_null() {
unsafe { drop(std::ffi::CString::from_raw(s)) };
}
}
#[no_mangle]
pub extern "C" fn rows_get_columns(
rows_ptr: *mut c_void,
out_length: *mut usize,
) -> *mut *const c_char {
if rows_ptr.is_null() || out_length.is_null() {
return std::ptr::null_mut();
}
let rows = TursoRows::from_ptr(rows_ptr);
let c_strings: Vec<std::ffi::CString> = rows
.rows
.columns()
.iter()
.map(|name| std::ffi::CString::new(name.as_str()).unwrap())
.collect();
let c_ptrs: Vec<*const c_char> = c_strings.iter().map(|s| s.as_ptr()).collect();
unsafe {
*out_length = c_ptrs.len();
}
let ptr = c_ptrs.as_ptr();
std::mem::forget(c_strings);
std::mem::forget(c_ptrs);
ptr as *mut *const c_char
}
#[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 TursoRows) };
}
}
#[no_mangle]
pub extern "C" fn free_columns(columns: *mut *const c_char) {
if columns.is_null() {
return;
}
unsafe {
let mut idx = 0;
while !(*columns.add(idx)).is_null() {
let _ = std::ffi::CString::from_raw(*columns.add(idx) as *mut c_char);
idx += 1;
}
let _ = Box::from_raw(columns);
}
}
#[no_mangle]
pub extern "C" fn free_rows(rows: *mut c_void) {
if rows.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(rows as *mut Rows);
}
}

View file

@ -1,7 +1,9 @@
use crate::types::ResultCode;
use crate::rows::TursoRows;
use crate::types::{AllocPool, ResultCode, TursoValue};
use crate::TursoConn;
use limbo_core::{Rows, Statement, StepResult, Value};
use limbo_core::{Statement, StepResult};
use std::ffi::{c_char, c_void};
use std::num::NonZero;
#[no_mangle]
pub extern "C" fn db_prepare(ctx: *mut c_void, query: *const c_char) -> *mut c_void {
@ -19,142 +21,119 @@ pub extern "C" fn db_prepare(ctx: *mut c_void, query: *const c_char) -> *mut c_v
}
}
struct TursoStatement<'a> {
statement: Statement,
conn: &'a TursoConn<'a>,
#[no_mangle]
pub extern "C" fn stmt_execute(
ctx: *mut c_void,
args_ptr: *mut TursoValue,
arg_count: usize,
changes: *mut i64,
) -> ResultCode {
if ctx.is_null() {
return ResultCode::Error;
}
let stmt = TursoStatement::from_ptr(ctx);
let args = if !args_ptr.is_null() && arg_count > 0 {
unsafe { std::slice::from_raw_parts(args_ptr, arg_count) }
} else {
&[]
};
for (i, arg) in args.iter().enumerate() {
let val = arg.to_value(&mut stmt.pool);
stmt.statement.bind_at(NonZero::new(i + 1).unwrap(), val);
}
loop {
match stmt.statement.step() {
Ok(StepResult::Row(_)) => {
// unexpected row during execution, error out.
return ResultCode::Error;
}
Ok(StepResult::Done) => {
stmt.conn.conn.total_changes();
if !changes.is_null() {
unsafe {
*changes = stmt.conn.conn.total_changes();
}
}
return ResultCode::Done;
}
Ok(StepResult::IO) => {
let _ = stmt.conn.io.run_once();
}
Ok(StepResult::Busy) => {
return ResultCode::Busy;
}
Ok(StepResult::Interrupt) => {
return ResultCode::Interrupt;
}
Err(_) => {
return ResultCode::Error;
}
}
}
}
impl<'a> TursoStatement<'a> {
fn new(statement: Statement, conn: &'a TursoConn<'a>) -> Self {
TursoStatement { statement, conn }
#[no_mangle]
pub extern "C" fn stmt_parameter_count(ctx: *mut c_void) -> i32 {
if ctx.is_null() {
return -1;
}
let stmt = TursoStatement::from_ptr(ctx);
stmt.statement.parameters_count() as i32
}
#[no_mangle]
pub extern "C" fn stmt_query(
ctx: *mut c_void,
args_ptr: *mut TursoValue,
args_count: usize,
) -> *mut c_void {
if ctx.is_null() {
return std::ptr::null_mut();
}
let stmt = TursoStatement::from_ptr(ctx);
let args = if !args_ptr.is_null() && args_count > 0 {
unsafe { std::slice::from_raw_parts(args_ptr, args_count) }
} else {
&[]
};
for (i, arg) in args.iter().enumerate() {
let val = arg.to_value(&mut stmt.pool);
stmt.statement.bind_at(NonZero::new(i + 1).unwrap(), val);
}
match stmt.statement.query() {
Ok(rows) => {
let stmt = unsafe { Box::from_raw(stmt) };
TursoRows::new(rows, stmt).to_ptr()
}
Err(_) => std::ptr::null_mut(),
}
}
pub struct TursoStatement<'conn> {
pub statement: Statement,
pub conn: &'conn mut TursoConn,
pub pool: AllocPool,
}
impl<'conn> TursoStatement<'conn> {
pub fn new(statement: Statement, conn: &'conn mut TursoConn) -> Self {
TursoStatement {
statement,
conn,
pool: AllocPool::new(),
}
}
#[allow(clippy::wrong_self_convention)]
fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoStatement<'a> {
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoStatement<'conn> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoStatement) }
}
}
#[no_mangle]
pub extern "C" fn db_get_columns(ctx: *mut c_void) -> *const c_void {
if ctx.is_null() {
return std::ptr::null();
}
let stmt = TursoStatement::from_ptr(ctx);
let columns = stmt.statement.columns();
let mut column_names = Vec::new();
for column in columns {
column_names.push(column.name().to_string());
}
let c_string = std::ffi::CString::new(column_names.join(",")).unwrap();
c_string.into_raw() as *const c_void
}
struct TursoRows<'a> {
rows: Rows<'a>,
conn: &'a mut TursoConn<'a>,
}
impl<'a> TursoRows<'a> {
fn new(rows: Rows<'a>, conn: &'a mut TursoConn<'a>) -> Self {
TursoRows { rows, conn }
}
fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoRows<'a> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoRows) }
}
}
#[no_mangle]
pub extern "C" fn rows_next(ctx: *mut c_void, rows_ptr: *mut c_void) -> ResultCode {
if rows_ptr.is_null() || ctx.is_null() {
return ResultCode::Error;
}
let rows = unsafe { &mut *(rows_ptr as *mut Rows) };
let conn = TursoConn::from_ptr(ctx);
match rows.next_row() {
Ok(StepResult::Row(row)) => {
conn.cursor = Some(row.values);
ResultCode::Row
}
Ok(StepResult::Done) => {
// No more rows
ResultCode::Done
}
Ok(StepResult::IO) => {
let _ = conn.io.run_once();
ResultCode::Io
}
Ok(StepResult::Busy) => ResultCode::Busy,
Ok(StepResult::Interrupt) => ResultCode::Interrupt,
Err(_) => ResultCode::Error,
}
}
#[no_mangle]
pub extern "C" fn rows_get_value(ctx: *mut c_void, col_idx: usize) -> *const c_char {
if ctx.is_null() {
return std::ptr::null();
}
let conn = TursoConn::from_ptr(ctx);
if let Some(ref cursor) = conn.cursor {
if let Some(value) = cursor.get(col_idx) {
let c_string = std::ffi::CString::new(value.to_string()).unwrap();
return c_string.into_raw(); // Caller must free this pointer
}
}
std::ptr::null() // No data or invalid index
}
// Free the returned string
#[no_mangle]
pub extern "C" fn free_c_string(s: *mut c_char) {
if !s.is_null() {
unsafe { drop(std::ffi::CString::from_raw(s)) };
}
}
#[no_mangle]
pub extern "C" fn rows_get_string(
ctx: *mut c_void,
rows_ptr: *mut c_void,
col_idx: i32,
) -> *const c_char {
if rows_ptr.is_null() || ctx.is_null() {
return std::ptr::null();
}
let _rows = unsafe { &mut *(rows_ptr as *mut Rows) };
let conn = TursoConn::from_ptr(ctx);
if col_idx > conn.cursor_idx as i32 || conn.cursor.is_none() {
return std::ptr::null();
}
if let Some(values) = &conn.cursor {
let value = &values[col_idx as usize];
match value {
Value::Text(s) => {
return s.as_ptr() as *const i8;
}
_ => return std::ptr::null(),
}
};
std::ptr::null()
}
#[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 Rows) };
}
}

View file

@ -1,14 +1,190 @@
use std::ffi::{c_char, c_void};
#[allow(dead_code)]
#[repr(C)]
pub enum ResultCode {
Error = -1,
Ok = 0,
Row = 1,
Busy = 2,
Done = 3,
Io = 4,
Interrupt = 5,
Invalid = 6,
Null = 7,
NoMem = 8,
ReadOnly = 9,
Io = 3,
Interrupt = 4,
Invalid = 5,
Null = 6,
NoMem = 7,
ReadOnly = 8,
NoData = 9,
Done = 10,
}
#[repr(C)]
pub enum ValueType {
Integer = 0,
Text = 1,
Blob = 2,
Real = 3,
Null = 4,
}
#[repr(C)]
pub struct TursoValue {
pub value_type: ValueType,
pub value: ValueUnion,
}
#[repr(C)]
pub union ValueUnion {
pub int_val: i64,
pub real_val: f64,
pub text_ptr: *const c_char,
pub blob_ptr: *const c_void,
}
#[repr(C)]
pub struct Blob {
pub data: *const u8,
pub len: usize,
}
impl Blob {
pub fn to_ptr(&self) -> *const c_void {
self as *const Blob as *const c_void
}
}
pub struct AllocPool {
strings: Vec<String>,
blobs: Vec<Vec<u8>>,
}
impl AllocPool {
pub fn new() -> Self {
AllocPool {
strings: Vec::new(),
blobs: Vec::new(),
}
}
pub fn add_string(&mut self, s: String) -> &String {
self.strings.push(s);
self.strings.last().unwrap()
}
pub fn add_blob(&mut self, b: Vec<u8>) -> &Vec<u8> {
self.blobs.push(b);
self.blobs.last().unwrap()
}
}
#[no_mangle]
pub extern "C" fn free_blob(blob_ptr: *mut c_void) {
if blob_ptr.is_null() {
return;
}
unsafe {
let _ = Box::from_raw(blob_ptr as *mut Blob);
}
}
#[allow(dead_code)]
impl ValueUnion {
fn from_str(s: &str) -> Self {
ValueUnion {
text_ptr: s.as_ptr() as *const c_char,
}
}
fn from_bytes(b: &[u8]) -> Self {
ValueUnion {
blob_ptr: Blob {
data: b.as_ptr(),
len: b.len(),
}
.to_ptr(),
}
}
fn from_int(i: i64) -> Self {
ValueUnion { int_val: i }
}
fn from_real(r: f64) -> Self {
ValueUnion { real_val: r }
}
fn from_null() -> Self {
ValueUnion { int_val: 0 }
}
pub fn to_int(&self) -> i64 {
unsafe { self.int_val }
}
pub fn to_real(&self) -> f64 {
unsafe { self.real_val }
}
pub fn to_str(&self) -> &str {
unsafe { std::ffi::CStr::from_ptr(self.text_ptr).to_str().unwrap() }
}
pub fn to_bytes(&self) -> &[u8] {
let blob = unsafe { self.blob_ptr as *const Blob };
let blob = unsafe { &*blob };
unsafe { std::slice::from_raw_parts(blob.data, blob.len) }
}
}
impl TursoValue {
pub fn new(value_type: ValueType, value: ValueUnion) -> Self {
TursoValue { value_type, value }
}
#[allow(clippy::wrong_self_convention)]
pub fn to_ptr(self) -> *const c_void {
Box::into_raw(Box::new(self)) as *const c_void
}
pub fn from_value(value: &limbo_core::Value<'_>) -> Self {
match value {
limbo_core::Value::Integer(i) => {
TursoValue::new(ValueType::Integer, ValueUnion::from_int(*i))
}
limbo_core::Value::Float(r) => {
TursoValue::new(ValueType::Real, ValueUnion::from_real(*r))
}
limbo_core::Value::Text(s) => TursoValue::new(ValueType::Text, ValueUnion::from_str(s)),
limbo_core::Value::Blob(b) => {
TursoValue::new(ValueType::Blob, ValueUnion::from_bytes(b))
}
limbo_core::Value::Null => TursoValue::new(ValueType::Null, ValueUnion::from_null()),
}
}
pub fn to_value<'pool>(&self, pool: &'pool mut AllocPool) -> limbo_core::Value<'pool> {
match self.value_type {
ValueType::Integer => limbo_core::Value::Integer(unsafe { self.value.int_val }),
ValueType::Real => limbo_core::Value::Float(unsafe { self.value.real_val }),
ValueType::Text => {
let cstr = unsafe { std::ffi::CStr::from_ptr(self.value.text_ptr) };
match cstr.to_str() {
Ok(utf8_str) => {
let owned = utf8_str.to_owned();
// statement needs to own these strings, will free when closed
let borrowed = pool.add_string(owned);
limbo_core::Value::Text(borrowed)
}
Err(_) => limbo_core::Value::Null,
}
}
ValueType::Blob => {
let blob_ptr = unsafe { self.value.blob_ptr as *const Blob };
if blob_ptr.is_null() {
limbo_core::Value::Null
} else {
let blob = unsafe { &*blob_ptr };
let data = unsafe { std::slice::from_raw_parts(blob.data, blob.len) };
let borrowed = pool.add_blob(data.to_vec());
limbo_core::Value::Blob(borrowed)
}
}
ValueType::Null => limbo_core::Value::Null,
}
}
}

View file

@ -1,77 +1,192 @@
package turso
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"io"
"unsafe"
)
type stmt struct {
ctx uintptr
sql string
// only construct tursoStmt with initStmt function to ensure proper initialization
type tursoStmt struct {
ctx uintptr
sql string
query stmtQueryFn
execute stmtExecuteFn
getParamCount func(uintptr) int32
}
type rows struct {
ctx uintptr
rowsPtr uintptr
columns []string
err error
// Initialize/register the FFI function pointers for the statement methods
func initStmt(ctx uintptr, sql string) *tursoStmt {
var query stmtQueryFn
var execute stmtExecuteFn
var getParamCount func(uintptr) int32
methods := []ExtFunc{{query, FfiStmtQuery}, {execute, FfiStmtExec}, {getParamCount, FfiStmtParameterCount}}
for i := range methods {
methods[i].initFunc()
}
return &tursoStmt{
ctx: uintptr(ctx),
sql: sql,
}
}
func (ls *stmt) Query(args []driver.Value) (driver.Rows, error) {
var dbPrepare func(uintptr, uintptr) uintptr
getExtFunc(&dbPrepare, "db_prepare")
func (st *tursoStmt) NumInput() int {
return int(st.getParamCount(st.ctx))
}
queryPtr := toCString(ls.sql)
defer freeCString(queryPtr)
func (st *tursoStmt) Exec(args []driver.Value) (driver.Result, error) {
argArray, err := buildArgs(args)
if err != nil {
return nil, err
}
argPtr := uintptr(0)
argCount := uint64(len(argArray))
if argCount > 0 {
argPtr = uintptr(unsafe.Pointer(&argArray[0]))
}
var changes uint64
rc := st.execute(st.ctx, argPtr, argCount, uintptr(unsafe.Pointer(&changes)))
switch ResultCode(rc) {
case Ok:
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")
case Invalid:
return nil, errors.New("invalid statement")
default:
return nil, fmt.Errorf("unexpected status: %d", rc)
}
}
rowsPtr := dbPrepare(ls.ctx, queryPtr)
func (st *tursoStmt) Query(args []driver.Value) (driver.Rows, error) {
queryArgs, err := buildArgs(args)
if err != nil {
return nil, err
}
rowsPtr := st.query(st.ctx, uintptr(unsafe.Pointer(&queryArgs[0])), uint64(len(queryArgs)))
if rowsPtr == 0 {
return nil, fmt.Errorf("failed to prepare query")
return nil, fmt.Errorf("query failed for: %q", st.sql)
}
var colFunc func(uintptr, uintptr) uintptr
getExtFunc(&colFunc, "columns")
rows := &rows{
ctx: ls.ctx,
rowsPtr: rowsPtr,
}
return rows, nil
return initRows(rowsPtr), nil
}
func (lr *rows) Columns() []string {
return lr.columns
func (ts *tursoStmt) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
stripped := namedValueToValue(args)
argArray, err := getArgsPtr(stripped)
if err != nil {
return nil, err
}
var changes uintptr
res := ts.execute(ts.ctx, argArray, uint64(len(args)), changes)
switch ResultCode(res) {
case Ok:
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)
}
}
func (lr *rows) Close() error {
var rowsClose func(uintptr)
getExtFunc(&rowsClose, "rows_close")
rowsClose(lr.rowsPtr)
func (st *tursoStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
queryArgs, err := buildNamedArgs(args)
if err != nil {
return nil, err
}
rowsPtr := st.query(st.ctx, uintptr(unsafe.Pointer(&queryArgs[0])), uint64(len(queryArgs)))
if rowsPtr == 0 {
return nil, fmt.Errorf("query failed for: %q", st.sql)
}
return initRows(rowsPtr), nil
}
// only construct tursoRows with initRows function to ensure proper initialization
type tursoRows struct {
ctx uintptr
columns []string
closed bool
getCols func(uintptr, *uint) uintptr
next func(uintptr) uintptr
getValue func(uintptr, int32) uintptr
closeRows func(uintptr) uintptr
freeCols func(uintptr) uintptr
}
// Initialize/register the FFI function pointers for the rows methods
// DO NOT construct 'tursoRows' without this function
func initRows(ctx uintptr) *tursoRows {
var getCols func(uintptr, *uint) uintptr
var getValue func(uintptr, int32) uintptr
var closeRows func(uintptr) uintptr
var freeCols func(uintptr) uintptr
var next func(uintptr) uintptr
methods := []ExtFunc{
{getCols, FfiRowsGetColumns},
{getValue, FfiRowsGetValue},
{closeRows, FfiRowsClose},
{freeCols, FfiFreeColumns},
{next, FfiRowsNext}}
for i := range methods {
methods[i].initFunc()
}
return &tursoRows{
ctx: ctx,
getCols: getCols,
getValue: getValue,
closeRows: closeRows,
freeCols: freeCols,
next: next,
}
}
func (r *tursoRows) Columns() []string {
if r.columns == nil {
var columnCount uint
colArrayPtr := r.getCols(r.ctx, &columnCount)
if colArrayPtr != 0 && columnCount > 0 {
r.columns = cArrayToGoStrings(colArrayPtr, columnCount)
if r.freeCols == nil {
getFfiFunc(&r.freeCols, FfiFreeColumns)
}
defer r.freeCols(colArrayPtr)
}
}
return r.columns
}
func (r *tursoRows) Close() error {
if r.closed {
return nil
}
r.closed = true
r.closeRows(r.ctx)
r.ctx = 0
return nil
}
func (lr *rows) Next(dest []driver.Value) error {
var rowsNext func(uintptr, uintptr) int32
getExtFunc(&rowsNext, "rows_next")
status := rowsNext(lr.ctx, lr.rowsPtr)
func (r *tursoRows) Next(dest []driver.Value) error {
status := r.next(r.ctx)
switch ResultCode(status) {
case Row:
for i := range dest {
getExtFunc(&rowsGetValue, "rows_get_value")
valPtr := rowsGetValue(lr.ctx, int32(i))
if valPtr != 0 {
val := cStringToGoString(valPtr)
dest[i] = val
freeCString(valPtr)
} else {
dest[i] = nil
}
valPtr := r.getValue(r.ctx, int32(i))
val := toGoValue(valPtr)
dest[i] = val
}
return nil
case 0: // No more rows
case Done:
return io.EOF
default:
return fmt.Errorf("unexpected status: %d", status)

View file

@ -4,43 +4,61 @@ import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"log/slog"
"os"
"runtime"
"sync"
"unsafe"
"github.com/ebitengine/purego"
"golang.org/x/sys/windows"
)
const (
turso = "../../target/debug/lib_turso_go.so"
)
const turso = "../../target/debug/lib_turso_go"
const driverName = "turso"
func toGoStr(ptr uintptr, length int) string {
if ptr == 0 {
return ""
var tursoLib uintptr
func getSystemLibrary() error {
switch runtime.GOOS {
case "darwin":
slib, err := purego.Dlopen(fmt.Sprintf("%s.dylib", turso), purego.RTLD_LAZY)
if err != nil {
return err
}
tursoLib = slib
case "linux":
slib, err := purego.Dlopen(fmt.Sprintf("%s.so", turso), purego.RTLD_LAZY)
if err != nil {
return err
}
tursoLib = slib
case "windows":
slib, err := windows.LoadLibrary(fmt.Sprintf("%s.dll", turso))
if err != nil {
return err
}
tursoLib = slib
default:
panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS))
}
uptr := unsafe.Pointer(ptr)
s := (*string)(uptr)
if s == nil {
// redundant
return ""
}
return *s
return nil
}
func init() {
slib, err := purego.Dlopen(turso, purego.RTLD_LAZY)
err := getSystemLibrary()
if err != nil {
slog.Error("Error opening turso library: ", err)
os.Exit(1)
}
lib = slib
sql.Register("turso", &tursoDriver{})
sql.Register(driverName, &tursoDriver{})
}
type tursoDriver struct {
tursoCtx
type tursoDriver struct{}
func (d tursoDriver) Open(name string) (driver.Conn, error) {
return openConn(name)
}
func toCString(s string) uintptr {
@ -48,80 +66,76 @@ func toCString(s string) uintptr {
return uintptr(unsafe.Pointer(&b[0]))
}
func getExtFunc(ptr interface{}, name string) {
purego.RegisterLibFunc(ptr, lib, name)
// helper to register an FFI function in the lib_turso_go library
func getFfiFunc(ptr interface{}, name string) {
purego.RegisterLibFunc(&ptr, tursoLib, name)
}
type conn struct {
type tursoConn struct {
ctx uintptr
sync.Mutex
writeTimeFmt string
lastInsertID int64
lastAffected int64
prepare func(uintptr, uintptr) uintptr
}
func newConn() *conn {
return &conn{
0,
func newConn(ctx uintptr) *tursoConn {
var prepare func(uintptr, uintptr) uintptr
getFfiFunc(&prepare, FfiDbPrepare)
return &tursoConn{
ctx,
sync.Mutex{},
"2006-01-02 15:04:05",
0,
0,
prepare,
}
}
func open(dsn string) (*conn, error) {
var open func(uintptr) uintptr
getExtFunc(&open, ExtDBOpen)
c := newConn()
path := toCString(dsn)
ctx := open(path)
c.ctx = ctx
return c, nil
}
func openConn(dsn string) (*tursoConn, error) {
var dbOpen func(uintptr) uintptr
getFfiFunc(&dbOpen, FfiDbOpen)
type tursoCtx struct {
conn *conn
tx *sql.Tx
err error
rows *sql.Rows
stmt *sql.Stmt
}
cStr := toCString(dsn)
defer freeCString(cStr)
func (lc tursoCtx) Open(dsn string) (driver.Conn, error) {
conn, err := open(dsn)
if err != nil {
return nil, err
ctx := dbOpen(cStr)
if ctx == 0 {
return nil, fmt.Errorf("failed to open database for dsn=%q", dsn)
}
nc := tursoCtx{conn: conn}
return nc, nil
return &tursoConn{ctx: ctx}, nil
}
func (lc tursoCtx) Close() error {
var closedb func(uintptr) uintptr
getExtFunc(&closedb, ExtDBClose)
closedb(lc.conn.ctx)
func (c *tursoConn) Close() error {
if c.ctx == 0 {
return nil
}
var dbClose func(uintptr) uintptr
getFfiFunc(&dbClose, FfiDbClose)
dbClose(c.ctx)
c.ctx = 0
return nil
}
// TODO: Begin not implemented
func (lc tursoCtx) Begin() (driver.Tx, error) {
return nil, nil
func (c *tursoConn) Prepare(query string) (driver.Stmt, error) {
if c.ctx == 0 {
return nil, errors.New("connection closed")
}
if c.prepare == nil {
var dbPrepare func(uintptr, uintptr) uintptr
getFfiFunc(&dbPrepare, FfiDbPrepare)
c.prepare = dbPrepare
}
qPtr := toCString(query)
stmtPtr := c.prepare(c.ctx, qPtr)
freeCString(qPtr)
if stmtPtr == 0 {
return nil, fmt.Errorf("prepare failed: %q", query)
}
return &tursoStmt{
ctx: stmtPtr,
sql: query,
}, nil
}
func (ls tursoCtx) Prepare(sql string) (driver.Stmt, error) {
var prepare func(uintptr, uintptr) uintptr
getExtFunc(&prepare, ExtDBPrepare)
s := toCString(sql)
statement := prepare(ls.conn.ctx, s)
if statement == 0 {
return nil, errors.New("no rows")
}
ls.stmt = stmt{
ctx: statement,
}
}
return nil, nil
// begin is needed to implement driver.Conn.. for now not implemented
func (c *tursoConn) Begin() (driver.Tx, error) {
return nil, errors.New("transactions not implemented")
}

View file

@ -1,28 +1,248 @@
package turso
import (
"database/sql/driver"
"fmt"
"unsafe"
)
type ResultCode int
const (
Error ResultCode = -1
Ok ResultCode = 0
Row ResultCode = 1
Busy ResultCode = 2
Done ResultCode = 3
Io ResultCode = 4
Interrupt ResultCode = 5
Invalid ResultCode = 6
Null ResultCode = 7
NoMem ResultCode = 8
ReadOnly ResultCode = 9
ExtDBOpen string = "db_open"
ExtDBClose string = "db_close"
ExtDBPrepare string = "db_prepare"
Error ResultCode = -1
Ok ResultCode = 0
Row ResultCode = 1
Busy ResultCode = 2
Io ResultCode = 3
Interrupt ResultCode = 4
Invalid ResultCode = 5
Null ResultCode = 6
NoMem ResultCode = 7
ReadOnly ResultCode = 8
NoData ResultCode = 9
Done ResultCode = 10
)
var (
lib uintptr
dbPrepare func(uintptr, uintptr) uintptr
rowsNext func(rowsPtr uintptr) int32
rowsGetValue func(rowsPtr uintptr, colIdx uint) uintptr
freeCString func(strPtr uintptr)
const (
FfiDbOpen string = "db_open"
FfiDbClose string = "db_close"
FfiDbPrepare string = "db_prepare"
FfiStmtExec string = "stmt_execute"
FfiStmtQuery string = "stmt_query"
FfiStmtParameterCount string = "stmt_parameter_count"
FfiRowsClose string = "rows_close"
FfiRowsGetColumns string = "rows_get_columns"
FfiRowsNext string = "rows_next"
FfiRowsGetValue string = "rows_get_value"
FfiFreeColumns string = "free_columns"
FfiFreeCString string = "free_string"
)
// convert a namedValue slice into normal values until named parameters are supported
func namedValueToValue(named []driver.NamedValue) []driver.Value {
out := make([]driver.Value, len(named))
for i, nv := range named {
out[i] = nv.Value
}
return out
}
func buildNamedArgs(named []driver.NamedValue) ([]tursoValue, error) {
args := make([]driver.Value, len(named))
for i, nv := range named {
args[i] = nv.Value
}
return buildArgs(args)
}
type ExtFunc struct {
funcPtr interface{}
funcName string
}
func (ef *ExtFunc) initFunc() {
getFfiFunc(&ef.funcPtr, ef.funcName)
}
type valueType int
const (
intVal valueType = iota
textVal
blobVal
realVal
nullVal
)
// struct to pass Go values over FFI
type tursoValue struct {
Type valueType
Value [8]byte
}
// struct to pass byte slices over FFI
type Blob struct {
Data uintptr
Len uint
}
// convert a tursoValue to a native Go value
func toGoValue(valPtr uintptr) interface{} {
val := (*tursoValue)(unsafe.Pointer(valPtr))
switch val.Type {
case intVal:
return *(*int64)(unsafe.Pointer(&val.Value))
case realVal:
return *(*float64)(unsafe.Pointer(&val.Value))
case textVal:
textPtr := *(*uintptr)(unsafe.Pointer(&val.Value))
return GoString(textPtr)
case blobVal:
blobPtr := *(*uintptr)(unsafe.Pointer(&val.Value))
return toGoBlob(blobPtr)
case nullVal:
return nil
default:
return nil
}
}
func getArgsPtr(args []driver.Value) (uintptr, error) {
if len(args) == 0 {
return 0, nil
}
argSlice, err := buildArgs(args)
if err != nil {
return 0, err
}
return uintptr(unsafe.Pointer(&argSlice[0])), nil
}
// convert a byte slice to a Blob type that can be sent over FFI
func makeBlob(b []byte) *Blob {
if len(b) == 0 {
return nil
}
blob := &Blob{
Data: uintptr(unsafe.Pointer(&b[0])),
Len: uint(len(b)),
}
return blob
}
// converts a blob received via FFI to a native Go byte slice
func toGoBlob(blobPtr uintptr) []byte {
if blobPtr == 0 {
return nil
}
blob := (*Blob)(unsafe.Pointer(blobPtr))
return unsafe.Slice((*byte)(unsafe.Pointer(blob.Data)), blob.Len)
}
var freeString func(*byte)
// free a C style string allocated via FFI
func freeCString(cstr uintptr) {
if cstr == 0 {
return
}
if freeString == nil {
getFfiFunc(&freeString, FfiFreeCString)
}
freeString((*byte)(unsafe.Pointer(cstr)))
}
func cArrayToGoStrings(arrayPtr uintptr, length uint) []string {
if arrayPtr == 0 || length == 0 {
return nil
}
ptrSlice := unsafe.Slice(
(**byte)(unsafe.Pointer(arrayPtr)),
length,
)
out := make([]string, 0, length)
for _, cstr := range ptrSlice {
out = append(out, GoString(uintptr(unsafe.Pointer(cstr))))
}
return out
}
// convert a Go slice of driver.Value to a slice of tursoValue that can be sent over FFI
func buildArgs(args []driver.Value) ([]tursoValue, error) {
argSlice := make([]tursoValue, len(args))
for i, v := range args {
switch val := v.(type) {
case nil:
argSlice[i].Type = nullVal
case int64:
argSlice[i].Type = intVal
storeInt64(&argSlice[i].Value, val)
case float64:
argSlice[i].Type = realVal
storeFloat64(&argSlice[i].Value, val)
case string:
argSlice[i].Type = textVal
cstr := CString(val)
storePointer(&argSlice[i].Value, cstr)
case []byte:
argSlice[i].Type = blobVal
blob := makeBlob(val)
*(*uintptr)(unsafe.Pointer(&argSlice[i].Value)) = uintptr(unsafe.Pointer(blob))
default:
return nil, fmt.Errorf("unsupported type: %T", v)
}
}
return argSlice, nil
}
func storeInt64(data *[8]byte, val int64) {
*(*int64)(unsafe.Pointer(data)) = val
}
func storeFloat64(data *[8]byte, val float64) {
*(*float64)(unsafe.Pointer(data)) = val
}
func storePointer(data *[8]byte, ptr *byte) {
*(*uintptr)(unsafe.Pointer(data)) = uintptr(unsafe.Pointer(ptr))
}
type stmtExecuteFn func(stmtPtr uintptr, argsPtr uintptr, argCount uint64, changes uintptr) int32
type stmtQueryFn func(stmtPtr uintptr, argsPtr uintptr, argCount uint64) uintptr
/* Credit below (Apache2 License) to:
https://github.com/ebitengine/purego/blob/main/internal/strings/strings.go
*/
func hasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
func CString(name string) *byte {
if hasSuffix(name, "\x00") {
return &(*(*[]byte)(unsafe.Pointer(&name)))[0]
}
b := make([]byte, len(name)+1)
copy(b, name)
return &b[0]
}
func GoString(c uintptr) string {
ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c))
if ptr == nil {
return ""
}
var length int
for {
if *(*byte)(unsafe.Add(ptr, uintptr(length))) == '\x00' {
break
}
length++
}
return string(unsafe.Slice((*byte)(ptr), length))
}

View file

@ -427,6 +427,10 @@ impl Connection {
let prev_total_changes = self.total_changes.get();
self.total_changes.set(prev_total_changes + nchange);
}
pub fn total_changes(&self) -> i64 {
self.total_changes.get()
}
}
pub struct Statement {
@ -473,6 +477,10 @@ impl Statement {
&self.program.parameters
}
pub fn parameters_count(&self) -> usize {
self.program.parameters.count()
}
pub fn bind_at(&mut self, index: NonZero<usize>, value: Value) {
self.state.bind_at(index, value.into());
}