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 }