package bnwasm import ( "encoding/json" "time" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" "github.com/google/uuid" "google.golang.org/protobuf/types/known/timestamppb" ) // FixtureTime is the fixed timestamp used by the shared DbValue fixtures, so // both sides of the boundary compare against one deterministic value. var FixtureTime = time.Date(2026, 7, 3, 12, 30, 0, 0, time.UTC) // FixtureUUID is the fixed UUID used by the shared DbValue fixtures. var FixtureUUID = uuid.MustParse("11111111-2222-3333-4444-555555555555") // FixtureUUIDs are the fixed, ORDERED UUIDs used by the uuid_array fixture; the // round-trip must preserve both their values and their order. var FixtureUUIDs = []uuid.UUID{ uuid.MustParse("aaaaaaaa-0000-0000-0000-000000000001"), uuid.MustParse("bbbbbbbb-0000-0000-0000-000000000002"), uuid.MustParse("cccccccc-0000-0000-0000-000000000003"), } // DbValueFixture is one entry in the canonical DbValue↔Go mapping table. type DbValueFixture struct { // Name identifies the DbValue variant. Name string // Value is the wire value the host would send back in a DbRowsResponse cell // (and the guest would send as an argument for the non-NULL scalar cases). Value *abiv1.DbValue // NewDest returns a fresh pointer to the canonical Go scan destination type // that plugin sqlc code binds for this variant. NewDest func() any // Want is the expected dereferenced value after scanning Value into // NewDest(). Compared with reflect.DeepEqual (times via .Equal). Want any // DriverValue is the expected database/sql driver.Value for this variant, // documenting the "bnwasm" driver's mapping (uuid/numeric→string, // text[]→Postgres array literal). DriverValue any } // DbValueFixtures is the authoritative DbValue↔Go mapping table. It is the // shared contract WO-WZ-007 (the CMS host executor) MUST mirror: every variant // the guest driver produces on scan, the host must be able to build on read, // and vice-versa. Exported (non-test) precisely so the host's tests in the CMS // module can import and assert against the same table rather than duplicating a // drift-prone copy. // // Every oneof arm of abiv1.DbValue.Kind appears at least once; several arms // carry extra entries that exercise encoding edge cases (the zero timestamp, // negative/very-large numerics, an empty text[], and text[] elements that force // encodePgTextArray's quoting/escaping paths) so the host mirror must reproduce // them too. // // Contract limit — NULL array elements: abiv1.TextArray is a repeated string, // which has no per-element NULL. A Postgres text[] value like '{a,NULL,b}' // therefore CANNOT round-trip through this boundary: a NULL element collapses to // the empty string "". The whole array can still be SQL NULL (a nil []string / // DbValue_Null), but an individual NULL *inside* the array is unrepresentable. // The WO-WZ-007 host executor MUST honor this same limit (encode a NULL element // as "" or reject it) — it must not invent a sentinel. var DbValueFixtures = []DbValueFixture{ { Name: "null", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Null{Null: true}}, NewDest: func() any { return new(*string) }, // **string: NULL → (*string)(nil) Want: (*string)(nil), DriverValue: nil, }, { Name: "bool", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_BoolValue{BoolValue: true}}, NewDest: func() any { return new(bool) }, Want: true, DriverValue: true, }, { Name: "int64", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Int64Value{Int64Value: 42}}, NewDest: func() any { return new(int64) }, Want: int64(42), DriverValue: int64(42), }, { Name: "int32", // narrowing coercion sqlc emits for int4 columns Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Int64Value{Int64Value: 7}}, NewDest: func() any { return new(int32) }, Want: int32(7), DriverValue: int64(7), }, { Name: "float64", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Float64Value{Float64Value: 3.5}}, NewDest: func() any { return new(float64) }, Want: 3.5, DriverValue: 3.5, }, { Name: "string", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_StringValue{StringValue: "hello"}}, NewDest: func() any { return new(string) }, Want: "hello", DriverValue: "hello", }, { Name: "bytes", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_BytesValue{BytesValue: []byte{0x01, 0x02, 0x03}}}, NewDest: func() any { return new([]byte) }, Want: []byte{0x01, 0x02, 0x03}, DriverValue: []byte{0x01, 0x02, 0x03}, }, { Name: "timestamp", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TimestampValue{TimestampValue: timestamppb.New(FixtureTime)}}, NewDest: func() any { return new(time.Time) }, Want: FixtureTime, DriverValue: FixtureTime, }, { Name: "uuid", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_UuidValue{UuidValue: FixtureUUID.String()}}, NewDest: func() any { return new(uuid.UUID) }, Want: FixtureUUID, DriverValue: FixtureUUID.String(), }, { Name: "jsonb", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_JsonbValue{JsonbValue: []byte(`{"a":1}`)}}, NewDest: func() any { return new(json.RawMessage) }, Want: json.RawMessage(`{"a":1}`), DriverValue: []byte(`{"a":1}`), }, { Name: "numeric", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_NumericValue{NumericValue: "123.45"}}, NewDest: func() any { return new(string) }, Want: "123.45", DriverValue: "123.45", }, { Name: "text_array", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TextArrayValue{TextArrayValue: &abiv1.TextArray{Values: []string{"a", "b"}}}}, NewDest: func() any { return new([]string) }, Want: []string{"a", "b"}, DriverValue: `{"a","b"}`, }, { // uuid[]: three ordered UUIDs bound as a native uuid[] parameter and // scanned back into []uuid.UUID. Ordering is part of the contract — the // host mirror (dbexec.go) must preserve element order. Name: "uuid_array", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_UuidArrayValue{UuidArrayValue: &abiv1.UuidArray{Values: []string{ FixtureUUIDs[0].String(), FixtureUUIDs[1].String(), FixtureUUIDs[2].String(), }}}}, NewDest: func() any { return new([]uuid.UUID) }, Want: FixtureUUIDs, DriverValue: `{"aaaaaaaa-0000-0000-0000-000000000001","bbbbbbbb-0000-0000-0000-000000000002","cccccccc-0000-0000-0000-000000000003"}`, }, // --- encoding edge cases (extra entries beyond one-per-variant) --- { Name: "timestamp_zero", // the Go zero time.Time (year 1) must survive the timestamppb round-trip Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TimestampValue{TimestampValue: timestamppb.New(time.Time{})}}, NewDest: func() any { return new(time.Time) }, Want: time.Time{}, DriverValue: time.Time{}, }, { Name: "numeric_negative", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_NumericValue{NumericValue: "-98765.4321"}}, NewDest: func() any { return new(string) }, Want: "-98765.4321", DriverValue: "-98765.4321", }, { Name: "numeric_large", // far beyond int64/float64 range: numeric stays a lossless decimal string Value: &abiv1.DbValue{Kind: &abiv1.DbValue_NumericValue{NumericValue: "123456789012345678901234567890.123456789"}}, NewDest: func() any { return new(string) }, Want: "123456789012345678901234567890.123456789", DriverValue: "123456789012345678901234567890.123456789", }, { Name: "text_array_empty", // empty-but-non-nil text[] → "{}" (distinct from a NULL array) Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TextArrayValue{TextArrayValue: &abiv1.TextArray{Values: []string{}}}}, NewDest: func() any { return new([]string) }, Want: []string(nil), DriverValue: "{}", }, { // Elements that force encodePgTextArray's quoting/escaping: a comma // (needs quoting), a double-quote and a backslash (need escaping), and an // empty-string element (renders as the empty quoted ""). Name: "text_array_quoting", Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TextArrayValue{TextArrayValue: &abiv1.TextArray{Values: []string{"a,b", `x"y`, `p\q`, ""}}}}, NewDest: func() any { return new([]string) }, Want: []string{"a,b", `x"y`, `p\q`, ""}, DriverValue: `{"a,b","x\"y","p\\q",""}`, }, }