github.com/weaviate/weaviate@v1.24.6/test/acceptance/schema/add_class_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 test
    13  
    14  import (
    15  	"fmt"
    16  	"testing"
    17  
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  
    21  	clschema "github.com/weaviate/weaviate/client/schema"
    22  	"github.com/weaviate/weaviate/entities/models"
    23  	"github.com/weaviate/weaviate/entities/schema"
    24  	"github.com/weaviate/weaviate/test/helper"
    25  )
    26  
    27  // this test prevents a regression on
    28  // https://github.com/weaviate/weaviate/issues/981
    29  func TestInvalidDataTypeInProperty(t *testing.T) {
    30  	t.Parallel()
    31  	className := "WrongPropertyClass"
    32  
    33  	t.Run("asserting that this class does not exist yet", func(t *testing.T) {
    34  		assert.NotContains(t, GetObjectClassNames(t), className)
    35  	})
    36  
    37  	t.Run("trying to import empty string as data type", func(t *testing.T) {
    38  		c := &models.Class{
    39  			Class: className,
    40  			Properties: []*models.Property{
    41  				{
    42  					Name:     "someProperty",
    43  					DataType: []string{""},
    44  				},
    45  			},
    46  		}
    47  
    48  		params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(c)
    49  		resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
    50  		helper.AssertRequestFail(t, resp, err, func() {
    51  			parsed, ok := err.(*clschema.SchemaObjectsCreateUnprocessableEntity)
    52  			require.True(t, ok, "error should be unprocessable entity")
    53  			assert.Equal(t, "property 'someProperty': invalid dataType: dataType cannot be an empty string",
    54  				parsed.Payload.Error[0].Message)
    55  		})
    56  	})
    57  }
    58  
    59  func TestInvalidPropertyName(t *testing.T) {
    60  	t.Parallel()
    61  	className := "WrongPropertyClass"
    62  
    63  	t.Run("asserting that this class does not exist yet", func(t *testing.T) {
    64  		assert.NotContains(t, GetObjectClassNames(t), className)
    65  	})
    66  
    67  	t.Run("trying to create class with invalid property name", func(t *testing.T) {
    68  		c := &models.Class{
    69  			Class: className,
    70  			Properties: []*models.Property{
    71  				{
    72  					Name:         "some-property",
    73  					DataType:     schema.DataTypeText.PropString(),
    74  					Tokenization: models.PropertyTokenizationWhitespace,
    75  				},
    76  			},
    77  		}
    78  
    79  		params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(c)
    80  		resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
    81  		helper.AssertRequestFail(t, resp, err, func() {
    82  			parsed, ok := err.(*clschema.SchemaObjectsCreateUnprocessableEntity)
    83  			require.True(t, ok, "error should be unprocessable entity")
    84  			assert.Equal(t, "'some-property' is not a valid property name. Property names in Weaviate "+
    85  				"are restricted to valid GraphQL names, which must be “/[_A-Za-z][_0-9A-Za-z]{0,230}/”.",
    86  				parsed.Payload.Error[0].Message)
    87  		})
    88  	})
    89  }
    90  
    91  func TestAddAndRemoveObjectClass(t *testing.T) {
    92  	randomObjectClassName := "YellowCars"
    93  
    94  	// Ensure that this name is not in the schema yet.
    95  	t.Log("Asserting that this class does not exist yet")
    96  	assert.NotContains(t, GetObjectClassNames(t), randomObjectClassName)
    97  
    98  	tc := &models.Class{
    99  		Class: randomObjectClassName,
   100  		ModuleConfig: map[string]interface{}{
   101  			"text2vec-contextionary": map[string]interface{}{
   102  				"vectorizeClassName": true,
   103  			},
   104  		},
   105  	}
   106  
   107  	t.Log("Creating class")
   108  	params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(tc)
   109  	resp, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
   110  	helper.AssertRequestOk(t, resp, err, nil)
   111  
   112  	t.Log("Asserting that this class is now created")
   113  	assert.Contains(t, GetObjectClassNames(t), randomObjectClassName)
   114  
   115  	t.Run("pure http - without the auto-generated client", testGetSchemaWithoutClient)
   116  
   117  	// Now clean up this class.
   118  	t.Log("Remove the class")
   119  	delParams := clschema.NewSchemaObjectsDeleteParams().WithClassName(randomObjectClassName)
   120  	delResp, err := helper.Client(t).Schema.SchemaObjectsDelete(delParams, nil)
   121  	helper.AssertRequestOk(t, delResp, err, nil)
   122  
   123  	// And verify that the class does not exist anymore.
   124  	assert.NotContains(t, GetObjectClassNames(t), randomObjectClassName)
   125  
   126  	t.Log("Verify schema cluster status")
   127  	statusResp, err := helper.Client(t).Schema.SchemaClusterStatus(
   128  		clschema.NewSchemaClusterStatusParams(), nil,
   129  	)
   130  	require.Nil(t, err)
   131  	assert.Equal(t, "", statusResp.Payload.Error)
   132  	assert.True(t, statusResp.Payload.Healthy)
   133  }
   134  
   135  // This test prevents a regression on
   136  // https://github.com/weaviate/weaviate/issues/1799
   137  //
   138  // This was related to adding ref props. For example in the case of a circular
   139  // dependency (A<>B), users would typically add A without refs, then add B with
   140  // a reference back to A, finally update A with a ref to B.
   141  //
   142  // This last update that would set the ref prop on an existing class was missing
   143  // module-specific defaults. So when comparing to-be-updated to existing we would
   144  // find differences in the properties, thus triggering the above error.
   145  func TestUpdateHNSWSettingsAfterAddingRefProps(t *testing.T) {
   146  	className := "RefUpdateIssueClass"
   147  
   148  	t.Run("asserting that this class does not exist yet", func(t *testing.T) {
   149  		assert.NotContains(t, GetObjectClassNames(t), className)
   150  	})
   151  
   152  	defer func(t *testing.T) {
   153  		params := clschema.NewSchemaObjectsDeleteParams().WithClassName(className)
   154  		_, err := helper.Client(t).Schema.SchemaObjectsDelete(params, nil)
   155  		assert.Nil(t, err)
   156  		if err != nil {
   157  			if typed, ok := err.(*clschema.SchemaObjectsDeleteBadRequest); ok {
   158  				fmt.Println(typed.Payload.Error[0].Message)
   159  			}
   160  		}
   161  	}(t)
   162  
   163  	t.Run("initially creating the class", func(t *testing.T) {
   164  		c := &models.Class{
   165  			Class: className,
   166  			Properties: []*models.Property{
   167  				{
   168  					Name:         "string_prop",
   169  					DataType:     schema.DataTypeText.PropString(),
   170  					Tokenization: models.PropertyTokenizationWhitespace,
   171  				},
   172  			},
   173  		}
   174  
   175  		params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(c)
   176  		_, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
   177  		assert.Nil(t, err)
   178  	})
   179  
   180  	t.Run("adding a ref prop after the fact", func(t *testing.T) {
   181  		params := clschema.NewSchemaObjectsPropertiesAddParams().
   182  			WithClassName(className).
   183  			WithBody(&models.Property{
   184  				DataType: []string{className},
   185  				Name:     "ref_prop",
   186  			})
   187  		_, err := helper.Client(t).Schema.SchemaObjectsPropertiesAdd(params, nil)
   188  		assert.Nil(t, err)
   189  	})
   190  
   191  	t.Run("obtaining the class, making an innocent change and trying to update it", func(t *testing.T) {
   192  		params := clschema.NewSchemaObjectsGetParams().
   193  			WithClassName(className)
   194  		res, err := helper.Client(t).Schema.SchemaObjectsGet(params, nil)
   195  		require.Nil(t, err)
   196  
   197  		class := res.Payload
   198  
   199  		class.VectorIndexConfig.(map[string]interface{})["ef"] = float64(1234)
   200  
   201  		updateParams := clschema.NewSchemaObjectsUpdateParams().
   202  			WithClassName(className).
   203  			WithObjectClass(class)
   204  		_, err = helper.Client(t).Schema.SchemaObjectsUpdate(updateParams, nil)
   205  		assert.Nil(t, err)
   206  	})
   207  
   208  	t.Run("obtaining the class, making a change to IndexNullState (immutable) property and update", func(t *testing.T) {
   209  		params := clschema.NewSchemaObjectsGetParams().
   210  			WithClassName(className)
   211  		res, err := helper.Client(t).Schema.SchemaObjectsGet(params, nil)
   212  		require.Nil(t, err)
   213  
   214  		class := res.Payload
   215  
   216  		// IndexNullState cannot be updated during runtime
   217  		class.InvertedIndexConfig.IndexNullState = true
   218  		updateParams := clschema.NewSchemaObjectsUpdateParams().
   219  			WithClassName(className).
   220  			WithObjectClass(class)
   221  		_, err = helper.Client(t).Schema.SchemaObjectsUpdate(updateParams, nil)
   222  		assert.NotNil(t, err)
   223  	})
   224  
   225  	t.Run("obtaining the class, making a change to IndexPropertyLength (immutable) property and update", func(t *testing.T) {
   226  		params := clschema.NewSchemaObjectsGetParams().
   227  			WithClassName(className)
   228  		res, err := helper.Client(t).Schema.SchemaObjectsGet(params, nil)
   229  		require.Nil(t, err)
   230  
   231  		class := res.Payload
   232  
   233  		// IndexPropertyLength cannot be updated during runtime
   234  		class.InvertedIndexConfig.IndexPropertyLength = true
   235  		updateParams := clschema.NewSchemaObjectsUpdateParams().
   236  			WithClassName(className).
   237  			WithObjectClass(class)
   238  		_, err = helper.Client(t).Schema.SchemaObjectsUpdate(updateParams, nil)
   239  		assert.NotNil(t, err)
   240  	})
   241  }
   242  
   243  // This test prevents a regression of
   244  // https://github.com/weaviate/weaviate/issues/2692
   245  //
   246  // In this issue, any time a class had no vector index set, any other update to
   247  // the class would be blocked
   248  func TestUpdateClassWithoutVectorIndex(t *testing.T) {
   249  	className := "IAintGotNoVectorIndex"
   250  
   251  	t.Run("asserting that this class does not exist yet", func(t *testing.T) {
   252  		assert.NotContains(t, GetObjectClassNames(t), className)
   253  	})
   254  
   255  	defer func(t *testing.T) {
   256  		params := clschema.NewSchemaObjectsDeleteParams().WithClassName(className)
   257  		_, err := helper.Client(t).Schema.SchemaObjectsDelete(params, nil)
   258  		assert.Nil(t, err)
   259  		if err != nil {
   260  			if typed, ok := err.(*clschema.SchemaObjectsDeleteBadRequest); ok {
   261  				fmt.Println(typed.Payload.Error[0].Message)
   262  			}
   263  		}
   264  	}(t)
   265  
   266  	t.Run("initially creating the class", func(t *testing.T) {
   267  		c := &models.Class{
   268  			Class: className,
   269  			InvertedIndexConfig: &models.InvertedIndexConfig{
   270  				Stopwords: &models.StopwordConfig{
   271  					Preset: "en",
   272  				},
   273  			},
   274  			Properties: []*models.Property{
   275  				{
   276  					Name:     "text_prop",
   277  					DataType: []string{"text"},
   278  				},
   279  			},
   280  			VectorIndexConfig: map[string]interface{}{
   281  				"skip": true,
   282  			},
   283  		}
   284  
   285  		params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(c)
   286  		_, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
   287  		assert.Nil(t, err)
   288  	})
   289  
   290  	t.Run("obtaining the class, making an innocent change and trying to update it", func(t *testing.T) {
   291  		params := clschema.NewSchemaObjectsGetParams().
   292  			WithClassName(className)
   293  		res, err := helper.Client(t).Schema.SchemaObjectsGet(params, nil)
   294  		require.Nil(t, err)
   295  
   296  		class := res.Payload
   297  
   298  		class.InvertedIndexConfig.Stopwords.Preset = "none"
   299  
   300  		updateParams := clschema.NewSchemaObjectsUpdateParams().
   301  			WithClassName(className).
   302  			WithObjectClass(class)
   303  		_, err = helper.Client(t).Schema.SchemaObjectsUpdate(updateParams, nil)
   304  		assert.Nil(t, err)
   305  	})
   306  }
   307  
   308  // This test prevents a regression of
   309  // https://github.com/weaviate/weaviate/issues//3177
   310  //
   311  // This test ensures that distance belongs to the immutable properties, i.e. no changes to it are possible after creating the class.
   312  func TestUpdateDistanceSettings(t *testing.T) {
   313  	className := "Cosine_Class"
   314  
   315  	t.Run("asserting that this class does not exist yet", func(t *testing.T) {
   316  		assert.NotContains(t, GetObjectClassNames(t), className)
   317  	})
   318  
   319  	defer func(t *testing.T) {
   320  		params := clschema.NewSchemaObjectsDeleteParams().WithClassName(className)
   321  		_, err := helper.Client(t).Schema.SchemaObjectsDelete(params, nil)
   322  		assert.Nil(t, err)
   323  		if err != nil {
   324  			if typed, ok := err.(*clschema.SchemaObjectsDeleteBadRequest); ok {
   325  				fmt.Println(typed.Payload.Error[0].Message)
   326  			}
   327  		}
   328  	}(t)
   329  
   330  	t.Run("initially creating the class", func(t *testing.T) {
   331  		c := &models.Class{
   332  			Class:      className,
   333  			Vectorizer: "none",
   334  			Properties: []*models.Property{
   335  				{
   336  					Name:         "name",
   337  					DataType:     schema.DataTypeText.PropString(),
   338  					Tokenization: models.PropertyTokenizationWhitespace,
   339  				},
   340  			},
   341  			VectorIndexConfig: map[string]interface{}{
   342  				"distance": "cosine",
   343  			},
   344  		}
   345  
   346  		params := clschema.NewSchemaObjectsCreateParams().WithObjectClass(c)
   347  		_, err := helper.Client(t).Schema.SchemaObjectsCreate(params, nil)
   348  		assert.Nil(t, err)
   349  	})
   350  
   351  	t.Run("Trying to change the distance measurement", func(t *testing.T) {
   352  		params := clschema.NewSchemaObjectsGetParams().
   353  			WithClassName(className)
   354  		res, err := helper.Client(t).Schema.SchemaObjectsGet(params, nil)
   355  		require.Nil(t, err)
   356  
   357  		class := res.Payload
   358  
   359  		class.VectorIndexConfig.(map[string]interface{})["distance"] = "l2-squared"
   360  
   361  		updateParams := clschema.NewSchemaObjectsUpdateParams().
   362  			WithClassName(className).
   363  			WithObjectClass(class)
   364  		_, err = helper.Client(t).Schema.SchemaObjectsUpdate(updateParams, nil)
   365  		assert.NotNil(t, err)
   366  	})
   367  }
   368  
   369  // TODO: https://github.com/weaviate/weaviate/issues/973
   370  // // This test prevents a regression on the fix for this bug:
   371  // // https://github.com/weaviate/weaviate/issues/831
   372  // func TestDeleteSingleProperties(t *testing.T) {
   373  // 	t.Parallel()
   374  
   375  // 	randomObjectClassName := "RedShip"
   376  
   377  // 	// Ensure that this name is not in the schema yet.
   378  // 	t.Log("Asserting that this class does not exist yet")
   379  // 	assert.NotContains(t, GetThingClassNames(t), randomThingClassName)
   380  
   381  // 	tc := &models.Class{
   382  // 		Class: randomThingClassName,
   383  // 		Properties: []*models.Property{
   384  // 			&models.Property{
   385  // DataType:     schema.DataTypeText.PropString(),
   386  // Tokenization: models.PropertyTokenizationWhitespace,
   387  // 				Name:     "name",
   388  // 			},
   389  // 			&models.Property{
   390  // DataType:     schema.DataTypeText.PropString(),
   391  // Tokenization: models.PropertyTokenizationWhitespace,
   392  // 				Name:     "description",
   393  // 			},
   394  // 		},
   395  // 	}
   396  
   397  // 	t.Log("Creating class")
   398  // 	params := clschema.NewSchemaThingsCreateParams().WithThingClass(tc)
   399  // 	resp, err := helper.Client(t).Schema.SchemaThingsCreate(params, nil)
   400  // 	helper.AssertRequestOk(t, resp, err, nil)
   401  
   402  // 	t.Log("Asserting that this class is now created")
   403  // 	assert.Contains(t, GetThingClassNames(t), randomThingClassName)
   404  
   405  // 	t.Log("adding an instance of this particular class that uses both properties")
   406  // 	instanceParams := things.NewThingsCreateParams().WithBody(
   407  // 		&models.Thing{
   408  // 			Class: randomThingClassName,
   409  // 			Schema: map[string]interface{}{
   410  // 				"name":        "my name",
   411  // 				"description": "my description",
   412  // 			},
   413  // 		})
   414  // 	instanceRes, err := helper.Client(t).Things.ThingsCreate(instanceParams, nil)
   415  // 	assert.Nil(t, err, "adding a class instance should not error")
   416  
   417  // 	t.Log("delete a single property of the class")
   418  // 	deleteParams := clschema.NewSchemaThingsPropertiesDeleteParams().
   419  // 		WithClassName(randomThingClassName).
   420  // 		WithPropertyName("description")
   421  // 	_, err = helper.Client(t).Schema.SchemaThingsPropertiesDelete(deleteParams, nil)
   422  // 	assert.Nil(t, err, "deleting the property should not error")
   423  
   424  // 	t.Log("retrieve the class and make sure the property is gone")
   425  // 	thing := assertGetThingEventually(t, instanceRes.Payload.ID)
   426  // 	expectedSchema := map[string]interface{}{
   427  // 		"name": "my name",
   428  // 	}
   429  // 	assert.Equal(t, expectedSchema, thing.Schema)
   430  
   431  // 	t.Log("verifying that we can still retrieve the thing through graphQL")
   432  // 	result := gql.AssertGraphQL(t, helper.RootAuth, "{  Get { Things { RedShip { name } } } }")
   433  // 	ships := result.Get("Get", "Things", "RedShip").AsSlice()
   434  // 	expectedShip := map[string]interface{}{
   435  // 		"name": "my name",
   436  // 	}
   437  // 	assert.Contains(t, ships, expectedShip)
   438  
   439  // 	t.Log("verifying other GQL/REST queries still work")
   440  // 	gql.AssertGraphQL(t, helper.RootAuth, "{  Meta { Things { RedShip { name { count } } } } }")
   441  // 	gql.AssertGraphQL(t, helper.RootAuth, `{  Aggregate { Things { RedShip(groupBy: ["name"]) { name { count } } } } }`)
   442  // 	_, err = helper.Client(t).Things.ThingsList(things.NewThingsListParams(), nil)
   443  // 	assert.Nil(t, err, "listing things should not error")
   444  
   445  // 	t.Log("verifying we could re-add the property with the same name")
   446  // 	readdParams := clschema.NewSchemaThingsPropertiesAddParams().
   447  // 		WithClassName(randomThingClassName).
   448  // 		WithBody(&models.Property{
   449  // 			Name:     "description",
   450  // DataType:     schema.DataTypeText.PropString(),
   451  // Tokenization: models.PropertyTokenizationWhitespace,
   452  // 		})
   453  
   454  // 	_, err = helper.Client(t).Schema.SchemaThingsPropertiesAdd(readdParams, nil)
   455  // 	assert.Nil(t, err, "adding the previously deleted property again should not error")
   456  
   457  // 	// Now clean up this class.
   458  // 	t.Log("Remove the class")
   459  // 	delParams := clschema.NewSchemaThingsDeleteParams().WithClassName(randomThingClassName)
   460  // 	delResp, err := helper.Client(t).Schema.SchemaThingsDelete(delParams, nil)
   461  // 	helper.AssertRequestOk(t, delResp, err, nil)
   462  
   463  // 	// And verify that the class does not exist anymore.
   464  // 	assert.NotContains(t, GetThingClassNames(t), randomThingClassName)
   465  // }