github.com/weaviate/weaviate@v1.24.6/usecases/objects/merge_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 objects 13 14 import ( 15 "context" 16 "encoding/json" 17 "errors" 18 "testing" 19 "time" 20 21 "github.com/go-openapi/strfmt" 22 "github.com/stretchr/testify/mock" 23 "github.com/weaviate/weaviate/entities/additional" 24 "github.com/weaviate/weaviate/entities/models" 25 "github.com/weaviate/weaviate/entities/schema/crossref" 26 "github.com/weaviate/weaviate/entities/search" 27 ) 28 29 type stage int 30 31 const ( 32 stageInit = iota 33 // stageInputValidation 34 stageAuthorization 35 stageUpdateValidation 36 stageObjectExists 37 // stageVectorization 38 // stageMerge 39 stageCount 40 ) 41 42 func Test_MergeObject(t *testing.T) { 43 t.Parallel() 44 var ( 45 uuid = strfmt.UUID("dd59815b-142b-4c54-9b12-482434bd54ca") 46 cls = "ZooAction" 47 lastTime int64 = 12345 48 errAny = errors.New("any error") 49 ) 50 51 tests := []struct { 52 name string 53 // inputs 54 previous *models.Object 55 updated *models.Object 56 vectorizerCalledWith *models.Object 57 58 // outputs 59 expectedOutput *MergeDocument 60 wantCode int 61 62 // control return errors 63 errMerge error 64 errUpdateObject error 65 errGetObject error 66 errExists error 67 stage 68 }{ 69 { 70 name: "empty class", 71 previous: nil, 72 updated: &models.Object{ 73 ID: uuid, 74 }, 75 wantCode: StatusBadRequest, 76 stage: stageInit, 77 }, 78 { 79 name: "empty uuid", 80 previous: nil, 81 updated: &models.Object{ 82 Class: cls, 83 }, 84 wantCode: StatusBadRequest, 85 stage: stageInit, 86 }, 87 { 88 name: "empty updates", 89 previous: nil, 90 wantCode: StatusBadRequest, 91 stage: stageInit, 92 }, 93 { 94 name: "object not found", 95 previous: nil, 96 updated: &models.Object{ 97 Class: cls, 98 ID: uuid, 99 Properties: map[string]interface{}{ 100 "name": "My little pony zoo with extra sparkles", 101 }, 102 }, 103 wantCode: StatusNotFound, 104 stage: stageObjectExists, 105 }, 106 { 107 name: "object failure", 108 previous: nil, 109 updated: &models.Object{ 110 Class: cls, 111 ID: uuid, 112 Properties: map[string]interface{}{ 113 "name": "My little pony zoo with extra sparkles", 114 }, 115 }, 116 wantCode: StatusInternalServerError, 117 errGetObject: errAny, 118 stage: stageObjectExists, 119 }, 120 { 121 name: "cross-ref not found", 122 previous: nil, 123 updated: &models.Object{ 124 Class: cls, 125 ID: uuid, 126 Properties: map[string]interface{}{ 127 "name": "My little pony zoo with extra sparkles", 128 "hasAnimals": []interface{}{ 129 map[string]interface{}{ 130 "beacon": "weaviate://localhost/a8ffc82c-9845-4014-876c-11369353c33c", 131 }, 132 }, 133 }, 134 }, 135 wantCode: StatusNotFound, 136 errExists: errAny, 137 stage: stageAuthorization, 138 }, 139 { 140 name: "merge failure", 141 previous: &models.Object{ 142 Class: cls, 143 Properties: map[string]interface{}{}, 144 Vectors: map[string]models.Vector{}, 145 }, 146 updated: &models.Object{ 147 Class: cls, 148 ID: uuid, 149 Properties: map[string]interface{}{ 150 "name": "My little pony zoo with extra sparkles", 151 }, 152 }, 153 vectorizerCalledWith: &models.Object{ 154 Class: cls, 155 Properties: map[string]interface{}{ 156 "name": "My little pony zoo with extra sparkles", 157 }, 158 }, 159 expectedOutput: &MergeDocument{ 160 UpdateTime: lastTime, 161 Class: cls, 162 ID: uuid, 163 Vector: []float32{1, 2, 3}, 164 PrimitiveSchema: map[string]interface{}{ 165 "name": "My little pony zoo with extra sparkles", 166 }, 167 Vectors: map[string]models.Vector{}, 168 }, 169 errMerge: errAny, 170 wantCode: StatusInternalServerError, 171 stage: stageCount, 172 }, 173 { 174 name: "vectorization failure", 175 previous: &models.Object{ 176 Class: cls, 177 Properties: map[string]interface{}{}, 178 }, 179 updated: &models.Object{ 180 Class: cls, 181 ID: uuid, 182 Properties: map[string]interface{}{ 183 "name": "My little pony zoo with extra sparkles", 184 }, 185 }, 186 vectorizerCalledWith: &models.Object{ 187 Class: cls, 188 Properties: map[string]interface{}{ 189 "name": "My little pony zoo with extra sparkles", 190 }, 191 }, 192 errUpdateObject: errAny, 193 wantCode: StatusInternalServerError, 194 stage: stageCount, 195 }, 196 { 197 name: "add property", 198 previous: &models.Object{ 199 Class: cls, 200 Properties: map[string]interface{}{}, 201 }, 202 updated: &models.Object{ 203 Class: cls, 204 ID: uuid, 205 Properties: map[string]interface{}{ 206 "name": "My little pony zoo with extra sparkles", 207 }, 208 }, 209 vectorizerCalledWith: &models.Object{ 210 Class: cls, 211 Properties: map[string]interface{}{ 212 "name": "My little pony zoo with extra sparkles", 213 }, 214 }, 215 expectedOutput: &MergeDocument{ 216 UpdateTime: lastTime, 217 Class: cls, 218 ID: uuid, 219 Vector: []float32{1, 2, 3}, 220 PrimitiveSchema: map[string]interface{}{ 221 "name": "My little pony zoo with extra sparkles", 222 }, 223 Vectors: map[string]models.Vector{}, 224 }, 225 stage: stageCount, 226 }, 227 { 228 name: "update property", 229 previous: &models.Object{ 230 Class: cls, 231 Properties: map[string]interface{}{"name": "this name"}, 232 Vector: []float32{0.7, 0.3}, 233 }, 234 updated: &models.Object{ 235 Class: cls, 236 ID: uuid, 237 Properties: map[string]interface{}{ 238 "name": "another name", 239 }, 240 }, 241 vectorizerCalledWith: &models.Object{ 242 Class: cls, 243 Properties: map[string]interface{}{ 244 "name": "another name", 245 }, 246 }, 247 expectedOutput: &MergeDocument{ 248 UpdateTime: lastTime, 249 Class: cls, 250 ID: uuid, 251 Vector: []float32{1, 2, 3}, 252 PrimitiveSchema: map[string]interface{}{ 253 "name": "another name", 254 }, 255 Vectors: map[string]models.Vector{}, 256 }, 257 stage: stageCount, 258 }, 259 { 260 name: "without properties", 261 previous: &models.Object{ 262 Class: cls, 263 }, 264 updated: &models.Object{ 265 Class: cls, 266 ID: uuid, 267 }, 268 vectorizerCalledWith: &models.Object{ 269 Class: cls, 270 Properties: map[string]interface{}{}, 271 }, 272 expectedOutput: &MergeDocument{ 273 UpdateTime: lastTime, 274 Class: cls, 275 ID: uuid, 276 Vector: []float32{1, 2, 3}, 277 PrimitiveSchema: map[string]interface{}{}, 278 Vectors: map[string]models.Vector{}, 279 }, 280 stage: stageCount, 281 }, 282 { 283 name: "add primitive properties of different types", 284 previous: &models.Object{ 285 Class: cls, 286 Properties: map[string]interface{}{}, 287 }, 288 updated: &models.Object{ 289 Class: cls, 290 ID: uuid, 291 Properties: map[string]interface{}{ 292 "name": "My little pony zoo with extra sparkles", 293 "area": 3.222, 294 "employees": json.Number("70"), 295 "located": map[string]interface{}{ 296 "latitude": 30.2, 297 "longitude": 60.2, 298 }, 299 "foundedIn": "2002-10-02T15:00:00Z", 300 }, 301 }, 302 vectorizerCalledWith: &models.Object{ 303 Class: cls, 304 Properties: map[string]interface{}{ 305 "name": "My little pony zoo with extra sparkles", 306 "area": 3.222, 307 "employees": int64(70), 308 "located": &models.GeoCoordinates{ 309 Latitude: ptFloat32(30.2), 310 Longitude: ptFloat32(60.2), 311 }, 312 "foundedIn": timeMustParse(time.RFC3339, "2002-10-02T15:00:00Z"), 313 }, 314 }, 315 expectedOutput: &MergeDocument{ 316 UpdateTime: lastTime, 317 Class: cls, 318 ID: uuid, 319 Vector: []float32{1, 2, 3}, 320 PrimitiveSchema: map[string]interface{}{ 321 "name": "My little pony zoo with extra sparkles", 322 "area": 3.222, 323 "employees": float64(70), 324 "located": &models.GeoCoordinates{ 325 Latitude: ptFloat32(30.2), 326 Longitude: ptFloat32(60.2), 327 }, 328 "foundedIn": timeMustParse(time.RFC3339, "2002-10-02T15:00:00Z"), 329 }, 330 Vectors: map[string]models.Vector{}, 331 }, 332 stage: stageCount, 333 }, 334 { 335 name: "add primitive and ref properties", 336 previous: &models.Object{ 337 Class: cls, 338 Properties: map[string]interface{}{}, 339 }, 340 updated: &models.Object{ 341 Class: cls, 342 ID: uuid, 343 Properties: map[string]interface{}{ 344 "name": "My little pony zoo with extra sparkles", 345 "hasAnimals": []interface{}{ 346 map[string]interface{}{ 347 "beacon": "weaviate://localhost/AnimalAction/a8ffc82c-9845-4014-876c-11369353c33c", 348 }, 349 }, 350 }, 351 }, 352 vectorizerCalledWith: &models.Object{ 353 Class: cls, 354 Properties: map[string]interface{}{ 355 "name": "My little pony zoo with extra sparkles", 356 }, 357 }, 358 expectedOutput: &MergeDocument{ 359 UpdateTime: lastTime, 360 Class: cls, 361 ID: uuid, 362 PrimitiveSchema: map[string]interface{}{ 363 "name": "My little pony zoo with extra sparkles", 364 }, 365 Vector: []float32{1, 2, 3}, 366 References: BatchReferences{ 367 BatchReference{ 368 From: crossrefMustParseSource("weaviate://localhost/ZooAction/dd59815b-142b-4c54-9b12-482434bd54ca/hasAnimals"), 369 To: crossrefMustParse("weaviate://localhost/AnimalAction/a8ffc82c-9845-4014-876c-11369353c33c"), 370 }, 371 }, 372 Vectors: map[string]models.Vector{}, 373 }, 374 stage: stageCount, 375 }, 376 { 377 name: "update vector non-vectorized class", 378 previous: &models.Object{ 379 Class: "NotVectorized", 380 Properties: map[string]interface{}{ 381 "description": "this description was set initially", 382 }, 383 Vector: []float32{0.7, 0.3}, 384 }, 385 updated: &models.Object{ 386 Class: "NotVectorized", 387 ID: uuid, 388 Vector: []float32{0.66, 0.22}, 389 }, 390 vectorizerCalledWith: nil, 391 expectedOutput: &MergeDocument{ 392 UpdateTime: lastTime, 393 Class: "NotVectorized", 394 ID: uuid, 395 Vector: []float32{0.66, 0.22}, 396 PrimitiveSchema: map[string]interface{}{}, 397 Vectors: map[string]models.Vector{}, 398 }, 399 stage: stageCount, 400 }, 401 { 402 name: "do not update vector non-vectorized class", 403 previous: &models.Object{ 404 Class: "NotVectorized", 405 Properties: map[string]interface{}{ 406 "description": "this description was set initially", 407 }, 408 Vector: []float32{0.7, 0.3}, 409 }, 410 updated: &models.Object{ 411 Class: "NotVectorized", 412 ID: uuid, 413 Properties: map[string]interface{}{ 414 "description": "this description was updated", 415 }, 416 }, 417 vectorizerCalledWith: nil, 418 expectedOutput: &MergeDocument{ 419 UpdateTime: lastTime, 420 Class: "NotVectorized", 421 ID: uuid, 422 Vector: []float32{0.7, 0.3}, 423 PrimitiveSchema: map[string]interface{}{ 424 "description": "this description was updated", 425 }, 426 Vectors: map[string]models.Vector{}, 427 }, 428 stage: stageCount, 429 }, 430 } 431 432 for _, tc := range tests { 433 t.Run(tc.name, func(t *testing.T) { 434 m := newFakeGetManager(zooAnimalSchemaForTest()) 435 m.timeSource = fakeTimeSource{} 436 cls := "" 437 if tc.updated != nil { 438 cls = tc.updated.Class 439 } 440 if tc.previous != nil { 441 m.repo.On("Object", cls, uuid, search.SelectProperties(nil), additional.Properties{}, ""). 442 Return(&search.Result{ 443 Schema: tc.previous.Properties, 444 ClassName: tc.previous.Class, 445 Vector: tc.previous.Vector, 446 }, nil) 447 } else if tc.stage >= stageAuthorization { 448 m.repo.On("Object", cls, uuid, search.SelectProperties(nil), additional.Properties{}, ""). 449 Return((*search.Result)(nil), tc.errGetObject) 450 } 451 452 if tc.expectedOutput != nil { 453 m.repo.On("Merge", *tc.expectedOutput).Return(tc.errMerge) 454 } 455 456 if tc.vectorizerCalledWith != nil { 457 if tc.errUpdateObject != nil { 458 m.modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)). 459 Return(nil, tc.errUpdateObject) 460 } else { 461 m.modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)). 462 Return(tc.expectedOutput.Vector, nil) 463 } 464 } 465 466 if tc.expectedOutput != nil && tc.expectedOutput.Vector != nil { 467 m.modulesProvider.On("UpdateVector", mock.Anything, mock.AnythingOfType(FindObjectFn)). 468 Return(tc.expectedOutput.Vector, tc.errUpdateObject) 469 } 470 471 // called during validation of cross-refs only. 472 m.repo.On("Exists", mock.Anything, mock.Anything).Maybe().Return(true, tc.errExists) 473 474 err := m.MergeObject(context.Background(), nil, tc.updated, nil) 475 code := 0 476 if err != nil { 477 code = err.Code 478 } 479 if tc.wantCode != code { 480 t.Fatalf("status code want: %v got: %v", tc.wantCode, code) 481 } else if code == 0 && err != nil { 482 t.Fatal(err) 483 } 484 485 m.repo.AssertExpectations(t) 486 m.modulesProvider.AssertExpectations(t) 487 }) 488 } 489 } 490 491 func timeMustParse(layout, value string) time.Time { 492 t, err := time.Parse(layout, value) 493 if err != nil { 494 panic(err) 495 } 496 return t 497 } 498 499 func crossrefMustParse(in string) *crossref.Ref { 500 ref, err := crossref.Parse(in) 501 if err != nil { 502 panic(err) 503 } 504 505 return ref 506 } 507 508 func crossrefMustParseSource(in string) *crossref.RefSource { 509 ref, err := crossref.ParseSource(in) 510 if err != nil { 511 panic(err) 512 } 513 514 return ref 515 } 516 517 type fakeTimeSource struct{} 518 519 func (f fakeTimeSource) Now() int64 { 520 return 12345 521 } 522 523 func ptFloat32(in float32) *float32 { 524 return &in 525 }