package idk // comment import ( "context" "encoding/json" "fmt" "io" "math/rand" "net/http" "os" "reflect" "sort" "strings" "testing" "time" pilosa "github.com/featurebasedb/featurebase/v3" "github.com/featurebasedb/featurebase/v3/authn" batch "github.com/featurebasedb/featurebase/v3/batch" pilosaclient "github.com/featurebasedb/featurebase/v3/client" "github.com/featurebasedb/featurebase/v3/dax" controllerclient "github.com/featurebasedb/featurebase/v3/dax/controller/client" "github.com/featurebasedb/featurebase/v3/idk/idktest" "github.com/featurebasedb/featurebase/v3/idk/serverless" "github.com/featurebasedb/featurebase/v3/logger" "github.com/featurebasedb/featurebase/v3/pql" "github.com/golang-jwt/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) func configureTestFlags(main *Main) { if pilosaHost, ok := os.LookupEnv("IDK_TEST_PILOSA_HOST"); ok { main.PilosaHosts = []string{pilosaHost} } else { main.PilosaHosts = []string{"pilosa:10101"} } if grpcHost, ok := os.LookupEnv("IDK_TEST_PILOSA_GRPC_HOST"); ok { main.PilosaGRPCHosts = []string{grpcHost} } else { main.PilosaGRPCHosts = []string{"pilosa:20101"} } main.Stats = "" } func configureTestFlagsController(main *Main, address dax.Address, qtbl *dax.QualifiedTable) { main.ControllerAddress = address.String() main.Stats = "" main.Pprof = "" main.PackBools = "" main.OrganizationID = qtbl.Qualifier().OrganizationID main.DatabaseID = qtbl.Qualifier().DatabaseID main.TableName = qtbl.Name main.Qtbl = qtbl main.SchemaManager = serverless.NewSchemaManager(address, qtbl.Qualifier(), logger.StderrLogger) main.Index = string(qtbl.Key()) controllerClient := controllerclient.New(dax.Address(address), logger.StderrLogger) main.NewImporterFn = func() pilosa.Importer { return serverless.NewImporter(controllerClient, qtbl.QualifiedDatabaseID, &qtbl.Table) } } type versionHolder struct { Version string } func TestFeaturebaseVersion(t *testing.T) { m := NewMain() configureTestFlags(m) // We don't need to fully run an ingester, but we do need its client set up. _, err := m.setupClient() if err != nil { t.Fatalf("setting up client: %v", err) } c := m.PilosaClient() status, body, err := c.HTTPRequest("GET", "/version", nil, nil) if err != nil { t.Fatalf("getting version: %v", err) } if status != http.StatusOK { t.Fatalf("unexpected status: %d (wanted StatusOK)", status) } var vh versionHolder err = json.Unmarshal(body, &vh) if err != nil { t.Fatalf("unmarshalling version: %v", err) } v := vh.Version expectedTag := os.Getenv("IDK_FEATUREBASE_TAG") if expectedTag != "" { // if a tag is set, we're in a tagged pipeline, and the version // should just be vX.Y or something similar, without a commit // hash. in that case, the tag passed to us from the environment // will look like vX.Y, but the version reported by the version // endpoint is just X.Y. Argh. if v != expectedTag && ("v"+v) != expectedTag { t.Fatalf("version %s does not match expected tag %s", v, expectedTag) } t.Logf("featurebase version %q matches expectations", expectedTag) return } // no tag. we expect to always have a hash, and if there's a specific // expected hash, we verify it. expectedHash := os.Getenv("IDK_FEATUREBASE_HASH") if expectedHash == "" { t.Skipf("featurebase version: %s [no expected version]", v) } offset := strings.LastIndex(v, "-g") if offset == -1 { t.Fatalf("version %s doesn't have -g followed by a hash", v) } hash := v[offset+2:] // allow either hash to be abbreviated if !strings.HasPrefix(expectedHash, hash) && !strings.HasPrefix(hash, expectedHash) { t.Fatalf("expected hash to look like %q, got %q", expectedHash, hash) } t.Logf("featurebase commit %q matches expectations", hash) } func TestErrFlush(t *testing.T) { ts := newTestSource([]Field{IDField{NameVal: "aval"}}, [][]interface{}{ {ErrFlush}, {ErrFlush}, {1}, {1}, {ErrFlush}, {1}, {1}, }) ingester := NewMain() ingester.AutoGenerate = true configureTestFlags(ingester) ingester.NewSource = func() (Source, error) { return ts, nil } rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("errflush%d", rand.Intn(10000000)) ingester.BatchSize = 10 ingester.CommitTimeout = 1 * time.Minute err := ingester.Run() if err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } client := ingester.PilosaClient() defer func() { if err := client.DeleteIndexByName(ingester.Index); err != nil { t.Fatal(err) } }() schema, err := client.Schema() if err != nil { t.Fatalf("getting schema: %v", err) } index := schema.Index(ingester.Index) qr, err := client.Query(index.RawQuery("Count(Row(aval=1))")) if err != nil { t.Fatalf("querying: %v", err) } if qr.Results()[0].Count() != 4 { t.Fatalf("unexpected number of records: %d", qr.Results()[0].Count()) } } func TestErrBatchNowStale(t *testing.T) { schema := []Field{IDField{NameVal: "id"}} records := make(chan []interface{}) committed := make(chan []interface{}, 5) ts := newSignalingTestSource(schema, records, committed) ingester := NewMain() ingester.BatchMaxStaleness = time.Millisecond configureTestFlags(ingester) ingester.NewSource = func() (Source, error) { return ts, nil } ingester.AutoGenerate = true rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("batchnowstaletest%d", rand.Intn(100000)) ingester.BatchSize = 5 defer func() { if err := ingester.PilosaClient().DeleteIndexByName(ingester.Index); err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, ingester.Index, err) } }() signal := make(chan struct{}) go func() { err := ingester.Run() if err != nil { t.Logf("%s: %v", idktest.ErrRunningIngest, err) } close(signal) }() records <- []interface{}{1} records <- []interface{}{2} time.Sleep(time.Millisecond) records <- []interface{}{3} select { case <-committed: case <-time.After(time.Second): t.Fatal("Nothing has been committed") } close(records) // make sure the Run goroutine actually finished <-signal } func TestIngestSignedIntBoolField(t *testing.T) { ts := newTestSource([]Field{StringField{NameVal: "rcid"}, SignedIntBoolKeyField{NameVal: "svals"}}, [][]interface{}{ {"a", int64(-22)}, {"a", int64(-44)}, {"a", int64(11)}, {"a", int64(5)}, {"a", int64(66)}, {"b", int64(11)}, {"b", int64(22)}, {"b", int64(-32)}, {"b", int64(-44)}, {"b", int64(9)}, {"b", int64(7)}, {"c", int64(5)}, }) ingester := NewMain() configureTestFlags(ingester) ingester.NewSource = func() (Source, error) { return ts, nil } rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("ingestint%d", rand.Intn(100000)) ingester.BatchSize = 2 ingester.PrimaryKeyFields = []string{"rcid"} err := ingester.Run() if err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } client := ingester.PilosaClient() defer func() { if err := client.DeleteIndexByName(ingester.Index); err != nil { t.Fatal(err) } }() svals := ingester.index.Field("svals") svalsex := ingester.index.Field("svals-exists") tests := []struct { field *pilosaclient.Field row int want []string }{ {field: svals, row: 22, want: []string{"b"}}, {field: svalsex, row: 22, want: []string{"a", "b"}}, {field: svalsex, row: 44, want: []string{"a", "b"}}, {field: svalsex, row: 5, want: []string{"a", "c"}}, {field: svals, row: 5, want: []string{"a", "c"}}, } for _, test := range tests { if resp, err := client.Query(test.field.Row(test.row)); err != nil { t.Fatalf("row %d: %v", test.row, err) } else if !stringSliceSame(resp.ResultList[0].Row().Keys, test.want) { t.Fatalf("wanted %+v, got: %+v", test.want, resp.ResultList[0].Row().Keys) } } } //The following two tests are used to test the functionality of a new feature in which we skip bad records coming into idk and log them. //First function checks that we can successfully skip some bad rows coming into idk //Second function checks whether we get error if we have more bad records than acceptable errors by idk mentioned by SkipBadRows parameter. func skipBadRowsTestSource() *testSource { ts := newTestSource([]Field{StringField{NameVal: "rcid"}, SignedIntBoolKeyField{NameVal: "svals"}}, [][]interface{}{ {"c", "badrecord1"}, {"d", "badrecord2"}, {"a", "badrecord3"}, {"x", int64(66)}, {"b", int64(11)}, {"b", int64(22)}, {"b", int64(-32)}, {"b", int64(-44)}, {"b", "badrecord4"}, {"b", int64(11)}, {"b", int64(7)}, {"c", int64(5)}, }) return ts } func ingesterCreationForSkipBadRowsTest(skipbadrows int) *Main { ts := skipBadRowsTestSource() ingester := NewMain() configureTestFlags(ingester) ingester.NewSource = func() (Source, error) { return ts, nil } rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("ingestint%d", rand.Intn(100000)) ingester.BatchSize = 2 ingester.SkipBadRows = skipbadrows ingester.PrimaryKeyFields = []string{"rcid"} return ingester } func TestSkipBadRowsFunctionality(t *testing.T) { ingester := ingesterCreationForSkipBadRowsTest(3) err := ingester.Run() if err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } } func TestSkipBadRowsFunctionalityWhenErrorCountIsMore(t *testing.T) { ingester := ingesterCreationForSkipBadRowsTest(1) err := ingester.Run() if err == nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } if !strings.Contains(err.Error(), "consecutive bad records exceeded limit") { t.Fatalf("did not receive expected error from idk %v", err.Error()) } } // TestSingleBoolClear essentially creates an import batch which // clears a bit in a particular fragment without setting a bit in that // same fragment. There's a potential optimization in the pilosa client // batch import code that will break this test if not done carefully. func TestSingleBoolClear(t *testing.T) { ts := newTestSource([]Field{IDField{NameVal: "id"}, BoolField{NameVal: "likes_chocolate"}}, [][]interface{}{ {0, true}, }) ingester := NewMain() configureTestFlags(ingester) ingester.NewSource = func() (Source, error) { return ts, nil } rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("single_bool_clear%d", rand.Intn(100000)) ingester.BatchSize = 1 ingester.IDField = "id" ingester.PackBools = "bools" if err := ingester.Run(); err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } client := ingester.PilosaClient() defer func() { if err := client.DeleteIndexByName(ingester.Index); err != nil { t.Fatal(err) } }() bools := ingester.index.Field("bools") //boolsExists := ingester.index.Field("bools-exists") if resp, err := client.Query(bools.Row("likes_chocolate")); err != nil { t.Fatalf("likes_chocolate: %v", err) } else if resp.ResultList[0].Row().Columns[0] != 0 { t.Fatalf("wanted [0], got: %+v", resp.ResultList[0].Row().Columns) } ts2 := newTestSource([]Field{IDField{NameVal: "id"}, BoolField{NameVal: "likes_chocolate"}}, [][]interface{}{ {0, false}, }) ingester2 := NewMain() configureTestFlags(ingester2) ingester2.NewSource = func() (Source, error) { return ts2, nil } ingester2.Index = ingester.Index ingester2.IDField = "id" ingester2.PackBools = "bools" if err := ingester2.Run(); err != nil { t.Fatalf("running ingester2: %v", err) } if resp, err := client.Query(bools.Row("likes_chocolate")); err != nil { t.Fatalf("likes_chocolate: %v", err) } else if len(resp.ResultList[0].Row().Columns) != 0 { t.Fatalf("wanted [], got: %+v", resp.ResultList[0].Row().Columns) } } func TestForeignKeyRegression(t *testing.T) { ts := newTestSource([]Field{IDField{NameVal: "id"}, IntField{NameVal: "fk1", ForeignIndex: "testusers834"}}, [][]interface{}{ {0, "blah"}, {1, "bleh"}, {2, "blue"}, }) ingester := NewMain() configureTestFlags(ingester) ingester.NewSource = func() (Source, error) { return ts, nil } rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("fkreg%d", rand.Intn(100000)) ingester.BatchSize = 2 ingester.IDField = "id" defer func() { if err := ingester.PilosaClient().DeleteIndexByName(ingester.Index); err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, ingester.Index, err) } }() onFinishRun, err := ingester.Setup() if err != nil { t.Fatalf("setting up ingester: %v", err) } defer onFinishRun() client := ingester.PilosaClient() schema, err := client.Schema() if err != nil { t.Fatalf("getting schema: %v", err) } index := schema.Index("testusers834", pilosaclient.OptIndexKeys(true)) err = client.SyncIndex(index) if err != nil { t.Fatalf("syncing schema: %v", err) } defer func() { if err := client.DeleteIndexByName("testusers834"); err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, "testusers834", err) } }() // this previously panicked because Batch.toTranslate wasn't // getting cleared for int fields. err = ingester.run() if err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } } // Test String Arrays w/ various ingester configurations func TestIngestStringArrays(t *testing.T) { // TODO: test with IDField ts := *newTestSource( []Field{ StringField{NameVal: "aba"}, StringField{NameVal: "db"}, IntField{NameVal: "id"}, StringArrayField{NameVal: "sa1"}, }, [][]interface{}{ {"a", "b", 1, []string{"a", "b", "c"}}, {"a", "b", 1, []string{"z"}}, {"a", "b", 1, []string{}}, {"a", "b", 1, []string{"q", "r", "s", "t"}}, {"a", "b", 1, nil}, {"a", "b", 1, []string{"a", "b", "c"}}, {"a", "b", 1, []string{"a", "b", "c"}}, {"a", "b", 1, []string{"z"}}, {"a", "b", 1, []string{}}, {"a", "b", 1, []string{"q", "r", "s", "t"}}, {"a", "b", 1, nil}, {"a", "b", 1, []string{"a", "b", "c"}}, }, ) certPath, ok := os.LookupEnv("IDK_TEST_CERT_PATH") if !ok { certPath = "/certs" } for _, tt := range []struct { name string ingester func() *Main source testSource }{ { name: "PrimaryKeyFields", ingester: func() *Main { ingester := NewMain() configureTestFlags(ingester) ingester.PrimaryKeyFields = []string{"aba", "db", "id"} return ingester }, source: ts, }, { name: "External-Generate+TLS", ingester: func() *Main { ingester := NewMain() configureTestFlags(ingester) ingester.AutoGenerate = true ingester.ExternalGenerate = true ingester.TLS = TLSConfig{ CertificatePath: certPath + "/theclient.crt", CertificateKeyPath: certPath + "/theclient.key", CACertPath: certPath + "/ca.crt", EnableClientVerification: true, } return ingester }, source: ts, }, } { t.Run(tt.name, func(t *testing.T) { ingester := tt.ingester() ingester.NewSource = func() (Source, error) { return &tt.source, nil } rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("ingeststring%d", rand.Intn(100000)) ingester.BatchSize = 5 defer func() { if err := ingester.PilosaClient().DeleteIndexByName(ingester.Index); err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, ingester.index, err) } }() err := ingester.Run() if err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } client := ingester.PilosaClient() sa1 := ingester.index.Field("sa1") _, err = client.Query(sa1.Row("a")) if err != nil { t.Fatalf("row a: %v", err) } }) } } func TestIngesterServesPrometheusEndpoint(t *testing.T) { schema := []Field{ StringField{NameVal: "aba"}, StringField{NameVal: "db"}, IntField{NameVal: "id"}, StringArrayField{NameVal: "sa1"}, } records := make(chan []interface{}) var committed chan []interface{} // make nil channel ts := newSignalingTestSource(schema, records, committed) ingester := NewMain() configureTestFlags(ingester) ingester.NewSource = func() (Source, error) { return ts, nil } ingester.PrimaryKeyFields = []string{"aba", "db", "id"} rand.Seed(time.Now().UTC().UnixNano()) ingester.Index = fmt.Sprintf("statstest%d", rand.Intn(100000)) ingester.BatchSize = 5 ingester.Stats = "localhost:9093" signal := make(chan struct{}) go func() { err := ingester.Run() if err != nil { t.Logf("%s: %v", idktest.ErrRunningIngest, err) } close(signal) }() records <- []interface{}{"a", "b", 1, []string{"a", "b", "c"}} // Try to read stats directly url := "http://" + ingester.Stats + "/metrics" response, err := http.Get(url) if err != nil { t.Errorf("request error: %v", err) } contents, err := io.ReadAll(response.Body) defer response.Body.Close() if err != nil { t.Errorf("read error: %v", err) } if !strings.Contains(string(contents), MetricIngesterRowsAdded) { t.Errorf("metric name missing: %v", MetricIngesterRowsAdded) } close(records) // make sure the Run goroutine actually finished <-signal } func TestDelete(t *testing.T) { rand.Seed(time.Now().UTC().UnixNano()) indexName := fmt.Sprintf("delete%d", rand.Intn(100000)) primaryKeyFields := []string{"aba", "db", "id"} tsWrite := newTestSource( []Field{ StringField{NameVal: "aba"}, StringField{NameVal: "db"}, IntField{NameVal: "id"}, StringArrayField{NameVal: "sa1"}, StringField{NameVal: "s1"}, StringField{NameVal: "s2", Mutex: true}, IntField{NameVal: "i1"}, DecimalField{NameVal: "d1"}, BoolField{NameVal: "bools|b1"}, }, [][]interface{}{ {"a", "b", 2, []string{"d", "e", "f"}, "v", "w", 14, 15.8, true}, {"a", "b", 3, []string{"d", "e", "f"}, "v", "w'll", 14, 15.8, true}, }, ) // first set up deleter - we want to make sure that even if it starts first before the index is created that things still work tsDelete := newTestSource( []Field{ //IDField{NameVal: "id"}, StringField{NameVal: "aba"}, StringField{NameVal: "db"}, IntField{NameVal: "id"}, StringArrayField{NameVal: "fields"}, }, [][]interface{}{ // {primarykey1, primarykey2, primarykey3, []fieldName} {"a", "b", 2, []string{"sa1"}}, {"a", "b", 2, []string{"s1"}}, {"a", "b", 2, []string{"s2"}}, {"a", "b", 3, []string{"s2"}}, // regression - try deleting value with single quote {"a", "b", 2, []string{"i1"}}, {"a", "b", 2, []string{"d1"}}, {"a", "b", 1, []string{"bools|b1"}}, // try deleting same fields again {"a", "b", 2, []string{"sa1"}}, {"a", "b", 2, []string{"s1"}}, {"a", "b", 2, []string{"s2"}}, {"a", "b", 2, []string{"i1"}}, {"a", "b", 2, []string{"d1"}}, {"a", "b", 1, []string{"bools|b1"}}, // try some bad messages for sanity check {"a", "b", 2, []string{}}, //{"a", "b", 1, []string{""}}, //{"a", "b", 1, []string{"not-exist"}}, // {"a", "b", []string{"s1"}}, // {4, nil}, }, ) deleter := NewMain() configureTestFlags(deleter) deleter.Delete = true deleter.PackBools = "bools" deleter.NewSource = func() (Source, error) { return tsDelete, nil } deleter.Index = indexName deleter.BatchSize = 5 deleter.PrimaryKeyFields = []string{"aba", "db", "id"} onFinishRun, err := deleter.Setup() if err != nil { t.Fatalf("setting up deleter: %v", err) } defer onFinishRun() ingester := NewMain() configureTestFlags(ingester) ingester.PackBools = "bools" ingester.NewSource = func() (Source, error) { return tsWrite, nil } ingester.PrimaryKeyFields = primaryKeyFields ingester.Index = indexName ingester.BatchSize = 1 defer func() { if err := ingester.PilosaClient().DeleteIndexByName(ingester.Index); err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, ingester.Index, err) } }() err = ingester.Run() if err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } client := ingester.PilosaClient() sa1 := ingester.index.Field("sa1") s2 := ingester.index.Field("s2") if resp, err := client.Query(sa1.Row("d")); err != nil { t.Fatalf("row sa1=d: %v", err) } else if keys := resp.Result().Row().Keys; !stringSliceSame(keys, []string{"a|b|2", "a|b|3"}) { t.Errorf("exp: %v\ngot: %v", "[a|b|2 a|b|3]", keys) } if resp, err := client.Query(s2.Row("w'll")); err != nil { t.Fatalf("row a: %v", err) } else if keys := resp.Result().Row().Keys; !stringSliceSame(keys, []string{"a|b|3"}) { t.Errorf("unexpected keys for s2: %v", resp.Result().Row().Keys) } if err := deleter.run(); err != nil && errors.Cause(err) != io.EOF { t.Fatalf("running deleter: %v", err) } if resp, err := client.Query(sa1.Row("d")); err != nil { t.Fatalf("row a: %v", err) } else if keys := resp.Result().Row().Keys; len(keys) != 1 || keys[0] != "a|b|3" { t.Errorf("exp: %v\ngot: %v", "[a|b|3]", keys) } if resp, err := client.Query(s2.Row("w'll")); err != nil { t.Fatalf("s2 row w'll: %v", err) } else if len(resp.Result().Row().Keys) != 0 { t.Errorf("unexpected keys for s2 w'll: %v", resp.Result().Row().Keys) } } func TestGetPrimaryKeyRecordizer(t *testing.T) { tests := []struct { name string schema []Field pkFields []string expErr string expSkip map[int]struct{} rawRec []interface{} expID interface{} }{ { name: "no schema", expErr: "can't call getPrimaryKeyRecordizer with empty schema", }, { name: "no pkfields", schema: []Field{StringField{}}, expErr: "can't call getPrimaryKeyRecordizer with empty pkFields", }, { name: "primary is StringArray", schema: []Field{StringArrayField{NameVal: "blah"}}, pkFields: []string{"blah"}, expErr: "field blah cannot be a primary key field because it is a StringArray field.", }, { name: "primary is StringArray complex", schema: []Field{StringField{NameVal: "zaa"}, IntField{NameVal: "hey"}, StringArrayField{NameVal: "blah"}}, pkFields: []string{"blah", "zaa"}, expErr: "field blah cannot be a primary key field because it is a StringArray field.", }, { name: "unknown pkfield", schema: []Field{StringField{NameVal: "zaa"}}, pkFields: []string{"zaa", "zz"}, expErr: "no field with primary key field name zz found", }, { name: "unknown pkfield complex", schema: []Field{StringField{NameVal: "zaa"}, IntField{NameVal: "hey"}, StringField{NameVal: "blah"}}, pkFields: []string{"blah", "zz", "zaa"}, expErr: "no field with primary key field name zz found", }, { name: "skip primary", schema: []Field{StringField{NameVal: "a"}, IntField{NameVal: "b"}}, pkFields: []string{"a"}, expSkip: map[int]struct{}{0: {}}, rawRec: []interface{}{"a", 9}, expID: "a", }, { name: "primaries as ints", schema: []Field{StringField{NameVal: "a"}, IntField{NameVal: "b"}, IntField{NameVal: "c"}, IntField{NameVal: "d"}}, pkFields: []string{"c", "d", "b"}, rawRec: []interface{}{"a", uint32(1), uint32(2), uint32(4)}, expID: []byte("2|4|1"), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { rdz, skips, err := getPrimaryKeyRecordizer(test.schema, test.pkFields) if test.expErr != "" { if err == nil { t.Fatalf("nil err, expected %s", test.expErr) } if !strings.Contains(err.Error(), test.expErr) { t.Fatalf("unmatched errs exp/got\n%s\n%v", test.expErr, err) } return } else if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(skips, test.expSkip) { t.Errorf("unmatched skips exp/got\n%+v\n%+v", test.expSkip, skips) } row := &batch.Row{} err = rdz(test.rawRec, row) if err != nil { t.Fatalf("unexpected error from recordizer: %v", err) } if !reflect.DeepEqual(test.expID, row.ID) { t.Fatalf("mismatched row IDs exp: %+v, got: %+v", test.expID, row.ID) } }) } } type serverInfo struct { Name string AuthToken string PilosaHosts []string } func TestBatchFromSchema(t *testing.T) { if testing.Short() { t.Skip() } type testcase struct { name string schema []Field IDField string pkFields []string autogen bool extgen bool packBools string rawRec []interface{} rowID interface{} rowVals []interface{} err string batchErr string rdzErrs []string time batch.QuantizedTime lookupWriteIdxs []int } getQuantizedTime := func(t time.Time) batch.QuantizedTime { qt := batch.QuantizedTime{} qt.Set(t) return qt } runTest := func(t *testing.T, test testcase, removeIndex bool, server serverInfo) { m := NewMain() configureTestFlags(m) m.Index = "cmd_test_index23lkjdkfjr2" m.PrimaryKeyFields = test.pkFields m.IDField = test.IDField m.AutoGenerate = test.autogen m.ExternalGenerate = test.extgen m.PackBools = test.packBools m.BatchSize = 2 m.Pprof = "" m.NewSource = func() (Source, error) { return nil, nil } if server.AuthToken != "" { m.AuthToken = server.AuthToken } m.PilosaHosts = server.PilosaHosts if removeIndex { defer func() { err := m.client.DeleteIndex(m.index) if err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, m.Index, err) } }() } onFinishRun, err := m.Setup() if strings.Contains("validation", test.name) && testErr(t, test.err, err) { return } if err != nil { t.Fatalf("%v", err) } defer onFinishRun() rdzs, batch, row, lookupWriteIdxs, err := m.batchFromSchema(test.schema) if testErr(t, test.err, err) { return } for k, rdz := range rdzs { err = rdz(test.rawRec, row) if test.rdzErrs != nil && test.rdzErrs[k] != "" && testErr(t, test.rdzErrs[k], err) { return } } if !reflect.DeepEqual(row.ID, test.rowID) { t.Fatalf("row IDs exp: %+v got %+v", test.rowID, row.ID) } if !reflect.DeepEqual(row.Time, test.time) { t.Fatalf("times not equal: exp: %+v, got: %+v", test.time, row.Time) } if !reflect.DeepEqual(row.Values, test.rowVals) { t.Errorf("row values exp/got:\n%+v %[1]T\n%+v %[2]T", test.rowVals, row.Values) if len(row.Values) == len(test.rowVals) { for i, v := range row.Values { if !reflect.DeepEqual(v, test.rowVals[i]) { t.Errorf("%v %[1]T != %v %[2]T", test.rowVals[i], v) } } } t.Fail() } if test.lookupWriteIdxs != nil && !reflect.DeepEqual(lookupWriteIdxs, test.lookupWriteIdxs) { t.Fatalf("lookupWriteIdxs exp: %+v got %+v", test.lookupWriteIdxs, lookupWriteIdxs) } if test.autogen { if row.ID != nil { t.Fatalf("expected ID not to be set when autogen is used (because it gets set from nexter in the ingest loop which isn't called in this test.)") } row.ID = uint64(1) // set so Add doesn't panic } err = batch.Add(*row) if testErr(t, test.batchErr, err) { return } } tests := []testcase{ { name: "validation", err: "must set exactly one of --primary-key-field , --id-field , --auto-generate", }, { name: "empty", autogen: true, err: "can't batch with no fields", }, { name: "empty-w/ExtGen", autogen: true, extgen: true, err: "can't batch with no fields", }, { name: "no id field", schema: []Field{StringField{}}, IDField: "nope", err: "ID field nope not found", }, { name: "pk error", pkFields: []string{"zoop"}, err: "getting primary key recordizer", }, { name: "pack bools", schema: []Field{BoolField{NameVal: "a"}, IDField{NameVal: "b"}, BoolField{NameVal: "c"}}, IDField: "b", packBools: "bff", rawRec: []interface{}{true, uint64(7), false}, rowID: uint64(7), rowVals: []interface{}{"a", "a", nil, "c"}, }, { name: "don't pack bools", schema: []Field{BoolField{NameVal: "a"}, IDField{NameVal: "b"}, BoolField{NameVal: "c"}}, IDField: "b", rawRec: []interface{}{true, uint64(7), false}, rowID: uint64(7), rowVals: []interface{}{true, false}, }, { name: "mutex field", schema: []Field{StringField{NameVal: "a", Mutex: true}, IDField{NameVal: "b"}}, IDField: "b", rawRec: []interface{}{"aval", uint64(7)}, rowID: uint64(7), rowVals: []interface{}{"aval"}, }, { name: "mutex field with time quantum", schema: []Field{StringField{NameVal: "a", Mutex: true, Quantum: "YM"}, IDField{NameVal: "b"}}, IDField: "b", rawRec: []interface{}{"aval", uint64(7)}, rowID: uint64(7), err: "can't specify a time quantum on a string mutex field", }, { name: "ttl", schema: []Field{StringField{NameVal: "a", Mutex: false, Quantum: "YM", TTL: "0s"}, IDField{NameVal: "b"}}, IDField: "b", rawRec: []interface{}{"ttl", uint64(7)}, rowID: uint64(7), rowVals: []interface{}{"ttl"}, }, { name: "bad ttl", schema: []Field{StringField{NameVal: "a", Mutex: false, Quantum: "YM", TTL: "bad-ttl"}, IDField{NameVal: "b"}}, IDField: "b", rawRec: []interface{}{"b", uint64(7)}, rowID: uint64(7), rowVals: []interface{}{"ttl"}, err: "unable to parse TTL from field", }, { name: "string array field", schema: []Field{StringArrayField{NameVal: "a"}, StringField{NameVal: "b"}}, pkFields: []string{"b"}, rawRec: []interface{}{[]string{"aval", "aval2"}, uint64(7)}, rowID: []byte("7"), rowVals: []interface{}{[]string{"aval", "aval2"}}, }, { name: "id array field", schema: []Field{IDArrayField{NameVal: "a"}, StringField{NameVal: "b"}}, pkFields: []string{"b"}, rawRec: []interface{}{"1,2", uint64(7)}, rowID: []byte("7"), rowVals: []interface{}{[]uint64{1, 2}}, }, { name: "id array field brackets", schema: []Field{IDArrayField{NameVal: "a"}, StringField{NameVal: "b"}}, pkFields: []string{"b"}, rawRec: []interface{}{"[1,2]", uint64(7)}, rowID: []byte("7"), rowVals: []interface{}{[]uint64{1, 2}}, }, { name: "decimal field", schema: []Field{StringField{NameVal: "a"}, DecimalField{NameVal: "b", Scale: 2}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", uint64(321)}, rowID: "blah", rowVals: []interface{}{int64(32100)}, }, { name: "decimal field scale too small", schema: []Field{StringField{NameVal: "a"}, DecimalField{NameVal: "b", Scale: -2}}, pkFields: []string{"a"}, err: "scale values outside the range", }, { name: "decimal field scale too large", schema: []Field{StringField{NameVal: "a"}, DecimalField{NameVal: "b", Scale: 20}}, pkFields: []string{"a"}, err: "scale values outside the range", }, { name: "date int field", schema: []Field{StringField{NameVal: "a"}, DateIntField{NameVal: "dif", Unit: Day}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", time.Date(1972, time.January, 1, 1, 1, 1, 1, time.UTC)}, rowID: "blah", rowVals: []interface{}{int64(730)}, }, { name: "date int field string", schema: []Field{StringField{NameVal: "a"}, DateIntField{NameVal: "dif", Unit: Day}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", "1972-01-01"}, rowID: "blah", rowVals: []interface{}{int64(730)}, }, { name: "record time field custom units", schema: []Field{StringField{NameVal: "b"}, RecordTimeField{NameVal: "recordTimeField1", Layout: "blah", Unit: Second}, StringField{NameVal: "domain", Quantum: "MDH"}}, pkFields: []string{"b"}, rawRec: []interface{}{"blaah", 123654895, "molecula.com"}, rowID: "blaah", rowVals: []interface{}{"molecula.com"}, time: getQuantizedTime(time.Unix(123654895, 0)), }, { name: "record time field layout", schema: []Field{StringField{NameVal: "b"}, RecordTimeField{NameVal: "recordTimeField1", Layout: time.RFC822}, StringField{NameVal: "domain", Quantum: "MDH"}}, pkFields: []string{"b"}, rawRec: []interface{}{"blaah", "08 Mar 09 21:00 UTC", "molecula.com"}, rowID: "blaah", rowVals: []interface{}{"molecula.com"}, time: getQuantizedTime(time.Unix(1236548940, 0).UTC()), }, { name: "record time field epoch", schema: []Field{StringField{NameVal: "b"}, RecordTimeField{NameVal: "recordTimeField1", Epoch: time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC), Unit: Second}, StringField{NameVal: "domain", Quantum: "MDH"}}, pkFields: []string{"b"}, rawRec: []interface{}{"blaah", 1, "molecula.com"}, rowID: "blaah", rowVals: []interface{}{"molecula.com"}, time: getQuantizedTime(time.Date(2010, time.January, 1, 0, 0, 1, 0, time.UTC)), }, { name: "bool parsing", schema: []Field{BoolField{NameVal: "a"}, IDField{NameVal: "b"}, BoolField{NameVal: "c"}, BoolField{NameVal: "d"}, BoolField{NameVal: "e"}, BoolField{NameVal: "f"}}, IDField: "b", packBools: "bff", rawRec: []interface{}{1, uint64(7), "1", "0", nil, '1'}, rowID: uint64(7), rowVals: []interface{}{"a", "a", "c", "c", nil, "d", nil, nil, "f", "f"}, }, { name: "timestamp field", schema: []Field{StringField{NameVal: "a"}, TimestampField{NameVal: "b", Granularity: "s", Unit: Millisecond, Epoch: time.Unix(10000, 0)}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", "5000"}, rowID: "blah", rowVals: []interface{}{int64(5)}, }, { name: "timestamp incorrect layout", schema: []Field{StringField{NameVal: "a"}, TimestampField{NameVal: "b", Layout: "Mon, 02 Jan 2006 15:04:05 MST"}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", "2025-05-15T05:05:05Z"}, rowID: "blah", rdzErrs: []string{"", "parsing time string 2025-05-15T05:05:05Z"}, }, { name: "timestamp empty string", schema: []Field{StringField{NameVal: "a"}, TimestampField{NameVal: "b", Epoch: time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC), Unit: Nanosecond}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", ""}, rowID: "blah", rowVals: []interface{}{nil}, }, { name: "timestamp nil input", schema: []Field{StringField{NameVal: "a"}, TimestampField{NameVal: "b", Epoch: time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC), Unit: Nanosecond}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", nil}, rowID: "blah", rowVals: []interface{}{nil}, }, { name: "timestamp epoch default granularity", schema: []Field{StringField{NameVal: "a"}, TimestampField{NameVal: "b", Epoch: time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC), Unit: Nanosecond}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", "5000000000"}, rowID: "blah", rowVals: []interface{}{int64(5)}, }, { name: "timestamp epoch default granularity with extgen", schema: []Field{StringField{NameVal: "a"}, TimestampField{NameVal: "b", Epoch: time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC), Unit: Nanosecond}}, autogen: true, extgen: true, rawRec: []interface{}{"blah", "5000000000"}, rowVals: []interface{}{"blah", int64(5)}, }, { name: "lookup text", schema: []Field{StringField{NameVal: "a"}, DateIntField{NameVal: "dif", Unit: Day}, LookupTextField{NameVal: "log"}}, pkFields: []string{"a"}, rawRec: []interface{}{"blah", "1972-01-01", "asdf"}, rowID: "blah", rowVals: []interface{}{int64(730)}, lookupWriteIdxs: []int{2}, }, { name: "int null", pkFields: []string{"user_id"}, rawRec: []interface{}{"1a", nil}, rowID: "1a", schema: []Field{StringField{NameVal: "user_id"}, IntField{NameVal: "int_val"}, }, rowVals: []interface{}{nil}, }, } servers := []serverInfo{ { Name: "WithAuth", AuthToken: getAuthToken(t), PilosaHosts: []string{"http://pilosa-auth:10105"}, }, { Name: "WithOutAuth", PilosaHosts: []string{"http://pilosa:10101"}, }, } for _, server := range servers { for _, test := range tests { // test on fresh Pilosa t.Run(test.name+"-1-"+server.Name, func(t *testing.T) { runTest(t, test, false, server) }) // test again with index/fields in place t.Run(test.name+"-2-"+server.Name, func(t *testing.T) { runTest(t, test, true, server) }) } } } // When ingesting into a field that already exists in Pilosa, // ensure that we check the compatibility of the Pilosa field // with the source field. func TestCheckFieldCompatibility(t *testing.T) { ingester := NewMain() configureTestFlags(ingester) // Get the pilosa client from ingester. if _, err := ingester.setupClient(); err != nil { t.Fatal(err) } client := ingester.PilosaClient() // Create various fields which we can use to check // ingest field compatibility. rand.Seed(time.Now().UTC().UnixNano()) idxName := fmt.Sprintf("compattest%d", rand.Intn(100000)) idx := pilosaclient.NewIndex(idxName) if err := client.CreateIndex(idx); err != nil { t.Fatal(err) } defer func() { if err := ingester.PilosaClient().DeleteIndexByName(idxName); err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, idxName, err) } }() if err := client.CreateField(idx.Field("pset", pilosaclient.OptFieldTypeSet(pilosaclient.CacheTypeNone, 0))); err != nil { t.Fatal(err) } if err := client.CreateField(idx.Field("pint", pilosaclient.OptFieldTypeInt(-1000, 1000))); err != nil { t.Fatal(err) } if err := client.CreateField(idx.Field("pttl", pilosaclient.OptFieldKeys(true), pilosaclient.OptFieldTypeTime("YMD"), pilosaclient.OptFieldTTL(0))); err != nil { t.Fatal(err) } if err := client.CreateField(idx.Field( "packbools-exists", pilosaclient.OptFieldTypeSet(pilosaclient.CacheTypeRanked, pilosaclient.CacheSizeDefault), pilosaclient.OptFieldKeys(false), )); err != nil { t.Fatal(err) } ingester.AutoGenerate = true ingester.Index = idxName ingester.index = idx ingester.BatchSize = 5 ingester.PackBools = "packbools" tests := []struct { fld Field val interface{} expErr string }{ { fld: StringField{NameVal: "pset"}, val: "a", expErr: "idk keys true differs from featurebase keys false", }, { fld: IntField{NameVal: "pset"}, val: 1, expErr: "idk field type int is incompatible with featurebase field type set", }, { fld: IntField{NameVal: "pint"}, val: 1, expErr: "", }, { fld: IntField{NameVal: "pint", Min: int64Ptr(-100)}, val: 1, expErr: "idk min -100 differs from featurebase min -1000", }, { fld: IntField{NameVal: "pint", Min: int64Ptr(-1000), Max: int64Ptr(100)}, val: 1, expErr: "idk max 100 differs from featurebase max 1000", }, { fld: BoolField{NameVal: "foo"}, val: true, expErr: "idk keys true differs from featurebase keys false: packbools-exists", }, { fld: StringField{NameVal: "pttl", Quantum: "YMD1", TTL: "1s"}, val: "a", expErr: "", }, { fld: StringField{NameVal: "pttl", Quantum: "YMD1", TTL: "bad-format"}, val: "a", expErr: "", }, } for i, test := range tests { t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { ts := newTestSource( []Field{test.fld}, [][]interface{}{ {test.val}, }, ) ingester.NewSource = func() (Source, error) { return ts, nil } err := ingester.Run() if test.expErr != "" { if err == nil || !strings.Contains(err.Error(), test.expErr) { t.Fatalf("expected error: '%s', but got: '%v'", test.expErr, err) } } }) } } func int64Ptr(i int64) *int64 { return &i } func testErr(t *testing.T, exp string, actual error) (done bool) { t.Helper() if exp == "" && actual == nil { return false } if exp == "" && actual != nil { t.Fatalf("unexpected errs exp: \n%s got: \n%v", exp, actual) } if exp != "" && actual == nil { t.Fatalf("expected errs exp: \n%s got: \n%v", exp, actual) } if !strings.Contains(actual.Error(), exp) { t.Fatalf("unmatched errs exp: \n%s got: \n%v", exp, actual) } return true } type signalingTestSource struct { records chan []interface{} schema []Field committed chan []interface{} } func (s *signalingTestSource) Record() (Record, error) { rec, ok := <-s.records if !ok { return nil, io.EOF } return newSliceRecord(rec, s.committed), nil } func (s *signalingTestSource) Schema() []Field { return s.schema } func (s *signalingTestSource) Close() error { return nil } func newSignalingTestSource(schema []Field, records chan []interface{}, committed chan []interface{}) *signalingTestSource { return &signalingTestSource{ schema: schema, records: records, committed: committed, } } type testSource struct { i int records [][]interface{} schema []Field } func (s *testSource) Close() error { return nil } type sliceRecord struct { data []interface{} committed chan []interface{} } func (s *sliceRecord) Commit(ctx context.Context) error { if s.committed == nil { return nil } select { case <-ctx.Done(): return ctx.Err() case s.committed <- s.data: return nil } } func (s *sliceRecord) Data() []interface{} { return s.data } func (s *sliceRecord) Schema() interface{} { return nil } func (s *testSource) Record() (Record, error) { s.i++ if s.i <= len(s.records) { if s.records[s.i-1][0] == ErrFlush { return nil, ErrFlush } return newSliceRecord(s.records[s.i-1], nil), nil } return nil, io.EOF } func newSliceRecord(data []interface{}, committed chan []interface{}) *sliceRecord { return &sliceRecord{ data: data, committed: committed, } } func (s *testSource) Schema() []Field { return s.schema } func newTestSource(schema []Field, records [][]interface{}) *testSource { return &testSource{ schema: schema, records: records, } } func stringSliceSame(a, b []string) bool { if len(a) != len(b) { return false } sort.Strings(a) sort.Strings(b) for i := range a { aa, bb := a[i], b[i] if aa != bb { return false } } return true } // generate authentication token for testing with pilosa-auth // featurebase.conf used for running pilosa-auth contains same auth parameters func getAuthToken(t *testing.T) string { t.Helper() var ( ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71" ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token" GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true" LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" ConfiguredIps = []string{} ) a, err := authn.NewAuth( logger.NewStandardLogger(os.Stdout), "http://localhost:10101/", Scopes, AuthorizeURL, TokenURL, GroupEndpointURL, LogoutURL, ClientID, ClientSecret, Key, ConfiguredIps, ) if err != nil { t.Fatal(err) } // make a valid token tkn := jwt.New(jwt.SigningMethodHS256) claims := tkn.Claims.(jwt.MapClaims) claims["oid"] = "42" claims["name"] = "valid" token, err1 := tkn.SignedString([]byte(a.SecretKey())) if err1 != nil { t.Fatal(err1) } return token } func TestSetup(t *testing.T) { m := NewMain() configureTestFlags(m) m.AutoGenerate = true m.Index = "test" m.NewSource = func() (Source, error) { return nil, nil } token := getAuthToken(t) m.AuthToken = token t.Run("test", func(t *testing.T) { _, err := m.Setup() if err != nil { t.Fatalf("Failed idk main setup: %s", err) } got := m.AuthToken want := "Bearer " + token if got != want { t.Fatalf("got %v, but want %v", got, want) } }) } func TestNilIngest(t *testing.T) { type testcase struct { name string schema []Field IDField string pkFields []string rawRec1 []interface{} rawRec2 []interface{} rawRec3 []interface{} rowID interface{} clearVals1 map[int]interface{} clearVals2 map[int]interface{} clearVals3 map[int]interface{} Vals1 []interface{} Vals2 []interface{} Vals3 []interface{} err string batchErr string rdzErrs []string packBools string } runTest := func(t *testing.T, test testcase, removeIndex bool, server serverInfo, rawRec []interface{}, clearmap map[int]interface{}, values []interface{}) { m := NewMain() configureTestFlags(m) m.Index = "cmd_test_index23l" m.PrimaryKeyFields = test.pkFields m.BatchSize = 2 m.Pprof = "" m.PackBools = test.packBools m.NewSource = func() (Source, error) { return nil, nil } if server.AuthToken != "" { m.AuthToken = server.AuthToken } m.PilosaHosts = server.PilosaHosts onFinishRun, err := m.Setup() if strings.Contains("validation", test.name) && testErr(t, test.err, err) { return } if err != nil { t.Fatalf("%v", err) } defer onFinishRun() if removeIndex { defer func() { err := m.client.DeleteIndex(m.index) if err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, m.Index, err) } }() } rdzs, batch, row, _, err := m.batchFromSchema(test.schema) if testErr(t, test.err, err) { return } for k, rdz := range rdzs { err = rdz(rawRec, row) if test.rdzErrs != nil && test.rdzErrs[k] != "" && testErr(t, test.rdzErrs[k], err) { return } } if !reflect.DeepEqual(row.Clears, clearmap) { t.Fatalf("clear exp: %+v got %+v", clearmap, row.Clears) } if !reflect.DeepEqual(row.Values, values) { t.Fatalf("values exp: %+v got %+v", values, row.Values) } err = batch.Add(*row) if testErr(t, test.batchErr, err) { return } } tests := []testcase{ { name: "int/dec null", pkFields: []string{"user_id"}, rawRec1: []interface{}{"1a", nil, 5, nil, 5.2}, rawRec2: []interface{}{"1a", 2, nil, 4.4, DELETE_SENTINEL}, rawRec3: []interface{}{"1a", DELETE_SENTINEL, 5, nil, 5.2}, rowID: "1a", schema: []Field{ StringField{NameVal: "user_id"}, IntField{NameVal: "int_val1"}, IntField{NameVal: "int_val2"}, DecimalField{NameVal: "dec_val1", Scale: 1}, DecimalField{NameVal: "dec_val2", Scale: 1}, }, clearVals1: map[int]interface{}{}, clearVals2: map[int]interface{}{3: uint64(0)}, clearVals3: map[int]interface{}{0: uint64(0)}, Vals1: []interface{}{nil, int64(5), nil, int64(52)}, Vals2: []interface{}{int64(2), nil, int64(44), nil}, Vals3: []interface{}{nil, int64(5), nil, int64(52)}, }, { name: "mutex null", pkFields: []string{"user_id"}, rawRec1: []interface{}{"1a", "g", uint64(1)}, rawRec2: []interface{}{"1a", DELETE_SENTINEL, nil}, rawRec3: []interface{}{"1a", nil, DELETE_SENTINEL}, rowID: "1a", schema: []Field{ StringField{NameVal: "user_id"}, StringField{NameVal: "string_val1", Mutex: true}, IDField{NameVal: "id_val1", Mutex: true}, }, clearVals1: map[int]interface{}{}, clearVals2: map[int]interface{}{0: nil}, clearVals3: map[int]interface{}{1: nil}, Vals1: []interface{}{"g", uint64(1)}, Vals2: []interface{}{nil, nil}, Vals3: []interface{}{nil, nil}, }, { name: "bools null", packBools: "bools", pkFields: []string{"user_id"}, rawRec1: []interface{}{"1a", true}, // bool and bool-exists rawRec2: []interface{}{"1a", DELETE_SENTINEL}, rawRec3: []interface{}{"1a", nil}, rowID: "1a", schema: []Field{ StringField{NameVal: "user_id"}, BoolField{NameVal: "bool_val_1"}, }, clearVals1: map[int]interface{}{}, clearVals2: map[int]interface{}{0: "bool_val_1"}, clearVals3: map[int]interface{}{}, Vals1: []interface{}{"bool_val_1", "bool_val_1"}, Vals2: []interface{}{nil, nil}, Vals3: []interface{}{nil, nil}, }, { name: "set no-op", pkFields: []string{"user_id"}, rawRec1: []interface{}{"1a", uint64(1)}, rawRec2: []interface{}{"1a", DELETE_SENTINEL}, rawRec3: []interface{}{"1a", nil}, rowID: "1a", schema: []Field{ StringField{NameVal: "user_id"}, IDField{NameVal: "id_val1"}, }, clearVals1: map[int]interface{}{}, clearVals2: map[int]interface{}{}, clearVals3: map[int]interface{}{}, Vals1: []interface{}{uint64(1)}, Vals2: []interface{}{nil}, Vals3: []interface{}{nil}, }, } servers := []serverInfo{ { Name: "WithAuth", AuthToken: getAuthToken(t), PilosaHosts: []string{"http://pilosa-auth:10105"}, }, { Name: "WithOutAuth", PilosaHosts: []string{"http://pilosa:10101"}, }, } for _, server := range servers { for _, test := range tests { // test on fresh Pilosa t.Run(test.name+"-1-"+server.Name, func(t *testing.T) { runTest(t, test, false, server, test.rawRec1, test.clearVals1, test.Vals1) }) // test again with index/fields in place t.Run(test.name+"-2-"+server.Name, func(t *testing.T) { runTest(t, test, false, server, test.rawRec2, test.clearVals2, test.Vals2) }) t.Run(test.name+"-3-"+server.Name, func(t *testing.T) { runTest(t, test, true, server, test.rawRec3, test.clearVals3, test.Vals3) }) } } } func TestBoolIngest(t *testing.T) { rand.Seed(time.Now().UTC().UnixNano()) indexName := fmt.Sprintf("boolingest%d", rand.Intn(100000)) primaryKeyFields := []string{"user_id"} tests := []struct { src *testSource expTrue []string expFalse []string expNull []string }{ { src: newTestSource( []Field{ StringField{NameVal: "user_id"}, BoolField{NameVal: "bool_val"}, }, [][]interface{}{ {"a1", true}, }, ), expTrue: []string{"a1"}, expFalse: nil, expNull: nil, }, { src: newTestSource( []Field{ StringField{NameVal: "user_id"}, BoolField{NameVal: "bool_val"}, }, [][]interface{}{ {"a1", false}, }, ), expTrue: nil, expFalse: []string{"a1"}, expNull: nil, }, { src: newTestSource( []Field{ StringField{NameVal: "user_id"}, BoolField{NameVal: "bool_val"}, }, [][]interface{}{ {"a1", nil}, }, ), expTrue: nil, expFalse: nil, expNull: []string{"a1"}, }, { src: newTestSource( []Field{ StringField{NameVal: "user_id"}, BoolField{NameVal: "bool_val"}, }, [][]interface{}{ {"a1", DELETE_SENTINEL}, }, ), expTrue: nil, expFalse: nil, expNull: []string{"a1"}, }, } var ing *Main defer func() { ing := ing if err := ing.PilosaClient().DeleteIndexByName(ing.Index); err != nil { t.Logf("%s for index %s: %v", idktest.ErrDeletingIndex, ing.Index, err) } }() for i, test := range tests { t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { ingester := NewMain() configureTestFlags(ingester) ingester.PackBools = "" ingester.NewSource = func() (Source, error) { return test.src, nil } ingester.PrimaryKeyFields = primaryKeyFields ingester.Index = indexName ingester.BatchSize = 1 ingester.UseShardTransactionalEndpoint = true // Set ing so the defer can do cleanup. if i == 0 { ing = ingester } if err := ingester.Run(); err != nil { t.Fatalf("%s: %v", idktest.ErrRunningIngest, err) } client := ingester.PilosaClient() idx := ingester.index fld := ingester.index.Field("bool_val") // Check true. { resp, err := client.Query(fld.Row(true)) assert.NoError(t, err) assert.Equal(t, test.expTrue, resp.Result().Row().Keys) } // Check false. { resp, err := client.Query(fld.Row(false)) assert.NoError(t, err) assert.Equal(t, test.expFalse, resp.Result().Row().Keys) } // Check nil. This is used to test the ingestion of nil and null. { resp, err := client.Query(idx.Difference(idx.All(), idx.Union(fld.Row(true), fld.Row(false)))) assert.NoError(t, err) assert.Equal(t, test.expNull, resp.Result().Row().Keys) } }) } } func TestBatchTargetServerless(t *testing.T) { var controllerHost string if controller, ok := os.LookupEnv("IDK_TEST_CONTROLLER_HOST"); ok { controllerHost = controller } else { controllerHost = "dax:8080" } controllerAddress := dax.Address(controllerHost + "/" + dax.ServicePrefixController) orgID := dax.OrganizationID("acme") dbID := dax.DatabaseID("db1") controllerClient := controllerclient.New(controllerAddress, logger.StderrLogger) ctx := context.Background() // Create the database. qdb := &dax.QualifiedDatabase{ OrganizationID: orgID, Database: dax.Database{ ID: dbID, Name: "dbname1", Options: dax.DatabaseOptions{ WorkersMin: 1, WorkersMax: 1, }, }, } controllerClient.CreateDatabase(ctx, qdb) t.Run("FieldTypes", func(t *testing.T) { tests := []struct { fieldType dax.BaseType fieldOptions dax.FieldOptions fieldFn fieldFn in [][]interface{} }{ // { // fieldType: types.FieldTypeBool, // fieldFn: boolFn, // in: [][]interface{}{ // {1, true}, // {2, false}, // }, // }, { fieldType: dax.BaseTypeDecimal, fieldOptions: dax.FieldOptions{ Scale: 4, Min: pql.NewDecimal(-100, 0), Max: pql.NewDecimal(100, 0), }, fieldFn: decimalFn, in: [][]interface{}{ {1, "12.3456"}, {2, "-7.8"}, }, }, { fieldType: dax.BaseTypeID, fieldFn: idFn, in: [][]interface{}{ {1, uint64(11)}, {2, uint64(22)}, }, }, { fieldType: dax.BaseTypeIDSet, fieldFn: idSetFn, in: [][]interface{}{ {1, []uint64{11, 12, 13}}, {2, []uint64{22, 24, 26, 28}}, }, }, { fieldType: dax.BaseTypeInt, fieldFn: intFn, in: [][]interface{}{ {1, int(11)}, {2, int(-22)}, }, fieldOptions: dax.FieldOptions{Min: pql.NewDecimal(-100, 0), Max: pql.NewDecimal(100, 0)}, }, { fieldType: dax.BaseTypeString, fieldFn: stringFn, in: [][]interface{}{ {1, "cycling"}, {2, "running"}, }, }, { fieldType: dax.BaseTypeStringSet, fieldFn: stringSetFn, in: [][]interface{}{ {1, []string{"cycling", "swimming"}}, {2, []string{"running", "cooking"}}, }, }, { fieldType: dax.BaseTypeTimestamp, fieldFn: timestampFn, in: [][]interface{}{ {1, time.Now()}, {2, time.Now()}, }, fieldOptions: dax.FieldOptions{TimeUnit: "s", Epoch: time.Unix(0, 0)}, // TODO w/o this, it used to silently fail (just logs in the controller svc). Eventually should probably have a default unit and not fail at all. }, } for i, test := range tests { t.Run(fmt.Sprintf("test-%s-%d", test.fieldType, i), func(t *testing.T) { // Generate a random tableName to use for the test. rand.Seed(time.Now().UTC().UnixNano()) tableName := fmt.Sprintf("tbl_%s_%d", test.fieldType, rand.Intn(100000)) fieldName := fmt.Sprintf("fld_%s_%d", test.fieldType, rand.Intn(100000)) tblName := dax.TableName(tableName) tbl := dax.NewTable(tblName) tbl.PartitionN = dax.DefaultPartitionN tbl.Fields = []*dax.Field{ { Name: dax.PrimaryKeyFieldName, Type: dax.BaseTypeID, }, { Name: dax.FieldName(fieldName), Type: test.fieldType, Options: test.fieldOptions, }, } qtbl := dax.NewQualifiedTable( dax.NewQualifiedDatabaseID(orgID, dbID), tbl, ) // Create the table in Controller Schemar. if err := controllerClient.CreateTable(ctx, qtbl); err != nil { t.Fatalf("creating table: %v", err) } // qtblWithID is the same as qtbl above, but now Controller has // assigned the table a unique ID. qtblWithID, err := controllerClient.Table(ctx, qtbl.QualifiedID()) assert.NoError(t, err) ts := newTestSource([]Field{ IDField{NameVal: "id"}, test.fieldFn(fieldName, test.fieldOptions), }, test.in) ingester := NewMain() configureTestFlagsController(ingester, controllerAddress, qtblWithID) ingester.NewSource = func() (Source, error) { return ts, nil } ingester.BatchSize = 10 ingester.IDField = "id" if err := ingester.Run(); err != nil { t.Fatalf("running ingester: %v", err) } }) } }) } type fieldFn func(string, dax.FieldOptions) Field func boolFn(name string, fo dax.FieldOptions) Field { return BoolField{NameVal: name} } func decimalFn(name string, fo dax.FieldOptions) Field { return DecimalField{NameVal: name, Scale: fo.Scale} } func idFn(name string, fo dax.FieldOptions) Field { return IDField{NameVal: name, Mutex: true} } func idSetFn(name string, fo dax.FieldOptions) Field { return IDArrayField{NameVal: name} } func intFn(name string, fo dax.FieldOptions) Field { return IntField{NameVal: name} } func stringFn(name string, fo dax.FieldOptions) Field { return StringField{NameVal: name, Mutex: true} } func stringSetFn(name string, fo dax.FieldOptions) Field { return StringArrayField{NameVal: name} } func timestampFn(name string, fo dax.FieldOptions) Field { return TimestampField{NameVal: name} }