mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-30 22:44:21 +01:00
Remove Go bindings (moved to their own repo tursodatabase/turso-go)
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"bindings/dart/rust",
|
||||
"bindings/go",
|
||||
"bindings/java",
|
||||
"bindings/javascript",
|
||||
"bindings/python",
|
||||
|
||||
4
bindings/go/.gitignore
vendored
4
bindings/go/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
# Ignore generated libraries directory
|
||||
/libs/*
|
||||
# But keep the directory structure
|
||||
!/libs/.gitkeep
|
||||
@@ -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"] }
|
||||
@@ -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.
|
||||
@@ -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)"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user