Skip to content

Commit

Permalink
feat(sdk/go): implement sqlite SDK for TinyGo
Browse files Browse the repository at this point in the history
Signed-off-by: Adam Reese <[email protected]>
  • Loading branch information
adamreese committed Sep 5, 2023
1 parent 3edb47a commit a91ea04
Show file tree
Hide file tree
Showing 7 changed files with 652 additions and 3 deletions.
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ build-examples: $(EXAMPLES_DIR)/http-tinygo/main.wasm
build-examples: $(EXAMPLES_DIR)/tinygo-outbound-redis/main.wasm
build-examples: $(EXAMPLES_DIR)/tinygo-redis/main.wasm
build-examples: $(EXAMPLES_DIR)/tinygo-key-value/main.wasm
build-examples: $(EXAMPLES_DIR)/tinygo-sqlite/main.wasm

$(EXAMPLES_DIR)/%/main.wasm: $(EXAMPLES_DIR)/%/main.go
tinygo build -target=wasi -gc=leaking -no-debug -o $@ $<
Expand All @@ -40,17 +41,20 @@ GENERATED_SPIN_HTTP = http/spin-http.c http/spin-http.h
GENERATED_OUTBOUND_REDIS = redis/outbound-redis.c redis/outbound-redis.h
GENERATED_SPIN_REDIS = redis/spin-redis.c redis/spin-redis.h
GENERATED_KEY_VALUE = key_value/key-value.c key_value/key-value.h
GENERATED_SQLITE = sqlite/sqlite.c sqlite/sqlite.h

SDK_VERSION_SOURCE_FILE = sdk_version/sdk-version-go-template.c

# NOTE: Please update this list if you add a new directory to the SDK:
SDK_VERSION_DEST_FILES = config/sdk-version-go.c http/sdk-version-go.c \
key_value/sdk-version-go.c redis/sdk-version-go.c
key_value/sdk-version-go.c redis/sdk-version-go.c \
sqlite/sdk-version-go.c

.PHONY: generate
generate: $(GENERATED_OUTBOUND_HTTP) $(GENERATED_SPIN_HTTP)
generate: $(GENERATED_OUTBOUND_REDIS) $(GENERATED_SPIN_REDIS)
generate: $(GENERATED_SPIN_CONFIG) $(GENERATED_KEY_VALUE)
generate: $(GENERATED_SQLITE)
generate: $(SDK_VERSION_DEST_FILES)

$(SDK_VERSION_DEST_FILES): $(SDK_VERSION_SOURCE_FILE)
Expand All @@ -76,6 +80,9 @@ $(GENERATED_SPIN_REDIS):
$(GENERATED_KEY_VALUE):
wit-bindgen c --import ../../wit/ephemeral/key-value.wit --out-dir ./key_value

$(GENERATED_SQLITE):
wit-bindgen c --import ../../wit/ephemeral/sqlite.wit --out-dir ./sqlite

# ----------------------------------------------------------------------
# Cleanup
# ----------------------------------------------------------------------
Expand All @@ -84,11 +91,13 @@ clean:
rm -f $(GENERATED_SPIN_CONFIG)
rm -f $(GENERATED_OUTBOUND_HTTP) $(GENERATED_SPIN_HTTP)
rm -f $(GENERATED_OUTBOUND_REDIS) $(GENERATED_SPIN_REDIS)
rm -f $(GENERATED_KEY_VALUE) $(GENERATED_SDK_VERSION)
rm -f $(GENERATED_KEY_VALUE) $(GENERATED_SQLITE)
rm -f $(GENERATED_SDK_VERSION)
rm -f http/testdata/http-tinygo/main.wasm
rm -f $(EXAMPLES_DIR)/http-tinygo/main.wasm
rm -f $(EXAMPLES_DIR)/http-tinygo-outbound-http/main.wasm
rm -f $(EXAMPLES_DIR)/tinygo-outbound-redis/main.wasm
rm -f $(EXAMPLES_DIR)/tinygo-redis/main.wasm
rm -f $(EXAMPLES_DIR)/tinygo-key-value/main.wasm
rm -f $(EXAMPLES_DIR)/tinygo-sqlite/main.wasm
rm -f $(SDK_VERSION_DEST_FILES)
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/fermyon/spin/sdk/go

go 1.17
go 1.20

require github.com/julienschmidt/httprouter v1.3.0
171 changes: 171 additions & 0 deletions sqlite/internals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package sqlite

// #include "sqlite.h"
import "C"
import (
"errors"
"fmt"
"unsafe"
)

func open(name string) (*Conn, error) {
var dbname C.sqlite_string_t
var ret C.sqlite_expected_connection_error_t

dbname = sqliteStr(name)
C.sqlite_open(&dbname, &ret)

if ret.is_err {
return nil, toErr((*C.sqlite_error_t)(unsafe.Pointer(&ret.val)))
}

sqliteConn := *((*C.sqlite_connection_t)(unsafe.Pointer(&ret.val)))
return &Conn{_ptr: sqliteConn}, nil
}

func (db *Conn) close() {
C.sqlite_close(db._ptr)
}

