Merge 'bindings/go: Begin implementation of Go database/sql driver' from Preston Thorpe

This WIP driver uses the [purego](github.com/ebitengine/purego) library,
that supports cross platform `Dlopen`/`Dlsym` and not a whole lot else.
I really didn't want to use CGO, have very little experience with WASM
and I heard nothing but good things about this library. It's very easy
to use and stable especially when you consider the use case here of 3
functions.
![image](https://github.com/user-
attachments/assets/ae28c8f2-1d11-4d25-b999-22af8bd65a92)
NOTE: The WIP state that this PR is in right at this moment, is not able
to run these simple queries. This screengrab was taken from a couple
days ago when I wrote up a quick demo to load the library, call a simple
query and had it println! the result to make sure everything was working
properly.
I am opening this so kind of like the Java bindings, I can incrementally
work on this. I didn't want to submit a massive PR, try to keep them at
~1k lines max. The state of what's in this PR is highly subject and
likely to change.
I will update when they are at a working state where they can be tested
out and make sure they work across platforms.

Closes #776
This commit is contained in:
Pekka Enberg
2025-01-26 08:51:27 +02:00
13 changed files with 1304 additions and 0 deletions

7
Cargo.lock generated
View File

@@ -2572,6 +2572,13 @@ version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
[[package]]
name = "turso-go"
version = "0.0.13"
dependencies = [
"limbo_core",
]
[[package]]
name = "typenum"
version = "1.17.0"

View File

@@ -7,6 +7,7 @@ members = [
"bindings/python",
"bindings/rust",
"bindings/wasm",
"bindings/go",
"cli",
"core",
"extensions/core",

23
bindings/go/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "turso-go"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
name = "_turso_go"
crate-type = ["cdylib"]
path = "rs_src/lib.rs"
[features]
default = ["io_uring"]
io_uring = ["limbo_core/io_uring"]
[dependencies]
limbo_core = { path = "../../core/" }
[target.'cfg(target_os = "linux")'.dependencies]
limbo_core = { path = "../../core/", features = ["io_uring"] }

8
bindings/go/go.mod Normal file
View File

@@ -0,0 +1,8 @@
module turso
go 1.23.4
require (
github.com/ebitengine/purego v0.8.2
golang.org/x/sys/windows v0.29.0
)

4
bindings/go/go.sum Normal file
View File

@@ -0,0 +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=

203
bindings/go/rs_src/lib.rs Normal file
View File

@@ -0,0 +1,203 @@
mod rows;
#[allow(dead_code)]
mod statement;
mod types;
use limbo_core::{Connection, Database, LimboError};
use std::{
ffi::{c_char, c_void},
rc::Rc,
str::FromStr,
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() {
println!("Path is null");
return std::ptr::null_mut();
}
let path = unsafe { std::ffi::CStr::from_ptr(path) };
let path = path.to_str().unwrap();
let db_options = parse_query_str(path);
if let Ok(io) = get_io(&db_options.path) {
let db = Database::open_file(io.clone(), &db_options.path.to_string());
match db {
Ok(db) => {
println!("Opened database: {}", path);
let conn = db.connect();
return TursoConn::new(conn, io).to_ptr();
}
Err(e) => {
println!("Error opening database: {}", e);
return std::ptr::null_mut();
}
};
}
std::ptr::null_mut()
}
#[allow(dead_code)]
struct TursoConn {
conn: Rc<Connection>,
io: Arc<dyn limbo_core::IO>,
}
impl TursoConn {
fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
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 {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoConn) }
}
}
/// Close the database connection
/// # Safety
/// safely frees the connection's memory
#[no_mangle]
pub unsafe extern "C" fn db_close(db: *mut c_void) {
if !db.is_null() {
let _ = unsafe { Box::from_raw(db as *mut TursoConn) };
}
}
#[allow(clippy::arc_with_non_send_sync)]
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()?),
_ => {
return Ok(Arc::new(limbo_core::PlatformIO::new()?));
}
})
}
#[allow(dead_code)]
struct DbOptions {
path: DbType,
params: Parameters,
}
#[derive(Default, Debug, Clone)]
enum DbType {
File(String),
#[default]
Memory,
}
impl std::fmt::Display for DbType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DbType::File(path) => write!(f, "{}", path),
DbType::Memory => write!(f, ":memory:"),
}
}
}
#[derive(Debug, Clone, Default)]
struct Parameters {
mode: Mode,
cache: Option<Cache>,
vfs: Option<String>,
nolock: bool,
immutable: bool,
modeof: Option<String>,
}
impl FromStr for Parameters {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.contains('?') {
return Ok(Parameters::default());
}
let mut params = Parameters::default();
for param in s.split('?').nth(1).unwrap().split('&') {
let mut kv = param.split('=');
match kv.next() {
Some("mode") => params.mode = kv.next().unwrap().parse().unwrap(),
Some("cache") => params.cache = Some(kv.next().unwrap().parse().unwrap()),
Some("vfs") => params.vfs = Some(kv.next().unwrap().to_string()),
Some("nolock") => params.nolock = true,
Some("immutable") => params.immutable = true,
Some("modeof") => params.modeof = Some(kv.next().unwrap().to_string()),
_ => {}
}
}
Ok(params)
}
}
#[derive(Default, Debug, Clone, Copy)]
enum Cache {
Shared,
#[default]
Private,
}
impl FromStr for Cache {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"shared" => Ok(Cache::Shared),
_ => Ok(Cache::Private),
}
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Default, Debug, Clone, Copy)]
enum Mode {
ReadOnly,
ReadWrite,
#[default]
ReadWriteCreate,
}
impl FromStr for Mode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"readonly" | "ro" => Ok(Mode::ReadOnly),
"readwrite" | "rw" => Ok(Mode::ReadWrite),
"readwritecreate" | "rwc" => Ok(Mode::ReadWriteCreate),
_ => Ok(Mode::default()),
}
}
}
// At this point we don't have configurable parameters but many
// DSN's are going to have query parameters
fn parse_query_str(mut path: &str) -> DbOptions {
if path == ":memory:" {
return DbOptions {
path: DbType::Memory,
params: Parameters::default(),
};
}
if path.starts_with("sqlite://") {
path = &path[10..];
}
if path.contains('?') {
let parameters = Parameters::from_str(path).unwrap();
let path = &path[..path.find('?').unwrap()];
DbOptions {
path: DbType::File(path.to_string()),
params: parameters,
}
} else {
DbOptions {
path: DbType::File(path.to_string()),
params: Parameters::default(),
}
}
}

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

