Remove Go bindings (moved to their own repo tursodatabase/turso-go)

This commit is contained in:
PThorpe92
2025-08-26 19:13:17 -04:00
parent d122105f8c
commit bcdcd47358
21 changed files with 0 additions and 2741 deletions

7
Cargo.lock generated
View File

@@ -3977,13 +3977,6 @@ dependencies = [
"turso_core",
]
[[package]]
name = "turso-go"
version = "0.1.4"
dependencies = [
"turso_core",
]
[[package]]
name = "turso-java"
version = "0.1.4"

View File

@@ -4,7 +4,6 @@
resolver = "2"
members = [
"bindings/dart/rust",
"bindings/go",
"bindings/java",
"bindings/javascript",
"bindings/python",

View File

@@ -1,4 +0,0 @@
# Ignore generated libraries directory
/libs/*
# But keep the directory structure
!/libs/.gitkeep

View File

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

View File

@@ -1,117 +0,0 @@
# Turso driver for Go's `database/sql` library
**NOTE:** this is currently __heavily__ W.I.P and is not yet in a usable state.
This driver uses the awesome [purego](https://github.com/ebitengine/purego) library to call C (in this case Rust with C ABI) functions from Go without the use of `CGO`.
## Embedded Library Support
This driver includes an embedded library feature that allows you to distribute a single binary without requiring users to set environment variables. The library for your platform is automatically embedded, extracted at runtime, and loaded dynamically.
### Building from Source
To build with embedded library support, follow these steps:
```bash
# Clone the repository
git clone https://github.com/tursodatabase/turso
# Navigate to the Go bindings directory
cd turso/bindings/go
# Build the library (defaults to release build)
./build_lib.sh
# Alternatively, for faster builds during development:
./build_lib.sh debug
```
### Build Options:
* Release Build (default): ./build_lib.sh or ./build_lib.sh release
- Optimized for performance and smaller binary size
- Takes longer to compile and requires more system resources
- Recommended for production use
* Debug Build: ./build_lib.sh debug
- Faster compilation times with less resource usage
- Larger binary size and slower runtime performance
- Recommended during development or if release build fails
If the embedded library cannot be found or extracted, the driver will fall back to the traditional method of finding the library in the system paths.
## To use: (_UNSTABLE_ testing or development purposes only)
### Option 1: Using the embedded library (recommended)
Build the driver with the embedded library as described above, then simply import and use. No environment variables needed!
### Option 2: Manual library setup
#### Linux | MacOS
_All commands listed are relative to the bindings/go directory in the turso repository_
```
cargo build --package turso-go
# Your LD_LIBRARY_PATH environment variable must include turso's `target/debug` directory
export LD_LIBRARY_PATH="/path/to/turso/target/debug:$LD_LIBRARY_PATH"
```
#### Windows
```
cargo build --package turso-go
# You must add turso's `target/debug` directory to your PATH
# or you could built + copy the .dll to a location in your PATH
# or just the CWD of your go module
cp path\to\turso\target\debug\lib_turso_go.dll .
go test
```
**Temporarily** you may have to clone the turso repository and run:
`go mod edit -replace github.com/tursodatabase/turso=/path/to/turso/bindings/go`
```go
import (
"fmt"
"database/sql"
_"github.com/tursodatabase/turso"
)
func main() {
conn, err := sql.Open("turso", ":memory:")
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
sql := "CREATE table go_turso (foo INTEGER, bar TEXT)"
_ = conn.Exec(sql)
sql = "INSERT INTO go_turso (foo, bar) values (?, ?)"
stmt, _ := conn.Prepare(sql)
defer stmt.Close()
_ = stmt.Exec(42, "turso")
rows, _ := conn.Query("SELECT * from go_turso")
defer rows.Close()
for rows.Next() {
var a int
var b string
_ = rows.Scan(&a, &b)
fmt.Printf("%d, %s", a, b)
}
}
```
## Implementation Notes
The embedded library feature was inspired by projects like [go-embed-python](https://github.com/kluctl/go-embed-python), which uses a similar approach for embedding and distributing native libraries with Go applications.

View File

@@ -1,70 +0,0 @@
#!/bin/bash
# bindings/go/build_lib.sh
set -e
# Accept build type as parameter, default to release
BUILD_TYPE=${1:-release}
echo "Building turso Go library for current platform (build type: $BUILD_TYPE)..."
# Determine platform-specific details
case "$(uname -s)" in
Darwin*)
OUTPUT_NAME="lib_turso_go.dylib"
# Map x86_64 to amd64 for Go compatibility
ARCH=$(uname -m)
if [ "$ARCH" == "x86_64" ]; then
ARCH="amd64"
fi
PLATFORM="darwin_${ARCH}"
;;
Linux*)
OUTPUT_NAME="lib_turso_go.so"
# Map x86_64 to amd64 for Go compatibility
ARCH=$(uname -m)
if [ "$ARCH" == "x86_64" ]; then
ARCH="amd64"
fi
PLATFORM="linux_${ARCH}"
;;
MINGW*|MSYS*|CYGWIN*)
OUTPUT_NAME="lib_turso_go.dll"
if [ "$(uname -m)" == "x86_64" ]; then
PLATFORM="windows_amd64"
else
PLATFORM="windows_386"
fi
;;
*)
echo "Unsupported platform: $(uname -s)"
exit 1
;;
esac
# Create output directory
OUTPUT_DIR="libs/${PLATFORM}"
mkdir -p "$OUTPUT_DIR"
# Set cargo build arguments based on build type
if [ "$BUILD_TYPE" == "debug" ]; then
CARGO_ARGS=""
TARGET_DIR="debug"
echo "NOTE: Debug builds are faster to compile but less efficient at runtime."
echo " For production use, consider using a release build with: ./build_lib.sh release"
else
CARGO_ARGS="--release"
TARGET_DIR="release"
echo "NOTE: Release builds may take longer to compile and require more system resources."
echo " If this build fails or takes too long, try a debug build with: ./build_lib.sh debug"
fi
# Build the library
echo "Running cargo build ${CARGO_ARGS} --package turso-go"
cargo build ${CARGO_ARGS} --package turso-go
# Copy to the appropriate directory
echo "Copying $OUTPUT_NAME to $OUTPUT_DIR/"
cp "../../target/${TARGET_DIR}/$OUTPUT_NAME" "$OUTPUT_DIR/"
echo "Library built successfully for $PLATFORM ($BUILD_TYPE build)"

View File

@@ -1,229 +0,0 @@
package turso
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"sync"
"github.com/ebitengine/purego"
)
func init() {
err := ensureLibLoaded()
if err != nil {
panic(err)
}
sql.Register(driverName, &tursoDriver{})
}
type tursoDriver struct {
sync.Mutex
}
var (
libOnce sync.Once
tursoLib uintptr
loadErr error
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
stmtGetError func(uintptr) uintptr
stmtClose func(uintptr) int32
)
// Register all the symbols on library load
func ensureLibLoaded() error {
libOnce.Do(func() {
tursoLib, loadErr = loadLibrary()
if loadErr != nil {
return
}
purego.RegisterLibFunc(&dbOpen, tursoLib, FfiDbOpen)
purego.RegisterLibFunc(&dbClose, tursoLib, FfiDbClose)
purego.RegisterLibFunc(&connPrepare, tursoLib, FfiDbPrepare)
purego.RegisterLibFunc(&connGetError, tursoLib, FfiDbGetError)
purego.RegisterLibFunc(&freeBlobFunc, tursoLib, FfiFreeBlob)
purego.RegisterLibFunc(&freeStringFunc, tursoLib, FfiFreeCString)
purego.RegisterLibFunc(&rowsGetColumns, tursoLib, FfiRowsGetColumns)
purego.RegisterLibFunc(&rowsGetColumnName, tursoLib, FfiRowsGetColumnName)
purego.RegisterLibFunc(&rowsGetValue, tursoLib, FfiRowsGetValue)
purego.RegisterLibFunc(&closeRows, tursoLib, FfiRowsClose)
purego.RegisterLibFunc(&rowsNext, tursoLib, FfiRowsNext)
purego.RegisterLibFunc(&rowsGetError, tursoLib, FfiRowsGetError)
purego.RegisterLibFunc(&stmtQuery, tursoLib, FfiStmtQuery)
purego.RegisterLibFunc(&stmtExec, tursoLib, FfiStmtExec)
purego.RegisterLibFunc(&stmtParamCount, tursoLib, FfiStmtParameterCount)
purego.RegisterLibFunc(&stmtGetError, tursoLib, FfiStmtGetError)
purego.RegisterLibFunc(&stmtClose, tursoLib, FfiStmtClose)
})
return loadErr
}
func (d *tursoDriver) Open(name string) (driver.Conn, error) {
d.Lock()
conn, err := openConn(name)
d.Unlock()
if err != nil {
return nil, err
}
return conn, nil
}
type tursoConn struct {
sync.Mutex
ctx uintptr
}
func openConn(dsn string) (*tursoConn, error) {
ctx := dbOpen(dsn)
if ctx == 0 {
return nil, fmt.Errorf("failed to open database for dsn=%q", dsn)
}
return &tursoConn{
sync.Mutex{},
ctx,
}, loadErr
}
func (c *tursoConn) Close() error {
if c.ctx == 0 {
return nil
}
c.Lock()
dbClose(c.ctx)
c.Unlock()
c.ctx = 0
return nil
}
func (c *tursoConn) 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 *tursoConn) Prepare(query string) (driver.Stmt, error) {
if c.ctx == 0 {
return nil, errors.New("connection closed")
}
c.Lock()
defer c.Unlock()
stmtPtr := connPrepare(c.ctx, query)
if stmtPtr == 0 {
return nil, c.getError()
}
return newStmt(stmtPtr, query), nil
}
// tursoTx implements driver.Tx
type tursoTx struct {
conn *tursoConn
}
// Begin starts a new transaction with default isolation level
func (c *tursoConn) Begin() (driver.Tx, error) {
c.Lock()
defer c.Unlock()
if c.ctx == 0 {
return nil, errors.New("connection closed")
}
// Execute BEGIN statement
stmtPtr := connPrepare(c.ctx, "BEGIN")
if stmtPtr == 0 {
return nil, c.getError()
}
stmt := newStmt(stmtPtr, "BEGIN")
defer stmt.Close()
_, err := stmt.Exec(nil)
if err != nil {
return nil, err
}
return &tursoTx{conn: c}, nil
}
// BeginTx starts a transaction with the specified options.
// Currently only supports default isolation level and non-read-only transactions.
func (c *tursoConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
// Skip handling non-default isolation levels and read-only mode
// for now, letting database/sql package handle these cases
if opts.Isolation != driver.IsolationLevel(sql.LevelDefault) || opts.ReadOnly {
return nil, driver.ErrSkip
}
// Check for context cancellation
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return c.Begin()
}
}
// Commit commits the transaction
func (tx *tursoTx) Commit() error {
tx.conn.Lock()
defer tx.conn.Unlock()
if tx.conn.ctx == 0 {
return errors.New("connection closed")
}
stmtPtr := connPrepare(tx.conn.ctx, "COMMIT")
if stmtPtr == 0 {
return tx.conn.getError()
}
stmt := newStmt(stmtPtr, "COMMIT")
defer stmt.Close()
_, err := stmt.Exec(nil)
return err
}
// Rollback aborts the transaction.
func (tx *tursoTx) Rollback() error {
tx.conn.Lock()
defer tx.conn.Unlock()
if tx.conn.ctx == 0 {
return errors.New("connection closed")
}
stmtPtr := connPrepare(tx.conn.ctx, "ROLLBACK")
if stmtPtr == 0 {
return tx.conn.getError()
}
stmt := newStmt(stmtPtr, "ROLLBACK")
defer stmt.Close()
_, err := stmt.Exec(nil)
return err
}

View File

@@ -1,133 +0,0 @@
// Go bindings for the turso database.
//
// This file implements library embedding and extraction at runtime, a pattern
// also used in several other Go projects that need to distribute native binaries:
//
// - github.com/kluctl/go-embed-python: Embeds a full Python distribution in Go
// binaries, extracting to temporary directories at runtime. The approach used here
// was directly inspired by its embed_util implementation.
//
// - github.com/kluctl/go-jinja2: Uses the same pattern to embed Jinja2 and related
// Python libraries, allowing Go applications to use Jinja2 templates without
// external dependencies.
//
// This approach has several advantages:
// - Allows distribution of a single, self-contained binary
// - Eliminates the need for users to set LD_LIBRARY_PATH or other environment variables
// - Works cross-platform with the same codebase
// - Preserves backward compatibility with existing methods
// - Extracts libraries only once per execution via sync.Once
//
// The embedded library is extracted to a user-specific temporary directory and
// loaded dynamically. If extraction fails, the code falls back to the traditional
// method of searching system paths.
package turso
import (
"embed"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sync"
)
//go:embed libs/*
var embeddedLibs embed.FS
var (
extractOnce sync.Once
extractedPath string
extractErr error
)
// extractEmbeddedLibrary extracts the library for the current platform
// to a temporary directory and returns the path to the extracted library
func extractEmbeddedLibrary() (string, error) {
extractOnce.Do(func() {
// Determine platform-specific details
var libName string
var platformDir string
switch runtime.GOOS {
case "darwin":
libName = "lib_turso_go.dylib"
case "linux":
libName = "lib_turso_go.so"
case "windows":
libName = "lib_turso_go.dll"
default:
extractErr = fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
return
}
// Determine architecture suffix
var archSuffix string
switch runtime.GOARCH {
case "amd64":
archSuffix = "amd64"
case "arm64":
archSuffix = "arm64"
case "386":
archSuffix = "386"
default:
extractErr = fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
return
}
// Create platform directory string
platformDir = fmt.Sprintf("%s_%s", runtime.GOOS, archSuffix)
// Create a unique temporary directory for the current user
tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("turso-go-%d", os.Getuid()))
if err := os.MkdirAll(tempDir, 0755); err != nil {
extractErr = fmt.Errorf("failed to create temp directory: %w", err)
return
}
// Path to the library within the embedded filesystem
libPath := filepath.Join("libs", platformDir, libName)
// Where the library will be extracted
extractedPath = filepath.Join(tempDir, libName)
// Check if library already exists and is valid
if stat, err := os.Stat(extractedPath); err == nil && stat.Size() > 0 {
// Library already exists, nothing to do
return
}
// Open the embedded library
embeddedLib, err := embeddedLibs.Open(libPath)
if err != nil {
extractErr = fmt.Errorf("failed to open embedded library %s: %w", libPath, err)
return
}
defer embeddedLib.Close()
// Create the output file
outFile, err := os.Create(extractedPath)
if err != nil {
extractErr = fmt.Errorf("failed to create output file: %w", err)
return
}
defer outFile.Close()
// Copy the library to the temporary directory
if _, err := io.Copy(outFile, embeddedLib); err != nil {
extractErr = fmt.Errorf("failed to extract library: %w", err)
return
}
// On Unix systems, make the library executable
if runtime.GOOS != "windows" {
if err := os.Chmod(extractedPath, 0755); err != nil {
extractErr = fmt.Errorf("failed to make library executable: %w", err)
return
}
}
})
return extractedPath, extractErr
}

View File

@@ -1,8 +0,0 @@
module github.com/tursodatabase/turso
go 1.23.4
require (
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615
golang.org/x/sys v0.29.0
)

View File

@@ -1,6 +0,0 @@
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 h1:W7mpP4uiOAbBOdDnRXT9EUdauFv7bz+ERT5rPIord00=
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615/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,121 +0,0 @@
package turso
import (
"database/sql/driver"
"errors"
"fmt"
"io"
"sync"
)
type tursoRows struct {
mu sync.Mutex
ctx uintptr
columns []string
err error
closed bool
}
func newRows(ctx uintptr) *tursoRows {
return &tursoRows{
mu: sync.Mutex{},
ctx: ctx,
columns: nil,
err: nil,
closed: false,
}
}
func (r *tursoRows) isClosed() bool {
if r.ctx == 0 || r.closed {
return true
}
return false
}
func (r *tursoRows) Columns() []string {
if r.isClosed() {
return nil
}
if r.columns == nil {
r.mu.Lock()
count := rowsGetColumns(r.ctx)
if count > 0 {
columns := make([]string, 0, count)
for i := 0; i < int(count); i++ {
cstr := rowsGetColumnName(r.ctx, int32(i))
columns = append(columns, fmt.Sprintf("%s", GoString(cstr)))
freeCString(cstr)
}
r.mu.Unlock()
r.columns = columns
}
}
return r.columns
}
func (r *tursoRows) Close() error {
r.err = errors.New(RowsClosedErr)
if r.isClosed() {
return r.err
}
r.mu.Lock()
r.closed = true
closeRows(r.ctx)
r.ctx = 0
r.mu.Unlock()
return nil
}
func (r *tursoRows) Err() error {
if r.err == nil {
r.mu.Lock()
defer r.mu.Unlock()
r.getError()
}
return r.err
}
func (r *tursoRows) 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) {
case Row:
for i := range dest {
valPtr := rowsGetValue(r.ctx, int32(i))
val := toGoValue(valPtr)
if val == nil {
r.getError()
}
dest[i] = val
}
return nil
case Io:
continue
case Done:
return io.EOF
default:
return r.getError()
}
}
}
// mutex will already be locked. this is always called after FFI
func (r *tursoRows) 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

@@ -1,88 +0,0 @@
mod rows;
#[allow(dead_code)]
mod statement;
mod types;
use std::{
ffi::{c_char, c_void},
sync::Arc,
};
use turso_core::{Connection, LimboError};
/// # Safety
/// Safe to be called from Go with null terminated DSN string.
/// performs null check on the path.
#[no_mangle]
#[allow(clippy::arc_with_non_send_sync)]
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 Ok((io, conn)) = Connection::from_uri(path, true, false, false) else {
panic!("Failed to open connection with path: {path}");
};
TursoConn::new(conn, io).to_ptr()
}
#[allow(dead_code)]
struct TursoConn {
conn: Arc<Connection>,
io: Arc<dyn turso_core::IO>,
err: Option<LimboError>,
}
impl TursoConn {
fn new(conn: Arc<Connection>, io: Arc<dyn turso_core::IO>) -> Self {
TursoConn {
conn,
io,
err: None,
}
}
#[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) }
}
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 = TursoConn::from_ptr(ctx);
conn.get_error()
}
/// 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) };
}
}

View File

@@ -1,144 +0,0 @@
use crate::{
types::{ResultCode, TursoValue},
TursoConn,
};
use std::ffi::{c_char, c_void};
use turso_core::{LimboError, Statement, StepResult, Value};
pub struct TursoRows<'conn> {
stmt: Box<Statement>,
_conn: &'conn mut TursoConn,
err: Option<LimboError>,
}
impl<'conn> TursoRows<'conn> {
pub fn new(stmt: Statement, conn: &'conn mut TursoConn) -> Self {
TursoRows {
stmt: Box::new(stmt),
_conn: conn,
err: 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) -> &'conn mut TursoRows<'conn> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoRows) }
}
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]
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.stmt.step() {
Ok(StepResult::Row) => ResultCode::Row,
Ok(StepResult::Done) => ResultCode::Done,
Ok(StepResult::IO) => {
let res = ctx.stmt.run_once();
if res.is_err() {
ResultCode::Error
} else {
ResultCode::Io
}
}
Ok(StepResult::Busy) => ResultCode::Busy,
Ok(StepResult::Interrupt) => ResultCode::Interrupt,
Err(err) => {
ctx.err = Some(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(row) = ctx.stmt.row() {
if let Ok(value) = row.get::<&Value>(col_idx) {
return TursoValue::from_db_value(value).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)) };
}
}
/// Function to get the number of expected ResultColumns in the prepared statement.
/// to avoid the needless complexity of returning an array of strings, this instead
/// works like rows_next/rows_get_value
#[no_mangle]
pub extern "C" fn rows_get_columns(rows_ptr: *mut c_void) -> i32 {
if rows_ptr.is_null() {
return -1;
}
let rows = TursoRows::from_ptr(rows_ptr);
rows.stmt.num_columns() as i32
}
/// Returns a pointer to a string with the name of the column at the given index.
/// The caller is responsible for freeing the memory, it should be copied on the Go side
/// immediately and 'free_string' called
#[no_mangle]
pub extern "C" fn rows_get_column_name(rows_ptr: *mut c_void, idx: i32) -> *const c_char {
if rows_ptr.is_null() {
return std::ptr::null_mut();
}
let rows = TursoRows::from_ptr(rows_ptr);
if idx < 0 || idx as usize >= rows.stmt.num_columns() {
return std::ptr::null_mut();
}
let name = rows.stmt.get_column_name(idx as usize);
let cstr = std::ffi::CString::new(name.as_bytes()).expect("Failed to create CString");
cstr.into_raw() as *const c_char
}
#[no_mangle]
pub extern "C" fn rows_get_error(ctx: *mut c_void) -> *const c_char {
if ctx.is_null() {
return std::ptr::null();
}
let ctx = TursoRows::from_ptr(ctx);
ctx.get_error()
}
#[no_mangle]
pub extern "C" fn rows_close(ctx: *mut c_void) {
if !ctx.is_null() {
let rows = TursoRows::from_ptr(ctx);
rows.stmt.reset();
rows.err = None;
}
unsafe {
let _ = Box::from_raw(ctx.cast::<TursoRows>());
}
}

View File

@@ -1,183 +0,0 @@
use crate::rows::TursoRows;
use crate::types::{AllocPool, ResultCode, TursoValue};
use crate::TursoConn;
use std::ffi::{c_char, c_void};
use std::num::NonZero;
use turso_core::{LimboError, Statement, StepResult};
#[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);
match stmt {
Ok(stmt) => TursoStatement::new(Some(stmt), db).to_ptr(),
Err(err) => {
db.err = Some(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 {
&[]
};
let mut pool = AllocPool::new();
let Some(statement) = stmt.statement.as_mut() else {
return ResultCode::Error;
};
for (i, arg) in args.iter().enumerate() {
let val = arg.to_value(&mut pool);
statement.bind_at(NonZero::new(i + 1).unwrap(), val);
}
loop {
match statement.step() {
Ok(StepResult::Row) => {
// unexpected row during execution, error out.
return ResultCode::Error;
}
Ok(StepResult::Done) => {
let total_changes = stmt.conn.conn.total_changes();
if !changes.is_null() {
unsafe {
*changes = total_changes;
}
}
return ResultCode::Done;
}
Ok(StepResult::IO) => {
let res = statement.run_once();
if res.is_err() {
return ResultCode::Error;
}
}
Ok(StepResult::Busy) => {
return ResultCode::Busy;
}
Ok(StepResult::Interrupt) => {
return ResultCode::Interrupt;
}
Err(err) => {
stmt.conn.err = Some(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);
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
}
#[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 {
&[]
};
let mut pool = AllocPool::new();
let Some(mut statement) = stmt.statement.take() else {
return std::ptr::null_mut();
};
for (i, arg) in args.iter().enumerate() {
let val = arg.to_value(&mut pool);
statement.bind_at(NonZero::new(i + 1).unwrap(), val);
}
// ownership of the statement is transferred to the TursoRows object.
TursoRows::new(statement, stmt.conn).to_ptr()
}
pub struct TursoStatement<'conn> {
/// If 'query' is ran on the statement, ownership is transferred to the TursoRows object
pub statement: Option<Statement>,
pub conn: &'conn mut TursoConn,
pub err: Option<LimboError>,
}
#[no_mangle]
pub extern "C" fn stmt_close(ctx: *mut c_void) -> ResultCode {
if !ctx.is_null() {
let stmt = unsafe { Box::from_raw(ctx as *mut TursoStatement) };
drop(stmt);
return ResultCode::Ok;
}
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 = TursoStatement::from_ptr(ctx);
stmt.get_error()
}
impl<'conn> TursoStatement<'conn> {
pub fn new(statement: Option<Statement>, conn: &'conn mut TursoConn) -> Self {
TursoStatement {
statement,
conn,
err: None,
}
}
#[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) -> &'conn mut TursoStatement<'conn> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoStatement) }
}
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

@@ -1,235 +0,0 @@
use std::{
ffi::{c_char, c_void},
fmt::Debug,
};
#[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,
SyntaxErr = 11,
ConstraintViolation = 12,
NoSuchEntity = 13,
}
#[derive(Debug)]
#[repr(C)]
pub enum ValueType {
Integer = 0,
Text = 1,
Blob = 2,
Real = 3,
Null = 4,
}
#[repr(C)]
pub struct TursoValue {
value_type: ValueType,
value: ValueUnion,
}
impl Debug for TursoValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.value_type {
ValueType::Integer => {
let i = self.value.to_int();
f.debug_struct("TursoValue").field("value", &i).finish()
}
ValueType::Real => {
let r = self.value.to_real();
f.debug_struct("TursoValue").field("value", &r).finish()
}
ValueType::Text => {
let t = self.value.to_str();
f.debug_struct("TursoValue").field("value", &t).finish()
}
ValueType::Blob => {
let blob = self.value.to_bytes();
f.debug_struct("TursoValue")
.field("value", &blob.to_vec())
.finish()
}
ValueType::Null => f
.debug_struct("TursoValue")
.field("value", &"NULL")
.finish(),
}
}
}
#[repr(C)]
union ValueUnion {
int_val: i64,
real_val: f64,
text_ptr: *const c_char,
blob_ptr: *const c_void,
}
#[repr(C)]
struct Blob {
data: *const u8,
len: i64,
}
pub struct AllocPool {
strings: Vec<String>,
}
impl AllocPool {
pub fn new() -> Self {
AllocPool {
strings: Vec::new(),
}
}
pub fn add_string(&mut self, s: String) -> &String {
self.strings.push(s);
self.strings.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 {
let cstr = std::ffi::CString::new(s).expect("Failed to create CString");
ValueUnion {
text_ptr: cstr.into_raw(),
}
}
fn from_bytes(b: &[u8]) -> Self {
let blob = Box::new(Blob {
data: b.as_ptr(),
len: b.len() as i64,
});
ValueUnion {
blob_ptr: Box::into_raw(blob) as *const c_void,
}
}
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 {
if self.text_ptr.is_null() {
return "";
}
std::ffi::CStr::from_ptr(self.text_ptr)
.to_str()
.unwrap_or("")
}
}
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 as usize) }
}
}
impl TursoValue {
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_db_value(value: &turso_core::Value) -> Self {
match value {
turso_core::Value::Integer(i) => {
TursoValue::new(ValueType::Integer, ValueUnion::from_int(*i))
}
turso_core::Value::Float(r) => {
TursoValue::new(ValueType::Real, ValueUnion::from_real(*r))
}
turso_core::Value::Text(s) => {
TursoValue::new(ValueType::Text, ValueUnion::from_str(s.as_str()))
}
turso_core::Value::Blob(b) => {
TursoValue::new(ValueType::Blob, ValueUnion::from_bytes(b.as_slice()))
}
turso_core::Value::Null => TursoValue::new(ValueType::Null, ValueUnion::from_null()),
}
}
// The values we get from Go need to be temporarily owned by the statement until they are bound
// then they can be cleaned up immediately afterwards
pub fn to_value(&self, pool: &mut AllocPool) -> turso_core::Value {
match self.value_type {
ValueType::Integer => {
if unsafe { self.value.int_val == 0 } {
return turso_core::Value::Null;
}
turso_core::Value::Integer(unsafe { self.value.int_val })
}
ValueType::Real => {
if unsafe { self.value.real_val == 0.0 } {
return turso_core::Value::Null;
}
turso_core::Value::Float(unsafe { self.value.real_val })
}
ValueType::Text => {
if unsafe { self.value.text_ptr.is_null() } {
return turso_core::Value::Null;
}
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();
let borrowed = pool.add_string(owned);
turso_core::Value::build_text(borrowed)
}
Err(_) => turso_core::Value::Null,
}
}
ValueType::Blob => {
if unsafe { self.value.blob_ptr.is_null() } {
return turso_core::Value::Null;
}
let bytes = self.value.to_bytes();
turso_core::Value::Blob(bytes.to_vec())
}
ValueType::Null => turso_core::Value::Null,
}
}
}

View File

@@ -1,176 +0,0 @@
package turso
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"sync"
"unsafe"
)
type tursoStmt struct {
mu sync.Mutex
ctx uintptr
sql string
err error
}
func newStmt(ctx uintptr, sql string) *tursoStmt {
return &tursoStmt{
ctx: uintptr(ctx),
sql: sql,
err: nil,
}
}
func (ls *tursoStmt) NumInput() int {
ls.mu.Lock()
defer ls.mu.Unlock()
res := int(stmtParamCount(ls.ctx))
if res < 0 {
// set the error from rust
_ = ls.getError()
}
return res
}
func (ls *tursoStmt) Close() error {
ls.mu.Lock()
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())
}
return nil
}
func (ls *tursoStmt) Exec(args []driver.Value) (driver.Result, error) {
argArray, cleanup, err := buildArgs(args)
defer cleanup()
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
ls.mu.Lock()
defer ls.mu.Unlock()
rc := stmtExec(ls.ctx, argPtr, argCount, uintptr(unsafe.Pointer(&changes)))
switch ResultCode(rc) {
case Ok, Done:
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, ls.getError()
}
}
func (ls *tursoStmt) Query(args []driver.Value) (driver.Rows, error) {
queryArgs, cleanup, err := buildArgs(args)
defer cleanup()
if err != nil {
return nil, err
}
argPtr := uintptr(0)
if len(args) > 0 {
argPtr = uintptr(unsafe.Pointer(&queryArgs[0]))
}
ls.mu.Lock()
defer ls.mu.Unlock()
rowsPtr := stmtQuery(ls.ctx, argPtr, uint64(len(queryArgs)))
if rowsPtr == 0 {
return nil, ls.getError()
}
return newRows(rowsPtr), nil
}
func (ls *tursoStmt) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
stripped := namedValueToValue(args)
argArray, cleanup, err := getArgsPtr(stripped)
defer cleanup()
if err != nil {
return nil, err
}
ls.mu.Lock()
select {
case <-ctx.Done():
ls.mu.Unlock()
return nil, ctx.Err()
default:
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()
}
}
}
func (ls *tursoStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
queryArgs, allocs, err := buildNamedArgs(args)
defer allocs()
if err != nil {
return nil, err
}
argsPtr := uintptr(0)
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
}
}
func (ls *tursoStmt) 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 *tursoStmt) 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

@@ -1,774 +0,0 @@
package turso_test
import (
"database/sql"
"fmt"
"log"
"math"
"slices"
"testing"
_ "github.com/tursodatabase/turso"
)
var (
conn *sql.DB
connErr error
)
func TestMain(m *testing.M) {
conn, connErr = sql.Open("turso", ":memory:")
if connErr != nil {
panic(connErr)
}
defer conn.Close()
err := createTable(conn)
if err != nil {
log.Fatalf("Error creating table: %v", err)
}
m.Run()
}
func TestInsertData(t *testing.T) {
err := insertData(conn)
if err != nil {
t.Fatalf("Error inserting data: %v", err)
}
}
func TestQuery(t *testing.T) {
query := "SELECT * FROM test;"
stmt, err := conn.Prepare(query)
if err != nil {
t.Fatalf("Error preparing query: %v", err)
}
defer stmt.Close()
rows, err := stmt.Query()
if err != nil {
t.Fatalf("Error executing query: %v", err)
}
defer rows.Close()
expectedCols := []string{"foo", "bar", "baz"}
cols, err := rows.Columns()
if err != nil {
t.Fatalf("Error getting columns: %v", err)
}
if len(cols) != len(expectedCols) {
t.Fatalf("Expected %d columns, got %d", len(expectedCols), len(cols))
}
for i, col := range cols {
if col != expectedCols[i] {
t.Errorf("Expected column %d to be %s, got %s", i, expectedCols[i], col)
}
}
i := 1
for rows.Next() {
var a int
var b string
var c []byte
err = rows.Scan(&a, &b, &c)
if err != nil {
t.Fatalf("Error scanning row: %v", err)
}
if a != i || b != rowsMap[i] || !slicesAreEq(c, []byte(rowsMap[i])) {
t.Fatalf("Expected %d, %s, %s, got %d, %s, %s", i, rowsMap[i], rowsMap[i], a, b, string(c))
}
fmt.Println("RESULTS: ", a, b, string(c))
i++
}
if err = rows.Err(); err != nil {
t.Fatalf("Row iteration error: %v", err)
}
}
func TestFunctions(t *testing.T) {
insert := "INSERT INTO test (foo, bar, baz) VALUES (?, ?, zeroblob(?));"
stmt, err := conn.Prepare(insert)
if err != nil {
t.Fatalf("Error preparing statement: %v", err)
}
_, err = stmt.Exec(60, "TestFunction", 400)
if err != nil {
t.Fatalf("Error executing statement with arguments: %v", err)
}
stmt.Close()
stmt, err = conn.Prepare("SELECT baz FROM test where foo = ?")
if err != nil {
t.Fatalf("Error preparing select stmt: %v", err)
}
defer stmt.Close()
rows, err := stmt.Query(60)
if err != nil {
t.Fatalf("Error executing select stmt: %v", err)
}
defer rows.Close()
for rows.Next() {
var b []byte
err = rows.Scan(&b)
if err != nil {
t.Fatalf("Error scanning row: %v", err)
}
if len(b) != 400 {
t.Fatalf("Expected 100 bytes, got %d", len(b))
}
}
sql := "SELECT uuid4_str();"
stmt, err = conn.Prepare(sql)
if err != nil {
t.Fatalf("Error preparing statement: %v", err)
}
defer stmt.Close()
rows, err = stmt.Query()
if err != nil {
t.Fatalf("Error executing query: %v", err)
}
defer rows.Close()
var i int
for rows.Next() {
var b string
err = rows.Scan(&b)
if err != nil {
t.Fatalf("Error scanning row: %v", err)
}
if len(b) != 36 {
t.Fatalf("Expected 36 bytes, got %d", len(b))
}
i++
fmt.Printf("uuid: %s\n", b)
}
if i != 1 {
t.Fatalf("Expected 1 row, got %d", i)
}
fmt.Println("zeroblob + uuid functions passed")
}
func TestDuplicateConnection(t *testing.T) {
newConn, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening new connection: %v", err)
}
err = createTable(newConn)
if err != nil {
t.Fatalf("Error creating table: %v", err)
}
err = insertData(newConn)
if err != nil {
t.Fatalf("Error inserting data: %v", err)
}
query := "SELECT * FROM test;"
rows, err := newConn.Query(query)
if err != nil {
t.Fatalf("Error executing query: %v", err)
}
defer rows.Close()
for rows.Next() {
var a int
var b string
var c []byte
err = rows.Scan(&a, &b, &c)
if err != nil {
t.Fatalf("Error scanning row: %v", err)
}
fmt.Println("RESULTS: ", a, b, string(c))
}
}
func TestDuplicateConnection2(t *testing.T) {
newConn, err := sql.Open("turso", ":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 (?, ?, uuid4());"
stmt, err := newConn.Prepare(sql)
stmt.Exec(242345, 2342434)
defer stmt.Close()
query := "SELECT * FROM test;"
rows, err := newConn.Query(query)
if err != nil {
t.Fatalf("Error executing query: %v", err)
}
defer rows.Close()
for rows.Next() {
var a int
var b int
var c []byte
err = rows.Scan(&a, &b, &c)
if err != nil {
t.Fatalf("Error scanning row: %v", err)
}
fmt.Println("RESULTS: ", a, b, string(c))
if len(c) != 16 {
t.Fatalf("Expected 16 bytes, got %d", len(c))
}
}
}
func TestConnectionError(t *testing.T) {
newConn, err := sql.Open("turso", ":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("turso", ":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("turso", ":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 TestTransaction(t *testing.T) {
// Open database connection
db, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening database: %v", err)
}
defer db.Close()
// Create a test table
_, err = db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
if err != nil {
t.Fatalf("Error creating table: %v", err)
}
// Insert initial data
_, err = db.Exec("INSERT INTO test (id, name) VALUES (1, 'Initial')")
if err != nil {
t.Fatalf("Error inserting initial data: %v", err)
}
// Begin a transaction
tx, err := db.Begin()
if err != nil {
t.Fatalf("Error starting transaction: %v", err)
}
// Insert data within the transaction
_, err = tx.Exec("INSERT INTO test (id, name) VALUES (2, 'Transaction')")
if err != nil {
t.Fatalf("Error inserting data in transaction: %v", err)
}
// Commit the transaction
err = tx.Commit()
if err != nil {
t.Fatalf("Error committing transaction: %v", err)
}
// Verify both rows are visible after commit
rows, err := db.Query("SELECT id, name FROM test ORDER BY id")
if err != nil {
t.Fatalf("Error querying data after commit: %v", err)
}
defer rows.Close()
expected := []struct {
id int
name string
}{
{1, "Initial"},
{2, "Transaction"},
}
i := 0
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
t.Fatalf("Error scanning row: %v", err)
}
if id != expected[i].id || name != expected[i].name {
t.Errorf("Row %d: expected (%d, %s), got (%d, %s)",
i, expected[i].id, expected[i].name, id, name)
}
i++
}
if i != 2 {
t.Fatalf("Expected 2 rows, got %d", i)
}
t.Log("Transaction test passed")
}
func TestVectorOperations(t *testing.T) {
db, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening connection: %v", err)
}
defer db.Close()
// Test creating table with vector columns
_, err = db.Exec(`CREATE TABLE vector_test (id INTEGER PRIMARY KEY, embedding F32_BLOB(64))`)
if err != nil {
t.Fatalf("Error creating vector table: %v", err)
}
// Test vector insertion
_, err = db.Exec(`INSERT INTO vector_test VALUES (1, vector('[0.1, 0.2, 0.3, 0.4, 0.5]'))`)
if err != nil {
t.Fatalf("Error inserting vector: %v", err)
}
// Test vector similarity calculation
var similarity float64
err = db.QueryRow(`SELECT vector_distance_cos(embedding, vector('[0.2, 0.3, 0.4, 0.5, 0.6]')) FROM vector_test WHERE id = 1`).Scan(&similarity)
if err != nil {
t.Fatalf("Error calculating vector similarity: %v", err)
}
if similarity <= 0 || similarity > 1 {
t.Fatalf("Expected similarity between 0 and 1, got %f", similarity)
}
// Test vector extraction
var extracted string
err = db.QueryRow(`SELECT vector_extract(embedding) FROM vector_test WHERE id = 1`).Scan(&extracted)
if err != nil {
t.Fatalf("Error extracting vector: %v", err)
}
fmt.Printf("Extracted vector: %s\n", extracted)
}
func TestSQLFeatures(t *testing.T) {
db, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening connection: %v", err)
}
defer db.Close()
// Create test tables
_, err = db.Exec(`
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT,
age INTEGER
)`)
if err != nil {
t.Fatalf("Error creating customers table: %v", err)
}
_, err = db.Exec(`
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount REAL,
date TEXT
)`)
if err != nil {
t.Fatalf("Error creating orders table: %v", err)
}
// Insert test data
_, err = db.Exec(`
INSERT INTO customers VALUES
(1, 'Alice', 30),
(2, 'Bob', 25),
(3, 'Charlie', 40)`)
if err != nil {
t.Fatalf("Error inserting customers: %v", err)
}
_, err = db.Exec(`
INSERT INTO orders VALUES
(1, 1, 100.50, '2024-01-01'),
(2, 1, 200.75, '2024-02-01'),
(3, 2, 50.25, '2024-01-15'),
(4, 3, 300.00, '2024-02-10')`)
if err != nil {
t.Fatalf("Error inserting orders: %v", err)
}
// Test JOIN
rows, err := db.Query(`
SELECT c.name, o.amount
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
ORDER BY o.amount DESC`)
if err != nil {
t.Fatalf("Error executing JOIN: %v", err)
}
defer rows.Close()
// Check JOIN results
expectedResults := []struct {
name string
amount float64
}{
{"Charlie", 300.00},
{"Alice", 200.75},
{"Alice", 100.50},
{"Bob", 50.25},
}
i := 0
for rows.Next() {
var name string
var amount float64
if err := rows.Scan(&name, &amount); err != nil {
t.Fatalf("Error scanning JOIN result: %v", err)
}
if i >= len(expectedResults) {
t.Fatalf("Too many rows returned from JOIN")
}
if name != expectedResults[i].name || amount != expectedResults[i].amount {
t.Fatalf("Row %d: expected (%s, %.2f), got (%s, %.2f)",
i, expectedResults[i].name, expectedResults[i].amount, name, amount)
}
i++
}
// Test GROUP BY with aggregation
var count int
var total float64
err = db.QueryRow(`
SELECT COUNT(*), SUM(amount)
FROM orders
WHERE customer_id = 1
GROUP BY customer_id`).Scan(&count, &total)
if err != nil {
t.Fatalf("Error executing GROUP BY: %v", err)
}
if count != 2 || total != 301.25 {
t.Fatalf("GROUP BY gave wrong results: count=%d, total=%.2f", count, total)
}
}
func TestDateTimeFunctions(t *testing.T) {
db, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening connection: %v", err)
}
defer db.Close()
// Test date()
var dateStr string
err = db.QueryRow(`SELECT date('now')`).Scan(&dateStr)
if err != nil {
t.Fatalf("Error with date() function: %v", err)
}
fmt.Printf("Current date: %s\n", dateStr)
// Test date arithmetic
err = db.QueryRow(`SELECT date('2024-01-01', '+1 month')`).Scan(&dateStr)
if err != nil {
t.Fatalf("Error with date arithmetic: %v", err)
}
if dateStr != "2024-02-01" {
t.Fatalf("Expected '2024-02-01', got '%s'", dateStr)
}
// Test strftime
var formatted string
err = db.QueryRow(`SELECT strftime('%Y-%m-%d', '2024-01-01')`).Scan(&formatted)
if err != nil {
t.Fatalf("Error with strftime function: %v", err)
}
if formatted != "2024-01-01" {
t.Fatalf("Expected '2024-01-01', got '%s'", formatted)
}
}
func TestMathFunctions(t *testing.T) {
db, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening connection: %v", err)
}
defer db.Close()
// Test basic math functions
var result float64
err = db.QueryRow(`SELECT abs(-15.5)`).Scan(&result)
if err != nil {
t.Fatalf("Error with abs function: %v", err)
}
if result != 15.5 {
t.Fatalf("abs(-15.5) should be 15.5, got %f", result)
}
// Test trigonometric functions
err = db.QueryRow(`SELECT round(sin(radians(30)), 4)`).Scan(&result)
if err != nil {
t.Fatalf("Error with sin function: %v", err)
}
if math.Abs(result-0.5) > 0.0001 {
t.Fatalf("sin(30 degrees) should be about 0.5, got %f", result)
}
// Test power functions
err = db.QueryRow(`SELECT pow(2, 3)`).Scan(&result)
if err != nil {
t.Fatalf("Error with pow function: %v", err)
}
if result != 8 {
t.Fatalf("2^3 should be 8, got %f", result)
}
}
func TestJSONFunctions(t *testing.T) {
db, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening connection: %v", err)
}
defer db.Close()
// Test json function
var valid int
err = db.QueryRow(`SELECT json_valid('{"name":"John","age":30}')`).Scan(&valid)
if err != nil {
t.Fatalf("Error with json_valid function: %v", err)
}
if valid != 1 {
t.Fatalf("Expected valid JSON to return 1, got %d", valid)
}
// Test json_extract
var name string
err = db.QueryRow(`SELECT json_extract('{"name":"John","age":30}', '$.name')`).Scan(&name)
if err != nil {
t.Fatalf("Error with json_extract function: %v", err)
}
if name != "John" {
t.Fatalf("Expected 'John', got '%s'", name)
}
// Test JSON shorthand
var age int
err = db.QueryRow(`SELECT '{"name":"John","age":30}' -> '$.age'`).Scan(&age)
if err != nil {
t.Fatalf("Error with JSON shorthand: %v", err)
}
if age != 30 {
t.Fatalf("Expected 30, got %d", age)
}
}
func TestParameterOrdering(t *testing.T) {
newConn, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening new connection: %v", err)
}
sql := "CREATE TABLE test (a,b,c);"
newConn.Exec(sql)
// Test inserting with parameters in a different order than
// the table definition.
sql = "INSERT INTO test (b, c ,a) VALUES (?, ?, ?);"
expectedValues := []int{1, 2, 3}
stmt, err := newConn.Prepare(sql)
_, err = stmt.Exec(expectedValues[1], expectedValues[2], expectedValues[0])
if err != nil {
t.Fatalf("Error preparing statement: %v", err)
}
// check that the values are in the correct order
query := "SELECT a,b,c FROM test;"
rows, err := newConn.Query(query)
if err != nil {
t.Fatalf("Error executing query: %v", err)
}
for rows.Next() {
var a, b, c int
err := rows.Scan(&a, &b, &c)
if err != nil {
t.Fatal("Error scanning row: ", err)
}
result := []int{a, b, c}
for i := range 3 {
if result[i] != expectedValues[i] {
fmt.Printf("RESULTS: %d, %d, %d\n", a, b, c)
fmt.Printf("EXPECTED: %d, %d, %d\n", expectedValues[0], expectedValues[1], expectedValues[2])
}
}
}
// -- part 2 --
// mixed parameters and regular values
sql2 := "CREATE TABLE test2 (a,b,c);"
newConn.Exec(sql2)
expectedValues2 := []int{1, 2, 3}
// Test inserting with parameters in a different order than
// the table definition, with a mixed regular parameter included
sql2 = "INSERT INTO test2 (a, b ,c) VALUES (1, ?, ?);"
_, err = newConn.Exec(sql2, expectedValues2[1], expectedValues2[2])
if err != nil {
t.Fatalf("Error preparing statement: %v", err)
}
// check that the values are in the correct order
query2 := "SELECT a,b,c FROM test2;"
rows2, err := newConn.Query(query2)
if err != nil {
t.Fatalf("Error executing query: %v", err)
}
for rows2.Next() {
var a, b, c int
err := rows2.Scan(&a, &b, &c)
if err != nil {
t.Fatal("Error scanning row: ", err)
}
result := []int{a, b, c}
fmt.Printf("RESULTS: %d, %d, %d\n", a, b, c)
fmt.Printf("EXPECTED: %d, %d, %d\n", expectedValues[0], expectedValues[1], expectedValues[2])
for i := range 3 {
if result[i] != expectedValues[i] {
t.Fatalf("Expected %d, got %d", expectedValues[i], result[i])
}
}
}
}
func TestIndex(t *testing.T) {
newConn, err := sql.Open("turso", ":memory:")
if err != nil {
t.Fatalf("Error opening new connection: %v", err)
}
sql := "CREATE TABLE users (name TEXT PRIMARY KEY, email TEXT)"
_, err = newConn.Exec(sql)
if err != nil {
t.Fatalf("Error creating table: %v", err)
}
sql = "CREATE INDEX email_idx ON users(email)"
_, err = newConn.Exec(sql)
if err != nil {
t.Fatalf("Error creating index: %v", err)
}
// Test inserting with parameters in a different order than
// the table definition.
sql = "INSERT INTO users VALUES ('alice', 'a@b.c'), ('bob', 'b@d.e')"
_, err = newConn.Exec(sql)
if err != nil {
t.Fatalf("Error inserting data: %v", err)
}
for filter, row := range map[string][]string{
"a@b.c": []string{"alice", "a@b.c"},
"b@d.e": []string{"bob", "b@d.e"},
} {
query := "SELECT * FROM users WHERE email = ?"
rows, err := newConn.Query(query, filter)
if err != nil {
t.Fatalf("Error executing query: %v", err)
}
for rows.Next() {
var name, email string
err := rows.Scan(&name, &email)
t.Log("name,email:", name, email)
if err != nil {
t.Fatal("Error scanning row: ", err)
}
if !slices.Equal([]string{name, email}, row) {
t.Fatal("Unexpected result", row, []string{name, email})
}
}
}
}
func slicesAreEq(a, b []byte) bool {
if len(a) != len(b) {
fmt.Printf("LENGTHS NOT EQUAL: %d != %d\n", len(a), len(b))
return false
}
for i := range a {
if a[i] != b[i] {
fmt.Printf("SLICES NOT EQUAL: %v != %v\n", a, b)
return false
}
}
return true
}
var rowsMap = map[int]string{1: "hello", 2: "world", 3: "foo", 4: "bar", 5: "baz"}
func createTable(conn *sql.DB) error {
insert := "CREATE TABLE test (foo INT, bar TEXT, baz BLOB);"
stmt, err := conn.Prepare(insert)
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec()
return err
}
func insertData(conn *sql.DB) error {
for i := 1; i <= 5; i++ {
insert := "INSERT INTO test (foo, bar, baz) VALUES (?, ?, ?);"
stmt, err := conn.Prepare(insert)
if err != nil {
return err
}
defer stmt.Close()
if _, err = stmt.Exec(i, rowsMap[i], []byte(rowsMap[i])); err != nil {
return err
}
}
return nil
}

View File

@@ -1,60 +0,0 @@
//go:build linux || darwin
package turso
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/ebitengine/purego"
)
func loadLibrary() (uintptr, error) {
// Try to extract embedded library first
libPath, err := extractEmbeddedLibrary()
if err == nil {
// Successfully extracted embedded library, try to load it
slib, dlerr := purego.Dlopen(libPath, purego.RTLD_NOW|purego.RTLD_GLOBAL)
if dlerr == nil {
return slib, nil
}
// If loading failed, log the error and fall back to system paths
fmt.Printf("Warning: Failed to load embedded library: %v\n", dlerr)
} else {
fmt.Printf("Warning: Failed to extract embedded library: %v\n", err)
}
// Fall back to original behavior for compatibility
var libraryName string
switch runtime.GOOS {
case "darwin":
libraryName = fmt.Sprintf("%s.dylib", libName)
case "linux":
libraryName = fmt.Sprintf("%s.so", libName)
default:
return 0, fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)
}
libPath = os.Getenv("LD_LIBRARY_PATH")
paths := strings.Split(libPath, ":")
cwd, err := os.Getwd()
if err != nil {
return 0, err
}
paths = append(paths, cwd)
for _, path := range paths {
libPath := filepath.Join(path, libraryName)
if _, err := os.Stat(libPath); err == nil {
slib, dlerr := purego.Dlopen(libPath, purego.RTLD_NOW|purego.RTLD_GLOBAL)
if dlerr != nil {
return 0, fmt.Errorf("failed to load library at %s: %w", libPath, dlerr)
}
return slib, nil
}
}
return 0, fmt.Errorf("%s library not found in LD_LIBRARY_PATH or CWD", libName)
}

View File

@@ -1,52 +0,0 @@
//go:build windows
package turso
import (
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/windows"
)
func loadLibrary() (uintptr, error) {
// Try to extract embedded library first
libPath, err := extractEmbeddedLibrary()
if err == nil {
// Successfully extracted embedded library, try to load it
slib, dlerr := windows.LoadLibrary(libPath)
if dlerr == nil {
return uintptr(slib), nil
}
// If loading failed, log the error and fall back to system paths
fmt.Printf("Warning: Failed to load embedded library: %v\n", dlerr)
} else {
fmt.Printf("Warning: Failed to extract embedded library: %v\n", err)
}
// Fall back to original behavior
libraryName := fmt.Sprintf("%s.dll", libName)
pathEnv := os.Getenv("PATH")
paths := strings.Split(pathEnv, ";")
cwd, err := os.Getwd()
if err != nil {
return 0, err
}
paths = append(paths, cwd)
for _, path := range paths {
dllPath := filepath.Join(path, libraryName)
if _, err := os.Stat(dllPath); err == nil {
slib, loadErr := windows.LoadLibrary(dllPath)
if loadErr != nil {
return 0, fmt.Errorf("failed to load library at %s: %w", dllPath, loadErr)
}
return uintptr(slib), nil
}
}
return 0, fmt.Errorf("library %s not found in PATH or CWD", libraryName)
}

View File

@@ -1,309 +0,0 @@
package turso
import (
"database/sql/driver"
"fmt"
"runtime"
"time"
"unsafe"
)
type ResultCode int32
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
SyntaxErr ResultCode = 11
ConstraintViolation ResultCode = 12
NoSuchEntity ResultCode = 13
)
func (rc ResultCode) String() string {
switch rc {
case Error:
return "Error"
case Ok:
return "Ok"
case Row:
return "Row"
case Busy:
return "Busy"
case Io:
return "Io"
case Interrupt:
return "Query was interrupted"
case Invalid:
return "Invalid"
case Null:
return "Null"
case NoMem:
return "Out of memory"
case ReadOnly:
return "Read Only"
case NoData:
return "No Data"
case Done:
return "Done"
case SyntaxErr:
return "Syntax Error"
case ConstraintViolation:
return "Constraint Violation"
case NoSuchEntity:
return "No such entity"
default:
return "Unknown response code"
}
}
const (
driverName = "turso"
libName = "lib_turso_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"
FfiStmtGetError = "stmt_get_error"
FfiStmtClose = "stmt_close"
FfiRowsClose = "rows_close"
FfiRowsGetColumns = "rows_get_columns"
FfiRowsGetColumnName = "rows_get_column_name"
FfiRowsNext = "rows_next"
FfiRowsGetValue = "rows_get_value"
FfiRowsGetError = "rows_get_error"
FfiFreeColumns = "free_columns"
FfiFreeCString = "free_string"
FfiFreeBlob = "free_blob"
)
// 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, func(), error) {
args := namedValueToValue(named)
return buildArgs(args)
}
type valueType int32
const (
intVal valueType = 0
textVal valueType = 1
blobVal valueType = 2
realVal valueType = 3
nullVal valueType = 4
)
func (vt valueType) String() string {
switch vt {
case intVal:
return "int"
case textVal:
return "text"
case blobVal:
return "blob"
case realVal:
return "real"
case nullVal:
return "null"
default:
return "unknown"
}
}
// struct to pass Go values over FFI
type tursoValue struct {
Type valueType
_ [4]byte
Value [8]byte
}
// struct to pass byte slices over FFI
type Blob struct {
Data uintptr
Len int64
}
// convert a tursoValue to a native Go value
func toGoValue(valPtr uintptr) interface{} {
if valPtr == 0 {
return nil
}
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))
defer freeCString(textPtr)
str := GoString(textPtr)
// Try to parse as RFC3339 time format
if t, err := time.Parse(time.RFC3339, str); err == nil {
return t
}
// If it doesn't parse as time, return as string
return str
case blobVal:
blobPtr := *(*uintptr)(unsafe.Pointer(&val.Value))
defer freeBlob(blobPtr)
return toGoBlob(blobPtr)
case nullVal:
return nil
default:
return nil
}
}
func getArgsPtr(args []driver.Value) (uintptr, func(), error) {
if len(args) == 0 {
return 0, nil, nil
}
argSlice, allocs, err := buildArgs(args)
if err != nil {
return 0, allocs, err
}
return uintptr(unsafe.Pointer(&argSlice[0])), allocs, 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
}
return &Blob{
Data: uintptr(unsafe.Pointer(&b[0])),
Len: int64(len(b)),
}
}
// 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))
if blob.Data == 0 || blob.Len == 0 {
return nil
}
data := unsafe.Slice((*byte)(unsafe.Pointer(blob.Data)), blob.Len)
copied := make([]byte, len(data))
copy(copied, data)
return copied
}
func freeBlob(blobPtr uintptr) {
if blobPtr == 0 {
return
}
freeBlobFunc(blobPtr)
}
func freeCString(cstrPtr uintptr) {
if cstrPtr == 0 {
return
}
freeStringFunc(cstrPtr)
}
// convert a Go slice of driver.Value to a slice of tursoValue that can be sent over FFI
// for Blob types, we have to pin them so they are not garbage collected before they can be copied
// into a buffer on the Rust side, so we return a function to unpin them that can be deferred after this call
func buildArgs(args []driver.Value) ([]tursoValue, func(), error) {
pinner := new(runtime.Pinner)
argSlice := make([]tursoValue, len(args))
for i, v := range args {
tursoVal := tursoValue{}
switch val := v.(type) {
case nil:
tursoVal.Type = nullVal
case int64:
tursoVal.Type = intVal
tursoVal.Value = *(*[8]byte)(unsafe.Pointer(&val))
case float64:
tursoVal.Type = realVal
tursoVal.Value = *(*[8]byte)(unsafe.Pointer(&val))
case bool:
tursoVal.Type = intVal
boolAsInt := int64(0)
if val {
boolAsInt = 1
}
tursoVal.Value = *(*[8]byte)(unsafe.Pointer(&boolAsInt))
case string:
tursoVal.Type = textVal
cstr := CString(val)
pinner.Pin(cstr)
*(*uintptr)(unsafe.Pointer(&tursoVal.Value)) = uintptr(unsafe.Pointer(cstr))
case []byte:
tursoVal.Type = blobVal
blob := makeBlob(val)
pinner.Pin(blob)
*(*uintptr)(unsafe.Pointer(&tursoVal.Value)) = uintptr(unsafe.Pointer(blob))
case time.Time:
tursoVal.Type = textVal
timeStr := val.Format(time.RFC3339)
cstr := CString(timeStr)
pinner.Pin(cstr)
*(*uintptr)(unsafe.Pointer(&tursoVal.Value)) = uintptr(unsafe.Pointer(cstr))
default:
return nil, pinner.Unpin, fmt.Errorf("unsupported type: %T", v)
}
argSlice[i] = tursoVal
}
return argSlice, pinner.Unpin, nil
}
/* 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))
}