github.com/weaviate/weaviate@v1.24.6/usecases/schema/add_test.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package schema
    13  
    14  import (
    15  	"context"
    16  	"fmt"
    17  	"strings"
    18  	"testing"
    19  
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  	"github.com/weaviate/weaviate/adapters/repos/db/helpers"
    23  	"github.com/weaviate/weaviate/adapters/repos/db/inverted/stopwords"
    24  	"github.com/weaviate/weaviate/entities/models"
    25  	"github.com/weaviate/weaviate/entities/schema"
    26  	"github.com/weaviate/weaviate/usecases/config"
    27  	"github.com/weaviate/weaviate/usecases/sharding"
    28  )
    29  
    30  func TestAddClass(t *testing.T) {
    31  	t.Run("with empty class name", func(t *testing.T) {
    32  		err := newSchemaManager().AddClass(context.Background(),
    33  			nil, &models.Class{})
    34  		require.EqualError(t, err, "'' is not a valid class name")
    35  	})
    36  
    37  	t.Run("with permuted-casing class names", func(t *testing.T) {
    38  		mgr := newSchemaManager()
    39  		err := mgr.AddClass(context.Background(),
    40  			nil, &models.Class{Class: "NewClass"})
    41  		require.Nil(t, err)
    42  		err = mgr.AddClass(context.Background(),
    43  			nil, &models.Class{Class: "NewCLASS"})
    44  		require.NotNil(t, err)
    45  		require.Equal(t,
    46  			"class name \"NewCLASS\" already exists as a permutation of: \"NewClass\". "+
    47  				"class names must be unique when lowercased", err.Error())
    48  	})
    49  
    50  	t.Run("with default BM25 params", func(t *testing.T) {
    51  		mgr := newSchemaManager()
    52  
    53  		expectedBM25Config := &models.BM25Config{
    54  			K1: config.DefaultBM25k1,
    55  			B:  config.DefaultBM25b,
    56  		}
    57  
    58  		err := mgr.AddClass(context.Background(),
    59  			nil, &models.Class{Class: "NewClass"})
    60  		require.Nil(t, err)
    61  
    62  		require.NotNil(t, mgr.schemaCache.ObjectSchema)
    63  		require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
    64  		require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class)
    65  		require.Equal(t, expectedBM25Config, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Bm25)
    66  	})
    67  
    68  	t.Run("with customized BM25 params", func(t *testing.T) {
    69  		mgr := newSchemaManager()
    70  
    71  		expectedBM25Config := &models.BM25Config{
    72  			K1: 1.88,
    73  			B:  0.44,
    74  		}
    75  
    76  		err := mgr.AddClass(context.Background(),
    77  			nil, &models.Class{
    78  				Class: "NewClass",
    79  				InvertedIndexConfig: &models.InvertedIndexConfig{
    80  					Bm25: expectedBM25Config,
    81  				},
    82  			})
    83  		require.Nil(t, err)
    84  
    85  		require.NotNil(t, mgr.schemaCache.ObjectSchema)
    86  		require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
    87  		require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class)
    88  		require.Equal(t, expectedBM25Config, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Bm25)
    89  	})
    90  
    91  	t.Run("with default Stopwords config", func(t *testing.T) {
    92  		mgr := newSchemaManager()
    93  
    94  		expectedStopwordConfig := &models.StopwordConfig{
    95  			Preset: stopwords.EnglishPreset,
    96  		}
    97  
    98  		err := mgr.AddClass(context.Background(),
    99  			nil, &models.Class{Class: "NewClass"})
   100  		require.Nil(t, err)
   101  
   102  		require.NotNil(t, mgr.schemaCache.ObjectSchema)
   103  		require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   104  		require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class)
   105  		require.Equal(t, expectedStopwordConfig, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Stopwords)
   106  	})
   107  
   108  	t.Run("with customized Stopwords config", func(t *testing.T) {
   109  		mgr := newSchemaManager()
   110  
   111  		expectedStopwordConfig := &models.StopwordConfig{
   112  			Preset:    "none",
   113  			Additions: []string{"monkey", "zebra", "octopus"},
   114  			Removals:  []string{"are"},
   115  		}
   116  
   117  		err := mgr.AddClass(context.Background(),
   118  			nil, &models.Class{
   119  				Class: "NewClass",
   120  				InvertedIndexConfig: &models.InvertedIndexConfig{
   121  					Stopwords: expectedStopwordConfig,
   122  				},
   123  			})
   124  		require.Nil(t, err)
   125  
   126  		require.NotNil(t, mgr.schemaCache.ObjectSchema)
   127  		require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   128  		require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class)
   129  		require.Equal(t, expectedStopwordConfig, mgr.schemaCache.ObjectSchema.Classes[0].InvertedIndexConfig.Stopwords)
   130  	})
   131  
   132  	t.Run("with tokenizations", func(t *testing.T) {
   133  		type testCase struct {
   134  			propName       string
   135  			dataType       []string
   136  			tokenization   string
   137  			expectedErrMsg string
   138  		}
   139  
   140  		propName := func(dataType schema.DataType, tokenization string) string {
   141  			dtStr := strings.ReplaceAll(string(dataType), "[]", "Array")
   142  			tStr := "empty"
   143  			if tokenization != "" {
   144  				tStr = tokenization
   145  			}
   146  			return fmt.Sprintf("%s_%s", dtStr, tStr)
   147  		}
   148  
   149  		runTestCases := func(t *testing.T, testCases []testCase, mgr *Manager) {
   150  			for i, tc := range testCases {
   151  				t.Run(tc.propName, func(t *testing.T) {
   152  					err := mgr.AddClass(context.Background(), nil, &models.Class{
   153  						Class: fmt.Sprintf("NewClass_%d", i),
   154  						Properties: []*models.Property{
   155  							{
   156  								Name:         tc.propName,
   157  								DataType:     tc.dataType,
   158  								Tokenization: tc.tokenization,
   159  							},
   160  						},
   161  					})
   162  
   163  					if tc.expectedErrMsg == "" {
   164  						require.Nil(t, err)
   165  						require.NotNil(t, mgr.schemaCache.ObjectSchema)
   166  						require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   167  					} else {
   168  						require.EqualError(t, err, tc.expectedErrMsg)
   169  					}
   170  				})
   171  			}
   172  		}
   173  
   174  		t.Run("text/textArray and all tokenizations", func(t *testing.T) {
   175  			testCases := []testCase{}
   176  			for _, dataType := range []schema.DataType{
   177  				schema.DataTypeText, schema.DataTypeTextArray,
   178  			} {
   179  				for _, tokenization := range append(helpers.Tokenizations, "") {
   180  					testCases = append(testCases, testCase{
   181  						propName:       propName(dataType, tokenization),
   182  						dataType:       dataType.PropString(),
   183  						tokenization:   tokenization,
   184  						expectedErrMsg: "",
   185  					})
   186  				}
   187  
   188  				tokenization := "non_existing"
   189  				testCases = append(testCases, testCase{
   190  					propName:       propName(dataType, tokenization),
   191  					dataType:       dataType.PropString(),
   192  					tokenization:   tokenization,
   193  					expectedErrMsg: fmt.Sprintf("Tokenization '%s' is not allowed for data type '%s'", tokenization, dataType),
   194  				})
   195  			}
   196  
   197  			runTestCases(t, testCases, newSchemaManager())
   198  		})
   199  
   200  		t.Run("non text/textArray and all tokenizations", func(t *testing.T) {
   201  			testCases := []testCase{}
   202  			for _, dataType := range schema.PrimitiveDataTypes {
   203  				switch dataType {
   204  				case schema.DataTypeText, schema.DataTypeTextArray:
   205  					continue
   206  				default:
   207  					tokenization := ""
   208  					testCases = append(testCases, testCase{
   209  						propName:       propName(dataType, tokenization),
   210  						dataType:       dataType.PropString(),
   211  						tokenization:   tokenization,
   212  						expectedErrMsg: "",
   213  					})
   214  
   215  					for _, tokenization := range append(helpers.Tokenizations, "non_existing") {
   216  						testCases = append(testCases, testCase{
   217  							propName:       propName(dataType, tokenization),
   218  							dataType:       dataType.PropString(),
   219  							tokenization:   tokenization,
   220  							expectedErrMsg: fmt.Sprintf("Tokenization is not allowed for data type '%s'", dataType),
   221  						})
   222  					}
   223  				}
   224  			}
   225  
   226  			runTestCases(t, testCases, newSchemaManager())
   227  		})
   228  
   229  		t.Run("non text/textArray and all tokenizations", func(t *testing.T) {
   230  			ctx := context.Background()
   231  			mgr := newSchemaManager()
   232  
   233  			_, err := mgr.addClass(ctx, &models.Class{Class: "SomeClass"})
   234  			require.Nil(t, err)
   235  			_, err = mgr.addClass(ctx, &models.Class{Class: "SomeOtherClass"})
   236  			require.Nil(t, err)
   237  			_, err = mgr.addClass(ctx, &models.Class{Class: "YetAnotherClass"})
   238  			require.Nil(t, err)
   239  
   240  			testCases := []testCase{}
   241  			for i, dataType := range [][]string{
   242  				{"SomeClass"},
   243  				{"SomeOtherClass", "YetAnotherClass"},
   244  			} {
   245  				testCases = append(testCases, testCase{
   246  					propName:       fmt.Sprintf("RefProp_%d_empty", i),
   247  					dataType:       dataType,
   248  					tokenization:   "",
   249  					expectedErrMsg: "",
   250  				})
   251  
   252  				for _, tokenization := range append(helpers.Tokenizations, "non_existing") {
   253  					testCases = append(testCases, testCase{
   254  						propName:       fmt.Sprintf("RefProp_%d_%s", i, tokenization),
   255  						dataType:       dataType,
   256  						tokenization:   tokenization,
   257  						expectedErrMsg: "Tokenization is not allowed for reference data type",
   258  					})
   259  				}
   260  			}
   261  
   262  			runTestCases(t, testCases, mgr)
   263  		})
   264  
   265  		t.Run("[deprecated string] string/stringArray and all tokenizations", func(t *testing.T) {
   266  			testCases := []testCase{}
   267  			for _, dataType := range []schema.DataType{
   268  				schema.DataTypeString, schema.DataTypeStringArray,
   269  			} {
   270  				for _, tokenization := range []string{
   271  					models.PropertyTokenizationWord, models.PropertyTokenizationField, "",
   272  				} {
   273  					testCases = append(testCases, testCase{
   274  						propName:       propName(dataType, tokenization),
   275  						dataType:       dataType.PropString(),
   276  						tokenization:   tokenization,
   277  						expectedErrMsg: "",
   278  					})
   279  				}
   280  
   281  				for _, tokenization := range append(helpers.Tokenizations, "non_existing") {
   282  					switch tokenization {
   283  					case models.PropertyTokenizationWord, models.PropertyTokenizationField:
   284  						continue
   285  					default:
   286  						testCases = append(testCases, testCase{
   287  							propName:       propName(dataType, tokenization),
   288  							dataType:       dataType.PropString(),
   289  							tokenization:   tokenization,
   290  							expectedErrMsg: fmt.Sprintf("Tokenization '%s' is not allowed for data type '%s'", tokenization, dataType),
   291  						})
   292  					}
   293  				}
   294  			}
   295  
   296  			runTestCases(t, testCases, newSchemaManager())
   297  		})
   298  	})
   299  
   300  	t.Run("with default vector distance metric", func(t *testing.T) {
   301  		mgr := newSchemaManager()
   302  
   303  		expected := fakeVectorConfig{raw: map[string]interface{}{"distance": "cosine"}}
   304  
   305  		err := mgr.AddClass(context.Background(),
   306  			nil, &models.Class{Class: "NewClass"})
   307  		require.Nil(t, err)
   308  
   309  		require.NotNil(t, mgr.schemaCache.ObjectSchema)
   310  		require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   311  		require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class)
   312  		require.Equal(t, expected, mgr.schemaCache.ObjectSchema.Classes[0].VectorIndexConfig)
   313  	})
   314  
   315  	t.Run("with default vector distance metric when class already has VectorIndexConfig", func(t *testing.T) {
   316  		mgr := newSchemaManager()
   317  
   318  		expected := fakeVectorConfig{raw: map[string]interface{}{
   319  			"distance":               "cosine",
   320  			"otherVectorIndexConfig": "1234",
   321  		}}
   322  
   323  		err := mgr.AddClass(context.Background(),
   324  			nil, &models.Class{
   325  				Class: "NewClass",
   326  				VectorIndexConfig: map[string]interface{}{
   327  					"otherVectorIndexConfig": "1234",
   328  				},
   329  			})
   330  		require.Nil(t, err)
   331  
   332  		require.NotNil(t, mgr.schemaCache.ObjectSchema)
   333  		require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   334  		require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class)
   335  		require.Equal(t, expected, mgr.schemaCache.ObjectSchema.Classes[0].VectorIndexConfig)
   336  	})
   337  
   338  	t.Run("with customized distance metric", func(t *testing.T) {
   339  		mgr := newSchemaManager()
   340  
   341  		expected := fakeVectorConfig{
   342  			raw: map[string]interface{}{"distance": "l2-squared"},
   343  		}
   344  
   345  		err := mgr.AddClass(context.Background(),
   346  			nil, &models.Class{
   347  				Class: "NewClass",
   348  				VectorIndexConfig: map[string]interface{}{
   349  					"distance": "l2-squared",
   350  				},
   351  			})
   352  		require.Nil(t, err)
   353  
   354  		require.NotNil(t, mgr.schemaCache.ObjectSchema)
   355  		require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   356  		require.Equal(t, "NewClass", mgr.schemaCache.ObjectSchema.Classes[0].Class)
   357  		require.Equal(t, expected, mgr.schemaCache.ObjectSchema.Classes[0].VectorIndexConfig)
   358  	})
   359  
   360  	t.Run("with two identical prop names", func(t *testing.T) {
   361  		mgr := newSchemaManager()
   362  
   363  		err := mgr.AddClass(context.Background(),
   364  			nil, &models.Class{
   365  				Class: "NewClass",
   366  				Properties: []*models.Property{
   367  					{
   368  						Name:     "my_prop",
   369  						DataType: []string{"text"},
   370  					},
   371  					{
   372  						Name:     "my_prop",
   373  						DataType: []string{"int"},
   374  					},
   375  				},
   376  			})
   377  		require.NotNil(t, err)
   378  		assert.Contains(t, err.Error(), "conflict for property")
   379  	})
   380  
   381  	t.Run("trying to add an identical prop later", func(t *testing.T) {
   382  		mgr := newSchemaManager()
   383  
   384  		err := mgr.AddClass(context.Background(),
   385  			nil, &models.Class{
   386  				Class: "NewClass",
   387  				Properties: []*models.Property{
   388  					{
   389  						Name:     "my_prop",
   390  						DataType: []string{"text"},
   391  					},
   392  					{
   393  						Name:     "otherProp",
   394  						DataType: []string{"text"},
   395  					},
   396  				},
   397  			})
   398  		require.Nil(t, err)
   399  
   400  		attempts := []string{
   401  			"my_prop",   // lowercase, same casing
   402  			"my_Prop",   // lowercase, different casing
   403  			"otherProp", // mixed case, same casing
   404  			"otherprop", // mixed case, all lower
   405  			"OtHerProP", // mixed case, other casing
   406  		}
   407  
   408  		for _, propName := range attempts {
   409  			t.Run(propName, func(t *testing.T) {
   410  				err = mgr.AddClassProperty(context.Background(), nil, "NewClass",
   411  					&models.Property{
   412  						Name:     propName,
   413  						DataType: []string{"int"},
   414  					})
   415  				require.NotNil(t, err)
   416  				assert.Contains(t, err.Error(), "conflict for property")
   417  			})
   418  		}
   419  	})
   420  
   421  	// To prevent a regression on
   422  	// https://github.com/weaviate/weaviate/issues/2530
   423  	t.Run("with two props that are identical when ignoring casing", func(t *testing.T) {
   424  		mgr := newSchemaManager()
   425  
   426  		err := mgr.AddClass(context.Background(),
   427  			nil, &models.Class{
   428  				Class: "NewClass",
   429  				Properties: []*models.Property{
   430  					{
   431  						Name:     "my_prop",
   432  						DataType: []string{"text"},
   433  					},
   434  					{
   435  						Name:     "mY_PrOP",
   436  						DataType: []string{"int"},
   437  					},
   438  				},
   439  			})
   440  		require.NotNil(t, err)
   441  		assert.Contains(t, err.Error(), "conflict for property")
   442  	})
   443  
   444  	t.Run("with multi tenancy enabled", func(t *testing.T) {
   445  		t.Run("valid multiTenancyConfig", func(t *testing.T) {
   446  			class := &models.Class{
   447  				Class: "NewClass",
   448  				Properties: []*models.Property{
   449  					{
   450  						Name:     "textProp",
   451  						DataType: []string{"text"},
   452  					},
   453  				},
   454  				MultiTenancyConfig: &models.MultiTenancyConfig{
   455  					Enabled: true,
   456  				},
   457  			}
   458  			mgr := newSchemaManager()
   459  			err := mgr.AddClass(context.Background(), nil, class)
   460  			require.Nil(t, err)
   461  			require.NotNil(t, class.ShardingConfig)
   462  			require.Zero(t, class.ShardingConfig.(sharding.Config).DesiredCount)
   463  		})
   464  
   465  		t.Run("multiTenancyConfig and shardingConfig both provided", func(t *testing.T) {
   466  			mgr := newSchemaManager()
   467  			err := mgr.AddClass(context.Background(),
   468  				nil,
   469  				&models.Class{
   470  					Class: "NewClass",
   471  					Properties: []*models.Property{
   472  						{
   473  							Name:     "uuidProp",
   474  							DataType: []string{"uuid"},
   475  						},
   476  					},
   477  					MultiTenancyConfig: &models.MultiTenancyConfig{
   478  						Enabled: true,
   479  					},
   480  					ShardingConfig: map[string]interface{}{
   481  						"desiredCount": 2,
   482  					},
   483  				},
   484  			)
   485  			require.NotNil(t, err)
   486  			require.Equal(t, "cannot have both shardingConfig and multiTenancyConfig", err.Error())
   487  		})
   488  
   489  		t.Run("multiTenancyConfig and shardingConfig both provided but multi tenancy config is set to false", func(t *testing.T) {
   490  			mgr := newSchemaManager()
   491  			err := mgr.AddClass(context.Background(),
   492  				nil,
   493  				&models.Class{
   494  					Class: "NewClass1",
   495  					Properties: []*models.Property{
   496  						{
   497  							Name:     "uuidProp",
   498  							DataType: []string{"uuid"},
   499  						},
   500  					},
   501  					MultiTenancyConfig: &models.MultiTenancyConfig{
   502  						Enabled: false,
   503  					},
   504  					ShardingConfig: map[string]interface{}{
   505  						"desiredCount": 2,
   506  					},
   507  				},
   508  			)
   509  			require.Nil(t, err)
   510  		})
   511  
   512  		t.Run("multiTenancyConfig and shardingConfig both provided but multi tenancy config is empty", func(t *testing.T) {
   513  			mgr := newSchemaManager()
   514  			err := mgr.AddClass(context.Background(),
   515  				nil,
   516  				&models.Class{
   517  					Class: "NewClass",
   518  					Properties: []*models.Property{
   519  						{
   520  							Name:     "uuidProp",
   521  							DataType: []string{"uuid"},
   522  						},
   523  					},
   524  					MultiTenancyConfig: &models.MultiTenancyConfig{},
   525  					ShardingConfig: map[string]interface{}{
   526  						"desiredCount": 2,
   527  					},
   528  				},
   529  			)
   530  			require.Nil(t, err)
   531  		})
   532  
   533  		t.Run("multiTenancyConfig and shardingConfig both provided but multi tenancy is nil", func(t *testing.T) {
   534  			mgr := newSchemaManager()
   535  			err := mgr.AddClass(context.Background(),
   536  				nil,
   537  				&models.Class{
   538  					Class: "NewClass",
   539  					Properties: []*models.Property{
   540  						{
   541  							Name:     "uuidProp",
   542  							DataType: []string{"uuid"},
   543  						},
   544  					},
   545  					MultiTenancyConfig: nil,
   546  					ShardingConfig: map[string]interface{}{
   547  						"desiredCount": 2,
   548  					},
   549  				},
   550  			)
   551  			require.Nil(t, err)
   552  		})
   553  	})
   554  }
   555  
   556  func TestAddClass_DefaultsAndMigration(t *testing.T) {
   557  	t.Run("set defaults and migrate string|stringArray datatype and tokenization", func(t *testing.T) {
   558  		type testCase struct {
   559  			propName     string
   560  			dataType     schema.DataType
   561  			tokenization string
   562  
   563  			expectedDataType     schema.DataType
   564  			expectedTokenization string
   565  		}
   566  
   567  		propName := func(dataType schema.DataType, tokenization string) string {
   568  			return strings.ReplaceAll(fmt.Sprintf("%s_%s", dataType, tokenization), "[]", "Array")
   569  		}
   570  
   571  		mgr := newSchemaManager()
   572  		ctx := context.Background()
   573  		className := "MigrationClass"
   574  
   575  		testCases := []testCase{}
   576  		for _, dataType := range []schema.DataType{
   577  			schema.DataTypeText, schema.DataTypeTextArray,
   578  		} {
   579  			for _, tokenization := range helpers.Tokenizations {
   580  				testCases = append(testCases, testCase{
   581  					propName:             propName(dataType, tokenization),
   582  					dataType:             dataType,
   583  					tokenization:         tokenization,
   584  					expectedDataType:     dataType,
   585  					expectedTokenization: tokenization,
   586  				})
   587  			}
   588  			tokenization := ""
   589  			testCases = append(testCases, testCase{
   590  				propName:             propName(dataType, tokenization),
   591  				dataType:             dataType,
   592  				tokenization:         tokenization,
   593  				expectedDataType:     dataType,
   594  				expectedTokenization: models.PropertyTokenizationWord,
   595  			})
   596  		}
   597  		for _, dataType := range []schema.DataType{
   598  			schema.DataTypeString, schema.DataTypeStringArray,
   599  		} {
   600  			for _, tokenization := range []string{
   601  				models.PropertyTokenizationWord, models.PropertyTokenizationField, "",
   602  			} {
   603  				var expectedDataType schema.DataType
   604  				switch dataType {
   605  				case schema.DataTypeStringArray:
   606  					expectedDataType = schema.DataTypeTextArray
   607  				default:
   608  					expectedDataType = schema.DataTypeText
   609  				}
   610  
   611  				var expectedTokenization string
   612  				switch tokenization {
   613  				case models.PropertyTokenizationField:
   614  					expectedTokenization = models.PropertyTokenizationField
   615  				default:
   616  					expectedTokenization = models.PropertyTokenizationWhitespace
   617  				}
   618  
   619  				testCases = append(testCases, testCase{
   620  					propName:             propName(dataType, tokenization),
   621  					dataType:             dataType,
   622  					tokenization:         tokenization,
   623  					expectedDataType:     expectedDataType,
   624  					expectedTokenization: expectedTokenization,
   625  				})
   626  			}
   627  		}
   628  
   629  		t.Run("create class with all properties", func(t *testing.T) {
   630  			properties := []*models.Property{}
   631  			for _, tc := range testCases {
   632  				properties = append(properties, &models.Property{
   633  					Name:         "created_" + tc.propName,
   634  					DataType:     tc.dataType.PropString(),
   635  					Tokenization: tc.tokenization,
   636  				})
   637  			}
   638  
   639  			err := mgr.AddClass(ctx, nil, &models.Class{
   640  				Class:      className,
   641  				Properties: properties,
   642  			})
   643  
   644  			require.Nil(t, err)
   645  			require.NotNil(t, mgr.schemaCache.ObjectSchema)
   646  			require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   647  			require.Equal(t, className, mgr.schemaCache.ObjectSchema.Classes[0].Class)
   648  		})
   649  
   650  		t.Run("add properties to existing class", func(t *testing.T) {
   651  			for _, tc := range testCases {
   652  				t.Run("added_"+tc.propName, func(t *testing.T) {
   653  					err := mgr.addClassProperty(ctx, className, &models.Property{
   654  						Name:         "added_" + tc.propName,
   655  						DataType:     tc.dataType.PropString(),
   656  						Tokenization: tc.tokenization,
   657  					})
   658  
   659  					require.Nil(t, err)
   660  				})
   661  			}
   662  		})
   663  
   664  		t.Run("verify defaults and migration", func(t *testing.T) {
   665  			class := mgr.schemaCache.ObjectSchema.Classes[0]
   666  			for _, tc := range testCases {
   667  				t.Run("created_"+tc.propName, func(t *testing.T) {
   668  					createdProperty, err := schema.GetPropertyByName(class, "created_"+tc.propName)
   669  
   670  					require.Nil(t, err)
   671  					assert.Equal(t, tc.expectedDataType.PropString(), createdProperty.DataType)
   672  					assert.Equal(t, tc.expectedTokenization, createdProperty.Tokenization)
   673  				})
   674  
   675  				t.Run("added_"+tc.propName, func(t *testing.T) {
   676  					addedProperty, err := schema.GetPropertyByName(class, "added_"+tc.propName)
   677  
   678  					require.Nil(t, err)
   679  					assert.Equal(t, tc.expectedDataType.PropString(), addedProperty.DataType)
   680  					assert.Equal(t, tc.expectedTokenization, addedProperty.Tokenization)
   681  				})
   682  			}
   683  		})
   684  	})
   685  
   686  	t.Run("set defaults and migrate IndexInverted to IndexFilterable + IndexSearchable", func(t *testing.T) {
   687  		vFalse := false
   688  		vTrue := true
   689  		allBoolPtrs := []*bool{nil, &vFalse, &vTrue}
   690  
   691  		type testCase struct {
   692  			propName        string
   693  			dataType        schema.DataType
   694  			indexInverted   *bool
   695  			indexFilterable *bool
   696  			indexSearchable *bool
   697  
   698  			expectedInverted   *bool
   699  			expectedFilterable *bool
   700  			expectedSearchable *bool
   701  		}
   702  
   703  		boolPtrToStr := func(ptr *bool) string {
   704  			if ptr == nil {
   705  				return "nil"
   706  			}
   707  			return fmt.Sprintf("%v", *ptr)
   708  		}
   709  		propName := func(dt schema.DataType, inverted, filterable, searchable *bool) string {
   710  			return fmt.Sprintf("%s_inverted_%s_filterable_%s_searchable_%s",
   711  				dt.String(), boolPtrToStr(inverted), boolPtrToStr(filterable), boolPtrToStr(searchable))
   712  		}
   713  
   714  		mgr := newSchemaManager()
   715  		ctx := context.Background()
   716  		className := "MigrationClass"
   717  
   718  		testCases := []testCase{}
   719  
   720  		for _, dataType := range []schema.DataType{schema.DataTypeText, schema.DataTypeInt} {
   721  			for _, inverted := range allBoolPtrs {
   722  				for _, filterable := range allBoolPtrs {
   723  					for _, searchable := range allBoolPtrs {
   724  						if inverted != nil {
   725  							if filterable != nil || searchable != nil {
   726  								// invalid combination, indexInverted can not be set
   727  								// together with indexFilterable or indexSearchable
   728  								continue
   729  							}
   730  						}
   731  
   732  						if searchable != nil && *searchable {
   733  							if dataType != schema.DataTypeText {
   734  								// invalid combination, indexSearchable can not be enabled
   735  								// for non text/text[] data type
   736  								continue
   737  							}
   738  						}
   739  
   740  						switch dataType {
   741  						case schema.DataTypeText:
   742  							if inverted != nil {
   743  								testCases = append(testCases, testCase{
   744  									propName:           propName(dataType, inverted, filterable, searchable),
   745  									dataType:           dataType,
   746  									indexInverted:      inverted,
   747  									indexFilterable:    filterable,
   748  									indexSearchable:    searchable,
   749  									expectedInverted:   nil,
   750  									expectedFilterable: inverted,
   751  									expectedSearchable: inverted,
   752  								})
   753  							} else {
   754  								expectedFilterable := filterable
   755  								if filterable == nil {
   756  									expectedFilterable = &vTrue
   757  								}
   758  								expectedSearchable := searchable
   759  								if searchable == nil {
   760  									expectedSearchable = &vTrue
   761  								}
   762  								testCases = append(testCases, testCase{
   763  									propName:           propName(dataType, inverted, filterable, searchable),
   764  									dataType:           dataType,
   765  									indexInverted:      inverted,
   766  									indexFilterable:    filterable,
   767  									indexSearchable:    searchable,
   768  									expectedInverted:   nil,
   769  									expectedFilterable: expectedFilterable,
   770  									expectedSearchable: expectedSearchable,
   771  								})
   772  							}
   773  						default:
   774  							if inverted != nil {
   775  								testCases = append(testCases, testCase{
   776  									propName:           propName(dataType, inverted, filterable, searchable),
   777  									dataType:           dataType,
   778  									indexInverted:      inverted,
   779  									indexFilterable:    filterable,
   780  									indexSearchable:    searchable,
   781  									expectedInverted:   nil,
   782  									expectedFilterable: inverted,
   783  									expectedSearchable: &vFalse,
   784  								})
   785  							} else {
   786  								expectedFilterable := filterable
   787  								if filterable == nil {
   788  									expectedFilterable = &vTrue
   789  								}
   790  								expectedSearchable := searchable
   791  								if searchable == nil {
   792  									expectedSearchable = &vFalse
   793  								}
   794  								testCases = append(testCases, testCase{
   795  									propName:           propName(dataType, inverted, filterable, searchable),
   796  									dataType:           dataType,
   797  									indexInverted:      inverted,
   798  									indexFilterable:    filterable,
   799  									indexSearchable:    searchable,
   800  									expectedInverted:   nil,
   801  									expectedFilterable: expectedFilterable,
   802  									expectedSearchable: expectedSearchable,
   803  								})
   804  							}
   805  						}
   806  					}
   807  				}
   808  			}
   809  		}
   810  
   811  		t.Run("create class with all properties", func(t *testing.T) {
   812  			properties := []*models.Property{}
   813  			for _, tc := range testCases {
   814  				properties = append(properties, &models.Property{
   815  					Name:            "created_" + tc.propName,
   816  					DataType:        tc.dataType.PropString(),
   817  					IndexInverted:   tc.indexInverted,
   818  					IndexFilterable: tc.indexFilterable,
   819  					IndexSearchable: tc.indexSearchable,
   820  				})
   821  			}
   822  
   823  			err := mgr.AddClass(ctx, nil, &models.Class{
   824  				Class:      className,
   825  				Properties: properties,
   826  			})
   827  
   828  			require.Nil(t, err)
   829  			require.NotNil(t, mgr.schemaCache.ObjectSchema)
   830  			require.NotEmpty(t, mgr.schemaCache.ObjectSchema.Classes)
   831  			require.Equal(t, className, mgr.schemaCache.ObjectSchema.Classes[0].Class)
   832  		})
   833  
   834  		t.Run("add properties to existing class", func(t *testing.T) {
   835  			for _, tc := range testCases {
   836  				t.Run("added_"+tc.propName, func(t *testing.T) {
   837  					err := mgr.addClassProperty(ctx, className, &models.Property{
   838  						Name:            "added_" + tc.propName,
   839  						DataType:        tc.dataType.PropString(),
   840  						IndexInverted:   tc.indexInverted,
   841  						IndexFilterable: tc.indexFilterable,
   842  						IndexSearchable: tc.indexSearchable,
   843  					})
   844  
   845  					require.Nil(t, err)
   846  				})
   847  			}
   848  		})
   849  
   850  		t.Run("verify migration", func(t *testing.T) {
   851  			class := mgr.schemaCache.ObjectSchema.Classes[0]
   852  			for _, tc := range testCases {
   853  				t.Run("created_"+tc.propName, func(t *testing.T) {
   854  					createdProperty, err := schema.GetPropertyByName(class, "created_"+tc.propName)
   855  
   856  					require.Nil(t, err)
   857  					assert.Equal(t, tc.expectedInverted, createdProperty.IndexInverted)
   858  					assert.Equal(t, tc.expectedFilterable, createdProperty.IndexFilterable)
   859  					assert.Equal(t, tc.expectedSearchable, createdProperty.IndexSearchable)
   860  				})
   861  
   862  				t.Run("added_"+tc.propName, func(t *testing.T) {
   863  					addedProperty, err := schema.GetPropertyByName(class, "added_"+tc.propName)
   864  
   865  					require.Nil(t, err)
   866  					assert.Equal(t, tc.expectedInverted, addedProperty.IndexInverted)
   867  					assert.Equal(t, tc.expectedFilterable, addedProperty.IndexFilterable)
   868  					assert.Equal(t, tc.expectedSearchable, addedProperty.IndexSearchable)
   869  				})
   870  			}
   871  		})
   872  	})
   873  }
   874  
   875  func Test_Defaults_NestedProperties(t *testing.T) {
   876  	for _, pdt := range schema.PrimitiveDataTypes {
   877  		t.Run(pdt.String(), func(t *testing.T) {
   878  			nestedProperties := []*models.NestedProperty{
   879  				{
   880  					Name:     "nested_" + pdt.String(),
   881  					DataType: pdt.PropString(),
   882  				},
   883  			}
   884  
   885  			for _, ndt := range schema.NestedDataTypes {
   886  				t.Run(ndt.String(), func(t *testing.T) {
   887  					propPrimitives := &models.Property{
   888  						Name:             "objectProp",
   889  						DataType:         ndt.PropString(),
   890  						NestedProperties: nestedProperties,
   891  					}
   892  					propLvl2Primitives := &models.Property{
   893  						Name:     "objectPropLvl2",
   894  						DataType: ndt.PropString(),
   895  						NestedProperties: []*models.NestedProperty{
   896  							{
   897  								Name:             "nested_object",
   898  								DataType:         ndt.PropString(),
   899  								NestedProperties: nestedProperties,
   900  							},
   901  						},
   902  					}
   903  
   904  					setPropertyDefaults(propPrimitives)
   905  					setPropertyDefaults(propLvl2Primitives)
   906  
   907  					t.Run("primitive data types", func(t *testing.T) {
   908  						for _, np := range []*models.NestedProperty{
   909  							propPrimitives.NestedProperties[0],
   910  							propLvl2Primitives.NestedProperties[0].NestedProperties[0],
   911  						} {
   912  							switch pdt {
   913  							case schema.DataTypeText, schema.DataTypeTextArray:
   914  								require.NotNil(t, np.IndexFilterable)
   915  								assert.True(t, *np.IndexFilterable)
   916  								require.NotNil(t, np.IndexSearchable)
   917  								assert.True(t, *np.IndexSearchable)
   918  								assert.Equal(t, models.PropertyTokenizationWord, np.Tokenization)
   919  							case schema.DataTypeBlob:
   920  								require.NotNil(t, np.IndexFilterable)
   921  								assert.False(t, *np.IndexFilterable)
   922  								require.NotNil(t, np.IndexSearchable)
   923  								assert.False(t, *np.IndexSearchable)
   924  								assert.Equal(t, "", np.Tokenization)
   925  							default:
   926  								require.NotNil(t, np.IndexFilterable)
   927  								assert.True(t, *np.IndexFilterable)
   928  								require.NotNil(t, np.IndexSearchable)
   929  								assert.False(t, *np.IndexSearchable)
   930  								assert.Equal(t, "", np.Tokenization)
   931  							}
   932  						}
   933  					})
   934  
   935  					t.Run("nested data types", func(t *testing.T) {
   936  						for _, indexFilterable := range []*bool{
   937  							propPrimitives.IndexFilterable,
   938  							propLvl2Primitives.IndexFilterable,
   939  							propLvl2Primitives.NestedProperties[0].IndexFilterable,
   940  						} {
   941  							require.NotNil(t, indexFilterable)
   942  							assert.True(t, *indexFilterable)
   943  						}
   944  						for _, indexSearchable := range []*bool{
   945  							propPrimitives.IndexSearchable,
   946  							propLvl2Primitives.IndexSearchable,
   947  							propLvl2Primitives.NestedProperties[0].IndexSearchable,
   948  						} {
   949  							require.NotNil(t, indexSearchable)
   950  							assert.False(t, *indexSearchable)
   951  						}
   952  						for _, tokenization := range []string{
   953  							propPrimitives.Tokenization,
   954  							propLvl2Primitives.Tokenization,
   955  							propLvl2Primitives.NestedProperties[0].Tokenization,
   956  						} {
   957  							assert.Equal(t, "", tokenization)
   958  						}
   959  					})
   960  				})
   961  			}
   962  		})
   963  	}
   964  }