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  }