@@ -0,0 +1,139 @@
use crate::rows::TursoRows;
use crate::types::{AllocPool, ResultCode, TursoValue};
use crate::TursoConn;
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 {
if ctx.is_null() || query.is_null() {
return std::ptr::null_mut();
}
let query_str = unsafe { std::ffi::CStr::from_ptr(query) }.to_str().unwrap();
let db = TursoConn::from_ptr(ctx);
let stmt = db.conn.prepare(query_str.to_string());
match stmt {
Ok(stmt) => TursoStatement::new(stmt, db).to_ptr(),
Err(_) => std::ptr::null_mut(),
}
}
#[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;
}
}
}
}
#[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<'conn> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoStatement) }
}
}

190
bindings/go/rs_src/types.rs Normal file
View File

@@ -0,0 +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,
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,
}
}
}

194
bindings/go/stmt.go Normal file
View File

@@ -0,0 +1,194 @@
package turso
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"io"
"unsafe"
)
// 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
}
// 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 (st *tursoStmt) NumInput() int {
return int(st.getParamCount(st.ctx))
}
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)
}
}
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("query failed for: %q", st.sql)
}
return initRows(rowsPtr), nil
}
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 (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 (r *tursoRows) Next(dest []driver.Value) error {
status := r.next(r.ctx)
switch ResultCode(status) {
case Row:
for i := range dest {
valPtr := r.getValue(r.ctx, int32(i))
val := toGoValue(valPtr)
dest[i] = val
}
return nil
case Done:
return io.EOF
default:
return fmt.Errorf("unexpected status: %d", status)
}
}

141
bindings/go/turso.go Normal file
View File

@@ -0,0 +1,141 @@
package turso
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"
const driverName = "turso"
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))
}
return nil
}
func init() {
err := getSystemLibrary()
if err != nil {
slog.Error("Error opening turso library: ", err)
os.Exit(1)
}
sql.Register(driverName, &tursoDriver{})
}
type tursoDriver struct{}
func (d tursoDriver) Open(name string) (driver.Conn, error) {
return openConn(name)
}
func toCString(s string) uintptr {
b := append([]byte(s), 0)
return uintptr(unsafe.Pointer(&b[0]))
}
// helper to register an FFI function in the lib_turso_go library
func getFfiFunc(ptr interface{}, name string) {
purego.RegisterLibFunc(&ptr, tursoLib, name)
}
type tursoConn struct {
ctx uintptr
sync.Mutex
prepare func(uintptr, uintptr) uintptr
}
func newConn(ctx uintptr) *tursoConn {
var prepare func(uintptr, uintptr) uintptr
getFfiFunc(&prepare, FfiDbPrepare)
return &tursoConn{
ctx,
sync.Mutex{},
prepare,
}
}
func openConn(dsn string) (*tursoConn, error) {
var dbOpen func(uintptr) uintptr
getFfiFunc(&dbOpen, FfiDbOpen)
cStr := toCString(dsn)
defer freeCString(cStr)
ctx := dbOpen(cStr)
if ctx == 0 {
return nil, fmt.Errorf("failed to open database for dsn=%q", dsn)
}
return &tursoConn{ctx: ctx}, nil
}
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
}
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
}
// 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")
}

248
bindings/go/types.go Normal file
View File

@@ -0,0 +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
Io ResultCode = 3
Interrupt ResultCode = 4
Invalid ResultCode = 5
Null ResultCode = 6
NoMem ResultCode = 7
ReadOnly ResultCode = 8
NoData ResultCode = 9
Done ResultCode = 10
)
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());
}