github.com/weaviate/weaviate@v1.24.6/usecases/schema/incoming_commit_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 "encoding/json" 17 "testing" 18 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 "github.com/weaviate/weaviate/entities/models" 22 "github.com/weaviate/weaviate/entities/schema" 23 "github.com/weaviate/weaviate/entities/schema/test_utils" 24 "github.com/weaviate/weaviate/usecases/cluster" 25 "github.com/weaviate/weaviate/usecases/sharding" 26 ) 27 28 func TestIncommingTxCommit(t *testing.T) { 29 type test struct { 30 name string 31 before func(t *testing.T, SM *Manager) 32 tx *cluster.Transaction 33 assertSchema func(t *testing.T, sm *Manager) 34 expectedErrContains string 35 } 36 37 vFalse := false 38 vTrue := true 39 propertyName := "object_prop" 40 objectProperty := &models.Property{ 41 Name: propertyName, 42 DataType: schema.DataTypeObject.PropString(), 43 IndexFilterable: &vTrue, 44 IndexSearchable: &vFalse, 45 Tokenization: "", 46 NestedProperties: []*models.NestedProperty{ 47 { 48 Name: "nested_int", 49 DataType: schema.DataTypeInt.PropString(), 50 IndexFilterable: &vTrue, 51 IndexSearchable: &vFalse, 52 Tokenization: "", 53 }, 54 { 55 Name: "nested_text", 56 DataType: schema.DataTypeText.PropString(), 57 IndexFilterable: &vTrue, 58 IndexSearchable: &vTrue, 59 Tokenization: models.PropertyTokenizationWord, 60 }, 61 { 62 Name: "nested_objects", 63 DataType: schema.DataTypeObjectArray.PropString(), 64 IndexFilterable: &vTrue, 65 IndexSearchable: &vFalse, 66 Tokenization: "", 67 NestedProperties: []*models.NestedProperty{ 68 { 69 Name: "nested_bool_lvl2", 70 DataType: schema.DataTypeBoolean.PropString(), 71 IndexFilterable: &vTrue, 72 IndexSearchable: &vFalse, 73 Tokenization: "", 74 }, 75 { 76 Name: "nested_numbers_lvl2", 77 DataType: schema.DataTypeNumberArray.PropString(), 78 IndexFilterable: &vTrue, 79 IndexSearchable: &vFalse, 80 Tokenization: "", 81 }, 82 }, 83 }, 84 }, 85 } 86 updatedObjectProperty := &models.Property{ 87 Name: propertyName, 88 DataType: schema.DataTypeObject.PropString(), 89 IndexFilterable: &vFalse, // different setting than existing class/prop 90 IndexSearchable: &vFalse, 91 Tokenization: "", 92 NestedProperties: []*models.NestedProperty{ 93 { 94 Name: "nested_number", 95 DataType: schema.DataTypeNumber.PropString(), 96 IndexFilterable: &vTrue, 97 IndexSearchable: &vFalse, 98 Tokenization: "", 99 }, 100 { 101 Name: "nested_text", 102 DataType: schema.DataTypeText.PropString(), 103 IndexFilterable: &vTrue, 104 IndexSearchable: &vTrue, 105 Tokenization: models.PropertyTokenizationField, // different setting than existing class/prop 106 }, 107 { 108 Name: "nested_objects", 109 DataType: schema.DataTypeObjectArray.PropString(), 110 IndexFilterable: &vTrue, 111 IndexSearchable: &vFalse, 112 Tokenization: "", 113 NestedProperties: []*models.NestedProperty{ 114 { 115 Name: "nested_date_lvl2", 116 DataType: schema.DataTypeDate.PropString(), 117 IndexFilterable: &vTrue, 118 IndexSearchable: &vFalse, 119 Tokenization: "", 120 }, 121 { 122 Name: "nested_numbers_lvl2", 123 DataType: schema.DataTypeNumberArray.PropString(), 124 IndexFilterable: &vFalse, // different setting than existing class/prop 125 IndexSearchable: &vFalse, 126 Tokenization: "", 127 }, 128 }, 129 }, 130 }, 131 } 132 expectedObjectProperty := &models.Property{ 133 Name: propertyName, 134 DataType: schema.DataTypeObject.PropString(), 135 IndexFilterable: &vTrue, 136 IndexSearchable: &vFalse, 137 Tokenization: "", 138 NestedProperties: []*models.NestedProperty{ 139 { 140 Name: "nested_int", 141 DataType: schema.DataTypeInt.PropString(), 142 IndexFilterable: &vTrue, 143 IndexSearchable: &vFalse, 144 Tokenization: "", 145 }, 146 { 147 Name: "nested_number", 148 DataType: schema.DataTypeNumber.PropString(), 149 IndexFilterable: &vTrue, 150 IndexSearchable: &vFalse, 151 Tokenization: "", 152 }, 153 { 154 Name: "nested_text", 155 DataType: schema.DataTypeText.PropString(), 156 IndexFilterable: &vTrue, 157 IndexSearchable: &vTrue, 158 Tokenization: models.PropertyTokenizationWord, // from existing class/prop 159 }, 160 { 161 Name: "nested_objects", 162 DataType: schema.DataTypeObjectArray.PropString(), 163 IndexFilterable: &vTrue, 164 IndexSearchable: &vFalse, 165 Tokenization: "", 166 NestedProperties: []*models.NestedProperty{ 167 { 168 Name: "nested_bool_lvl2", 169 DataType: schema.DataTypeBoolean.PropString(), 170 IndexFilterable: &vTrue, 171 IndexSearchable: &vFalse, 172 Tokenization: "", 173 }, 174 { 175 Name: "nested_date_lvl2", 176 DataType: schema.DataTypeDate.PropString(), 177 IndexFilterable: &vTrue, 178 IndexSearchable: &vFalse, 179 Tokenization: "", 180 }, 181 { 182 Name: "nested_numbers_lvl2", 183 DataType: schema.DataTypeNumberArray.PropString(), 184 IndexFilterable: &vTrue, // from existing class/prop 185 IndexSearchable: &vFalse, 186 Tokenization: "", 187 }, 188 }, 189 }, 190 }, 191 } 192 193 tests := []test{ 194 { 195 name: "successful add class", 196 tx: &cluster.Transaction{ 197 Type: AddClass, 198 Payload: AddClassPayload{ 199 Class: &models.Class{ 200 Class: "SecondClass", 201 VectorIndexType: "hnsw", 202 }, 203 State: &sharding.State{}, 204 }, 205 }, 206 assertSchema: func(t *testing.T, sm *Manager) { 207 class, err := sm.GetClass(context.Background(), nil, "SecondClass") 208 require.Nil(t, err) 209 assert.Equal(t, "SecondClass", class.Class) 210 }, 211 }, 212 { 213 name: "add class with incorrect payload", 214 tx: &cluster.Transaction{ 215 Type: AddClass, 216 Payload: "wrong-payload", 217 }, 218 expectedErrContains: "expected commit payload to be", 219 }, 220 { 221 name: "add class with vector parse error", 222 tx: &cluster.Transaction{ 223 Type: AddClass, 224 Payload: AddClassPayload{ 225 Class: &models.Class{ 226 Class: "SecondClass", 227 VectorIndexType: "some-weird-pq-based-index", 228 }, 229 State: &sharding.State{}, 230 }, 231 }, 232 expectedErrContains: "unsupported vector index type", 233 }, 234 { 235 name: "add class with sharding parse error", 236 tx: &cluster.Transaction{ 237 Type: AddClass, 238 Payload: AddClassPayload{ 239 Class: &models.Class{ 240 Class: "SecondClass", 241 VectorIndexType: "hnsw", 242 ShardingConfig: "this-cant-be-a-string", 243 }, 244 State: &sharding.State{}, 245 }, 246 }, 247 expectedErrContains: "parse sharding config", 248 }, 249 { 250 name: "successful add property", 251 tx: &cluster.Transaction{ 252 Type: AddProperty, 253 Payload: AddPropertyPayload{ 254 ClassName: "FirstClass", 255 Property: &models.Property{ 256 DataType: schema.DataTypeText.PropString(), 257 Tokenization: models.PropertyTokenizationWhitespace, 258 Name: "new_prop", 259 }, 260 }, 261 }, 262 assertSchema: func(t *testing.T, sm *Manager) { 263 class, err := sm.GetClass(context.Background(), nil, "FirstClass") 264 require.Nil(t, err) 265 assert.Equal(t, "new_prop", class.Properties[0].Name) 266 }, 267 }, 268 { 269 name: "add property with incorrect payload", 270 tx: &cluster.Transaction{ 271 Type: AddProperty, 272 Payload: "wrong-payload", 273 }, 274 expectedErrContains: "expected commit payload to be", 275 }, 276 { 277 name: "successful delete class", 278 tx: &cluster.Transaction{ 279 Type: DeleteClass, 280 Payload: DeleteClassPayload{ 281 ClassName: "FirstClass", 282 }, 283 }, 284 assertSchema: func(t *testing.T, sm *Manager) { 285 class, err := sm.GetClass(context.Background(), nil, "FirstClass") 286 require.Nil(t, err) 287 assert.Nil(t, class) 288 }, 289 }, 290 { 291 name: "delete class with incorrect payload", 292 tx: &cluster.Transaction{ 293 Type: DeleteClass, 294 Payload: "wrong-payload", 295 }, 296 expectedErrContains: "expected commit payload to be", 297 }, 298 { 299 name: "successful update class", 300 tx: &cluster.Transaction{ 301 Type: UpdateClass, 302 Payload: UpdateClassPayload{ 303 ClassName: "FirstClass", 304 Class: &models.Class{ 305 Class: "FirstClass", 306 VectorIndexType: "hnsw", 307 Properties: []*models.Property{ 308 { 309 Name: "added_through_update", 310 DataType: []string{"int"}, 311 }, 312 }, 313 }, 314 State: &sharding.State{}, 315 }, 316 }, 317 assertSchema: func(t *testing.T, sm *Manager) { 318 class, err := sm.GetClass(context.Background(), nil, "FirstClass") 319 require.Nil(t, err) 320 assert.Equal(t, "added_through_update", class.Properties[0].Name) 321 }, 322 }, 323 { 324 name: "update class with incorrect payload", 325 tx: &cluster.Transaction{ 326 Type: UpdateClass, 327 Payload: "wrong-payload", 328 }, 329 expectedErrContains: "expected commit payload to be", 330 }, 331 { 332 name: "update class with invalid vector index", 333 tx: &cluster.Transaction{ 334 Type: UpdateClass, 335 Payload: UpdateClassPayload{ 336 ClassName: "FirstClass", 337 Class: &models.Class{ 338 Class: "FirstClass", 339 VectorIndexType: "nope", 340 }, 341 State: &sharding.State{}, 342 }, 343 }, 344 expectedErrContains: "parse vector index", 345 }, 346 { 347 name: "update class with invalid sharding config", 348 tx: &cluster.Transaction{ 349 Type: UpdateClass, 350 Payload: UpdateClassPayload{ 351 ClassName: "FirstClass", 352 Class: &models.Class{ 353 Class: "FirstClass", 354 VectorIndexType: "hnsw", 355 ShardingConfig: "this-cant-be-a-string", 356 }, 357 State: &sharding.State{}, 358 }, 359 }, 360 expectedErrContains: "parse sharding config", 361 }, 362 { 363 name: "invalid commit type", 364 tx: &cluster.Transaction{ 365 Type: "i-dont-exist", 366 }, 367 expectedErrContains: "unrecognized commit type", 368 }, 369 370 { 371 name: "successfully add tenants", 372 tx: &cluster.Transaction{ 373 Type: addTenants, 374 Payload: AddTenantsPayload{ 375 Class: "FirstClass", 376 Tenants: []TenantCreate{{Name: "P1"}, {Name: "P2"}}, 377 }, 378 }, 379 assertSchema: func(t *testing.T, sm *Manager) { 380 st := sm.CopyShardingState("FirstClass") 381 require.NotNil(t, st) 382 require.Contains(t, st.Physical, "P1") 383 require.Contains(t, st.Physical, "P2") 384 }, 385 }, 386 { 387 name: "add partition to an unknown class", 388 tx: &cluster.Transaction{ 389 Type: addTenants, 390 Payload: AddTenantsPayload{ 391 Class: "UnknownClass", 392 Tenants: []TenantCreate{{Name: "P1"}, {Name: "P2"}}, 393 }, 394 }, 395 expectedErrContains: "UnknownClass", 396 }, 397 { 398 name: "add tenants with incorrect payload", 399 tx: &cluster.Transaction{ 400 Type: addTenants, 401 Payload: AddPropertyPayload{}, 402 }, 403 expectedErrContains: "expected commit payload to be", 404 }, 405 406 { 407 name: "successfully update tenants", 408 before: func(t *testing.T, sm *Manager) { 409 err := sm.handleCommit(context.Background(), &cluster.Transaction{ 410 Type: addTenants, 411 Payload: AddTenantsPayload{ 412 Class: "FirstClass", 413 Tenants: []TenantCreate{ 414 {Name: "P1"}, 415 {Name: "P2", Status: models.TenantActivityStatusHOT}, 416 }, 417 }, 418 }) 419 require.Nil(t, err) 420 }, 421 tx: &cluster.Transaction{ 422 Type: updateTenants, 423 Payload: UpdateTenantsPayload{ 424 Class: "FirstClass", 425 Tenants: []TenantUpdate{ 426 {Name: "P1", Status: models.TenantActivityStatusCOLD}, 427 {Name: "P2", Status: models.TenantActivityStatusCOLD}, 428 }, 429 }, 430 }, 431 assertSchema: func(t *testing.T, sm *Manager) { 432 st := sm.CopyShardingState("FirstClass") 433 require.NotNil(t, st) 434 require.Contains(t, st.Physical, "P1") 435 require.Contains(t, st.Physical, "P2") 436 assert.Equal(t, st.Physical["P1"].Status, models.TenantActivityStatusCOLD) 437 assert.Equal(t, st.Physical["P2"].Status, models.TenantActivityStatusCOLD) 438 }, 439 }, 440 { 441 name: "update tenants of unknown class", 442 tx: &cluster.Transaction{ 443 Type: updateTenants, 444 Payload: UpdateTenantsPayload{ 445 Class: "UnknownClass", 446 Tenants: []TenantUpdate{ 447 {Name: "P1", Status: models.TenantActivityStatusCOLD}, 448 {Name: "P2", Status: models.TenantActivityStatusCOLD}, 449 }, 450 }, 451 }, 452 expectedErrContains: "UnknownClass", 453 }, 454 { 455 name: "update tenants with incorrect payload", 456 tx: &cluster.Transaction{ 457 Type: updateTenants, 458 Payload: AddPropertyPayload{}, 459 }, 460 expectedErrContains: "expected commit payload to be", 461 }, 462 463 { 464 name: "merge object property of unknown class", 465 tx: &cluster.Transaction{ 466 Type: mergeObjectProperty, 467 Payload: MergeObjectPropertyPayload{ 468 ClassName: "UnknownClass", 469 Property: updatedObjectProperty, 470 }, 471 }, 472 expectedErrContains: "class not found", 473 }, 474 { 475 name: "merge object property of unknown property", 476 tx: &cluster.Transaction{ 477 Type: mergeObjectProperty, 478 Payload: MergeObjectPropertyPayload{ 479 ClassName: "FirstClass", 480 Property: updatedObjectProperty, 481 }, 482 }, 483 expectedErrContains: "property not found", 484 }, 485 { 486 name: "merge object property", 487 before: func(t *testing.T, sm *Manager) { 488 err := sm.handleCommit(context.Background(), &cluster.Transaction{ 489 Type: AddProperty, 490 Payload: AddPropertyPayload{ 491 ClassName: "FirstClass", 492 Property: objectProperty, 493 }, 494 }) 495 require.Nil(t, err) 496 }, 497 tx: &cluster.Transaction{ 498 Type: mergeObjectProperty, 499 Payload: MergeObjectPropertyPayload{ 500 ClassName: "FirstClass", 501 Property: updatedObjectProperty, 502 }, 503 }, 504 assertSchema: func(t *testing.T, sm *Manager) { 505 updatedClass := sm.getClassByName("FirstClass") 506 507 require.NotNil(t, updatedClass) 508 require.Len(t, updatedClass.Properties, 1) 509 510 mergedProperty := updatedClass.Properties[0] 511 require.NotNil(t, mergedProperty) 512 assert.Equal(t, expectedObjectProperty.DataType, mergedProperty.DataType) 513 assert.Equal(t, expectedObjectProperty.IndexFilterable, mergedProperty.IndexFilterable) 514 assert.Equal(t, expectedObjectProperty.IndexSearchable, mergedProperty.IndexSearchable) 515 assert.Equal(t, expectedObjectProperty.Tokenization, mergedProperty.Tokenization) 516 517 test_utils.AssertNestedPropsMatch(t, expectedObjectProperty.NestedProperties, mergedProperty.NestedProperties) 518 }, 519 }, 520 { 521 name: "merge object property with invalid payload", 522 tx: &cluster.Transaction{ 523 Type: mergeObjectProperty, 524 Payload: AddPropertyPayload{ 525 ClassName: "FirstClass", 526 Property: updatedObjectProperty, 527 }, 528 }, 529 expectedErrContains: "expected commit payload to be", 530 }, 531 } 532 533 for _, test := range tests { 534 t.Run(test.name, func(t *testing.T) { 535 schemaBefore := &State{ 536 ObjectSchema: &models.Schema{ 537 Classes: []*models.Class{ 538 { 539 Class: "FirstClass", 540 VectorIndexType: "hnsw", 541 }, 542 }, 543 }, 544 } 545 sm, err := newManagerWithClusterAndTx(t, 546 &fakeClusterState{hosts: []string{"node1"}}, &fakeTxClient{}, 547 schemaBefore) 548 require.Nil(t, err) 549 550 if test.before != nil { 551 test.before(t, sm) 552 } 553 554 err = sm.handleCommit(context.Background(), test.tx) 555 if test.expectedErrContains == "" { 556 require.Nil(t, err) 557 test.assertSchema(t, sm) 558 } else { 559 require.NotNil(t, err) 560 assert.Contains(t, err.Error(), test.expectedErrContains) 561 } 562 }) 563 } 564 } 565 566 func TestTxResponse(t *testing.T) { 567 type test struct { 568 name string 569 tx *cluster.Transaction 570 assertTx func(t *testing.T, tx *cluster.Transaction, payload json.RawMessage) 571 } 572 573 tests := []test{ 574 { 575 name: "ignore write transactions", 576 tx: &cluster.Transaction{ 577 Type: AddClass, 578 Payload: AddClassPayload{ 579 Class: &models.Class{ 580 Class: "SecondClass", 581 VectorIndexType: "hnsw", 582 }, 583 State: &sharding.State{}, 584 }, 585 }, 586 assertTx: func(t *testing.T, tx *cluster.Transaction, payload json.RawMessage) { 587 _, ok := tx.Payload.(AddClassPayload) 588 assert.True(t, ok, "write tx was not changed") 589 }, 590 }, 591 { 592 name: "respond with schema on ReadSchema", 593 tx: &cluster.Transaction{ 594 Type: ReadSchema, 595 Payload: nil, 596 }, 597 assertTx: func(t *testing.T, tx *cluster.Transaction, payload json.RawMessage) { 598 pl, err := unmarshalRawJson[ReadSchemaPayload](payload) 599 require.Nil(t, err) 600 require.Len(t, pl.Schema.ObjectSchema.Classes, 1) 601 assert.Equal(t, "FirstClass", pl.Schema.ObjectSchema.Classes[0].Class) 602 }, 603 }, 604 } 605 606 for _, test := range tests { 607 t.Run(test.name, func(t *testing.T) { 608 schemaBefore := &State{ 609 ObjectSchema: &models.Schema{ 610 Classes: []*models.Class{ 611 { 612 Class: "FirstClass", 613 VectorIndexType: "hnsw", 614 }, 615 }, 616 }, 617 } 618 sm, err := newManagerWithClusterAndTx(t, 619 &fakeClusterState{hosts: []string{"node1"}}, &fakeTxClient{}, 620 schemaBefore) 621 require.Nil(t, err) 622 623 data, err := sm.handleTxResponse(context.Background(), test.tx) 624 require.Nil(t, err) 625 if test.tx.Type == ReadSchema { 626 var txRes txResponsePayload 627 err = json.Unmarshal(data, &txRes) 628 require.Nil(t, err) 629 test.assertTx(t, test.tx, txRes.Payload) 630 631 } 632 }) 633 } 634 } 635 636 type txResponsePayload struct { 637 Type cluster.TransactionType `json:"type"` 638 ID string `json:"id"` 639 Payload json.RawMessage `json:"payload"` 640 }