-
Notifications
You must be signed in to change notification settings - Fork 8
feat(sqlite): implement sqlite in wasip2 #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: wasip2
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
CREATE TABLE pets (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, prey VARCHAR(100), is_finicky BOOL NOT NULL); | ||
INSERT INTO pets VALUES (1, 'Splodge', NULL, false); | ||
INSERT INTO pets VALUES (2, 'Kiki', 'Cicadas', false); | ||
INSERT INTO pets VALUES (3, 'Slats', 'Temptations', true); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
module github.com/spinframework/spin-go-sdk/v3/examples/sqlite | ||
|
||
go 1.24 | ||
|
||
require github.com/spinframework/spin-go-sdk/v3 v3.0.0 | ||
|
||
require ( | ||
github.com/julienschmidt/httprouter v1.3.0 // indirect | ||
go.bytecodealliance.org/cm v0.2.2 // indirect | ||
) | ||
|
||
replace github.com/spinframework/spin-go-sdk/v3 => ../../ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= | ||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= | ||
go.bytecodealliance.org/cm v0.2.2 h1:M9iHS6qs884mbQbIjtLX1OifgyPG9DuMs2iwz8G4WQA= | ||
go.bytecodealliance.org/cm v0.2.2/go.mod h1:JD5vtVNZv7sBoQQkvBvAAVKJPhR/bqBH7yYXTItMfZI= |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
|
||
spinhttp "github.com/spinframework/spin-go-sdk/v3/http" | ||
"github.com/spinframework/spin-go-sdk/v3/sqlite" | ||
) | ||
|
||
type Pet struct { | ||
ID int64 | ||
Name string | ||
Prey *string // nullable field must be a pointer | ||
IsFinicky bool | ||
} | ||
|
||
func init() { | ||
spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { | ||
db := sqlite.Open("default") | ||
defer db.Close() | ||
|
||
_, err := db.Query("REPLACE INTO pets (id, name, prey, is_finicky) VALUES (4, 'Maya', ?, false);", "bananas") | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
rows, err := db.Query("SELECT id, name, prey, is_finicky FROM pets") | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
var pets []*Pet | ||
for rows.Next() { | ||
var pet Pet | ||
if err := rows.Scan(&pet.ID, &pet.Name, &pet.Prey, &pet.IsFinicky); err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
pets = append(pets, &pet) | ||
} | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
json.NewEncoder(w).Encode(pets) | ||
adamreese marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
} | ||
|
||
func main() {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
spin_manifest_version = 2 | ||
|
||
[application] | ||
name = "sqlite-example" | ||
version = "0.1.0" | ||
authors = ["Fermyon Engineering <[email protected]>"] | ||
description = "" | ||
|
||
[[trigger.http]] | ||
route = "/..." | ||
component = "sqlite" | ||
|
||
[component.sqlite] | ||
source = "main.wasm" | ||
allowed_outbound_hosts = [] | ||
sqlite_databases = ["default"] | ||
[component.sqlite.build] | ||
command = "tinygo build -target=wasip2 --wit-package $(go list -mod=readonly -m -f '{{.Dir}}' github.com/spinframework/spin-go-sdk/v3)/wit --wit-world http-trigger -gc=leaking -o main.wasm main.go" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, |
||
watch = ["**/*.go", "go.mod"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package db | ||
|
||
import "database/sql/driver" | ||
|
||
// GlobalParameterConverter is a global valueConverter instance to convert parameters. | ||
var GlobalParameterConverter = &valueConverter{} | ||
|
||
var _ driver.ValueConverter = (*valueConverter)(nil) | ||
|
||
// valueConverter is a no-op value converter. | ||
type valueConverter struct{} | ||
|
||
func (c *valueConverter) ConvertValue(v any) (driver.Value, error) { | ||
return driver.Value(v), nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// Package sqlite provides an interface to sqlite database stores within Spin | ||
// components. | ||
// | ||
// This package is implemented as a driver that conforms to the built-in | ||
// database/sql interface. | ||
// | ||
// db := sqlite.Open("default") | ||
// defer db.Close() | ||
// | ||
// s, err := db.Prepare("REPLACE INTO pets VALUES (4, 'Maya', ?, false);") | ||
// // if err != nil { ... } | ||
// | ||
// _, err = s.Query("bananas") | ||
// // if err != nil { ... } | ||
// | ||
// rows, err := db.Query("SELECT * FROM pets") | ||
// // if err != nil { ... } | ||
// | ||
// var pets []*Pet | ||
// for rows.Next() { | ||
// var pet Pet | ||
// if err := rows.Scan(&pet.ID, &pet.Name, &pet.Prey, &pet.IsFinicky); err != nil { | ||
// ... | ||
// } | ||
// pets = append(pets, &pet) | ||
// } | ||
package sqlite |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
package sqlite | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"database/sql/driver" | ||
"errors" | ||
"fmt" | ||
"io" | ||
|
||
spindb "github.com/spinframework/spin-go-sdk/v3/internal/db" | ||
"github.com/spinframework/spin-go-sdk/v3/internal/fermyon/spin/v2.0.0/sqlite" | ||
"go.bytecodealliance.org/cm" | ||
) | ||
|
||
// Open returns a new connection to the database. | ||
func Open(name string) *sql.DB { | ||
return sql.OpenDB(&connector{name: name}) | ||
} | ||
|
||
// conn represents a database connection. | ||
type conn struct { | ||
spinConn sqlite.Connection | ||
} | ||
|
||
// Close the connection. | ||
func (c *conn) Close() error { | ||
return nil | ||
} | ||
|
||
// Prepare returns a prepared statement, bound to this connection. | ||
func (c *conn) Prepare(query string) (driver.Stmt, error) { | ||
return &stmt{conn: c, query: query}, nil | ||
} | ||
|
||
// Begin isn't supported. | ||
func (c *conn) Begin() (driver.Tx, error) { | ||
return nil, errors.New("transactions are unsupported by this driver") | ||
} | ||
|
||
// connector implements driver.Connector. | ||
type connector struct { | ||
conn *conn | ||
name string | ||
} | ||
|
||
// Connect returns a connection to the database. | ||
func (d *connector) Connect(_ context.Context) (driver.Conn, error) { | ||
if d.conn != nil { | ||
return d.conn, nil | ||
} | ||
return d.Open(d.name) | ||
} | ||
|
||
// Driver returns the underlying Driver of the Connector. | ||
func (d *connector) Driver() driver.Driver { | ||
return d | ||
} | ||
|
||
// Open returns a new connection to the database. | ||
func (d *connector) Open(name string) (driver.Conn, error) { | ||
results := sqlite.ConnectionOpen(name) | ||
if results.IsErr() { | ||
return nil, toError(results.Err()) | ||
} | ||
d.conn = &conn{ | ||
spinConn: *results.OK(), | ||
} | ||
return d.conn, nil | ||
} | ||
|
||
// Close closes the connection to the database. | ||
func (d *connector) Close() error { | ||
if d.conn != nil { | ||
d.conn.Close() | ||
} | ||
return nil | ||
} | ||
|
||
type rows struct { | ||
columns []string | ||
pos int | ||
numRows int | ||
rows [][]any | ||
} | ||
|
||
var _ driver.Rows = (*rows)(nil) | ||
|
||
// Columns return column names. | ||
func (r *rows) Columns() []string { | ||
return r.columns | ||
} | ||
|
||
// Close closes the rows iterator. | ||
func (r *rows) Close() error { | ||
r.rows = nil | ||
r.pos = 0 | ||
r.numRows = 0 | ||
return nil | ||
} | ||
|
||
// Next moves the cursor to the next row. | ||
func (r *rows) Next(dest []driver.Value) error { | ||
if !r.HasNextResultSet() { | ||
return io.EOF | ||
} | ||
for i := 0; i != len(r.columns); i++ { | ||
dest[i] = driver.Value(r.rows[r.pos][i]) | ||
} | ||
r.pos++ | ||
return nil | ||
} | ||
|
||
// HasNextResultSet is called at the end of the current result set and | ||
// reports whether there is another result set after the current one. | ||
func (r *rows) HasNextResultSet() bool { | ||
return r.pos < r.numRows | ||
} | ||
|
||
// NextResultSet advances the driver to the next result set even | ||
// if there are remaining rows in the current result set. | ||
// | ||
// NextResultSet should return io.EOF when there are no more result sets. | ||
func (r *rows) NextResultSet() error { | ||
if r.HasNextResultSet() { | ||
r.pos++ | ||
return nil | ||
} | ||
return io.EOF // Per interface spec. | ||
} | ||
|
||
type stmt struct { | ||
conn *conn | ||
query string | ||
} | ||
|
||
var _ driver.Stmt = (*stmt)(nil) | ||
var _ driver.ColumnConverter = (*stmt)(nil) | ||
|
||
// Close closes the statement. | ||
func (s *stmt) Close() error { | ||
return nil | ||
} | ||
|
||
// NumInput returns the number of placeholder parameters. | ||
func (s *stmt) NumInput() int { | ||
// Golang sql won't sanity check argument counts before Query. | ||
return -1 | ||
} | ||
|
||
func toRow(row []sqlite.Value) []any { | ||
ret := make([]any, len(row)) | ||
for i, v := range row { | ||
switch v.String() { | ||
case "integer": | ||
ret[i] = *v.Integer() | ||
case "real": | ||
ret[i] = *v.Real() | ||
case "text": | ||
ret[i] = *v.Text() | ||
case "blob": | ||
// TODO: check this | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this now TODONE? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call, It's not. For some reason |
||
ret[i] = *v.Blob() | ||
case "null": | ||
ret[i] = nil | ||
default: | ||
panic("unknown value type") | ||
} | ||
} | ||
return ret | ||
} | ||
|
||
func toWasiValue(x any) sqlite.Value { | ||
switch v := x.(type) { | ||
case int: | ||
return sqlite.ValueInteger(int64(v)) | ||
case int64: | ||
return sqlite.ValueInteger(v) | ||
case float64: | ||
return sqlite.ValueReal(v) | ||
case string: | ||
return sqlite.ValueText(v) | ||
case []byte: | ||
return sqlite.ValueBlob(cm.ToList([]uint8(v))) | ||
case nil: | ||
return sqlite.ValueNull() | ||
default: | ||
panic("unknown value type") | ||
} | ||
} | ||
|
||
// Query executes a query that may return rows, such as a SELECT. | ||
func (s *stmt) Query(args []driver.Value) (driver.Rows, error) { | ||
params := make([]sqlite.Value, len(args)) | ||
for i := range args { | ||
params[i] = toWasiValue(args[i]) | ||
} | ||
results, err, isErr := s.conn.spinConn.Execute(s.query, cm.ToList(params)).Result() | ||
if isErr { | ||
return nil, toError(&err) | ||
} | ||
|
||
cols := results.Columns.Slice() | ||
|
||
rowLen := results.Rows.Len() | ||
allrows := make([][]any, rowLen) | ||
for rownum, row := range results.Rows.Slice() { | ||
allrows[rownum] = toRow(row.Values.Slice()) | ||
} | ||
rows := &rows{ | ||
columns: cols, | ||
rows: allrows, | ||
numRows: int(rowLen), | ||
} | ||
return rows, nil | ||
} | ||
|
||
// Exec executes a query that doesn't return rows, such as an INSERT or | ||
// UPDATE. | ||
func (s *stmt) Exec(args []driver.Value) (driver.Result, error) { | ||
params := make([]sqlite.Value, len(args)) | ||
for i := range args { | ||
params[i] = toWasiValue(args[i]) | ||
} | ||
_, err, isErr := s.conn.spinConn.Execute(s.query, cm.ToList(params)).Result() | ||
if isErr { | ||
return nil, toError(&err) | ||
} | ||
return &result{}, nil | ||
} | ||
|
||
// ColumnConverter returns GlobalParameterConverter to prevent using driver.DefaultParameterConverter. | ||
func (s *stmt) ColumnConverter(_ int) driver.ValueConverter { | ||
return spindb.GlobalParameterConverter | ||
} | ||
|
||
type result struct{} | ||
|
||
func (r result) LastInsertId() (int64, error) { | ||
return -1, errors.New("LastInsertId is unsupported by this driver") | ||
} | ||
|
||
func (r result) RowsAffected() (int64, error) { | ||
return -1, errors.New("RowsAffected is unsupported by this driver") | ||
} | ||
|
||
func toError(err *sqlite.Error) error { | ||
if err == nil { | ||
return nil | ||
} | ||
if err.String() == "io" { | ||
return fmt.Errorf("io: %s", *err.IO()) | ||
} | ||
return errors.New(err.String()) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.