mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-18 09:04:19 +01:00
310 lines
7.1 KiB
Go
310 lines
7.1 KiB
Go
package limbo
|
|
|
|
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 = "sqlite3"
|
|
libName = "lib_limbo_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) ([]limboValue, 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 limboValue struct {
|
|
Type valueType
|
|
_ [4]byte
|
|
Value [8]byte
|
|
}
|
|
|
|
// struct to pass byte slices over FFI
|
|
type Blob struct {
|
|
Data uintptr
|
|
Len int64
|
|
}
|
|
|
|
// convert a limboValue to a native Go value
|
|
func toGoValue(valPtr uintptr) interface{} {
|
|
if valPtr == 0 {
|
|
return nil
|
|
}
|
|
val := (*limboValue)(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 limboValue 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) ([]limboValue, func(), error) {
|
|
pinner := new(runtime.Pinner)
|
|
argSlice := make([]limboValue, len(args))
|
|
for i, v := range args {
|
|
limboVal := limboValue{}
|
|
switch val := v.(type) {
|
|
case nil:
|
|
limboVal.Type = nullVal
|
|
case int64:
|
|
limboVal.Type = intVal
|
|
limboVal.Value = *(*[8]byte)(unsafe.Pointer(&val))
|
|
case float64:
|
|
limboVal.Type = realVal
|
|
limboVal.Value = *(*[8]byte)(unsafe.Pointer(&val))
|
|
case bool:
|
|
limboVal.Type = intVal
|
|
boolAsInt := int64(0)
|
|
if val {
|
|
boolAsInt = 1
|
|
}
|
|
limboVal.Value = *(*[8]byte)(unsafe.Pointer(&boolAsInt))
|
|
case string:
|
|
limboVal.Type = textVal
|
|
cstr := CString(val)
|
|
pinner.Pin(cstr)
|
|
*(*uintptr)(unsafe.Pointer(&limboVal.Value)) = uintptr(unsafe.Pointer(cstr))
|
|
case []byte:
|
|
limboVal.Type = blobVal
|
|
blob := makeBlob(val)
|
|
pinner.Pin(blob)
|
|
*(*uintptr)(unsafe.Pointer(&limboVal.Value)) = uintptr(unsafe.Pointer(blob))
|
|
case time.Time:
|
|
limboVal.Type = textVal
|
|
timeStr := val.Format(time.RFC3339)
|
|
cstr := CString(timeStr)
|
|
pinner.Pin(cstr)
|
|
*(*uintptr)(unsafe.Pointer(&limboVal.Value)) = uintptr(unsafe.Pointer(cstr))
|
|
default:
|
|
return nil, pinner.Unpin, fmt.Errorf("unsupported type: %T", v)
|
|
}
|
|
argSlice[i] = limboVal
|
|
}
|
|
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))
|
|
}
|