Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a mechanical block/core/X -> block/pluginsdk/X import rewrite. - abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms authoring source (cms/backend/abi/proto/v1) with go_package retargeted to git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept cms/core proto duplication (audit gap 4). - abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1 save the embedded go_package path. - plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim, caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the guest-facing type packages: blocks (+builtin/shared/tags), templates (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai, subscriptions, menus, datasources. Internal imports rewritten core -> pluginsdk; zero block/core references remain. - README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one binding; no replace directives; templates/bn is a synced copy authored in cms. Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md 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/pluginsdk/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
|
|
}
|