Guest-side database access over the db.* host calls (abiv1 db.proto), two surfaces sharing one injected transport: - database/sql driver registered as "bnwasm" (driver.go) — QueryContext/ ExecContext/BeginTx over db.query/db.exec/db.tx_*; named args rejected. - plugin.Pool (pool.go) handing out a pgx.Tx-shaped value (tx.go), so the pgx-flavored sqlc DBTX every current plugin generates against (sql_package: pgx/v5) is satisfied with no source edits. This is the primary path: their DBTX needs pgconn.CommandTag/pgx.Rows/pgx.Row, which database/sql cannot produce. DbValue↔Go mapping (dbvalue.go) covers all 11 oneof arms both directions; DbError surfaces as *pgconn.PgError (SQLSTATE preserved for errors.As); nested tx/savepoints rejected with a clear error (no fleet plugin uses them). The scan contract is pinned in the exported DbValueFixtures table (dbvalue_fixtures.go) that the WO-WZ-007 host executor mirrors. Tests: driver_test.go (fake host — every DbValue variant round-trips with correct scan types, exec rows-affected, ordered host-call assertions for tx commit/rollback sequences, post-rollback autocommit carries no handle) and sqlcgen_test.go (vendored sqlc-style Queries + WithTx run against the fake host). Native + wasip1 builds green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
5.5 KiB
Go
202 lines
5.5 KiB
Go
package bnwasm
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
|
|
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
|
|
)
|
|
|
|
// DriverName is the database/sql driver name registered by this package.
|
|
const DriverName = "bnwasm"
|
|
|
|
func init() { sql.Register(DriverName, Driver{}) }
|
|
|
|
// defaultTransport is the process-wide transport the "bnwasm" database/sql
|
|
// driver uses (the guest has exactly one host). The wasip1 shim sets it via
|
|
// SetDefaultTransport; native builds leave it nil so opened connections fail
|
|
// cleanly with errNoHost. Guarded because sql.DB may dial from any goroutine.
|
|
var (
|
|
defaultMu sync.RWMutex
|
|
defaultTransport Transport
|
|
)
|
|
|
|
// SetDefaultTransport binds the transport used by database/sql connections
|
|
// opened via sql.Open("bnwasm", ...). Called once from the wasm guest shim.
|
|
func SetDefaultTransport(t Transport) {
|
|
defaultMu.Lock()
|
|
defaultTransport = t
|
|
defaultMu.Unlock()
|
|
}
|
|
|
|
func currentTransport() Transport {
|
|
defaultMu.RLock()
|
|
defer defaultMu.RUnlock()
|
|
return defaultTransport
|
|
}
|
|
|
|
// Driver is the database/sql/driver.Driver for the wasm DB ABI.
|
|
type Driver struct{}
|
|
|
|
var (
|
|
_ driver.Driver = Driver{}
|
|
_ driver.DriverContext = Driver{}
|
|
)
|
|
|
|
// Open ignores its DSN — the transport is process-global (one host per guest).
|
|
func (d Driver) Open(name string) (driver.Conn, error) {
|
|
return &conn{t: currentTransport()}, nil
|
|
}
|
|
|
|
// OpenConnector lets sql.OpenDB bypass DSN parsing entirely.
|
|
func (d Driver) OpenConnector(name string) (driver.Connector, error) {
|
|
return connector{}, nil
|
|
}
|
|
|
|
// NewConnector returns a driver.Connector bound to an explicit transport, for
|
|
// callers that construct a *sql.DB via sql.OpenDB without touching the global.
|
|
func NewConnector(t Transport) driver.Connector { return connector{t: t, explicit: true} }
|
|
|
|
type connector struct {
|
|
t Transport
|
|
explicit bool
|
|
}
|
|
|
|
func (c connector) Connect(context.Context) (driver.Conn, error) {
|
|
t := c.t
|
|
if !c.explicit {
|
|
t = currentTransport()
|
|
}
|
|
return &conn{t: t}, nil
|
|
}
|
|
|
|
func (c connector) Driver() driver.Driver { return Driver{} }
|
|
|
|
// conn is one logical connection: it holds no real connection state guest-side,
|
|
// only the transport and the current transaction handle (0 = autocommit).
|
|
type conn struct {
|
|
t Transport
|
|
txHandle uint64
|
|
inTx bool
|
|
}
|
|
|
|
var (
|
|
_ driver.Conn = (*conn)(nil)
|
|
_ driver.QueryerContext = (*conn)(nil)
|
|
_ driver.ExecerContext = (*conn)(nil)
|
|
_ driver.ConnBeginTx = (*conn)(nil)
|
|
)
|
|
|
|
func (c *conn) Prepare(query string) (driver.Stmt, error) {
|
|
return nil, fmt.Errorf("bnwasm: prepared statements are not supported; use QueryContext/ExecContext")
|
|
}
|
|
|
|
func (c *conn) Close() error { return nil }
|
|
|
|
func (c *conn) Begin() (driver.Tx, error) { return c.BeginTx(context.Background(), driver.TxOptions{}) }
|
|
|
|
func (c *conn) BeginTx(ctx context.Context, _ driver.TxOptions) (driver.Tx, error) {
|
|
if c.inTx {
|
|
return nil, errNestedTx
|
|
}
|
|
handle, err := c.t.txBegin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.txHandle = handle
|
|
c.inTx = true
|
|
return &sqlTx{c: c}, nil
|
|
}
|
|
|
|
func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
|
posArgs, err := namedToPositional(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tag, err := c.t.exec(ctx, query, c.txHandle, posArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sqlResult{rowsAffected: tag.RowsAffected()}, nil
|
|
}
|
|
|
|
func (c *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
|
posArgs, err := namedToPositional(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.t.query(ctx, query, c.txHandle, posArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newDriverRows(resp), nil
|
|
}
|
|
|
|
// sqlTx is the database/sql transaction handle; it clears the conn's handle on
|
|
// completion so subsequent statements run in autocommit again.
|
|
type sqlTx struct{ c *conn }
|
|
|
|
func (t sqlTx) Commit() error {
|
|
err := t.c.t.txCommit(context.Background(), t.c.txHandle)
|
|
t.c.txHandle, t.c.inTx = 0, false
|
|
return err
|
|
}
|
|
|
|
func (t sqlTx) Rollback() error {
|
|
err := t.c.t.txRollback(context.Background(), t.c.txHandle)
|
|
t.c.txHandle, t.c.inTx = 0, false
|
|
return err
|
|
}
|
|
|
|
type sqlResult struct{ rowsAffected int64 }
|
|
|
|
func (r sqlResult) LastInsertId() (int64, error) {
|
|
return 0, fmt.Errorf("bnwasm: LastInsertId is not supported (Postgres uses RETURNING)")
|
|
}
|
|
func (r sqlResult) RowsAffected() (int64, error) { return r.rowsAffected, nil }
|
|
|
|
// driverRows adapts a buffered DbRowsResponse to database/sql/driver.Rows.
|
|
type driverRows struct {
|
|
resp *abiv1.DbRowsResponse
|
|
idx int
|
|
}
|
|
|
|
func newDriverRows(resp *abiv1.DbRowsResponse) *driverRows { return &driverRows{resp: resp, idx: 0} }
|
|
|
|
func (r *driverRows) Columns() []string { return r.resp.GetColumns() }
|
|
|
|
func (r *driverRows) Close() error { return nil }
|
|
|
|
func (r *driverRows) Next(dest []driver.Value) error {
|
|
rows := r.resp.GetRows()
|
|
if r.idx >= len(rows) {
|
|
return io.EOF
|
|
}
|
|
cells := rows[r.idx].GetValues()
|
|
if len(dest) != len(cells) {
|
|
return fmt.Errorf("bnwasm: expected %d columns, got %d destinations", len(cells), len(dest))
|
|
}
|
|
for i, cell := range cells {
|
|
dest[i] = driverValue(cell)
|
|
}
|
|
r.idx++
|
|
return nil
|
|
}
|
|
|
|
// namedToPositional flattens database/sql NamedValues to positional args,
|
|
// rejecting any that carry a name (sqlc emits positional $N parameters only).
|
|
func namedToPositional(args []driver.NamedValue) ([]any, error) {
|
|
out := make([]any, len(args))
|
|
for i, a := range args {
|
|
if a.Name != "" {
|
|
return nil, fmt.Errorf("bnwasm: named argument %q is not supported; use positional parameters", a.Name)
|
|
}
|
|
out[i] = a.Value
|
|
}
|
|
return out, nil
|
|
}
|