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  }