| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- package storage
- import (
- "bytes"
- "encoding/binary"
- "errors"
- "reflect"
- "testing"
- "github.com/goccy/go-json"
- )
- func TestEncodeRowDeterministic(t *testing.T) {
- row := Row{
- "b": int64(2),
- "a": int64(1),
- "c": int64(3),
- }
- first, err := encodeRow(row)
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- // Rebuild with the same pairs in a different insertion order.
- rowAgain := Row{}
- rowAgain["c"] = int64(3)
- rowAgain["a"] = int64(1)
- rowAgain["b"] = int64(2)
- second, err := encodeRow(rowAgain)
- if err != nil {
- t.Fatalf("encodeRow again: %v", err)
- }
- if !bytes.Equal(first, second) {
- t.Fatalf("encoding is not deterministic:\n%x\n%x", first, second)
- }
- }
- func TestEncodeRowBinaryBytes(t *testing.T) {
- got, err := encodeRow(Row{"a": int64(1)})
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- want := []byte{
- 'P', 'Z', 'S', 'Q', 'L', 'R', 'O', 'W', // magic
- 0x01, // version
- 0x01, 0x00, 0x00, 0x00, // count = 1
- 0x01, 0x00, 0x00, 0x00, // nameLen = 1
- 'a', // name
- 0x03, // tagInt
- 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // int64(1)
- }
- if !bytes.Equal(got, want) {
- t.Fatalf("binary bytes = %x, want %x", got, want)
- }
- }
- func TestEncodeDecodeRoundTripAllTypes(t *testing.T) {
- in := Row{
- "nil": nil,
- "bt": true,
- "bf": false,
- "i": int(42),
- "i8": int8(-8),
- "i16": int16(-1600),
- "i32": int32(-70000),
- "i64": int64(-9000000000000000000),
- "u": uint(7),
- "u8": uint8(200),
- "u16": uint16(60000),
- "u32": uint32(4000000000),
- "u64": uint64(18446744073709551615),
- "f32": float32(1.5),
- "f64": float64(-2.25),
- "str": "hello",
- "bytes": []byte{0x00, 0xff, 0x01, '\n'},
- "num": json.Number("12345678901234567890"),
- }
- want := Row{
- "nil": nil,
- "bt": true,
- "bf": false,
- "i": int64(42),
- "i8": int64(-8),
- "i16": int64(-1600),
- "i32": int64(-70000),
- "i64": int64(-9000000000000000000),
- "u": uint64(7),
- "u8": uint64(200),
- "u16": uint64(60000),
- "u32": uint64(4000000000),
- "u64": uint64(18446744073709551615),
- "f32": float32(1.5),
- "f64": float64(-2.25),
- "str": "hello",
- "bytes": []byte{0x00, 0xff, 0x01, '\n'},
- "num": json.Number("12345678901234567890"),
- }
- data, err := encodeRow(in)
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- if len(data) < len(rowMagic) || string(data[:len(rowMagic)]) != rowMagic {
- t.Fatalf("binary row missing magic prefix: %x", data)
- }
- got, err := decodeRow(data)
- if err != nil {
- t.Fatalf("decodeRow: %v", err)
- }
- if !reflect.DeepEqual(got, want) {
- t.Fatalf("round trip mismatch:\n got = %#v\nwant = %#v", got, want)
- }
- }
- func TestEncodeRowJSONNumberExact(t *testing.T) {
- // A decimal that would lose precision as a float64 must round-trip exactly.
- row := Row{"n": json.Number("0.123456789012345678901234567890")}
- data, err := encodeRow(row)
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- got, err := decodeRow(data)
- if err != nil {
- t.Fatalf("decodeRow: %v", err)
- }
- if got["n"] != json.Number("0.123456789012345678901234567890") {
- t.Fatalf("number = %#v, want exact json.Number", got["n"])
- }
- }
- func TestDecodeLegacyJSON(t *testing.T) {
- legacy := []byte(`{"_rowid_":7,"name":"alice","score":12.5,"active":true,"extra":null}`)
- got, err := decodeRow(legacy)
- if err != nil {
- t.Fatalf("decodeRow: %v", err)
- }
- if got["_rowid_"] != float64(7) {
- t.Fatalf("_rowid_ = %#v, want float64(7)", got["_rowid_"])
- }
- if got["name"] != "alice" {
- t.Fatalf("name = %#v", got["name"])
- }
- if got["score"] != float64(12.5) {
- t.Fatalf("score = %#v", got["score"])
- }
- if got["active"] != true {
- t.Fatalf("active = %#v", got["active"])
- }
- if got["extra"] != nil {
- t.Fatalf("extra = %#v", got["extra"])
- }
- }
- func TestEncodeRowUnsupportedValueFallsBackToJSON(t *testing.T) {
- row := Row{"id": int64(1), "tags": []string{"a", "b"}}
- data, err := encodeRow(row)
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- if len(data) >= len(rowMagic) && string(data[:len(rowMagic)]) == rowMagic {
- t.Fatalf("expected JSON fallback, got binary magic: %x", data)
- }
- var decoded Row
- if err := json.Unmarshal(data, &decoded); err != nil {
- t.Fatalf("fallback is not valid JSON: %v", err)
- }
- if decoded["id"] != float64(1) {
- t.Fatalf("id = %#v", decoded["id"])
- }
- tags, ok := decoded["tags"].([]interface{})
- if !ok || len(tags) != 2 || tags[0] != "a" || tags[1] != "b" {
- t.Fatalf("tags = %#v", decoded["tags"])
- }
- }
- func TestEncodeRowJSONFallbackRoundTripsThroughDecode(t *testing.T) {
- row := Row{"nested": map[string]interface{}{"x": 1, "y": []interface{}{true, nil}}}
- data, err := encodeRow(row)
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- got, err := decodeRow(data)
- if err != nil {
- t.Fatalf("decodeRow: %v", err)
- }
- if _, ok := got["nested"].(map[string]interface{}); !ok {
- t.Fatalf("nested = %#v, want map", got["nested"])
- }
- }
- func TestDecodeBinaryRowTruncated(t *testing.T) {
- data, err := encodeRow(Row{"name": "alice", "id": int64(5), "payload": []byte("data")})
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- for _, n := range []int{1, len(rowMagic), rowHeaderLen, rowHeaderLen + 1, len(data) - 1} {
- trunc := data[:n]
- if _, err := decodeRow(trunc); err == nil {
- t.Fatalf("decodeRow(truncated to %d bytes) succeeded, want error", n)
- }
- }
- }
- func TestDecodeBinaryRowTrailingBytes(t *testing.T) {
- data, err := encodeRow(Row{"id": int64(1)})
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- withTrailing := append(append([]byte(nil), data...), 0x00, 0x01, 0x02)
- if _, err := decodeRow(withTrailing); err == nil {
- t.Fatalf("decodeRow with trailing bytes succeeded, want error")
- }
- }
- func TestDecodeBinaryRowUnknownTag(t *testing.T) {
- var buf []byte
- buf = append(buf, rowMagic...)
- buf = append(buf, rowVersion)
- buf = appendU32(buf, 1)
- buf = appendU32(buf, 2)
- buf = append(buf, "id"...)
- buf = append(buf, 0x7f) // unknown tag
- buf = append(buf, 0, 0, 0, 0, 0, 0, 0, 0)
- if _, err := decodeRow(buf); !errors.Is(err, errMalformedRow) {
- t.Fatalf("err = %v, want errMalformedRow", err)
- }
- }
- func TestDecodeBinaryRowUnknownVersion(t *testing.T) {
- data, err := encodeRow(Row{"id": int64(1)})
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- corrupted := append([]byte(nil), data...)
- corrupted[len(rowMagic)] = 0x7f
- if _, err := decodeRow(corrupted); !errors.Is(err, errMalformedRow) {
- t.Fatalf("err = %v, want errMalformedRow", err)
- }
- }
- func TestDecodeBinaryRowOversizedNameLength(t *testing.T) {
- var buf []byte
- buf = append(buf, rowMagic...)
- buf = append(buf, rowVersion)
- buf = appendU32(buf, 1)
- buf = appendU32(buf, uint32(maxRowFieldLen+1)) // oversized name length
- buf = append(buf, 'x')
- if _, err := decodeRow(buf); err == nil {
- t.Fatalf("decodeRow with oversized name length succeeded, want error")
- }
- }
- func TestDecodeBinaryRowOversizedValueLength(t *testing.T) {
- var buf []byte
- buf = append(buf, rowMagic...)
- buf = append(buf, rowVersion)
- buf = appendU32(buf, 1)
- buf = appendU32(buf, 1)
- buf = append(buf, 'a')
- buf = append(buf, tagString)
- buf = appendU32(buf, uint32(maxRowFieldLen+1)) // oversized string length
- if _, err := decodeRow(buf); err == nil {
- t.Fatalf("decodeRow with oversized value length succeeded, want error")
- }
- }
- func TestDecodeBinaryRowStringLengthExceedsInput(t *testing.T) {
- var buf []byte
- buf = append(buf, rowMagic...)
- buf = append(buf, rowVersion)
- buf = appendU32(buf, 1)
- buf = appendU32(buf, 1)
- buf = append(buf, 'a')
- buf = append(buf, tagString)
- buf = appendU32(buf, 100) // claims 100 bytes but only 0 follow
- if _, err := decodeRow(buf); err == nil {
- t.Fatalf("decodeRow with lying string length succeeded, want error")
- }
- }
- func TestDecodeBinaryRowImpossibleFieldCount(t *testing.T) {
- var buf []byte
- buf = append(buf, rowMagic...)
- buf = append(buf, rowVersion)
- buf = appendU32(buf, 0xffffffff) // far more fields than bytes available
- if _, err := decodeRow(buf); err == nil {
- t.Fatalf("decodeRow with impossible field count succeeded, want error")
- }
- }
- func TestDecodeMalformedTaggedBinaryNotReinterpretedAsJSON(t *testing.T) {
- // Bytes carrying the magic prefix must never fall back to the JSON path,
- // even if the tail happens to look JSON-ish.
- corrupted := append([]byte(nil), rowMagic...)
- corrupted = append(corrupted, rowVersion)
- corrupted = append(corrupted, 0xff, 0xff, 0xff, 0xff) // bogus count
- corrupted = append(corrupted, 'g', 'a', 'r', 'b', 'a', 'g', 'e')
- if _, err := decodeRow(corrupted); err == nil {
- t.Fatalf("decodeRow succeeded on malformed tagged binary, want error")
- }
- }
- func TestDecodeNonJSONNonBinaryInput(t *testing.T) {
- // No magic prefix and not valid JSON must fail rather than panic or return
- // a partial row.
- if _, err := decodeRow([]byte{0x01, 0x02, 0x03, 0x04}); err == nil {
- t.Fatalf("decodeRow on garbage succeeded, want error")
- }
- }
- func TestEncodeDecodeEmptyRow(t *testing.T) {
- data, err := encodeRow(Row{})
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- got, err := decodeRow(data)
- if err != nil {
- t.Fatalf("decodeRow: %v", err)
- }
- if len(got) != 0 {
- t.Fatalf("empty row decoded to %#v", got)
- }
- }
- func TestEncodeDecodeNilRow(t *testing.T) {
- data, err := encodeRow(nil)
- if err != nil {
- t.Fatalf("encodeRow(nil): %v", err)
- }
- got, err := decodeRow(data)
- if err != nil {
- t.Fatalf("decodeRow: %v", err)
- }
- if len(got) != 0 {
- t.Fatalf("nil row decoded to %#v", got)
- }
- }
- func TestEncodeRowSortedFieldNames(t *testing.T) {
- data, err := encodeRow(Row{"z": int64(3), "a": int64(1), "m": int64(2)})
- if err != nil {
- t.Fatalf("encodeRow: %v", err)
- }
- // Verify the field names appear in sorted order by walking the encoding.
- pos := rowHeaderLen
- count := binary.LittleEndian.Uint32(data[pos-4 : pos])
- names := make([]string, 0, count)
- for i := uint32(0); i < count; i++ {
- nameLen := binary.LittleEndian.Uint32(data[pos : pos+4])
- pos += 4
- names = append(names, string(data[pos:pos+int(nameLen)]))
- pos += int(nameLen)
- pos++ // skip tag
- switch data[pos-1] {
- case tagInt, tagUint, tagFloat64:
- pos += 8
- case tagFloat32:
- pos += 4
- case tagString, tagBytes, tagNumber:
- l := binary.LittleEndian.Uint32(data[pos : pos+4])
- pos += 4 + int(l)
- }
- }
- if !reflect.DeepEqual(names, []string{"a", "m", "z"}) {
- t.Fatalf("field names = %v, want [a m z]", names)
- }
- }
- func BenchmarkRowCodec(b *testing.B) {
- row := Row{
- "_rowid_": int64(4812),
- "id": int64(4812),
- "symbol": "PIZZA",
- "price": 104.25,
- "active": true,
- "payload": []byte{0, 1, 2, '|', '\r', '\n'},
- }
- binaryRow, err := encodeRow(row)
- if err != nil {
- b.Fatal(err)
- }
- jsonRow, err := json.Marshal(row)
- if err != nil {
- b.Fatal(err)
- }
- b.Run("encode_binary", func(b *testing.B) {
- b.ReportAllocs()
- for b.Loop() {
- if _, err := encodeRow(row); err != nil {
- b.Fatal(err)
- }
- }
- })
- b.Run("encode_json", func(b *testing.B) {
- b.ReportAllocs()
- for b.Loop() {
- if _, err := json.Marshal(row); err != nil {
- b.Fatal(err)
- }
- }
- })
- b.Run("decode_binary", func(b *testing.B) {
- b.ReportAllocs()
- for b.Loop() {
- if _, err := decodeRow(binaryRow); err != nil {
- b.Fatal(err)
- }
- }
- })
- b.Run("decode_json", func(b *testing.B) {
- b.ReportAllocs()
- for b.Loop() {
- if _, err := decodeRow(jsonRow); err != nil {
- b.Fatal(err)
- }
- }
- })
- }
|