func (db *Conn) execute(statement string, args []any) (*results, error) {
var ret C.sqlite_expected_query_result_error_t
defer C.sqlite_expected_query_result_error_free(&ret)

sqliteStatement := sqliteStr(statement)
params := toSqliteListValue(args)

C.sqlite_execute(db._ptr, &sqliteStatement, &params, &ret)

if ret.is_err {
spinErr := (*C.sqlite_error_t)(unsafe.Pointer(&ret.val))
return nil, toErr(spinErr)
}

qr := (*C.sqlite_query_result_t)(unsafe.Pointer(&ret.val))
cols := fromSqliteListString(qr.columns)
rows := fromSqliteListRowResult(qr.rows)

result := &results{
columns: cols,
rows: rows,
len: int(qr.rows.len),
}

return result, nil
}

func fromSqliteListRowResult(list C.sqlite_list_row_result_t) [][]any {
listLen := int(list.len)
ret := make([][]any, listLen)
slice := unsafe.Slice(list.ptr, listLen)
for i := 0; i < listLen; i++ {
row := *((*C.sqlite_list_value_t)(unsafe.Pointer(&slice[i])))
ret[i] = fromSqliteListValue(row)
}
return ret

}

func fromSqliteListString(list C.sqlite_list_string_t) []string {
listLen := int(list.len)
ret := make([]string, listLen)
slice := unsafe.Slice(list.ptr, listLen)
for i := 0; i < listLen; i++ {
str := slice[i]
ret[i] = C.GoStringN(str.ptr, C.int(str.len))
}
return ret
}

func fromSqliteListValue(list C.sqlite_list_value_t) []any {
listLen := int(list.len)
ret := make([]any, listLen)
slice := unsafe.Slice(list.ptr, listLen)
for i := 0; i < listLen; i++ {
ret[i] = fromSqliteValue(slice[i])
}
return ret
}

func toSqliteListValue(xv []any) C.sqlite_list_value_t {
if len(xv) == 0 {
return C.sqlite_list_value_t{}
}
cxv := make([]C.sqlite_value_t, len(xv))
for i := 0; i < len(xv); i++ {
cxv[i] = toSqliteValue(xv[i])
}
return C.sqlite_list_value_t{ptr: &cxv[0], len: C.size_t(len(cxv))}
}

const (
valueInt uint8 = iota
valueReal
valueText
valueBlob
valueNull
)

func toSqliteValue(x any) C.sqlite_value_t {
var ret C.sqlite_value_t
switch v := x.(type) {
case int:
*(*C.int64_t)(unsafe.Pointer(&ret.val)) = int64(v)
ret.tag = valueInt
case int64:
*(*C.int64_t)(unsafe.Pointer(&ret.val)) = v
ret.tag = valueInt
case float64:
*(*C.double)(unsafe.Pointer(&ret.val)) = v
ret.tag = valueReal
case string:
str := sqliteStr(v)
*(*C.sqlite_string_t)(unsafe.Pointer(&ret.val)) = str
ret.tag = valueText
case []byte:
blob := C.sqlite_list_u8_t{ptr: &v[0], len: C.size_t(len(v))}
*(*C.sqlite_list_u8_t)(unsafe.Pointer(&ret.val)) = blob
ret.tag = valueBlob
default:
ret.tag = valueNull
}
return ret
}

func fromSqliteValue(x C.sqlite_value_t) any {
switch x.tag {
case valueInt:
return int64(*(*C.int64_t)(unsafe.Pointer(&x.val)))
case valueReal:
return float64(*(*C.double)(unsafe.Pointer(&x.val)))
case valueBlob:
blob := (*C.sqlite_list_u8_t)(unsafe.Pointer(&x.val))
return C.GoBytes(unsafe.Pointer(blob.ptr), C.int(blob.len))
case valueText:
str := (*C.sqlite_string_t)(unsafe.Pointer(&x.val))
return C.GoStringN(str.ptr, C.int(str.len))
}
return nil
}

func sqliteStr(x string) C.sqlite_string_t {
return C.sqlite_string_t{ptr: C.CString(x), len: C.size_t(len(x))}
}

func toErr(err *C.sqlite_error_t) error {
switch err.tag {
case 0:
return errors.New("no such database")
case 1:
return errors.New("access denied")
case 2:
return errors.New("invalid connection")
case 3:
return errors.New("database full")
case 4:
str := (*C.sqlite_string_t)(unsafe.Pointer(&err.val))
return errors.New(fmt.Sprintf("io error: %s", C.GoStringN(str.ptr, C.int(str.len))))
default:
return errors.New(fmt.Sprintf("unrecognized error: %v", err.tag))
}
}
38 changes: 38 additions & 0 deletions sqlite/internals_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package sqlite

import (
"reflect"
"testing"
)

func TestValue(t *testing.T) {
tests := []any{
int64(1234),
3.14,
"foo",
[]byte("bar"),
nil,
}

for _, tc := range tests {
got := fromSqliteValue(toSqliteValue(tc))
if !reflect.DeepEqual(tc, got) {
t.Errorf("want %T(%#v), got %T(%#v)", tc, tc, got, got)
}
}
}

func TestValueList(t *testing.T) {
tc := []any{
int64(1234),
3.14,
"foo",
[]byte("bar"),
nil,
}

got := fromSqliteListValue(toSqliteListValue(tc))
if !reflect.DeepEqual(tc, got) {
t.Errorf("want %v, got %v", tc, got)
}
}
Loading

0 comments on commit a91ea04

Please sign in to comment.