package bnwasm import ( "context" "testing" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" ) // --- Vendored sqlc output --------------------------------------------------- // // The block below is byte-for-byte in the shape `sqlc generate` emits for the // pgx/v5 sql_package with the same overrides symposium uses (uuid→uuid.UUID, // jsonb→json.RawMessage, emit_pointers_for_null_types). It is checked in so a // compile of THIS test is proof that generated plugin code binds to the bnwasm // Pool/Tx (as its DBTX) and pgxRows/pgxRow (via Scan) with no edits — the same // DBTX interface, the same pgconn.CommandTag / pgx.Rows / pgx.Row signatures. type DBTX interface { Exec(context.Context, string, ...any) (pgconn.CommandTag, error) Query(context.Context, string, ...any) (pgx.Rows, error) QueryRow(context.Context, string, ...any) pgx.Row } func New(db DBTX) *Queries { return &Queries{db: db} } type Queries struct{ db DBTX } func (q *Queries) WithTx(tx pgx.Tx) *Queries { return &Queries{db: tx} } type Widget struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Notes *string `json:"notes"` Count int32 `json:"count"` CreatedAt pgtype.Timestamptz `json:"created_at"` } const createWidget = `-- name: CreateWidget :one INSERT INTO widgets (name, notes) VALUES ($1, $2) RETURNING id ` type CreateWidgetParams struct { Name string `json:"name"` Notes *string `json:"notes"` } func (q *Queries) CreateWidget(ctx context.Context, arg CreateWidgetParams) (uuid.UUID, error) { row := q.db.QueryRow(ctx, createWidget, arg.Name, arg.Notes) var id uuid.UUID err := row.Scan(&id) return id, err } const getWidget = `-- name: GetWidget :one SELECT id, name, notes, count, created_at FROM widgets WHERE id = $1 ` func (q *Queries) GetWidget(ctx context.Context, id uuid.UUID) (Widget, error) { row := q.db.QueryRow(ctx, getWidget, id) var i Widget err := row.Scan(&i.ID, &i.Name, &i.Notes, &i.Count, &i.CreatedAt) return i, err } const listWidgets = `-- name: ListWidgets :many SELECT id, name, notes, count, created_at FROM widgets ORDER BY name ` func (q *Queries) ListWidgets(ctx context.Context) ([]Widget, error) { rows, err := q.db.Query(ctx, listWidgets) if err != nil { return nil, err } defer rows.Close() var items []Widget for rows.Next() { var i Widget if err := rows.Scan(&i.ID, &i.Name, &i.Notes, &i.Count, &i.CreatedAt); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const deleteWidget = `-- name: DeleteWidget :exec DELETE FROM widgets WHERE id = $1 ` func (q *Queries) DeleteWidget(ctx context.Context, id uuid.UUID) error { _, err := q.db.Exec(ctx, deleteWidget, id) return err } // --- Compile-time proof the bnwasm surfaces satisfy the generated interfaces -- var ( _ DBTX = (*Pool)(nil) _ DBTX = (*Tx)(nil) _ pgx.Tx = (*Tx)(nil) ) // --- End-to-end runs against the fake host ---------------------------------- func widgetRow(id uuid.UUID, name string, count int32) *abiv1.DbRow { return &abiv1.DbRow{Values: []*abiv1.DbValue{ {Kind: &abiv1.DbValue_UuidValue{UuidValue: id.String()}}, {Kind: &abiv1.DbValue_StringValue{StringValue: name}}, {Kind: &abiv1.DbValue_Null{Null: true}}, // notes → NULL → (*string)(nil) {Kind: &abiv1.DbValue_Int64Value{Int64Value: int64(count)}}, {Kind: &abiv1.DbValue_Null{Null: true}}, // created_at → NULL Timestamptz }} } func TestSqlcGetWidget(t *testing.T) { id := uuid.New() host := &fakeHost{queryResp: &abiv1.DbRowsResponse{ Columns: []string{"id", "name", "notes", "count", "created_at"}, Rows: []*abiv1.DbRow{widgetRow(id, "gadget", 3)}, }} q := New(NewPool(host.call)) w, err := q.GetWidget(context.Background(), id) if err != nil { t.Fatal(err) } if w.ID != id || w.Name != "gadget" || w.Count != 3 { t.Fatalf("unexpected widget: %+v", w) } if w.Notes != nil { t.Fatalf("notes should be nil for NULL column, got %v", *w.Notes) } if w.CreatedAt.Valid { t.Fatalf("created_at should be invalid for NULL column") } } func TestSqlcListWidgets(t *testing.T) { host := &fakeHost{queryResp: &abiv1.DbRowsResponse{ Columns: []string{"id", "name", "notes", "count", "created_at"}, Rows: []*abiv1.DbRow{ widgetRow(uuid.New(), "alpha", 1), widgetRow(uuid.New(), "beta", 2), }, }} q := New(NewPool(host.call)) items, err := q.ListWidgets(context.Background()) if err != nil { t.Fatal(err) } if len(items) != 2 || items[0].Name != "alpha" || items[1].Name != "beta" { t.Fatalf("unexpected list: %+v", items) } } // TestSqlcWithTxCreate proves the generated WithTx path binds to the bnwasm Tx // and that CreateWidget's RETURNING id scans, all inside one transaction with // the right ordered host calls. func TestSqlcWithTxCreate(t *testing.T) { newID := uuid.New() host := &fakeHost{ txHandle: 42, queryResp: &abiv1.DbRowsResponse{ Columns: []string{"id"}, Rows: []*abiv1.DbRow{{Values: []*abiv1.DbValue{{Kind: &abiv1.DbValue_UuidValue{UuidValue: newID.String()}}}}}, }, } pool := NewPool(host.call) ctx := context.Background() tx, err := pool.Begin(ctx) if err != nil { t.Fatal(err) } notes := "hi" gotID, err := New(pool).WithTx(tx).CreateWidget(ctx, CreateWidgetParams{Name: "w", Notes: ¬es}) if err != nil { t.Fatal(err) } if gotID != newID { t.Fatalf("returning id: want %s got %s", newID, gotID) } if err := tx.Commit(ctx); err != nil { t.Fatal(err) } // begin → query(RETURNING) → commit; the query carried the tx handle. if got := host.methods(); len(got) != 3 || got[0] != methodTxBegin || got[1] != methodQuery || got[2] != methodTxCommit { t.Fatalf("method sequence: %v", got) } if host.calls[1].txHandle != 42 { t.Fatalf("in-tx query should carry handle 42, got %d", host.calls[1].txHandle) } // The NULL-able *string arg marshaled as a real string arg. if len(host.calls[1].args) != 2 { t.Fatalf("want 2 args, got %d", len(host.calls[1].args)) } }