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>
121 lines
2.9 KiB
Go
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
|
|
}
|