Alex Dunmow 7881aeee05 feat(wasmguest): guest DB driver bnwasm (WO-WZ-004)
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>
2026-07-03 14:41:45 +08:00

121 lines
2.9 KiB
Go

package bnwasm
import (
"errors"
"fmt"
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
// pgxRows adapts a buffered DbRowsResponse to the pgx.Rows interface. The full
// result set is already materialized (the host returns it in one message), so
// iteration is a slice cursor. Only the methods sqlc-generated code exercises
// carry real behavior; the rest satisfy the interface.
type pgxRows struct {
columns []string
rows []*abiv1.DbRow
idx int // index of the row Next() last advanced to; -1 before first Next
err error
closed bool
}
func newPgxRows(resp *abiv1.DbRowsResponse) *pgxRows {
return &pgxRows{columns: resp.GetColumns(), rows: resp.GetRows(), idx: -1}
}
func (r *pgxRows) Close() { r.closed = true }
func (r *pgxRows) Err() error { return r.err }
func (r *pgxRows) CommandTag() pgconn.CommandTag {
return pgconn.NewCommandTag(fmt.Sprintf("SELECT %d", len(r.rows)))
}
func (r *pgxRows) FieldDescriptions() []pgconn.FieldDescription {
fds := make([]pgconn.FieldDescription, len(r.columns))
for i, c := range r.columns {
fds[i] = pgconn.FieldDescription{Name: c}
}
return fds
}
func (r *pgxRows) Next() bool {
if r.err != nil || r.closed {
return false
}
if r.idx+1 >= len(r.rows) {
r.Close() // pgx auto-closes when iteration is exhausted
return false
}
r.idx++
return true
}
func (r *pgxRows) Scan(dest ...any) error {
if r.err != nil {
return r.err
}
if r.idx < 0 || r.idx >= len(r.rows) {
return errors.New("bnwasm: Scan called without a current row (call Next first)")
}
if err := scanRow(r.rows[r.idx], dest); err != nil {
r.err = err
r.Close()
return err
}
return nil
}
func (r *pgxRows) Values() ([]any, error) {
if r.idx < 0 || r.idx >= len(r.rows) {
return nil, errors.New("bnwasm: Values called without a current row")
}
cells := r.rows[r.idx].GetValues()
out := make([]any, len(cells))
for i, c := range cells {
v, _ := naturalValue(c)
out[i] = v
}
return out, nil
}
func (r *pgxRows) RawValues() [][]byte { return nil }
func (r *pgxRows) Conn() *pgx.Conn { return nil }
// pgxRow is the single-row QueryRow result. Like pgx, it defers all errors
// (including the query error) to Scan, and reports pgx.ErrNoRows when empty.
type pgxRow struct {
rows *pgxRows
err error
}
func (r *pgxRow) Scan(dest ...any) error {
if r.err != nil {
return r.err
}
defer r.rows.Close()
if !r.rows.Next() {
if r.rows.Err() != nil {
return r.rows.Err()
}
return pgx.ErrNoRows
}
return r.rows.Scan(dest...)
}
func scanRow(row *abiv1.DbRow, dest []any) error {
cells := row.GetValues()
if len(dest) != len(cells) {
return fmt.Errorf("bnwasm: scan expected %d destinations, got %d columns", len(dest), len(cells))
}
for i, d := range dest {
if err := scanValue(cells[i], d); err != nil {
return fmt.Errorf("bnwasm: scan column %d: %w", i, err)
}
}
return nil
}