Skip to content

Commit 08fd1cc

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

File tree

8 files changed

+385
-0
lines changed

8 files changed

+385
-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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
spinhttp "github.com/spinframework/spin-go-sdk/v3/http"
9+
"github.com/spinframework/spin-go-sdk/v3/sqlite"
10+
)
11+
12+
type Pet struct {
13+
ID int64
14+
Name string
15+
Prey *string // nullable field must be a pointer
16+
IsFinicky bool
17+
}
18+
19+
func init() {
20+
spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
21+
db := sqlite.Open("default")
22+
defer db.Close()
23+
24+
_, err := db.Query("REPLACE INTO pets VALUES (4, 'Maya', ?, false);", "bananas")
25+
if err != nil {
26+
http.Error(w, err.Error(), http.StatusInternalServerError)
27+
return
28+
}
29+
30+
rows, err := db.Query("SELECT * FROM pets")
31+
if err != nil {
32+
http.Error(w, err.Error(), http.StatusInternalServerError)
33+
return
34+
}
35+
36+
var pets []*Pet
37+
for rows.Next() {
38+
var pet Pet
39+
if err := rows.Scan(&pet.ID, &pet.Name, &pet.Prey, &pet.IsFinicky); err != nil {
40+
fmt.Println(err)
41+
}
42+
pets = append(pets, &pet)
43+
}
44+
json.NewEncoder(w).Encode(pets)
45+
})
46+
}
47+
48+
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: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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+
len 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.len = 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.len
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 := s.conn.spinConn.Execute(s.query, cm.ToList(params))
199+
if results.IsErr() {
200+
return nil, toError(results.Err())
201+
}
202+
203+
okresult := results.OK()
204+
cols := okresult.Columns.Slice()
205+
206+
rowLen := okresult.Rows.Len()
207+
allrows := make([][]any, rowLen)
208+
for rownum, row := range okresult.Rows.Slice() {
209+
allrows[rownum] = toRow(row.Values.Slice())
210+
}
211+
rows := &rows{
212+
columns: cols,
213+
rows: allrows,
214+
len: int(rowLen),
215+
}
216+
return rows, nil
217+
}
218+
219+
// Exec executes a query that doesn't return rows, such as an INSERT or
220+
// UPDATE.
221+
func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
222+
params := make([]sqlite.Value, len(args))
223+
for i := range args {
224+
params[i] = toWasiValue(args[i])
225+
}
226+
results := s.conn.spinConn.Execute(s.query, cm.ToList(params))
227+
if results.IsErr() {
228+
return nil, toError(results.Err())
229+
}
230+
return &result{}, nil
231+
}
232+
233+
// ColumnConverter returns GlobalParameterConverter to prevent using driver.DefaultParameterConverter.
234+
func (s *stmt) ColumnConverter(_ int) driver.ValueConverter {
235+
return spindb.GlobalParameterConverter
236+
}
237+
238+
type result struct{}
239+
240+
func (r result) LastInsertId() (int64, error) {
241+
return -1, errors.New("LastInsertId is unsupported by this driver")
242+
}
243+
244+
func (r result) RowsAffected() (int64, error) {
245+
return -1, errors.New("RowsAffected is unsupported by this driver")
246+
}
247+
248+
func toError(err *sqlite.Error) error {
249+
if err == nil {
250+
return nil
251+
}
252+
if err.String() == "io" {
253+
return fmt.Errorf("io: %s", *err.IO())
254+
}
255+
return errors.New(err.String())
256+
}

0 commit comments

Comments
 (0)