Skip to content

Commit 27c9b9d

Browse files
committed
feat(sqlite): implement sqlite in wasip2
Signed-off-by: Adam Reese <[email protected]>
1 parent a65f13a commit 27c9b9d

File tree

8 files changed

+386
-0
lines changed

8 files changed

+386
-0
lines changed

v3/examples/sqlite/db/pets.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE pets (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, prey VARCHAR(100), is_finicky BOOL NOT NULL);
2+
INSERT INTO pets VALUES (1, 'Splodge', NULL, false);
3+
INSERT INTO pets VALUES (2, 'Kiki', 'Cicadas', false);
4+
INSERT INTO pets VALUES (3, 'Slats', 'Temptations', true);

v3/examples/sqlite/go.mod

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module github.com/spinframework/spin-go-sdk/v3/examples/sqlite
2+
3+
go 1.24
4+
5+
require github.com/spinframework/spin-go-sdk/v3 v3.0.0
6+
7+
require (
8+
github.com/julienschmidt/httprouter v1.3.0 // indirect
9+
go.bytecodealliance.org/cm v0.2.2 // indirect
10+
)
11+
12+
replace github.com/spinframework/spin-go-sdk/v3 => ../../

v3/examples/sqlite/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
2+
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
3+
go.bytecodealliance.org/cm v0.2.2 h1:M9iHS6qs884mbQbIjtLX1OifgyPG9DuMs2iwz8G4WQA=
4+
go.bytecodealliance.org/cm v0.2.2/go.mod h1:JD5vtVNZv7sBoQQkvBvAAVKJPhR/bqBH7yYXTItMfZI=

v3/examples/sqlite/main.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
7+
spinhttp "github.com/spinframework/spin-go-sdk/v3/http"
8+
"github.com/spinframework/spin-go-sdk/v3/sqlite"
9+
)
10+
11+
type Pet struct {
12+
ID int64
13+
Name string
14+
Prey *string // nullable field must be a pointer
15+
IsFinicky bool
16+
}
17+
18+
func init() {
19+
spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
20+
db := sqlite.Open("default")
21+
defer db.Close()
22+
23+
_, err := db.Query("REPLACE INTO pets (id, name, prey, is_finicky) VALUES (4, 'Maya', ?, false);", "bananas")
24+
if err != nil {
25+
http.Error(w, err.Error(), http.StatusInternalServerError)
26+
return
27+
}
28+
29+
rows, err := db.Query("SELECT id, name, prey, is_finicky FROM pets")
30+
if err != nil {
31+
http.Error(w, err.Error(), http.StatusInternalServerError)
32+
return
33+
}
34+
35+
var pets []*Pet
36+
for rows.Next() {
37+
var pet Pet
38+
if err := rows.Scan(&pet.ID, &pet.Name, &pet.Prey, &pet.IsFinicky); err != nil {
39+
http.Error(w, err.Error(), http.StatusInternalServerError)
40+
return
41+
}
42+
pets = append(pets, &pet)
43+
}
44+
45+
w.Header().Set("Content-Type", "application/json")
46+
json.NewEncoder(w).Encode(pets)
47+
})
48+
}
49+
50+
func main() {}

v3/examples/sqlite/spin.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
spin_manifest_version = 2
2+
3+
[application]
4+
name = "sqlite-example"
5+
version = "0.1.0"
6+
authors = ["Fermyon Engineering <[email protected]>"]
7+
description = ""
8+
9+
[[trigger.http]]
10+
route = "/..."
11+
component = "sqlite"
12+
13+
[component.sqlite]
14+
source = "main.wasm"
15+
allowed_outbound_hosts = []
16+
sqlite_databases = ["default"]
17+
[component.sqlite.build]
18+
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"
19+
watch = ["**/*.go", "go.mod"]

v3/internal/db/driver.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package db
2+
3+
import "database/sql/driver"
4+
5+
// GlobalParameterConverter is a global valueConverter instance to convert parameters.
6+
var GlobalParameterConverter = &valueConverter{}
7+
8+
var _ driver.ValueConverter = (*valueConverter)(nil)
9+
10+
// valueConverter is a no-op value converter.
11+
type valueConverter struct{}
12+
13+
func (c *valueConverter) ConvertValue(v any) (driver.Value, error) {
14+
return driver.Value(v), nil
15+
}

