// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. package pgserver import ( "bytes" "strconv" "testing" "five/hbrt" ) // TestEncodeText_Numeric pins the text-format encoding for the four // numeric Five variants psql actually receives. Regressions here // would surface as silently mis-formatted DataRow values that some // clients render and others reject — easier to catch with a focused // unit test than via a psql round-trip. func TestEncodeText_Numeric(t *testing.T) { cases := []struct { name string v hbrt.Value want []byte }{ {"int-positive", hbrt.MakeInt(42), []byte("42")}, {"int-negative", hbrt.MakeInt(-7), []byte("-7")}, {"long", hbrt.MakeLong(9876543210), []byte("9876543210")}, // MakeDouble's metadata: (value, len, dec) — dec=2 should // surface as "50000.00" not "50000". {"decimal-2dp", hbrt.MakeDouble(50000.0, 10, 2), []byte("50000.00")}, {"decimal-fraction", hbrt.MakeDouble(42000.5, 10, 2), []byte("42000.50")}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := encodeText(tc.v) if !bytes.Equal(got, tc.want) { t.Errorf("encodeText: want %q, got %q", tc.want, got) } }) } } // TestEncodeText_Strings covers the trivial case but also the NIL // → nil-slice contract that DataRow uses to distinguish NULL from // empty string ("" sends length=0; NIL sends length=-1). func TestEncodeText_Strings(t *testing.T) { if got := encodeText(hbrt.MakeString("hello")); !bytes.Equal(got, []byte("hello")) { t.Errorf("string encode: got %q", got) } if got := encodeText(hbrt.MakeString("")); got == nil { t.Error("empty string must encode as []byte{}, not nil (NULL marker)") } if got := encodeText(hbrt.MakeNil()); got != nil { t.Errorf("NIL must encode as nil slice (PG NULL marker), got %q", got) } if got := encodeText(hbrt.MakeBool(true)); !bytes.Equal(got, []byte("t")) { t.Errorf("bool true: got %q", got) } if got := encodeText(hbrt.MakeBool(false)); !bytes.Equal(got, []byte("f")) { t.Errorf("bool false: got %q", got) } } // TestPgTypeFor verifies OID selection for the column-type // detection path. Integer-shaped numerics that fit int32 must // transit as INT4 so BI tools display them right-aligned with // no decimal point. func TestPgTypeFor(t *testing.T) { type ent struct { v hbrt.Value wantOID uint32 } for i, tc := range []ent{ {hbrt.MakeInt(0), oidInt4}, {hbrt.MakeInt(2147483647), oidInt4}, {hbrt.MakeLong(9999999999), oidInt8}, {hbrt.MakeDouble(1.5, 10, 2), oidNumeric}, {hbrt.MakeString("x"), oidText}, {hbrt.MakeBool(true), oidBool}, {hbrt.MakeNil(), oidText}, // fallback when no sample } { oid, _ := pgTypeFor(tc.v) if oid != tc.wantOID { t.Errorf("case %d: want oid %d, got %d", i, tc.wantOID, oid) } } } // TestSqlStateFor verifies the FiveSql2-error-code → SQLSTATE map. // Drivers dispatch on the leading two chars (class code), so the // table needs to match the canonical PG layout for libpq-style // exception handling to work. func TestSqlStateFor(t *testing.T) { want := map[int]string{ 1: "42601", 2: "42P01", 3: "42703", 8: "25P02", 99: "XX000", } for code, expect := range want { got := sqlStateFor(code) if got != expect { t.Errorf("sqlStateFor(%d) = %q, want %q", code, got, expect) } } } // TestCommandTagFor pins the CommandComplete tag verbs. Tagged // rows (n) come in Phase 3; for v1.0 we always emit "VERB 0" so // psql-style row-count display works (it prints "(0 행)" but // doesn't error out). func TestCommandTagFor(t *testing.T) { cases := []struct{ sql, want string }{ {"SELECT * FROM x", "SELECT 0"}, {" select 1", "SELECT 0"}, {"INSERT INTO x VALUES (1)", "INSERT 0"}, {"UPDATE x SET a=1", "UPDATE 0"}, {"DELETE FROM x", "DELETE 0"}, {"BEGIN", "BEGIN"}, {"COMMIT", "COMMIT"}, {"CREATE TABLE foo (x INT)", "CREATE"}, } for _, c := range cases { if got := commandTagFor(c.sql); got != c.want { t.Errorf("commandTagFor(%q) = %q, want %q", c.sql, got, c.want) } } _ = strconv.Itoa // keep import; will be used in Phase 3 with row counts }