// Copyright 2022 Molecula Corp. (DBA FeatureBase). // SPDX-License-Identifier: Apache-2.0 package pilosa_test import ( "bytes" "encoding/json" "math" "testing" "time" pilosa "github.com/featurebasedb/featurebase/v3" "github.com/featurebasedb/featurebase/v3/pql" "github.com/featurebasedb/featurebase/v3/roaring" "github.com/featurebasedb/featurebase/v3/test" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" ) // Ensure a field can set & read a bsiGroup value. func TestField_SetValue(t *testing.T) { t.Run("OK", func(t *testing.T) { h, idx := test.MustOpenIndex(t) f, err := idx.CreateField("f", "", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64)) if err != nil { t.Fatal(err) } qcx := h.Txf().NewWritableQcx() defer qcx.Abort() // You're going to note the lack of any commits here. That's // because, when you have a writable Qcx, *every individual // sub-transaction commits immediately*. In theory, we ought // to be doing provisional writes and the entire set of writes // ought to be able to be reverted. Actually no. We're just committing // everything as we go anyway. // Set value on field. if changed, err := f.SetValue(qcx, 100, 21); err != nil { t.Fatal(err) } else if !changed { t.Fatal("expected change") } // Read value. if value, exists, err := f.Value(qcx, 100); err != nil { t.Fatal(err) } else if value != 21 { t.Fatalf("unexpected value: %d", value) } else if !exists { t.Fatal("expected value to exist") } // Setting value should return no change. if changed, err := f.SetValue(qcx, 100, 21); err != nil { t.Fatal(err) } else if changed { t.Fatal("expected no change") } }) t.Run("Overwrite", func(t *testing.T) { h, idx := test.MustOpenIndex(t) f, err := idx.CreateField("f", "", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64)) if err != nil { t.Fatal(err) } qcx := h.Txf().NewWritableQcx() defer qcx.Abort() // Set value. if changed, err := f.SetValue(qcx, 100, 21); err != nil { t.Fatal(err) } else if !changed { t.Fatal("expected change") } // Set different value. if changed, err := f.SetValue(qcx, 100, 23); err != nil { t.Fatal(err) } else if !changed { t.Fatal("expected change") } // Read value. if value, exists, err := f.Value(qcx, 100); err != nil { t.Fatal(err) } else if value != 23 { t.Fatalf("unexpected value: %d", value) } else if !exists { t.Fatal("expected value to exist") } }) t.Run("ErrBSIGroupNotFound", func(t *testing.T) { h, idx := test.MustOpenIndex(t) f, err := idx.CreateField("f", "") if err != nil { t.Fatal(err) } qcx := h.Txf().NewWritableQcx() defer qcx.Abort() // Set value. if _, err := f.SetValue(qcx, 100, 21); err != pilosa.ErrBSIGroupNotFound { t.Fatalf("unexpected error: %s", err) } }) t.Run("ErrBSIGroupValueTooLow", func(t *testing.T) { h, idx := test.MustOpenIndex(t) f, err := idx.CreateField("f", "", pilosa.OptFieldTypeInt(20, 30)) if err != nil { t.Fatal(err) } qcx := h.Txf().NewWritableQcx() defer qcx.Abort() // Set value. if _, err := f.SetValue(qcx, 100, 15); !errors.Is(err, pilosa.ErrBSIGroupValueTooLow) { t.Fatalf("unexpected error: %s", err) } }) t.Run("ErrBSIGroupValueTooHigh", func(t *testing.T) { h, idx := test.MustOpenIndex(t) f, err := idx.CreateField("f", "", pilosa.OptFieldTypeInt(20, 30)) if err != nil { t.Fatal(err) } qcx := h.Txf().NewWritableQcx() defer qcx.Abort() // Set value. if _, err := f.SetValue(qcx, 100, 31); !errors.Is(err, pilosa.ErrBSIGroupValueTooHigh) { t.Fatalf("unexpected error: %s", err) } }) } // Ensure that field name validation is consistent. func TestField_NameValidation(t *testing.T) { validFieldNames := []string{ "foo", "hyphen-ated", "under_score", "abc123", "trailing_", "charact2301234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", } invalidFieldNames := []string{ "", "123abc", "x.y", "_foo", "-bar", "abc def", "camelCase", "UPPERCASE", ".meta", "charact23112345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901", } _, idx := test.MustOpenIndex(t) for _, name := range validFieldNames { _, err := idx.CreateField(name, "") if err != nil { t.Fatalf("unexpected field name: %s %s", name, err) } } for _, name := range invalidFieldNames { _, err := idx.CreateField(name, "") if err == nil { t.Fatalf("expected error on field name: %s", name) } } } const includeRemote = false // for calls to Index.AvailableShards(localOnly bool) // Ensure can update and delete available shards. func TestField_AvailableShards(t *testing.T) { h, idx := test.MustOpenIndex(t) f, err := idx.CreateField("fld-shards", "") if err != nil { t.Fatal(err) } qcx := h.Txf().NewWritableQcx() defer qcx.Abort() // Set values on shards 0 & 2, and verify. if _, err := f.SetBit(qcx, 0, 100, nil); err != nil { t.Fatal(err) } else if _, err := f.SetBit(qcx, 0, ShardWidth*2, nil); err != nil { t.Fatal(err) } else if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 2}); diff != "" { t.Fatal(diff) } // Set remote shards and verify. if err := f.AddRemoteAvailableShards(roaring.NewBitmap(1, 2, 4)); err != nil { t.Fatalf("adding remote shards: %v", err) } if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 1, 2, 4}); diff != "" { t.Fatal(diff) } // Delete shards; only local shards should remain. for i := uint64(0); i < 5; i++ { err := f.RemoveAvailableShard(i) if err != nil { t.Fatalf("removing shard %d: %v", i, err) } } if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 2}); diff != "" { t.Fatal(diff) } } func TestField_ClearValue(t *testing.T) { t.Run("OK", func(t *testing.T) { h, idx := test.MustOpenIndex(t) f, err := idx.CreateField("f", "", pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64)) if err != nil { t.Fatal(err) } qcx := h.Txf().NewWritableQcx() defer qcx.Abort() // Set value on field. if changed, err := f.SetValue(qcx, 100, 21); err != nil { t.Fatal(err) } else if !changed { t.Fatal("expected change") } // Read value. if value, exists, err := f.Value(qcx, 100); err != nil { t.Fatal(err) } else if value != 21 { t.Fatalf("unexpected value: %d", value) } else if !exists { t.Fatal("expected value to exist") } if changed, err := f.ClearValue(qcx, 100); err != nil { t.Fatal(err) } else if !changed { t.Fatal(err) } // Read value. if _, exists, err := f.Value(qcx, 100); err != nil { t.Fatal(err) } else if exists { t.Fatal("expected value to not exist") } }) } func TestFieldInfoMarshal(t *testing.T) { f := &pilosa.FieldInfo{Name: "timestamp", CreatedAt: 1649270079233541000, Options: pilosa.FieldOptions{ Base: 0, BitDepth: 0x0, Min: pql.NewDecimal(-4294967296, 0), Max: pql.NewDecimal(4294967296, 0), Scale: 0, Keys: false, NoStandardView: false, CacheType: "", Type: "timestamp", TimeUnit: "s", TimeQuantum: "", ForeignIndex: "", TTL: 0, }, Cardinality: (*uint64)(nil), } a, err := json.Marshal(f) if err != nil { t.Fatalf("unexpected error marshalling index info, %v", err) } expected := []byte(`{"name":"timestamp","createdAt":1649270079233541000,"owner":"","options":{"type":"timestamp","epoch":"1970-01-01T00:00:00Z","bitDepth":0,"min":-4294967296,"max":4294967296,"timeUnit":"s"}}`) if !bytes.Equal(a, expected) { t.Fatalf("expected %s, got %s", expected, a) } } func TestCheckUnixNanoOverflow(t *testing.T) { minNano = pilosa.MinTimestampNano.UnixNano() maxNano = pilosa.MaxTimestampNano.UnixNano() tests := []struct { name string epoch time.Time wantErr bool }{ { name: "too small", epoch: time.Unix(-1, minNano), wantErr: true, }, { name: "just right-1", epoch: time.Unix(0, minNano), wantErr: false, }, { name: "just right-2", epoch: time.Unix(0, maxNano), wantErr: false, }, { name: "too large", epoch: time.Unix(1, maxNano), wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := pilosa.CheckEpochOutOfRange(tt.epoch, pilosa.MinTimestampNano, pilosa.MaxTimestampNano); (err != nil) != tt.wantErr { t.Errorf("checkUnixNanoOverflow() error = %v, wantErr %v", err, tt.wantErr) } }) } }