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 }