github.com/weaviate/weaviate@v1.24.6/usecases/schema/validation_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 "testing" 18 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 "github.com/weaviate/weaviate/adapters/repos/db/helpers" 22 "github.com/weaviate/weaviate/entities/models" 23 "github.com/weaviate/weaviate/entities/schema" 24 ) 25 26 func Test_Validation_ClassNames(t *testing.T) { 27 type testCase struct { 28 input string 29 valid bool 30 storedAs string 31 name string 32 } 33 34 // all inputs represent class names (!) 35 tests := []testCase{ 36 // valid names 37 { 38 name: "Single uppercase word", 39 input: "Car", 40 valid: true, 41 storedAs: "Car", 42 }, 43 { 44 name: "Single lowercase word, stored as uppercase", 45 input: "car", 46 valid: true, 47 storedAs: "Car", 48 }, 49 { 50 name: "empty class", 51 input: "", 52 valid: false, 53 }, 54 } 55 56 t.Run("adding a class", func(t *testing.T) { 57 t.Run("different class names without keywords or properties", func(t *testing.T) { 58 for _, test := range tests { 59 t.Run(test.name+" as thing class", func(t *testing.T) { 60 class := &models.Class{ 61 Vectorizer: "text2vec-contextionary", 62 Class: test.input, 63 } 64 65 m := newSchemaManager() 66 err := m.AddClass(context.Background(), nil, class) 67 t.Log(err) 68 assert.Equal(t, test.valid, err == nil) 69 70 // only proceed if input was supposed to be valid 71 if test.valid == false { 72 return 73 } 74 75 classNames := testGetClassNames(m) 76 assert.Contains(t, classNames, test.storedAs, "class should be stored correctly") 77 }) 78 } 79 }) 80 81 t.Run("different class names with valid keywords", func(t *testing.T) { 82 for _, test := range tests { 83 t.Run(test.name+" as thing class", func(t *testing.T) { 84 class := &models.Class{ 85 Vectorizer: "text2vec-contextionary", 86 Class: test.input, 87 } 88 89 m := newSchemaManager() 90 err := m.AddClass(context.Background(), nil, class) 91 t.Log(err) 92 assert.Equal(t, test.valid, err == nil) 93 94 // only proceed if input was supposed to be valid 95 if test.valid == false { 96 return 97 } 98 99 classNames := testGetClassNames(m) 100 assert.Contains(t, classNames, test.storedAs, "class should be stored correctly") 101 }) 102 } 103 }) 104 }) 105 } 106 107 func Test_Validation_PropertyNames(t *testing.T) { 108 type testCase struct { 109 input string 110 valid bool 111 storedAs string 112 name string 113 } 114 115 // for all test cases keep in mind that the word "carrot" is not present in 116 // the fake c11y, but every other word is 117 // 118 // all inputs represent property names (!) 119 tests := []testCase{ 120 // valid names 121 { 122 name: "Single uppercase word, stored as lowercase", 123 input: "Brand", 124 valid: true, 125 storedAs: "brand", 126 }, 127 { 128 name: "Single lowercase word", 129 input: "brand", 130 valid: true, 131 storedAs: "brand", 132 }, 133 { 134 name: "Property with underscores", 135 input: "property_name", 136 valid: true, 137 storedAs: "property_name", 138 }, 139 { 140 name: "Property with underscores and numbers", 141 input: "property_name_2", 142 valid: true, 143 storedAs: "property_name_2", 144 }, 145 { 146 name: "Property starting with underscores", 147 input: "_property_name", 148 valid: true, 149 storedAs: "_property_name", 150 }, 151 { 152 name: "empty prop name", 153 input: "", 154 valid: false, 155 }, 156 { 157 name: "reserved prop name: id", 158 input: "id", 159 valid: false, 160 }, 161 { 162 name: "reserved prop name: _id", 163 input: "_id", 164 valid: false, 165 }, 166 { 167 name: "reserved prop name: _additional", 168 input: "_additional", 169 valid: false, 170 }, 171 } 172 173 t.Run("when adding a new class", func(t *testing.T) { 174 t.Run("different property names without keywords for the prop", func(t *testing.T) { 175 for _, test := range tests { 176 t.Run(test.name+" as thing class", func(t *testing.T) { 177 class := &models.Class{ 178 Vectorizer: "text2vec-contextionary", 179 Class: "ValidName", 180 Properties: []*models.Property{{ 181 DataType: schema.DataTypeText.PropString(), 182 Name: test.input, 183 }}, 184 } 185 186 m := newSchemaManager() 187 err := m.AddClass(context.Background(), nil, class) 188 t.Log(err) 189 assert.Equal(t, test.valid, err == nil) 190 191 // only proceed if input was supposed to be valid 192 if test.valid == false { 193 return 194 } 195 196 schema, _ := m.GetSchema(nil) 197 propName := schema.Objects.Classes[0].Properties[0].Name 198 assert.Equal(t, propName, test.storedAs, "class should be stored correctly") 199 }) 200 } 201 }) 202 203 t.Run("different property names with valid keywords for the prop", func(t *testing.T) { 204 for _, test := range tests { 205 t.Run(test.name+" as thing class", func(t *testing.T) { 206 class := &models.Class{ 207 Vectorizer: "text2vec-contextionary", 208 Class: "ValidName", 209 Properties: []*models.Property{{ 210 DataType: schema.DataTypeText.PropString(), 211 Name: test.input, 212 }}, 213 } 214 215 m := newSchemaManager() 216 err := m.AddClass(context.Background(), nil, class) 217 t.Log(err) 218 assert.Equal(t, test.valid, err == nil) 219 220 // only proceed if input was supposed to be valid 221 if test.valid == false { 222 return 223 } 224 225 schema, _ := m.GetSchema(nil) 226 propName := schema.Objects.Classes[0].Properties[0].Name 227 assert.Equal(t, propName, test.storedAs, "class should be stored correctly") 228 }) 229 } 230 }) 231 }) 232 233 t.Run("when updating an existing class with a new property", func(t *testing.T) { 234 t.Run("different property names without keywords for the prop", func(t *testing.T) { 235 for _, test := range tests { 236 t.Run(test.name+" as thing class", func(t *testing.T) { 237 class := &models.Class{ 238 Vectorizer: "text2vec-contextionary", 239 Class: "ValidName", 240 Properties: []*models.Property{ 241 { 242 Name: "dummyPropSoWeDontRunIntoAllNoindexedError", 243 DataType: schema.DataTypeText.PropString(), 244 }, 245 }, 246 } 247 248 m := newSchemaManager() 249 err := m.AddClass(context.Background(), nil, class) 250 require.Nil(t, err) 251 252 property := &models.Property{ 253 DataType: schema.DataTypeText.PropString(), 254 Name: test.input, 255 ModuleConfig: map[string]interface{}{ 256 "text2vec-contextionary": map[string]interface{}{}, 257 }, 258 } 259 err = m.AddClassProperty(context.Background(), nil, "ValidName", property) 260 t.Log(err) 261 require.Equal(t, test.valid, err == nil) 262 263 // only proceed if input was supposed to be valid 264 if test.valid == false { 265 return 266 } 267 268 schema, _ := m.GetSchema(nil) 269 propName := schema.Objects.Classes[0].Properties[1].Name 270 assert.Equal(t, propName, test.storedAs, "class should be stored correctly") 271 }) 272 } 273 }) 274 275 t.Run("different property names with valid keywords for the prop", func(t *testing.T) { 276 for _, test := range tests { 277 t.Run(test.name+" as thing class", func(t *testing.T) { 278 class := &models.Class{ 279 Vectorizer: "text2vec-contextionary", 280 Class: "ValidName", 281 Properties: []*models.Property{{ 282 DataType: schema.DataTypeText.PropString(), 283 Name: test.input, 284 }}, 285 } 286 287 m := newSchemaManager() 288 err := m.AddClass(context.Background(), nil, class) 289 t.Log(err) 290 assert.Equal(t, test.valid, err == nil) 291 292 // only proceed if input was supposed to be valid 293 if test.valid == false { 294 return 295 } 296 297 schema, _ := m.GetSchema(nil) 298 propName := schema.Objects.Classes[0].Properties[0].Name 299 assert.Equal(t, propName, test.storedAs, "class should be stored correctly") 300 }) 301 } 302 }) 303 }) 304 } 305 306 func Test_Validation_PropertyTokenization(t *testing.T) { 307 type testCase struct { 308 name string 309 tokenization string 310 propertyDataType schema.PropertyDataType 311 expectedErrMsg string 312 } 313 314 runTestCases := func(t *testing.T, testCases []testCase) { 315 m := newSchemaManager() 316 for _, tc := range testCases { 317 t.Run(tc.name, func(t *testing.T) { 318 err := m.validatePropertyTokenization(tc.tokenization, tc.propertyDataType) 319 if tc.expectedErrMsg == "" { 320 assert.Nil(t, err) 321 } else { 322 assert.NotNil(t, err) 323 assert.EqualError(t, err, tc.expectedErrMsg) 324 } 325 }) 326 } 327 } 328 329 t.Run("validates text/textArray and all tokenizations", func(t *testing.T) { 330 testCases := []testCase{} 331 for _, dataType := range []schema.DataType{ 332 schema.DataTypeText, schema.DataTypeTextArray, 333 } { 334 for _, tokenization := range helpers.Tokenizations { 335 testCases = append(testCases, testCase{ 336 name: fmt.Sprintf("%s + '%s'", dataType, tokenization), 337 propertyDataType: newFakePrimitivePDT(dataType), 338 tokenization: tokenization, 339 expectedErrMsg: "", 340 }) 341 } 342 343 for _, tokenization := range []string{"non_existing", ""} { 344 testCases = append(testCases, testCase{ 345 name: fmt.Sprintf("%s + '%s'", dataType, tokenization), 346 propertyDataType: newFakePrimitivePDT(dataType), 347 tokenization: tokenization, 348 expectedErrMsg: fmt.Sprintf("Tokenization '%s' is not allowed for data type '%s'", tokenization, dataType), 349 }) 350 } 351 } 352 353 runTestCases(t, testCases) 354 }) 355 356 t.Run("validates non text/textArray and all tokenizations", func(t *testing.T) { 357 testCases := []testCase{} 358 for _, dataType := range schema.PrimitiveDataTypes { 359 switch dataType { 360 case schema.DataTypeText, schema.DataTypeTextArray: 361 continue 362 default: 363 testCases = append(testCases, testCase{ 364 name: fmt.Sprintf("%s + ''", dataType), 365 propertyDataType: newFakePrimitivePDT(dataType), 366 tokenization: "", 367 expectedErrMsg: "", 368 }) 369 370 for _, tokenization := range append(helpers.Tokenizations, "non_existing") { 371 testCases = append(testCases, testCase{ 372 name: fmt.Sprintf("%s + '%s'", dataType, tokenization), 373 propertyDataType: newFakePrimitivePDT(dataType), 374 tokenization: tokenization, 375 expectedErrMsg: fmt.Sprintf("Tokenization is not allowed for data type '%s'", dataType), 376 }) 377 } 378 } 379 } 380 381 runTestCases(t, testCases) 382 }) 383 384 t.Run("validates nested datatype and all tokenizations", func(t *testing.T) { 385 testCases := []testCase{} 386 for _, dataType := range schema.NestedDataTypes { 387 testCases = append(testCases, testCase{ 388 name: fmt.Sprintf("%s + ''", dataType), 389 propertyDataType: newFakeNestedPDT(dataType), 390 tokenization: "", 391 expectedErrMsg: "", 392 }) 393 394 for _, tokenization := range append(helpers.Tokenizations, "non_existent") { 395 testCases = append(testCases, testCase{ 396 name: fmt.Sprintf("%s + '%s'", dataType, tokenization), 397 propertyDataType: newFakeNestedPDT(dataType), 398 tokenization: tokenization, 399 expectedErrMsg: "Tokenization is not allowed for object/object[] data types", 400 }) 401 } 402 } 403 404 runTestCases(t, testCases) 405 }) 406 407 t.Run("validates ref datatype (empty) and all tokenizations", func(t *testing.T) { 408 testCases := []testCase{} 409 410 testCases = append(testCases, testCase{ 411 name: "ref + ''", 412 propertyDataType: newFakePrimitivePDT(""), 413 tokenization: "", 414 expectedErrMsg: "", 415 }) 416 417 for _, tokenization := range append(helpers.Tokenizations, "non_existing") { 418 testCases = append(testCases, testCase{ 419 name: fmt.Sprintf("ref + '%s'", tokenization), 420 propertyDataType: newFakePrimitivePDT(""), 421 tokenization: tokenization, 422 expectedErrMsg: "Tokenization is not allowed for reference data type", 423 }) 424 } 425 426 runTestCases(t, testCases) 427 }) 428 429 t.Run("[deprecated string] validates string/stringArray and all tokenizations", func(t *testing.T) { 430 testCases := []testCase{} 431 for _, dataType := range []schema.DataType{ 432 schema.DataTypeString, schema.DataTypeStringArray, 433 } { 434 for _, tokenization := range append(helpers.Tokenizations, "non_existing") { 435 switch tokenization { 436 case models.PropertyTokenizationWord, models.PropertyTokenizationField: 437 testCases = append(testCases, testCase{ 438 name: fmt.Sprintf("%s + %s", dataType, tokenization), 439 propertyDataType: newFakePrimitivePDT(dataType), 440 tokenization: tokenization, 441 expectedErrMsg: "", 442 }) 443 default: 444 testCases = append(testCases, testCase{ 445 name: fmt.Sprintf("%s + %s", dataType, tokenization), 446 propertyDataType: newFakePrimitivePDT(dataType), 447 tokenization: tokenization, 448 expectedErrMsg: fmt.Sprintf("Tokenization '%s' is not allowed for data type '%s'", tokenization, dataType), 449 }) 450 } 451 } 452 } 453 454 runTestCases(t, testCases) 455 }) 456 } 457 458 func Test_Validation_PropertyIndexing(t *testing.T) { 459 t.Run("validates indexInverted / indexFilterable / indexSearchable combinations", func(t *testing.T) { 460 vFalse := false 461 vTrue := true 462 allBoolPtrs := []*bool{nil, &vFalse, &vTrue} 463 464 type testCase struct { 465 propName string 466 dataType schema.DataType 467 indexInverted *bool 468 indexFilterable *bool 469 indexSearchable *bool 470 471 expectedErrMsg string 472 } 473 474 boolPtrToStr := func(ptr *bool) string { 475 if ptr == nil { 476 return "nil" 477 } 478 return fmt.Sprintf("%v", *ptr) 479 } 480 propName := func(dt schema.DataType, inverted, filterable, searchable *bool) string { 481 return fmt.Sprintf("%s_inverted_%s_filterable_%s_searchable_%s", 482 dt.String(), boolPtrToStr(inverted), boolPtrToStr(filterable), boolPtrToStr(searchable)) 483 } 484 485 testCases := []testCase{} 486 487 for _, dataType := range []schema.DataType{schema.DataTypeText, schema.DataTypeInt, schema.DataTypeObject} { 488 for _, inverted := range allBoolPtrs { 489 for _, filterable := range allBoolPtrs { 490 for _, searchable := range allBoolPtrs { 491 if inverted != nil { 492 if filterable != nil || searchable != nil { 493 testCases = append(testCases, testCase{ 494 propName: propName(dataType, inverted, filterable, searchable), 495 dataType: dataType, 496 indexInverted: inverted, 497 indexFilterable: filterable, 498 indexSearchable: searchable, 499 expectedErrMsg: "`indexInverted` is deprecated and can not be set together with `indexFilterable` or `indexSearchable`", 500 }) 501 continue 502 } 503 } 504 505 if searchable != nil && *searchable { 506 if dataType != schema.DataTypeText { 507 testCases = append(testCases, testCase{ 508 propName: propName(dataType, inverted, filterable, searchable), 509 dataType: dataType, 510 indexInverted: inverted, 511 indexFilterable: filterable, 512 indexSearchable: searchable, 513 expectedErrMsg: "`indexSearchable` is not allowed for other than text/text[] data types", 514 }) 515 continue 516 } 517 } 518 519 testCases = append(testCases, testCase{ 520 propName: propName(dataType, inverted, filterable, searchable), 521 dataType: dataType, 522 indexInverted: inverted, 523 indexFilterable: filterable, 524 indexSearchable: searchable, 525 expectedErrMsg: "", 526 }) 527 } 528 } 529 } 530 } 531 532 mgr := newSchemaManager() 533 for _, tc := range testCases { 534 t.Run(tc.propName, func(t *testing.T) { 535 err := mgr.validatePropertyIndexing(&models.Property{ 536 Name: tc.propName, 537 DataType: tc.dataType.PropString(), 538 IndexInverted: tc.indexInverted, 539 IndexFilterable: tc.indexFilterable, 540 IndexSearchable: tc.indexSearchable, 541 }) 542 543 if tc.expectedErrMsg != "" { 544 require.NotNil(t, err) 545 assert.EqualError(t, err, tc.expectedErrMsg) 546 } else { 547 require.Nil(t, err) 548 } 549 }) 550 } 551 }) 552 } 553 554 func Test_Validation_NestedProperties(t *testing.T) { 555 vFalse := false 556 vTrue := true 557 558 t.Run("does not validate wrong names", func(t *testing.T) { 559 for _, name := range []string{"prop@1", "prop-2", "prop$3", "4prop"} { 560 t.Run(name, func(t *testing.T) { 561 nestedProperties := []*models.NestedProperty{ 562 { 563 Name: name, 564 DataType: schema.DataTypeInt.PropString(), 565 IndexFilterable: &vFalse, 566 IndexSearchable: &vFalse, 567 Tokenization: "", 568 }, 569 } 570 571 for _, ndt := range schema.NestedDataTypes { 572 t.Run(ndt.String(), func(t *testing.T) { 573 propPrimitives := &models.Property{ 574 Name: "objectProp", 575 DataType: ndt.PropString(), 576 IndexFilterable: &vFalse, 577 IndexSearchable: &vFalse, 578 Tokenization: "", 579 NestedProperties: nestedProperties, 580 } 581 propLvl2Primitives := &models.Property{ 582 Name: "objectPropLvl2", 583 DataType: ndt.PropString(), 584 IndexFilterable: &vFalse, 585 IndexSearchable: &vFalse, 586 Tokenization: "", 587 NestedProperties: []*models.NestedProperty{ 588 { 589 Name: "nested_object", 590 DataType: ndt.PropString(), 591 IndexFilterable: &vFalse, 592 IndexSearchable: &vFalse, 593 Tokenization: "", 594 NestedProperties: nestedProperties, 595 }, 596 }, 597 } 598 599 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 600 t.Run(prop.Name, func(t *testing.T) { 601 err := validateNestedProperties(prop.NestedProperties, prop.Name) 602 assert.ErrorContains(t, err, prop.Name) 603 assert.ErrorContains(t, err, "is not a valid nested property name") 604 }) 605 } 606 }) 607 } 608 }) 609 } 610 }) 611 612 t.Run("validates primitive data types", func(t *testing.T) { 613 nestedProperties := []*models.NestedProperty{} 614 for _, pdt := range schema.PrimitiveDataTypes { 615 tokenization := "" 616 switch pdt { 617 case schema.DataTypeGeoCoordinates, schema.DataTypePhoneNumber: 618 // skip - not supported as nested 619 continue 620 case schema.DataTypeText, schema.DataTypeTextArray: 621 tokenization = models.PropertyTokenizationWord 622 default: 623 // do nothing 624 } 625 626 nestedProperties = append(nestedProperties, &models.NestedProperty{ 627 Name: "nested_" + pdt.AsName(), 628 DataType: pdt.PropString(), 629 IndexFilterable: &vFalse, 630 IndexSearchable: &vFalse, 631 Tokenization: tokenization, 632 }) 633 } 634 635 for _, ndt := range schema.NestedDataTypes { 636 t.Run(ndt.String(), func(t *testing.T) { 637 propPrimitives := &models.Property{ 638 Name: "objectProp", 639 DataType: ndt.PropString(), 640 IndexFilterable: &vFalse, 641 IndexSearchable: &vFalse, 642 Tokenization: "", 643 NestedProperties: nestedProperties, 644 } 645 propLvl2Primitives := &models.Property{ 646 Name: "objectPropLvl2", 647 DataType: ndt.PropString(), 648 IndexFilterable: &vFalse, 649 IndexSearchable: &vFalse, 650 Tokenization: "", 651 NestedProperties: []*models.NestedProperty{ 652 { 653 Name: "nested_object", 654 DataType: ndt.PropString(), 655 IndexFilterable: &vFalse, 656 IndexSearchable: &vFalse, 657 Tokenization: "", 658 NestedProperties: nestedProperties, 659 }, 660 }, 661 } 662 663 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 664 t.Run(prop.Name, func(t *testing.T) { 665 err := validateNestedProperties(prop.NestedProperties, prop.Name) 666 assert.NoError(t, err) 667 }) 668 } 669 }) 670 } 671 }) 672 673 t.Run("does not validate deprecated primitive types", func(t *testing.T) { 674 for _, pdt := range schema.DeprecatedPrimitiveDataTypes { 675 t.Run(pdt.String(), func(t *testing.T) { 676 nestedProperties := []*models.NestedProperty{ 677 { 678 Name: "nested_" + pdt.AsName(), 679 DataType: pdt.PropString(), 680 IndexFilterable: &vFalse, 681 IndexSearchable: &vFalse, 682 Tokenization: "", 683 }, 684 } 685 686 for _, ndt := range schema.NestedDataTypes { 687 t.Run(ndt.String(), func(t *testing.T) { 688 propPrimitives := &models.Property{ 689 Name: "objectProp", 690 DataType: ndt.PropString(), 691 IndexFilterable: &vFalse, 692 IndexSearchable: &vFalse, 693 Tokenization: "", 694 NestedProperties: nestedProperties, 695 } 696 propLvl2Primitives := &models.Property{ 697 Name: "objectPropLvl2", 698 DataType: ndt.PropString(), 699 IndexFilterable: &vFalse, 700 IndexSearchable: &vFalse, 701 Tokenization: "", 702 NestedProperties: []*models.NestedProperty{ 703 { 704 Name: "nested_object", 705 DataType: ndt.PropString(), 706 IndexFilterable: &vFalse, 707 IndexSearchable: &vFalse, 708 Tokenization: "", 709 NestedProperties: nestedProperties, 710 }, 711 }, 712 } 713 714 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 715 t.Run(prop.Name, func(t *testing.T) { 716 err := validateNestedProperties(prop.NestedProperties, prop.Name) 717 assert.ErrorContains(t, err, prop.Name) 718 assert.ErrorContains(t, err, fmt.Sprintf("data type '%s' is deprecated and not allowed as nested property", pdt.String())) 719 }) 720 } 721 }) 722 } 723 }) 724 } 725 }) 726 727 t.Run("does not validate unsupported primitive types", func(t *testing.T) { 728 for _, pdt := range []schema.DataType{schema.DataTypeGeoCoordinates, schema.DataTypePhoneNumber} { 729 t.Run(pdt.String(), func(t *testing.T) { 730 nestedProperties := []*models.NestedProperty{ 731 { 732 Name: "nested_" + pdt.AsName(), 733 DataType: pdt.PropString(), 734 IndexFilterable: &vFalse, 735 IndexSearchable: &vFalse, 736 Tokenization: "", 737 }, 738 } 739 740 for _, ndt := range schema.NestedDataTypes { 741 t.Run(ndt.String(), func(t *testing.T) { 742 propPrimitives := &models.Property{ 743 Name: "objectProp", 744 DataType: ndt.PropString(), 745 IndexFilterable: &vFalse, 746 IndexSearchable: &vFalse, 747 Tokenization: "", 748 NestedProperties: nestedProperties, 749 } 750 propLvl2Primitives := &models.Property{ 751 Name: "objectPropLvl2", 752 DataType: ndt.PropString(), 753 IndexFilterable: &vFalse, 754 IndexSearchable: &vFalse, 755 Tokenization: "", 756 NestedProperties: []*models.NestedProperty{ 757 { 758 Name: "nested_object", 759 DataType: ndt.PropString(), 760 IndexFilterable: &vFalse, 761 IndexSearchable: &vFalse, 762 Tokenization: "", 763 NestedProperties: nestedProperties, 764 }, 765 }, 766 } 767 768 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 769 t.Run(prop.Name, func(t *testing.T) { 770 err := validateNestedProperties(prop.NestedProperties, prop.Name) 771 assert.ErrorContains(t, err, prop.Name) 772 assert.ErrorContains(t, err, fmt.Sprintf("data type '%s' not allowed as nested property", pdt.String())) 773 }) 774 } 775 }) 776 } 777 }) 778 } 779 }) 780 781 t.Run("does not validate ref types", func(t *testing.T) { 782 nestedProperties := []*models.NestedProperty{ 783 { 784 Name: "nested_ref", 785 DataType: []string{"SomeClass"}, 786 IndexFilterable: &vFalse, 787 IndexSearchable: &vFalse, 788 Tokenization: "", 789 }, 790 } 791 792 for _, ndt := range schema.NestedDataTypes { 793 t.Run(ndt.String(), func(t *testing.T) { 794 propPrimitives := &models.Property{ 795 Name: "objectProp", 796 DataType: ndt.PropString(), 797 IndexFilterable: &vFalse, 798 IndexSearchable: &vFalse, 799 Tokenization: "", 800 NestedProperties: nestedProperties, 801 } 802 propLvl2Primitives := &models.Property{ 803 Name: "objectPropLvl2", 804 DataType: ndt.PropString(), 805 IndexFilterable: &vFalse, 806 IndexSearchable: &vFalse, 807 Tokenization: "", 808 NestedProperties: []*models.NestedProperty{ 809 { 810 Name: "nested_object", 811 DataType: ndt.PropString(), 812 IndexFilterable: &vFalse, 813 IndexSearchable: &vFalse, 814 Tokenization: "", 815 NestedProperties: nestedProperties, 816 }, 817 }, 818 } 819 820 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 821 t.Run(prop.Name, func(t *testing.T) { 822 err := validateNestedProperties(prop.NestedProperties, prop.Name) 823 assert.ErrorContains(t, err, prop.Name) 824 assert.ErrorContains(t, err, "reference data type not allowed") 825 }) 826 } 827 }) 828 } 829 }) 830 831 t.Run("does not validate empty nested properties", func(t *testing.T) { 832 for _, ndt := range schema.NestedDataTypes { 833 t.Run(ndt.String(), func(t *testing.T) { 834 propPrimitives := &models.Property{ 835 Name: "objectProp", 836 DataType: ndt.PropString(), 837 IndexFilterable: &vFalse, 838 IndexSearchable: &vFalse, 839 Tokenization: "", 840 } 841 propLvl2Primitives := &models.Property{ 842 Name: "objectPropLvl2", 843 DataType: ndt.PropString(), 844 IndexFilterable: &vFalse, 845 IndexSearchable: &vFalse, 846 Tokenization: "", 847 NestedProperties: []*models.NestedProperty{ 848 { 849 Name: "nested_object", 850 DataType: ndt.PropString(), 851 IndexFilterable: &vFalse, 852 IndexSearchable: &vFalse, 853 Tokenization: "", 854 }, 855 }, 856 } 857 858 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 859 t.Run(prop.Name, func(t *testing.T) { 860 err := validateNestedProperties(prop.NestedProperties, prop.Name) 861 assert.ErrorContains(t, err, prop.Name) 862 assert.ErrorContains(t, err, "At least one nested property is required for data type object/object[]") 863 }) 864 } 865 }) 866 } 867 }) 868 869 t.Run("does not validate tokenization on non text/text[] primitive data types", func(t *testing.T) { 870 for _, pdt := range schema.PrimitiveDataTypes { 871 switch pdt { 872 case schema.DataTypeText, schema.DataTypeTextArray: 873 continue 874 case schema.DataTypeGeoCoordinates, schema.DataTypePhoneNumber: 875 // skip - not supported as nested 876 continue 877 default: 878 // do nothing 879 } 880 881 t.Run(pdt.String(), func(t *testing.T) { 882 nestedProperties := []*models.NestedProperty{ 883 { 884 Name: "nested_" + pdt.AsName(), 885 DataType: pdt.PropString(), 886 IndexFilterable: &vFalse, 887 IndexSearchable: &vFalse, 888 Tokenization: models.PropertyTokenizationWord, 889 }, 890 } 891 892 for _, ndt := range schema.NestedDataTypes { 893 t.Run(ndt.String(), func(t *testing.T) { 894 propPrimitives := &models.Property{ 895 Name: "objectProp", 896 DataType: ndt.PropString(), 897 IndexFilterable: &vFalse, 898 IndexSearchable: &vFalse, 899 Tokenization: "", 900 NestedProperties: nestedProperties, 901 } 902 propLvl2Primitives := &models.Property{ 903 Name: "objectPropLvl2", 904 DataType: ndt.PropString(), 905 IndexFilterable: &vFalse, 906 IndexSearchable: &vFalse, 907 Tokenization: "", 908 NestedProperties: []*models.NestedProperty{ 909 { 910 Name: "nested_object", 911 DataType: ndt.PropString(), 912 IndexFilterable: &vFalse, 913 IndexSearchable: &vFalse, 914 Tokenization: "", 915 NestedProperties: nestedProperties, 916 }, 917 }, 918 } 919 920 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 921 t.Run(prop.Name, func(t *testing.T) { 922 err := validateNestedProperties(prop.NestedProperties, prop.Name) 923 assert.ErrorContains(t, err, prop.Name) 924 assert.ErrorContains(t, err, fmt.Sprintf("Tokenization is not allowed for data type '%s'", pdt.String())) 925 }) 926 } 927 }) 928 } 929 }) 930 } 931 }) 932 933 t.Run("does not validate tokenization on nested data types", func(t *testing.T) { 934 nestedProperties := []*models.NestedProperty{ 935 { 936 Name: "nested_int", 937 DataType: schema.DataTypeInt.PropString(), 938 IndexFilterable: &vFalse, 939 IndexSearchable: &vFalse, 940 Tokenization: "", 941 }, 942 } 943 944 for _, ndt := range schema.NestedDataTypes { 945 t.Run(ndt.String(), func(t *testing.T) { 946 propLvl2Primitives := &models.Property{ 947 Name: "objectPropLvl2", 948 DataType: ndt.PropString(), 949 IndexFilterable: &vFalse, 950 IndexSearchable: &vFalse, 951 Tokenization: "", 952 NestedProperties: []*models.NestedProperty{ 953 { 954 Name: "nested_object", 955 DataType: ndt.PropString(), 956 IndexFilterable: &vFalse, 957 IndexSearchable: &vFalse, 958 Tokenization: models.PropertyTokenizationWord, 959 NestedProperties: nestedProperties, 960 }, 961 }, 962 } 963 964 for _, prop := range []*models.Property{propLvl2Primitives} { 965 t.Run(prop.Name, func(t *testing.T) { 966 err := validateNestedProperties(prop.NestedProperties, prop.Name) 967 assert.ErrorContains(t, err, prop.Name) 968 assert.ErrorContains(t, err, "Tokenization is not allowed for object/object[] data types") 969 }) 970 } 971 }) 972 } 973 }) 974 975 t.Run("validates indexFilterable on primitive data types", func(t *testing.T) { 976 nestedProperties := []*models.NestedProperty{} 977 for _, pdt := range schema.PrimitiveDataTypes { 978 tokenization := "" 979 switch pdt { 980 case schema.DataTypeBlob: 981 // skip - not indexable 982 continue 983 case schema.DataTypeGeoCoordinates, schema.DataTypePhoneNumber: 984 // skip - not supported as nested 985 continue 986 case schema.DataTypeText, schema.DataTypeTextArray: 987 tokenization = models.PropertyTokenizationWord 988 default: 989 // do nothing 990 } 991 992 nestedProperties = append(nestedProperties, &models.NestedProperty{ 993 Name: "nested_" + pdt.AsName(), 994 DataType: pdt.PropString(), 995 IndexFilterable: &vTrue, 996 IndexSearchable: &vFalse, 997 Tokenization: tokenization, 998 }) 999 } 1000 1001 for _, ndt := range schema.NestedDataTypes { 1002 t.Run(ndt.String(), func(t *testing.T) { 1003 propPrimitives := &models.Property{ 1004 Name: "objectProp", 1005 DataType: ndt.PropString(), 1006 IndexFilterable: &vFalse, 1007 IndexSearchable: &vFalse, 1008 Tokenization: "", 1009 NestedProperties: nestedProperties, 1010 } 1011 propLvl2Primitives := &models.Property{ 1012 Name: "objectPropLvl2", 1013 DataType: ndt.PropString(), 1014 IndexFilterable: &vFalse, 1015 IndexSearchable: &vFalse, 1016 Tokenization: "", 1017 NestedProperties: []*models.NestedProperty{ 1018 { 1019 Name: "nested_object", 1020 DataType: ndt.PropString(), 1021 IndexFilterable: &vFalse, 1022 IndexSearchable: &vFalse, 1023 Tokenization: "", 1024 NestedProperties: nestedProperties, 1025 }, 1026 }, 1027 } 1028 1029 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 1030 t.Run(prop.Name, func(t *testing.T) { 1031 err := validateNestedProperties(prop.NestedProperties, prop.Name) 1032 assert.NoError(t, err) 1033 }) 1034 } 1035 }) 1036 } 1037 }) 1038 1039 t.Run("does not validate indexFilterable on blob data type", func(t *testing.T) { 1040 nestedProperties := []*models.NestedProperty{ 1041 { 1042 Name: "nested_blob", 1043 DataType: schema.DataTypeBlob.PropString(), 1044 IndexFilterable: &vTrue, 1045 IndexSearchable: &vFalse, 1046 Tokenization: "", 1047 }, 1048 } 1049 1050 for _, ndt := range schema.NestedDataTypes { 1051 t.Run(ndt.String(), func(t *testing.T) { 1052 propPrimitives := &models.Property{ 1053 Name: "objectProp", 1054 DataType: ndt.PropString(), 1055 IndexFilterable: &vFalse, 1056 IndexSearchable: &vFalse, 1057 Tokenization: "", 1058 NestedProperties: nestedProperties, 1059 } 1060 propLvl2Primitives := &models.Property{ 1061 Name: "objectPropLvl2", 1062 DataType: ndt.PropString(), 1063 IndexFilterable: &vFalse, 1064 IndexSearchable: &vFalse, 1065 Tokenization: "", 1066 NestedProperties: []*models.NestedProperty{ 1067 { 1068 Name: "nested_object", 1069 DataType: ndt.PropString(), 1070 IndexFilterable: &vFalse, 1071 IndexSearchable: &vFalse, 1072 Tokenization: "", 1073 NestedProperties: nestedProperties, 1074 }, 1075 }, 1076 } 1077 1078 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 1079 t.Run(prop.Name, func(t *testing.T) { 1080 err := validateNestedProperties(prop.NestedProperties, prop.Name) 1081 assert.ErrorContains(t, err, prop.Name) 1082 assert.ErrorContains(t, err, "indexFilterable is not allowed for blob data type") 1083 }) 1084 } 1085 }) 1086 } 1087 }) 1088 1089 t.Run("validates indexFilterable on nested data types", func(t *testing.T) { 1090 nestedProperties := []*models.NestedProperty{ 1091 { 1092 Name: "nested_int", 1093 DataType: schema.DataTypeInt.PropString(), 1094 IndexFilterable: &vFalse, 1095 IndexSearchable: &vFalse, 1096 Tokenization: "", 1097 }, 1098 } 1099 1100 for _, ndt := range schema.NestedDataTypes { 1101 t.Run(ndt.String(), func(t *testing.T) { 1102 propLvl2Primitives := &models.Property{ 1103 Name: "objectPropLvl2", 1104 DataType: ndt.PropString(), 1105 IndexFilterable: &vTrue, 1106 IndexSearchable: &vFalse, 1107 Tokenization: "", 1108 NestedProperties: []*models.NestedProperty{ 1109 { 1110 Name: "nested_object", 1111 DataType: ndt.PropString(), 1112 IndexFilterable: &vFalse, 1113 IndexSearchable: &vFalse, 1114 Tokenization: "", 1115 NestedProperties: nestedProperties, 1116 }, 1117 }, 1118 } 1119 1120 for _, prop := range []*models.Property{propLvl2Primitives} { 1121 t.Run(prop.Name, func(t *testing.T) { 1122 err := validateNestedProperties(prop.NestedProperties, prop.Name) 1123 assert.NoError(t, err) 1124 }) 1125 } 1126 }) 1127 } 1128 }) 1129 1130 t.Run("validates indexSearchable on text/text[] data types", func(t *testing.T) { 1131 nestedProperties := []*models.NestedProperty{} 1132 for _, pdt := range []schema.DataType{schema.DataTypeText, schema.DataTypeTextArray} { 1133 nestedProperties = append(nestedProperties, &models.NestedProperty{ 1134 Name: "nested_" + pdt.AsName(), 1135 DataType: pdt.PropString(), 1136 IndexFilterable: &vFalse, 1137 IndexSearchable: &vTrue, 1138 Tokenization: models.PropertyTokenizationWord, 1139 }) 1140 } 1141 1142 for _, ndt := range schema.NestedDataTypes { 1143 t.Run(ndt.String(), func(t *testing.T) { 1144 propPrimitives := &models.Property{ 1145 Name: "objectProp", 1146 DataType: ndt.PropString(), 1147 IndexFilterable: &vFalse, 1148 IndexSearchable: &vFalse, 1149 Tokenization: "", 1150 NestedProperties: nestedProperties, 1151 } 1152 propLvl2Primitives := &models.Property{ 1153 Name: "objectPropLvl2", 1154 DataType: ndt.PropString(), 1155 IndexFilterable: &vFalse, 1156 IndexSearchable: &vFalse, 1157 Tokenization: "", 1158 NestedProperties: []*models.NestedProperty{ 1159 { 1160 Name: "nested_object", 1161 DataType: ndt.PropString(), 1162 IndexFilterable: &vFalse, 1163 IndexSearchable: &vFalse, 1164 Tokenization: "", 1165 NestedProperties: nestedProperties, 1166 }, 1167 }, 1168 } 1169 1170 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 1171 t.Run(prop.Name, func(t *testing.T) { 1172 err := validateNestedProperties(prop.NestedProperties, prop.Name) 1173 assert.NoError(t, err) 1174 }) 1175 } 1176 }) 1177 } 1178 }) 1179 1180 t.Run("does not validate indexSearchable on primitive data types", func(t *testing.T) { 1181 nestedProperties := []*models.NestedProperty{} 1182 for _, pdt := range schema.PrimitiveDataTypes { 1183 switch pdt { 1184 case schema.DataTypeText, schema.DataTypeTextArray: 1185 continue 1186 case schema.DataTypeGeoCoordinates, schema.DataTypePhoneNumber: 1187 // skip - not supported as nested 1188 continue 1189 default: 1190 // do nothing 1191 } 1192 1193 t.Run(pdt.String(), func(t *testing.T) { 1194 nestedProperties = append(nestedProperties, &models.NestedProperty{ 1195 Name: "nested_" + pdt.AsName(), 1196 DataType: pdt.PropString(), 1197 IndexFilterable: &vFalse, 1198 IndexSearchable: &vTrue, 1199 Tokenization: "", 1200 }) 1201 1202 for _, ndt := range schema.NestedDataTypes { 1203 t.Run(ndt.String(), func(t *testing.T) { 1204 propPrimitives := &models.Property{ 1205 Name: "objectProp", 1206 DataType: ndt.PropString(), 1207 IndexFilterable: &vFalse, 1208 IndexSearchable: &vFalse, 1209 Tokenization: "", 1210 NestedProperties: nestedProperties, 1211 } 1212 propLvl2Primitives := &models.Property{ 1213 Name: "objectPropLvl2", 1214 DataType: ndt.PropString(), 1215 IndexFilterable: &vFalse, 1216 IndexSearchable: &vFalse, 1217 Tokenization: "", 1218 NestedProperties: []*models.NestedProperty{ 1219 { 1220 Name: "nested_object", 1221 DataType: ndt.PropString(), 1222 IndexFilterable: &vFalse, 1223 IndexSearchable: &vFalse, 1224 Tokenization: "", 1225 NestedProperties: nestedProperties, 1226 }, 1227 }, 1228 } 1229 1230 for _, prop := range []*models.Property{propPrimitives, propLvl2Primitives} { 1231 t.Run(prop.Name, func(t *testing.T) { 1232 err := validateNestedProperties(prop.NestedProperties, prop.Name) 1233 assert.ErrorContains(t, err, prop.Name) 1234 assert.ErrorContains(t, err, "`indexSearchable` is not allowed for other than text/text[] data types") 1235 }) 1236 } 1237 }) 1238 } 1239 }) 1240 } 1241 }) 1242 1243 t.Run("does not validate indexSearchable on nested data types", func(t *testing.T) { 1244 nestedProperties := []*models.NestedProperty{ 1245 { 1246 Name: "nested_int", 1247 DataType: schema.DataTypeInt.PropString(), 1248 IndexFilterable: &vFalse, 1249 IndexSearchable: &vFalse, 1250 Tokenization: "", 1251 }, 1252 } 1253 1254 for _, ndt := range schema.NestedDataTypes { 1255 t.Run(ndt.String(), func(t *testing.T) { 1256 propLvl2Primitives := &models.Property{ 1257 Name: "objectPropLvl2", 1258 DataType: ndt.PropString(), 1259 IndexFilterable: &vFalse, 1260 IndexSearchable: &vFalse, 1261 Tokenization: "", 1262 NestedProperties: []*models.NestedProperty{ 1263 { 1264 Name: "nested_object", 1265 DataType: ndt.PropString(), 1266 IndexFilterable: &vFalse, 1267 IndexSearchable: &vTrue, 1268 Tokenization: "", 1269 NestedProperties: nestedProperties, 1270 }, 1271 }, 1272 } 1273 1274 for _, prop := range []*models.Property{propLvl2Primitives} { 1275 t.Run(prop.Name, func(t *testing.T) { 1276 err := validateNestedProperties(prop.NestedProperties, prop.Name) 1277 assert.ErrorContains(t, err, prop.Name) 1278 assert.ErrorContains(t, err, "`indexSearchable` is not allowed for other than text/text[] data types") 1279 }) 1280 } 1281 }) 1282 } 1283 }) 1284 } 1285 1286 type fakePropertyDataType struct { 1287 primitiveDataType schema.DataType 1288 nestedDataType schema.DataType 1289 } 1290 1291 func newFakePrimitivePDT(primitiveDataType schema.DataType) schema.PropertyDataType { 1292 return &fakePropertyDataType{primitiveDataType: primitiveDataType} 1293 } 1294 1295 func newFakeNestedPDT(nestedDataType schema.DataType) schema.PropertyDataType { 1296 return &fakePropertyDataType{nestedDataType: nestedDataType} 1297 } 1298 1299 func (pdt *fakePropertyDataType) Kind() schema.PropertyKind { 1300 if pdt.IsPrimitive() { 1301 return schema.PropertyKindPrimitive 1302 } 1303 if pdt.IsNested() { 1304 return schema.PropertyKindNested 1305 } 1306 return schema.PropertyKindRef 1307 } 1308 1309 func (pdt *fakePropertyDataType) IsPrimitive() bool { 1310 return pdt.primitiveDataType != "" 1311 } 1312 1313 func (pdt *fakePropertyDataType) AsPrimitive() schema.DataType { 1314 return pdt.primitiveDataType 1315 } 1316 1317 func (pdt *fakePropertyDataType) IsReference() bool { 1318 return !pdt.IsPrimitive() && !pdt.IsNested() 1319 } 1320 1321 func (pdt *fakePropertyDataType) Classes() []schema.ClassName { 1322 if !pdt.IsReference() { 1323 return nil 1324 } 1325 return []schema.ClassName{} 1326 } 1327 1328 func (pdt *fakePropertyDataType) ContainsClass(name schema.ClassName) bool { 1329 return false 1330 } 1331 1332 func (pdt *fakePropertyDataType) IsNested() bool { 1333 return pdt.nestedDataType != "" 1334 } 1335 1336 func (pdt *fakePropertyDataType) AsNested() schema.DataType { 1337 return pdt.nestedDataType 1338 }