Alex Dunmow b6d40ed8ac feat: bootstrap block/pluginsdk — the proto-first plugin SDK (P1)
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>
2026-07-07 10:51:00 +08:00

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
}