package sqltypes import ( "encoding/json" "testing" "time" "github.com/google/uuid" ) // record mimics a typical DB model composed of sqltypes fields, exercising // marshalling/unmarshalling of the whole set together as encoding/json would // when used on a real struct (not just the individual types in isolation). type record struct { ID SqlUUID `json:"id"` Name SqlString `json:"name"` Bio SqlString `json:"bio"` Age SqlInt32 `json:"age"` Score SqlFloat64 `json:"score"` Active SqlBool `json:"active"` CreatedAt SqlTimeStamp `json:"created_at"` BirthDate SqlDate `json:"birth_date"` Avatar SqlByteArray `json:"avatar"` Tags SqlStringArray `json:"tags"` Scores SqlInt32Array `json:"scores"` Metadata SqlJSONB `json:"metadata"` Embedding SqlVector `json:"embedding"` Extras []uuid.UUID `json:"-"` } func TestStruct_JSON_RoundTrip_AllFieldsPresent(t *testing.T) { id := uuid.New() createdAt := time.Date(2024, 3, 10, 12, 30, 0, 0, time.UTC) birthDate := time.Date(1990, 5, 20, 0, 0, 0, 0, time.UTC) original := record{ ID: NewSqlUUID(id), Name: NewSqlString("Ada Lovelace"), Bio: SqlString{}, // intentionally null Age: NewSqlInt32(36), Score: NewSqlFloat64(98.6), Active: NewSqlBool(true), CreatedAt: NewSqlTimeStamp(createdAt), BirthDate: NewSqlDate(birthDate), Avatar: NewSqlByteArray([]byte{0xDE, 0xAD, 0xBE, 0xEF}), Tags: NewSqlStringArray([]string{"engineer", "mathematician"}), Scores: NewSqlInt32Array([]int32{10, 20, 30}), Metadata: SqlJSONB(`{"role":"admin"}`), Embedding: NewSqlVector([]float32{0.1, 0.2, 0.3}), } data, err := json.Marshal(original) if err != nil { t.Fatalf("Marshal failed: %v", err) } var decoded record if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if decoded.ID.UUID() != id { t.Errorf("ID: expected %v, got %v", id, decoded.ID.UUID()) } if decoded.Name.String() != "Ada Lovelace" { t.Errorf("Name: expected Ada Lovelace, got %q", decoded.Name.String()) } if decoded.Bio.Valid { t.Errorf("Bio: expected invalid/null, got %v", decoded.Bio) } if decoded.Age.Int64() != 36 { t.Errorf("Age: expected 36, got %d", decoded.Age.Int64()) } if decoded.Score.Float64() != 98.6 { t.Errorf("Score: expected 98.6, got %v", decoded.Score.Float64()) } if !decoded.Active.Bool() { t.Errorf("Active: expected true") } if decoded.CreatedAt.Time().Format("2006-01-02T15:04:05") != createdAt.Format("2006-01-02T15:04:05") { t.Errorf("CreatedAt: expected %v, got %v", createdAt, decoded.CreatedAt.Time()) } if decoded.BirthDate.String() != "1990-05-20" { t.Errorf("BirthDate: expected 1990-05-20, got %q", decoded.BirthDate.String()) } if string(decoded.Avatar.Val) != string(original.Avatar.Val) { t.Errorf("Avatar: expected %v, got %v", original.Avatar.Val, decoded.Avatar.Val) } if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[0] != "engineer" { t.Errorf("Tags: unexpected value %v", decoded.Tags.Val) } if len(decoded.Scores.Val) != 3 || decoded.Scores.Val[2] != 30 { t.Errorf("Scores: unexpected value %v", decoded.Scores.Val) } m, err := decoded.Metadata.AsMap() if err != nil || m["role"] != "admin" { t.Errorf("Metadata: expected role=admin, got %v (err=%v)", m, err) } if len(decoded.Embedding.Val) != 3 || decoded.Embedding.Val[1] != 0.2 { t.Errorf("Embedding: unexpected value %v", decoded.Embedding.Val) } } func TestStruct_JSON_RoundTrip_AllNull(t *testing.T) { var original record data, err := json.Marshal(original) if err != nil { t.Fatalf("Marshal failed: %v", err) } var decoded record if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if decoded.ID.Valid || decoded.Name.Valid || decoded.Age.Valid || decoded.Score.Valid || decoded.Active.Valid || decoded.CreatedAt.Valid || decoded.BirthDate.Valid || decoded.Avatar.Valid || decoded.Tags.Valid || decoded.Scores.Valid || decoded.Embedding.Valid { t.Errorf("expected all fields invalid/null after round-trip, got %+v", decoded) } if decoded.Metadata != nil { t.Errorf("expected nil Metadata, got %v", decoded.Metadata) } } func TestStruct_JSON_UnmarshalFromRawJSON(t *testing.T) { raw := `{ "id": "3e843d4e-6b3c-4f2e-9a1e-6f0b2f3c9d10", "name": "Grace Hopper", "bio": null, "age": 85, "score": 100, "active": false, "created_at": "2023-12-01T08:00:00", "birth_date": "1906-12-09", "avatar": "AQIDBA==", "tags": ["navy", "compiler"], "scores": [1, 2, 3], "metadata": {"key": "value"}, "embedding": [1.0, 2.0] }` var decoded record if err := json.Unmarshal([]byte(raw), &decoded); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if decoded.Name.String() != "Grace Hopper" { t.Errorf("expected Grace Hopper, got %q", decoded.Name.String()) } if decoded.Age.Int64() != 85 { t.Errorf("expected age 85, got %d", decoded.Age.Int64()) } if decoded.Active.Valid && decoded.Active.Bool() { t.Errorf("expected active=false") } if string(decoded.Avatar.Val) != string([]byte{1, 2, 3, 4}) { t.Errorf("expected avatar bytes [1 2 3 4], got %v", decoded.Avatar.Val) } if len(decoded.Tags.Val) != 2 || decoded.Tags.Val[1] != "compiler" { t.Errorf("unexpected tags %v", decoded.Tags.Val) } }