diff --git a/Cargo.lock b/Cargo.lock index 57cf52cd2..c141cca43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 0dbb4b1fa..832ab8d09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ resolver = "2" members = [ "bindings/dart/rust", - "bindings/go", "bindings/java", "bindings/javascript", "bindings/python", diff --git a/bindings/go/.gitignore b/bindings/go/.gitignore deleted file mode 100644 index 6dd54c50a..000000000 --- a/bindings/go/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore generated libraries directory -/libs/* -# But keep the directory structure -!/libs/.gitkeep diff --git a/bindings/go/Cargo.toml b/bindings/go/Cargo.toml deleted file mode 100644 index 8f8a55d76..000000000 --- a/bindings/go/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/bindings/go/README.md b/bindings/go/README.md deleted file mode 100644 index af74b98a4..000000000 --- a/bindings/go/README.md +++ /dev/null @@ -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. diff --git a/bindings/go/build_lib.sh b/bindings/go/build_lib.sh deleted file mode 100755 index 1b77bfa26..000000000 --- a/bindings/go/build_lib.sh +++ /dev/null @@ -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)" diff --git a/bindings/go/connection.go b/bindings/go/connection.go deleted file mode 100644 index 2d0a1dc7b..000000000 --- a/bindings/go/connection.go +++ /dev/null @@ -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 -} diff --git a/bindings/go/embedded.go b/bindings/go/embedded.go deleted file mode 100644 index 2a44795d6..000000000 --- a/bindings/go/embedded.go +++ /dev/null @@ -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 -} diff --git a/bindings/go/go.mod b/bindings/go/go.mod deleted file mode 100644 index 0c9e9228f..000000000 --- a/bindings/go/go.mod +++ /dev/null @@ -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 -) diff --git a/bindings/go/go.sum b/bindings/go/go.sum deleted file mode 100644 index aacf58639..000000000 --- a/bindings/go/go.sum +++ /dev/null @@ -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= diff --git a/bindings/go/libs/.gitkeep b/bindings/go/libs/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/bindings/go/rows.go b/bindings/go/rows.go deleted file mode 100644 index c82bd2e65..000000000 --- a/bindings/go/rows.go +++ /dev/null @@ -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 -} diff --git a/bindings/go/rs_src/lib.rs b/bindings/go/rs_src/lib.rs deleted file mode 100644 index 26a2a4dfd..000000000 --- a/bindings/go/rs_src/lib.rs +++ /dev/null @@ -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, - io: Arc, - err: Option, -} - -impl TursoConn { - fn new(conn: Arc, io: Arc) -> 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) }; - } -} diff --git a/bindings/go/rs_src/rows.rs b/bindings/go/rs_src/rows.rs deleted file mode 100644 index 8a05440a5..000000000 --- a/bindings/go/rs_src/rows.rs +++ /dev/null @@ -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, - _conn: &'conn mut TursoConn, - err: Option, -} - -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::()); - } -} diff --git a/bindings/go/rs_src/statement.rs b/bindings/go/rs_src/statement.rs deleted file mode 100644 index 65859161d..000000000 --- a/bindings/go/rs_src/statement.rs +++ /dev/null @@ -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, - pub conn: &'conn mut TursoConn, - pub err: Option, -} - -#[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, 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() - } - } -} diff --git a/bindings/go/rs_src/types.rs b/bindings/go/rs_src/types.rs deleted file mode 100644 index 9ec06b3bf..000000000 --- a/bindings/go/rs_src/types.rs +++ /dev/null @@ -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, -} - -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, - } - } -} diff --git a/bindings/go/stmt.go b/bindings/go/stmt.go deleted file mode 100644 index c12ae9d71..000000000 --- a/bindings/go/stmt.go +++ /dev/null @@ -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 -} diff --git a/bindings/go/turso_test.go b/bindings/go/turso_test.go deleted file mode 100644 index ff2bc90a4..000000000 --- a/bindings/go/turso_test.go +++ /dev/null @@ -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 -} diff --git a/bindings/go/turso_unix.go b/bindings/go/turso_unix.go deleted file mode 100644 index 3e61f278f..000000000 --- a/bindings/go/turso_unix.go +++ /dev/null @@ -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) -} diff --git a/bindings/go/turso_windows.go b/bindings/go/turso_windows.go deleted file mode 100644 index 3926bedc9..000000000 --- a/bindings/go/turso_windows.go +++ /dev/null @@ -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) -} diff --git a/bindings/go/types.go b/bindings/go/types.go deleted file mode 100644 index 1c9f00d62..000000000 --- a/bindings/go/types.go +++ /dev/null @@ -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)) -}