v3/sqlite/doc.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Package sqlite provides an interface to sqlite database stores within Spin
2+
// components.
3+
//
4+
// This package is implemented as a driver that conforms to the built-in
5+
// database/sql interface.
6+
//
7+
// db := sqlite.Open("default")
8+
// defer db.Close()
9+
//
10+
// s, err := db.Prepare("REPLACE INTO pets VALUES (4, 'Maya', ?, false);")
11+
// // if err != nil { ... }
12+
//
13+
// _, err = s.Query("bananas")
14+
// // if err != nil { ... }
15+
//
16+
// rows, err := db.Query("SELECT * FROM pets")
17+
// // if err != nil { ... }
18+
//
19+
// var pets []*Pet
20+
// for rows.Next() {
21+
// var pet Pet
22+
// if err := rows.Scan(&pet.ID, &pet.Name, &pet.Prey, &pet.IsFinicky); err != nil {
23+
// ...
24+
// }
25+
// pets = append(pets, &pet)
26+
// }
27+
package sqlite

v3/sqlite/sqlite.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package sqlite
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"database/sql/driver"
7+
"errors"
8+
"fmt"
9+
"io"
10+
11+
spindb "github.com/spinframework/spin-go-sdk/v3/internal/db"
12+
"github.com/spinframework/spin-go-sdk/v3/internal/fermyon/spin/v2.0.0/sqlite"
13+
"go.bytecodealliance.org/cm"
14+
)
15+
16+
// Open returns a new connection to the database.
17+
func Open(name string) *sql.DB {
18+
return sql.OpenDB(&connector{name: name})
19+
}
20+
21+
// conn represents a database connection.
22+
type conn struct {
23+
spinConn sqlite.Connection
24+
}
25+
26+
// Close the connection.
27+
func (c *conn) Close() error {
28+
return nil
29+
}
30+
31+
// Prepare returns a prepared statement, bound to this connection.
32+
func (c *conn) Prepare(query string) (driver.Stmt, error) {
33+
return &stmt{conn: c, query: query}, nil
34+
}
35+
36+
// Begin isn't supported.
37+
func (c *conn) Begin() (driver.Tx, error) {
38+
return nil, errors.New("transactions are unsupported by this driver")
39+
}
40+
41+
// connector implements driver.Connector.
42+
type connector struct {
43+
conn *conn
44+
name string
45+
}
46+
47+
// Connect returns a connection to the database.
48+
func (d *connector) Connect(_ context.Context) (driver.Conn, error) {
49+
if d.conn != nil {
50+
return d.conn, nil
51+
}
52+
return d.Open(d.name)
53+
}
54+
55+
// Driver returns the underlying Driver of the Connector.
56+
func (d *connector) Driver() driver.Driver {
57+
return d
58+
}
59+
60+
// Open returns a new connection to the database.
61+
func (d *connector) Open(name string) (driver.Conn, error) {
62+
results := sqlite.ConnectionOpen(name)
63+
if results.IsErr() {
64+
return nil, toError(results.Err())
65+
}
66+
d.conn = &conn{
67+
spinConn: *results.OK(),
68+
}
69+
return d.conn, nil
70+
}
71+
72+
// Close closes the connection to the database.
73+
func (d *connector) Close() error {
74+
if d.conn != nil {
75+
d.conn.Close()
76+
}
77+
return nil
78+
}
79+
80+
type rows struct {
81+
columns []string
82+
pos int
83+
numRows int
84+
rows [][]any
85+
}
86+
87+
var _ driver.Rows = (*rows)(nil)
88+
89+
// Columns return column names.
90+
func (r *rows) Columns() []string {
91+
return r.columns
92+
}
93+
94+
// Close closes the rows iterator.
95+
func (r *rows) Close() error {
96+
r.rows = nil
97+
r.pos = 0
98+
r.numRows = 0
99+
return nil
100+
}
101+
102+
// Next moves the cursor to the next row.
103+
func (r *rows) Next(dest []driver.Value) error {
104+
if !r.HasNextResultSet() {
105+
return io.EOF
106+
}
107+
for i := 0; i != len(r.columns); i++ {
108+
dest[i] = driver.Value(r.rows[r.pos][i])
109+
}
110+
r.pos++
111+
return nil
112+
}
113+
114+
// HasNextResultSet is called at the end of the current result set and
115+
// reports whether there is another result set after the current one.
116+
func (r *rows) HasNextResultSet() bool {
117+
return r.pos < r.numRows
118+
}
119+
120+
// NextResultSet advances the driver to the next result set even
121+
// if there are remaining rows in the current result set.
122+
//
123+
// NextResultSet should return io.EOF when there are no more result sets.
124+
func (r *rows) NextResultSet() error {
125+
if r.HasNextResultSet() {
126+
r.pos++
127+
return nil
128+
}
129+
return io.EOF // Per interface spec.
130+
}
131+
132+
type stmt struct {
133+
conn *conn
134+
query string
135+
}
136+
137+
var _ driver.Stmt = (*stmt)(nil)
138+
var _ driver.ColumnConverter = (*stmt)(nil)
139+
140+
// Close closes the statement.
141+
func (s *stmt) Close() error {
142+
return nil
143+
}
144+
145+
// NumInput returns the number of placeholder parameters.
146+
func (s *stmt) NumInput() int {
147+
// Golang sql won't sanity check argument counts before Query.
148+
return -1
149+
}
150+
151+
func toRow(row []sqlite.Value) []any {
152+
ret := make([]any, len(row))
153+
for i, v := range row {
154+
switch v.String() {
155+
case "integer":
156+
ret[i] = *v.Integer()
157+
case "real":
158+
ret[i] = *v.Real()
159+
case "text":
160+
ret[i] = *v.Text()
161+
case "blob":
162+
// TODO: check this
163+
ret[i] = *v.Blob()
164+
case "null":
165+
ret[i] = nil
166+
default:
167+
panic("unknown value type")
168+
}
169+
}
170+
return ret
171+
}
172+
173+
func toWasiValue(x any) sqlite.Value {
174+
switch v := x.(type) {
175+
case int:
176+
return sqlite.ValueInteger(int64(v))
177+
case int64:
178+
return sqlite.ValueInteger(v)
179+
case float64:
180+
return sqlite.ValueReal(v)
181+
case string:
182+
return sqlite.ValueText(v)
183+
case []byte:
184+
return sqlite.ValueBlob(cm.ToList([]uint8(v)))
185+
case nil:
186+
return sqlite.ValueNull()
187+
default:
188+
panic("unknown value type")
189+
}
190+
}
191+
192+
// Query executes a query that may return rows, such as a SELECT.
193+
func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
194+
params := make([]sqlite.Value, len(args))
195+
for i := range args {
196+
params[i] = toWasiValue(args[i])
197+
}
198+
results, err, isErr := s.conn.spinConn.Execute(s.query, cm.ToList(params)).Result()
199+
if isErr {
200+
return nil, toError(&err)
201+
}
202+
203+
cols := results.Columns.Slice()
204+
205+
rowLen := results.Rows.Len()
206+
allrows := make([][]any, rowLen)
207+
for rownum, row := range results.Rows.Slice() {
208+
allrows[rownum] = toRow(row.Values.Slice())
209+
}
210+
rows := &rows{
211+
columns: cols,
212+
rows: allrows,
213+
numRows: int(rowLen),
214+
}
215+
return rows, nil
216+
}
217+
218+
// Exec executes a query that doesn't return rows, such as an INSERT or
219+
// UPDATE.
220+
func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
221+
params := make([]sqlite.Value, len(args))
222+
for i := range args {
223+
params[i] = toWasiValue(args[i])
224+
}
225+
_, err, isErr := s.conn.spinConn.Execute(s.query, cm.ToList(params)).Result()
226+
if isErr {
227+
return nil, toError(&err)
228+
}
229+
return &result{}, nil
230+
}
231+
232+
// ColumnConverter returns GlobalParameterConverter to prevent using driver.DefaultParameterConverter.
233+
func (s *stmt) ColumnConverter(_ int) driver.ValueConverter {
234+
return spindb.GlobalParameterConverter
235+
}
236+
237+
type result struct{}
238+
239+
func (r result) LastInsertId() (int64, error) {
240+
return -1, errors.New("LastInsertId is unsupported by this driver")
241+
}
242+
243+
func (r result) RowsAffected() (int64, error) {
244+
return -1, errors.New("RowsAffected is unsupported by this driver")
245+
}
246+
247+
func toError(err *sqlite.Error) error {
248+
if err == nil {
249+
return nil
250+
}
251+
if err.String() == "io" {
252+
return fmt.Errorf("io: %s", *err.IO())
253+
}
254+
return errors.New(err.String())
255+
}

0 commit comments

Comments
 (0)