Spaces:
Running
Running
| // _ _ | |
| // __ _____ __ ___ ___ __ _| |_ ___ | |
| // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ | |
| // \ V V / __/ (_| |\ V /| | (_| | || __/ | |
| // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| | |
| // | |
| // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. | |
| // | |
| // CONTACT: [email protected] | |
| // | |
| package schema | |
| import ( | |
| "context" | |
| "fmt" | |
| "strings" | |
| "testing" | |
| "github.com/stretchr/testify/assert" | |
| "github.com/stretchr/testify/require" | |
| "github.com/weaviate/weaviate/adapters/repos/db/helpers" | |
| "github.com/weaviate/weaviate/adapters/repos/db/inverted/stopwords" | |
| "github.com/weaviate/weaviate/entities/models" | |
| "github.com/weaviate/weaviate/entities/schema" | |
| "github.com/weaviate/weaviate/usecases/config" | |
| "github.com/weaviate/weaviate/usecases/sharding" | |
| ) | |
| func TestAddClass(t *testing.T) { | |
| t.Run("with empty class name", func(t *testing.T) { | |
| err := newSchemaManager().AddClass(context.Background(), | |
| nil, &models.Class{}) | |
| require.EqualError(t, err, "'' is not a valid class name") | |
| }) | |
| t.Run("with permuted-casing class names", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{Class: "NewClass"}) | |
| require.Nil(t, err) | |
| err = mgr.AddClass(context.Background(), | |
| nil, &models.Class{Class: "NewCLASS"}) | |
| require.NotNil(t, err) | |
| require.Equal(t, | |
| "class name \"NewCLASS\" already exists as a permutation of: \"NewClass\". "+ | |
| "class names must be unique when lowercased", err.Error()) | |
| }) | |
| t.Run("with default BM25 params", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| expectedBM25Config := &models.BM25Config{ | |
| K1: config.DefaultBM25k1, | |
| B: config.DefaultBM25b, | |
| } | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{Class: "NewClass"}) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| require.Equal(t, expectedBM25Config, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Bm25) | |
| }) | |
| t.Run("with customized BM25 params", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| expectedBM25Config := &models.BM25Config{ | |
| K1: 1.88, | |
| B: 0.44, | |
| } | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{ | |
| Class: "NewClass", | |
| InvertedIndexConfig: &models.InvertedIndexConfig{ | |
| Bm25: expectedBM25Config, | |
| }, | |
| }) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| require.Equal(t, expectedBM25Config, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Bm25) | |
| }) | |
| t.Run("with default Stopwords config", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| expectedStopwordConfig := &models.StopwordConfig{ | |
| Preset: stopwords.EnglishPreset, | |
| } | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{Class: "NewClass"}) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| require.Equal(t, expectedStopwordConfig, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Stopwords) | |
| }) | |
| t.Run("with customized Stopwords config", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| expectedStopwordConfig := &models.StopwordConfig{ | |
| Preset: "none", | |
| Additions: []string{"monkey", "zebra", "octopus"}, | |
| Removals: []string{"are"}, | |
| } | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{ | |
| Class: "NewClass", | |
| InvertedIndexConfig: &models.InvertedIndexConfig{ | |
| Stopwords: expectedStopwordConfig, | |
| }, | |
| }) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| require.Equal(t, expectedStopwordConfig, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Stopwords) | |
| }) | |
| t.Run("with tokenizations", func(t *testing.T) { | |
| type testCase struct { | |
| propName string | |
| dataType []string | |
| tokenization string | |
| expectedErrMsg string | |
| } | |
| propName := func(dataType schema.DataType, tokenization string) string { | |
| dtStr := strings.ReplaceAll(string(dataType), "[]", "Array") | |
| tStr := "empty" | |
| if tokenization != "" { | |
| tStr = tokenization | |
| } | |
| return fmt.Sprintf("%s_%s", dtStr, tStr) | |
| } | |
| runTestCases := func(t *testing.T, testCases []testCase, mgr *Manager) { | |
| for i, tc := range testCases { | |
| t.Run(tc.propName, func(t *testing.T) { | |
| err := mgr.AddClass(context.Background(), nil, &models.Class{ | |
| Class: fmt.Sprintf("NewClass_%d", i), | |
| Properties: []*models.Property{ | |
| { | |
| Name: tc.propName, | |
| DataType: tc.dataType, | |
| Tokenization: tc.tokenization, | |
| }, | |
| }, | |
| }) | |
| if tc.expectedErrMsg == "" { | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| } else { | |
| require.EqualError(t, err, tc.expectedErrMsg) | |
| } | |
| }) | |
| } | |
| } | |
| t.Run("text/textArray and all tokenizations", func(t *testing.T) { | |
| testCases := []testCase{} | |
| for _, dataType := range []schema.DataType{ | |
| schema.DataTypeText, schema.DataTypeTextArray, | |
| } { | |
| for _, tokenization := range append(helpers.Tokenizations, "") { | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType.PropString(), | |
| tokenization: tokenization, | |
| expectedErrMsg: "", | |
| }) | |
| } | |
| tokenization := "non_existing" | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType.PropString(), | |
| tokenization: tokenization, | |
| expectedErrMsg: fmt.Sprintf("Tokenization '%s' is not allowed for data type '%s'", tokenization, dataType), | |
| }) | |
| } | |
| runTestCases(t, testCases, newSchemaManager()) | |
| }) | |
| t.Run("non text/textArray and all tokenizations", func(t *testing.T) { | |
| testCases := []testCase{} | |
| for _, dataType := range schema.PrimitiveDataTypes { | |
| switch dataType { | |
| case schema.DataTypeText, schema.DataTypeTextArray: | |
| continue | |
| default: | |
| tokenization := "" | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType.PropString(), | |
| tokenization: tokenization, | |
| expectedErrMsg: "", | |
| }) | |
| for _, tokenization := range append(helpers.Tokenizations, "non_existing") { | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType.PropString(), | |
| tokenization: tokenization, | |
| expectedErrMsg: fmt.Sprintf("Tokenization is not allowed for data type '%s'", dataType), | |
| }) | |
| } | |
| } | |
| } | |
| runTestCases(t, testCases, newSchemaManager()) | |
| }) | |
| t.Run("non text/textArray and all tokenizations", func(t *testing.T) { | |
| ctx := context.Background() | |
| mgr := newSchemaManager() | |
| _, err := mgr.addClass(ctx, &models.Class{Class: "SomeClass"}) | |
| require.Nil(t, err) | |
| _, err = mgr.addClass(ctx, &models.Class{Class: "SomeOtherClass"}) | |
| require.Nil(t, err) | |
| _, err = mgr.addClass(ctx, &models.Class{Class: "YetAnotherClass"}) | |
| require.Nil(t, err) | |
| testCases := []testCase{} | |
| for i, dataType := range [][]string{ | |
| {"SomeClass"}, | |
| {"SomeOtherClass", "YetAnotherClass"}, | |
| } { | |
| testCases = append(testCases, testCase{ | |
| propName: fmt.Sprintf("RefProp_%d_empty", i), | |
| dataType: dataType, | |
| tokenization: "", | |
| expectedErrMsg: "", | |
| }) | |
| for _, tokenization := range append(helpers.Tokenizations, "non_existing") { | |
| testCases = append(testCases, testCase{ | |
| propName: fmt.Sprintf("RefProp_%d_%s", i, tokenization), | |
| dataType: dataType, | |
| tokenization: tokenization, | |
| expectedErrMsg: "Tokenization is not allowed for reference data type", | |
| }) | |
| } | |
| } | |
| runTestCases(t, testCases, mgr) | |
| }) | |
| t.Run("[deprecated string] string/stringArray and all tokenizations", func(t *testing.T) { | |
| testCases := []testCase{} | |
| for _, dataType := range []schema.DataType{ | |
| schema.DataTypeString, schema.DataTypeStringArray, | |
| } { | |
| for _, tokenization := range []string{ | |
| models.PropertyTokenizationWord, models.PropertyTokenizationField, "", | |
| } { | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType.PropString(), | |
| tokenization: tokenization, | |
| expectedErrMsg: "", | |
| }) | |
| } | |
| for _, tokenization := range append(helpers.Tokenizations, "non_existing") { | |
| switch tokenization { | |
| case models.PropertyTokenizationWord, models.PropertyTokenizationField: | |
| continue | |
| default: | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType.PropString(), | |
| tokenization: tokenization, | |
| expectedErrMsg: fmt.Sprintf("Tokenization '%s' is not allowed for data type '%s'", tokenization, dataType), | |
| }) | |
| } | |
| } | |
| } | |
| runTestCases(t, testCases, newSchemaManager()) | |
| }) | |
| }) | |
| t.Run("with default vector distance metric", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| expected := fakeVectorConfig{raw: map[string]interface{}{"distance": "cosine"}} | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{Class: "NewClass"}) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| require.Equal(t, expected, mgr.schemaCache.ObjectSchema.Classes[0].VectorIndexConfig) | |
| }) | |
| t.Run("with default vector distance metric when class already has VectorIndexConfig", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| expected := fakeVectorConfig{raw: map[string]interface{}{ | |
| "distance": "cosine", | |
| "otherVectorIndexConfig": "1234", | |
| }} | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{ | |
| Class: "NewClass", | |
| VectorIndexConfig: map[string]interface{}{ | |
| "otherVectorIndexConfig": "1234", | |
| }, | |
| }) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| require.Equal(t, expected, mgr.schemaCache.ObjectSchema.Classes[0].VectorIndexConfig) | |
| }) | |
| t.Run("with customized distance metric", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| expected := fakeVectorConfig{ | |
| raw: map[string]interface{}{"distance": "l2-squared"}, | |
| } | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{ | |
| Class: "NewClass", | |
| VectorIndexConfig: map[string]interface{}{ | |
| "distance": "l2-squared", | |
| }, | |
| }) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| require.Equal(t, expected, mgr.schemaCache.ObjectSchema.Classes[0].VectorIndexConfig) | |
| }) | |
| t.Run("with two identical prop names", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{ | |
| Class: "NewClass", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "my_prop", | |
| DataType: []string{"text"}, | |
| }, | |
| { | |
| Name: "my_prop", | |
| DataType: []string{"int"}, | |
| }, | |
| }, | |
| }) | |
| require.NotNil(t, err) | |
| assert.Contains(t, err.Error(), "conflict for property") | |
| }) | |
| t.Run("trying to add an identical prop later", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{ | |
| Class: "NewClass", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "my_prop", | |
| DataType: []string{"text"}, | |
| }, | |
| { | |
| Name: "otherProp", | |
| DataType: []string{"text"}, | |
| }, | |
| }, | |
| }) | |
| require.Nil(t, err) | |
| attempts := []string{ | |
| "my_prop", // lowercase, same casing | |
| "my_Prop", // lowercase, different casing | |
| "otherProp", // mixed case, same casing | |
| "otherprop", // mixed case, all lower | |
| "OtHerProP", // mixed case, other casing | |
| } | |
| for _, propName := range attempts { | |
| t.Run(propName, func(t *testing.T) { | |
| err = mgr.AddClassProperty(context.Background(), nil, "NewClass", | |
| &models.Property{ | |
| Name: propName, | |
| DataType: []string{"int"}, | |
| }) | |
| require.NotNil(t, err) | |
| assert.Contains(t, err.Error(), "conflict for property") | |
| }) | |
| } | |
| }) | |
| // To prevent a regression on | |
| // https://github.com/weaviate/weaviate/issues/2530 | |
| t.Run("with two props that are identical when ignoring casing", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, &models.Class{ | |
| Class: "NewClass", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "my_prop", | |
| DataType: []string{"text"}, | |
| }, | |
| { | |
| Name: "mY_PrOP", | |
| DataType: []string{"int"}, | |
| }, | |
| }, | |
| }) | |
| require.NotNil(t, err) | |
| assert.Contains(t, err.Error(), "conflict for property") | |
| }) | |
| t.Run("with multi tenancy enabled", func(t *testing.T) { | |
| t.Run("valid multiTenancyConfig", func(t *testing.T) { | |
| class := &models.Class{ | |
| Class: "NewClass", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "textProp", | |
| DataType: []string{"text"}, | |
| }, | |
| }, | |
| MultiTenancyConfig: &models.MultiTenancyConfig{ | |
| Enabled: true, | |
| }, | |
| } | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), nil, class) | |
| require.Nil(t, err) | |
| require.NotNil(t, class.ShardingConfig) | |
| require.Zero(t, class.ShardingConfig.(sharding.Config).DesiredCount) | |
| }) | |
| t.Run("multiTenancyConfig and shardingConfig both provided", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, | |
| &models.Class{ | |
| Class: "NewClass", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "uuidProp", | |
| DataType: []string{"uuid"}, | |
| }, | |
| }, | |
| MultiTenancyConfig: &models.MultiTenancyConfig{ | |
| Enabled: true, | |
| }, | |
| ShardingConfig: map[string]interface{}{ | |
| "desiredCount": 2, | |
| }, | |
| }, | |
| ) | |
| require.NotNil(t, err) | |
| require.Equal(t, "cannot have both shardingConfig and multiTenancyConfig", err.Error()) | |
| }) | |
| t.Run("multiTenancyConfig and shardingConfig both provided but multi tenancy config is set to false", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, | |
| &models.Class{ | |
| Class: "NewClass1", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "uuidProp", | |
| DataType: []string{"uuid"}, | |
| }, | |
| }, | |
| MultiTenancyConfig: &models.MultiTenancyConfig{ | |
| Enabled: false, | |
| }, | |
| ShardingConfig: map[string]interface{}{ | |
| "desiredCount": 2, | |
| }, | |
| }, | |
| ) | |
| require.Nil(t, err) | |
| }) | |
| t.Run("multiTenancyConfig and shardingConfig both provided but multi tenancy config is empty", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, | |
| &models.Class{ | |
| Class: "NewClass", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "uuidProp", | |
| DataType: []string{"uuid"}, | |
| }, | |
| }, | |
| MultiTenancyConfig: &models.MultiTenancyConfig{}, | |
| ShardingConfig: map[string]interface{}{ | |
| "desiredCount": 2, | |
| }, | |
| }, | |
| ) | |
| require.Nil(t, err) | |
| }) | |
| t.Run("multiTenancyConfig and shardingConfig both provided but multi tenancy is nil", func(t *testing.T) { | |
| mgr := newSchemaManager() | |
| err := mgr.AddClass(context.Background(), | |
| nil, | |
| &models.Class{ | |
| Class: "NewClass", | |
| Properties: []*models.Property{ | |
| { | |
| Name: "uuidProp", | |
| DataType: []string{"uuid"}, | |
| }, | |
| }, | |
| MultiTenancyConfig: nil, | |
| ShardingConfig: map[string]interface{}{ | |
| "desiredCount": 2, | |
| }, | |
| }, | |
| ) | |
| require.Nil(t, err) | |
| }) | |
| }) | |
| } | |
| func TestAddClass_DefaultsAndMigration(t *testing.T) { | |
| t.Run("set defaults and migrate string|stringArray datatype and tokenization", func(t *testing.T) { | |
| type testCase struct { | |
| propName string | |
| dataType schema.DataType | |
| tokenization string | |
| expectedDataType schema.DataType | |
| expectedTokenization string | |
| } | |
| propName := func(dataType schema.DataType, tokenization string) string { | |
| return strings.ReplaceAll(fmt.Sprintf("%s_%s", dataType, tokenization), "[]", "Array") | |
| } | |
| mgr := newSchemaManager() | |
| ctx := context.Background() | |
| className := "MigrationClass" | |
| testCases := []testCase{} | |
| for _, dataType := range []schema.DataType{ | |
| schema.DataTypeText, schema.DataTypeTextArray, | |
| } { | |
| for _, tokenization := range helpers.Tokenizations { | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType, | |
| tokenization: tokenization, | |
| expectedDataType: dataType, | |
| expectedTokenization: tokenization, | |
| }) | |
| } | |
| tokenization := "" | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType, | |
| tokenization: tokenization, | |
| expectedDataType: dataType, | |
| expectedTokenization: models.PropertyTokenizationWord, | |
| }) | |
| } | |
| for _, dataType := range []schema.DataType{ | |
| schema.DataTypeString, schema.DataTypeStringArray, | |
| } { | |
| for _, tokenization := range []string{ | |
| models.PropertyTokenizationWord, models.PropertyTokenizationField, "", | |
| } { | |
| var expectedDataType schema.DataType | |
| switch dataType { | |
| case schema.DataTypeStringArray: | |
| expectedDataType = schema.DataTypeTextArray | |
| default: | |
| expectedDataType = schema.DataTypeText | |
| } | |
| var expectedTokenization string | |
| switch tokenization { | |
| case models.PropertyTokenizationField: | |
| expectedTokenization = models.PropertyTokenizationField | |
| default: | |
| expectedTokenization = models.PropertyTokenizationWhitespace | |
| } | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, tokenization), | |
| dataType: dataType, | |
| tokenization: tokenization, | |
| expectedDataType: expectedDataType, | |
| expectedTokenization: expectedTokenization, | |
| }) | |
| } | |
| } | |
| t.Run("create class with all properties", func(t *testing.T) { | |
| properties := []*models.Property{} | |
| for _, tc := range testCases { | |
| properties = append(properties, &models.Property{ | |
| Name: "created_" + tc.propName, | |
| DataType: tc.dataType.PropString(), | |
| Tokenization: tc.tokenization, | |
| }) | |
| } | |
| err := mgr.AddClass(ctx, nil, &models.Class{ | |
| Class: className, | |
| Properties: properties, | |
| }) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, className, mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| }) | |
| t.Run("add properties to existing class", func(t *testing.T) { | |
| for _, tc := range testCases { | |
| t.Run("added_"+tc.propName, func(t *testing.T) { | |
| err := mgr.addClassProperty(ctx, className, &models.Property{ | |
| Name: "added_" + tc.propName, | |
| DataType: tc.dataType.PropString(), | |
| Tokenization: tc.tokenization, | |
| }) | |
| require.Nil(t, err) | |
| }) | |
| } | |
| }) | |
| t.Run("verify defaults and migration", func(t *testing.T) { | |
| class := mgr.schemaCache.ObjectSchema.Classes[0] | |
| for _, tc := range testCases { | |
| t.Run("created_"+tc.propName, func(t *testing.T) { | |
| createdProperty, err := schema.GetPropertyByName(class, "created_"+tc.propName) | |
| require.Nil(t, err) | |
| assert.Equal(t, tc.expectedDataType.PropString(), createdProperty.DataType) | |
| assert.Equal(t, tc.expectedTokenization, createdProperty.Tokenization) | |
| }) | |
| t.Run("added_"+tc.propName, func(t *testing.T) { | |
| addedProperty, err := schema.GetPropertyByName(class, "added_"+tc.propName) | |
| require.Nil(t, err) | |
| assert.Equal(t, tc.expectedDataType.PropString(), addedProperty.DataType) | |
| assert.Equal(t, tc.expectedTokenization, addedProperty.Tokenization) | |
| }) | |
| } | |
| }) | |
| }) | |
| t.Run("set defaults and migrate IndexInverted to IndexFilterable + IndexSearchable", func(t *testing.T) { | |
| vFalse := false | |
| vTrue := true | |
| allBoolPtrs := []*bool{nil, &vFalse, &vTrue} | |
| type testCase struct { | |
| propName string | |
| dataType schema.DataType | |
| indexInverted *bool | |
| indexFilterable *bool | |
| indexSearchable *bool | |
| expectedInverted *bool | |
| expectedFilterable *bool | |
| expectedSearchable *bool | |
| } | |
| boolPtrToStr := func(ptr *bool) string { | |
| if ptr == nil { | |
| return "nil" | |
| } | |
| return fmt.Sprintf("%v", *ptr) | |
| } | |
| propName := func(dt schema.DataType, inverted, filterable, searchable *bool) string { | |
| return fmt.Sprintf("%s_inverted_%s_filterable_%s_searchable_%s", | |
| dt.String(), boolPtrToStr(inverted), boolPtrToStr(filterable), boolPtrToStr(searchable)) | |
| } | |
| mgr := newSchemaManager() | |
| ctx := context.Background() | |
| className := "MigrationClass" | |
| testCases := []testCase{} | |
| for _, dataType := range []schema.DataType{schema.DataTypeText, schema.DataTypeInt} { | |
| for _, inverted := range allBoolPtrs { | |
| for _, filterable := range allBoolPtrs { | |
| for _, searchable := range allBoolPtrs { | |
| if inverted != nil { | |
| if filterable != nil || searchable != nil { | |
| // invalid combination, indexInverted can not be set | |
| // together with indexFilterable or indexSearchable | |
| continue | |
| } | |
| } | |
| if searchable != nil && *searchable { | |
| if dataType != schema.DataTypeText { | |
| // invalid combination, indexSearchable can not be enabled | |
| // for non text/text[] data type | |
| continue | |
| } | |
| } | |
| switch dataType { | |
| case schema.DataTypeText: | |
| if inverted != nil { | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, inverted, filterable, searchable), | |
| dataType: dataType, | |
| indexInverted: inverted, | |
| indexFilterable: filterable, | |
| indexSearchable: searchable, | |
| expectedInverted: nil, | |
| expectedFilterable: inverted, | |
| expectedSearchable: inverted, | |
| }) | |
| } else { | |
| expectedFilterable := filterable | |
| if filterable == nil { | |
| expectedFilterable = &vTrue | |
| } | |
| expectedSearchable := searchable | |
| if searchable == nil { | |
| expectedSearchable = &vTrue | |
| } | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, inverted, filterable, searchable), | |
| dataType: dataType, | |
| indexInverted: inverted, | |
| indexFilterable: filterable, | |
| indexSearchable: searchable, | |
| expectedInverted: nil, | |
| expectedFilterable: expectedFilterable, | |
| expectedSearchable: expectedSearchable, | |
| }) | |
| } | |
| default: | |
| if inverted != nil { | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, inverted, filterable, searchable), | |
| dataType: dataType, | |
| indexInverted: inverted, | |
| indexFilterable: filterable, | |
| indexSearchable: searchable, | |
| expectedInverted: nil, | |
| expectedFilterable: inverted, | |
| expectedSearchable: &vFalse, | |
| }) | |
| } else { | |
| expectedFilterable := filterable | |
| if filterable == nil { | |
| expectedFilterable = &vTrue | |
| } | |
| expectedSearchable := searchable | |
| if searchable == nil { | |
| expectedSearchable = &vFalse | |
| } | |
| testCases = append(testCases, testCase{ | |
| propName: propName(dataType, inverted, filterable, searchable), | |
| dataType: dataType, | |
| indexInverted: inverted, | |
| indexFilterable: filterable, | |
| indexSearchable: searchable, | |
| expectedInverted: nil, | |
| expectedFilterable: expectedFilterable, | |
| expectedSearchable: expectedSearchable, | |
| }) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| t.Run("create class with all properties", func(t *testing.T) { | |
| properties := []*models.Property{} | |
| for _, tc := range testCases { | |
| properties = append(properties, &models.Property{ | |
| Name: "created_" + tc.propName, | |
| DataType: tc.dataType.PropString(), | |
| IndexInverted: tc.indexInverted, | |
| IndexFilterable: tc.indexFilterable, | |
| IndexSearchable: tc.indexSearchable, | |
| }) | |
| } | |
| err := mgr.AddClass(ctx, nil, &models.Class{ | |
| Class: className, | |
| Properties: properties, | |
| }) | |
| require.Nil(t, err) | |
| require.NotNil(t, mgr.schemaCache.ObjectSchema) | |
| require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes) | |
| require.Equal(t, className, mgr.schemaCache.ObjectSchema.Classes[0].Class) | |
| }) | |
| t.Run("add properties to existing class", func(t *testing.T) { | |
| for _, tc := range testCases { | |
| t.Run("added_"+tc.propName, func(t *testing.T) { | |
| err := mgr.addClassProperty(ctx, className, &models.Property{ | |
| Name: "added_" + tc.propName, | |
| DataType: tc.dataType.PropString(), | |
| IndexInverted: tc.indexInverted, | |
| IndexFilterable: tc.indexFilterable, | |
| IndexSearchable: tc.indexSearchable, | |
| }) | |
| require.Nil(t, err) | |
| }) | |
| } | |
| }) | |
| t.Run("verify migration", func(t *testing.T) { | |
| class := mgr.schemaCache.ObjectSchema.Classes[0] | |
| for _, tc := range testCases { | |
| t.Run("created_"+tc.propName, func(t *testing.T) { | |
| createdProperty, err := schema.GetPropertyByName(class, "created_"+tc.propName) | |
| require.Nil(t, err) | |
| assert.Equal(t, tc.expectedInverted, createdProperty.IndexInverted) | |
| assert.Equal(t, tc.expectedFilterable, createdProperty.IndexFilterable) | |
| assert.Equal(t, tc.expectedSearchable, createdProperty.IndexSearchable) | |
| }) | |
| t.Run("added_"+tc.propName, func(t *testing.T) { | |
| addedProperty, err := schema.GetPropertyByName(class, "added_"+tc.propName) | |
| require.Nil(t, err) | |
| assert.Equal(t, tc.expectedInverted, addedProperty.IndexInverted) | |
| assert.Equal(t, tc.expectedFilterable, addedProperty.IndexFilterable) | |
| assert.Equal(t, tc.expectedSearchable, addedProperty.IndexSearchable) | |
| }) | |
| } | |
| }) | |
| }) | |
| } | |
| func Test_Defaults_NestedProperties(t *testing.T) { | |
| for _, pdt := range schema.PrimitiveDataTypes { | |
| t.Run(pdt.String(), func(t *testing.T) { | |
| nestedProperties := []*models.NestedProperty{ | |
| { | |
| Name: "nested_" + pdt.String(), | |
| DataType: pdt.PropString(), | |
| }, | |
| } | |
| for _, ndt := range schema.NestedDataTypes { | |
| t.Run(ndt.String(), func(t *testing.T) { | |
| propPrimitives := &models.Property{ | |
| Name: "objectProp", | |
| DataType: ndt.PropString(), | |
| NestedProperties: nestedProperties, | |
| } | |
| propLvl2Primitives := &models.Property{ | |
| Name: "objectPropLvl2", | |
| DataType: ndt.PropString(), | |
| NestedProperties: []*models.NestedProperty{ | |
| { | |
| Name: "nested_object", | |
| DataType: ndt.PropString(), | |
| NestedProperties: nestedProperties, | |
| }, | |
| }, | |
| } | |
| setPropertyDefaults(propPrimitives) | |
| setPropertyDefaults(propLvl2Primitives) | |
| t.Run("primitive data types", func(t *testing.T) { | |
| for _, np := range []*models.NestedProperty{ | |
| propPrimitives.NestedProperties[0], | |
| propLvl2Primitives.NestedProperties[0].NestedProperties[0], | |
| } { | |
| switch pdt { | |
| case schema.DataTypeText, schema.DataTypeTextArray: | |
| require.NotNil(t, np.IndexFilterable) | |
| assert.True(t, *np.IndexFilterable) | |
| require.NotNil(t, np.IndexSearchable) | |
| assert.True(t, *np.IndexSearchable) | |
| assert.Equal(t, models.PropertyTokenizationWord, np.Tokenization) | |
| case schema.DataTypeBlob: | |
| require.NotNil(t, np.IndexFilterable) | |
| assert.False(t, *np.IndexFilterable) | |
| require.NotNil(t, np.IndexSearchable) | |
| assert.False(t, *np.IndexSearchable) | |
| assert.Equal(t, "", np.Tokenization) | |
| default: | |
| require.NotNil(t, np.IndexFilterable) | |
| assert.True(t, *np.IndexFilterable) | |
| require.NotNil(t, np.IndexSearchable) | |
| assert.False(t, *np.IndexSearchable) | |
| assert.Equal(t, "", np.Tokenization) | |
| } | |
| } | |
| }) | |
| t.Run("nested data types", func(t *testing.T) { | |
| for _, indexFilterable := range []*bool{ | |
| propPrimitives.IndexFilterable, | |
| propLvl2Primitives.IndexFilterable, | |
| propLvl2Primitives.NestedProperties[0].IndexFilterable, | |
| } { | |
| require.NotNil(t, indexFilterable) | |
| assert.True(t, *indexFilterable) | |
| } | |
| for _, indexSearchable := range []*bool{ | |
| propPrimitives.IndexSearchable, | |
| propLvl2Primitives.IndexSearchable, | |
| propLvl2Primitives.NestedProperties[0].IndexSearchable, | |
| } { | |
| require.NotNil(t, indexSearchable) | |
| assert.False(t, *indexSearchable) | |
| } | |
| for _, tokenization := range []string{ | |
| propPrimitives.Tokenization, | |
| propLvl2Primitives.Tokenization, | |
| propLvl2Primitives.NestedProperties[0].Tokenization, | |
| } { | |
| assert.Equal(t, "", tokenization) | |
| } | |
| }) | |
| }) | |
| } | |
| }) | |
| } | |
| } | |