package tsm1_test import ( "context" "os" "testing" "github.com/google/go-cmp/cmp" "github.com/influxdata/influxdb/v2/tsdb/cursors" "github.com/influxdata/influxdb/v2/tsdb/tsm1" ) func TestFileStore_Array(t *testing.T) { makeFile := func(d ...interface{}) keyValues { t.Helper() if len(d)&1 == 1 { panic("input should be even") } vals := make([]tsm1.Value, len(d)/2) for i := 0; i < len(d); i += 2 { vals[i/2] = tsm1.NewFloatValue(int64(d[i].(int)), d[i+1].(float64)) } return keyValues{key: "cpu", values: vals} } // sel selects files and values from the keyValues slice // and used to build the expected output. type sel struct { // f is the index of the tsm file f int // i is the index of the value to select from the file i int } // del represents a file delete in order to generate tombstones type del struct { // f is the index of the tsm file to perform a delete. // Specifying -1 will perform a delete over the entire FileStore. f int min, max int64 } type read []sel cases := []struct { name string data []keyValues time int64 asc bool deletes []del reads []read }{ { name: "SeekToAsc_FromStart", data: []keyValues{ makeFile(0, 1.0), makeFile(1, 2.0), makeFile(2, 3.0), }, time: 0, asc: true, reads: []read{ []sel{{0, 0}}, }, }, { name: "SeekToAsc_BeforeStart", data: []keyValues{ makeFile(1, 1.0), makeFile(2, 2.0), makeFile(3, 3.0), }, time: 0, asc: true, reads: []read{ []sel{{0, 0}}, }, }, { // Tests that seeking and reading all blocks that contain overlapping points does // not skip any blocks. name: "SeekToAsc_BeforeStart_OverlapFloat", data: []keyValues{ makeFile(0, 0.0, 1, 1.0), makeFile(2, 2.0), makeFile(3, 3.0), makeFile(0, 4.0, 2, 7.0), }, time: 0, asc: true, reads: []read{ []sel{{3, 0}, {0, 1}, {3, 1}}, []sel{{2, 0}}, }, }, { // Tests that blocks with a lower min time in later files are not returned // more than once causing unsorted results. name: "SeekToAsc_OverlapMinFloat", data: []keyValues{ makeFile(1, 1.0, 3, 3.0), makeFile(2, 2.0, 4, 4.0), makeFile(0, 0.0, 1, 1.1), makeFile(2, 2.2), }, time: 0, asc: true, reads: []read{ []sel{{2, 0}, {2, 1}, {3, 0}, {0, 1}}, []sel{{1, 1}}, []sel{}, }, }, { name: "SeekToAsc_Middle", data: []keyValues{ makeFile(1, 1.0, 2, 2.0, 3, 3.0), makeFile(4, 4.0), }, time: 3, asc: true, reads: []read{ []sel{{0, 2}}, []sel{{1, 0}}, }, }, { name: "SeekToAsc_End", data: []keyValues{ makeFile(0, 1.0), makeFile(1, 2.0), makeFile(2, 3.0), }, time: 2, asc: true, reads: []read{ []sel{{2, 0}}, }, }, // descending cursor tests { name: "SeekToDesc_FromStart", data: []keyValues{ makeFile(0, 1.0), makeFile(1, 2.0), makeFile(2, 3.0), }, time: 0, asc: false, reads: []read{ []sel{{0, 0}}, }, }, { name: "SeekToDesc_Duplicate", data: []keyValues{ makeFile(0, 4.0), makeFile(0, 1.0), makeFile(2, 2.0), makeFile(2, 3.0), }, time: 2, asc: false, reads: []read{ []sel{{3, 0}}, []sel{{1, 0}}, }, }, { name: "SeekToDesc_OverlapMaxFloat", data: []keyValues{ makeFile(1, 1.0, 3, 3.0), makeFile(2, 2.0, 4, 4.0), makeFile(0, 0.0, 1, 1.1), makeFile(2, 2.2), }, time: 5, asc: false, reads: []read{ []sel{{3, 0}, {0, 1}, {1, 1}}, []sel{{2, 0}, {2, 1}}, }, }, { name: "SeekToDesc_AfterEnd", data: []keyValues{ makeFile(1, 1.0), makeFile(2, 2.0), makeFile(3, 3.0), }, time: 4, asc: false, reads: []read{ []sel{{2, 0}}, }, }, { name: "SeekToDesc_AfterEnd_OverlapFloat", data: []keyValues{ makeFile(8, 0.0, 9, 1.0), makeFile(2, 2.0), makeFile(3, 3.0), makeFile(3, 4.0, 7, 7.0), }, time: 10, asc: false, reads: []read{ []sel{{0, 0}, {0, 1}}, []sel{{3, 0}, {3, 1}}, []sel{{1, 0}}, []sel{}, }, }, { name: "SeekToDesc_Middle", data: []keyValues{ makeFile(1, 1.0), makeFile(2, 2.0, 3, 3.0, 4, 4.0), }, time: 3, asc: false, reads: []read{ []sel{{1, 0}, {1, 1}}, }, }, { name: "SeekToDesc_End", data: []keyValues{ makeFile(0, 1.0), makeFile(1, 2.0), makeFile(2, 3.0), }, time: 2, asc: false, reads: []read{ []sel{{2, 0}}, }, }, // tombstone tests { name: "TombstoneRange", data: []keyValues{ makeFile(0, 1.0), makeFile(1, 2.0), makeFile(2, 3.0), }, time: 0, asc: true, deletes: []del{ {-1, 1, 1}, }, reads: []read{ []sel{{0, 0}}, []sel{{2, 0}}, []sel{}, }, }, { name: "TombstoneRange_PartialFirst", data: []keyValues{ makeFile(0, 0.0, 1, 1.0), makeFile(2, 2.0), }, time: 0, asc: true, deletes: []del{ {0, 1, 3}, }, reads: []read{ []sel{{0, 0}}, []sel{{1, 0}}, []sel{}, }, }, { name: "TombstoneRange_PartialFloat", data: []keyValues{ makeFile(0, 0.0, 1, 1.0, 2, 2.0), }, time: 0, asc: true, deletes: []del{ {-1, 1, 1}, }, reads: []read{ []sel{{0, 0}, {0, 2}}, []sel{}, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { dir := MustTempDir() defer os.RemoveAll(dir) fs := tsm1.NewFileStore(dir) files, err := newFiles(dir, tc.data...) if err != nil { t.Fatalf("unexpected error creating files: %v", err) } for _, del := range tc.deletes { if del.f > -1 { // Delete part of the block in the first file. r := MustOpenTSMReader(files[del.f]) r.DeleteRange([][]byte{[]byte("cpu")}, del.min, del.max) } } fs.Replace(nil, files) for _, del := range tc.deletes { if del.f == -1 { if err := fs.DeleteRange([][]byte{[]byte("cpu")}, del.min, del.max); err != nil { t.Fatalf("unexpected error delete range: %v", err) } } } buf := cursors.NewFloatArrayLen(1000) c := fs.KeyCursor(context.Background(), []byte("cpu"), tc.time, tc.asc) for i, read := range tc.reads { // Search for an entry that exists in the second file values, err := c.ReadFloatArrayBlock(buf) if err != nil { t.Fatalf("read %d failed: unexpected error reading values: %v", i, err) } exp := &cursors.FloatArray{} for _, s := range read { vals := tc.data[s.f].values exp.Timestamps = append(exp.Timestamps, vals[s.i].UnixNano()) exp.Values = append(exp.Values, vals[s.i].Value().(float64)) } if len(read) == 0 { exp = cursors.NewFloatArrayLen(0) } if !cmp.Equal(values, exp) { t.Fatalf("read %d failed: unexpected values -got/+exp\n%s", i, cmp.Diff(values, exp)) } c.Next() } }) } }