github.com/weaviate/weaviate@v1.24.6/usecases/schema/add_property.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  	"fmt"
    18  	"strings"
    19  
    20  	"github.com/weaviate/weaviate/entities/models"
    21  	"github.com/weaviate/weaviate/entities/schema"
    22  )
    23  
    24  // AddClassProperty to an existing Class
    25  func (m *Manager) AddClassProperty(ctx context.Context, principal *models.Principal,
    26  	class string, property *models.Property,
    27  ) error {
    28  	err := m.Authorizer.Authorize(principal, "update", "schema/objects")
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	if property.Name == "" {
    34  		return fmt.Errorf("property must contain name")
    35  	}
    36  	if property.DataType == nil {
    37  		return fmt.Errorf("property must contain dataType")
    38  	}
    39  
    40  	return m.addClassProperty(ctx, class, property)
    41  }
    42  
    43  func (m *Manager) addClassProperty(ctx context.Context,
    44  	className string, prop *models.Property,
    45  ) error {
    46  	m.Lock()
    47  	defer m.Unlock()
    48  
    49  	class, err := m.schemaCache.readOnlyClass(className)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	prop.Name = schema.LowercaseFirstLetter(prop.Name)
    54  
    55  	existingPropertyNames := map[string]bool{}
    56  	for _, existingProperty := range class.Properties {
    57  		existingPropertyNames[strings.ToLower(existingProperty.Name)] = true
    58  	}
    59  
    60  	m.setNewPropDefaults(class, prop)
    61  	if err := m.validateProperty(prop, class, existingPropertyNames, false); err != nil {
    62  		return err
    63  	}
    64  	// migrate only after validation in completed
    65  	migratePropertySettings(prop)
    66  
    67  	tx, err := m.cluster.BeginTransaction(ctx, AddProperty,
    68  		AddPropertyPayload{className, prop}, DefaultTxTTL)
    69  	if err != nil {
    70  		// possible causes for errors could be nodes down (we expect every node to
    71  		// the up for a schema transaction) or concurrent transactions from other
    72  		// nodes
    73  		return fmt.Errorf("open cluster-wide transaction: %w", err)
    74  	}
    75  
    76  	if err = m.cluster.CommitWriteTransaction(ctx, tx); err != nil {
    77  		// Only log the commit error, but do not abort the changes locally. Once
    78  		// we've told others to commit, we also need to commit ourselves!
    79  		//
    80  		// The idea is that if we abort our changes we are guaranteed to create an
    81  		// inconsistency as soon as any other node honored the commit. This would
    82  		// for example be the case in a 3-node cluster where node 1 is the
    83  		// coordinator, node 2 honored the commit and node 3 died during the commit
    84  		// phase.
    85  		//
    86  		// In this scenario it is far more desirable to make sure that node 1 and
    87  		// node 2 stay in sync, as node 3 - who may or may not have missed the
    88  		// update - can use a local WAL from the first TX phase to replay any
    89  		// missing changes once it's back.
    90  		m.logger.WithError(err).Errorf("not every node was able to commit")
    91  	}
    92  
    93  	return m.addClassPropertyApplyChanges(ctx, className, prop)
    94  }
    95  
    96  func (m *Manager) setNewPropDefaults(class *models.Class, prop *models.Property) {
    97  	setPropertyDefaults(prop)
    98  	m.moduleConfig.SetSinglePropertyDefaults(class, prop)
    99  }
   100  
   101  func (m *Manager) validatePropModuleConfig(class *models.Class, prop *models.Property) error {
   102  	if prop.ModuleConfig == nil {
   103  		return nil
   104  	}
   105  	modconfig, ok := prop.ModuleConfig.(map[string]interface{})
   106  	if !ok {
   107  		return fmt.Errorf("%v property config invalid", prop.Name)
   108  	}
   109  
   110  	if !hasTargetVectors(class) {
   111  		configuredVectorizers := make([]string, 0, len(modconfig))
   112  		for modName := range modconfig {
   113  			if err := m.vectorizerValidator.ValidateVectorizer(modName); err == nil {
   114  				configuredVectorizers = append(configuredVectorizers, modName)
   115  			}
   116  		}
   117  		if len(configuredVectorizers) > 1 {
   118  			return fmt.Errorf("multiple vectorizers configured in property's %q moduleConfig: %v. class.vectorizer is set to %q",
   119  				prop.Name, configuredVectorizers, class.Vectorizer)
   120  		}
   121  
   122  		vectorizerConfig, ok := modconfig[class.Vectorizer]
   123  		if !ok {
   124  			if class.Vectorizer == "none" {
   125  				return nil
   126  			}
   127  			return fmt.Errorf("%v vectorizer module not part of the property", class.Vectorizer)
   128  		}
   129  		_, ok = vectorizerConfig.(map[string]interface{})
   130  		if !ok {
   131  			return fmt.Errorf("vectorizer config for vectorizer %v, not of type map[string]interface{}", class.Vectorizer)
   132  		}
   133  		return nil
   134  	}
   135  
   136  	// TODO reuse for multiple props?
   137  	vectorizersSet := map[string]struct{}{}
   138  	for _, cfg := range class.VectorConfig {
   139  		if vm, ok := cfg.Vectorizer.(map[string]interface{}); ok && len(vm) == 1 {
   140  			for vectorizer := range vm {
   141  				vectorizersSet[vectorizer] = struct{}{}
   142  			}
   143  		}
   144  	}
   145  	for vectorizer, cfg := range modconfig {
   146  		if _, ok := vectorizersSet[vectorizer]; !ok {
   147  			return fmt.Errorf("vectorizer %q not configured for any of target vectors", vectorizer)
   148  		}
   149  		if _, ok := cfg.(map[string]interface{}); !ok {
   150  			return fmt.Errorf("vectorizer config for vectorizer %q not of type map[string]interface{}", vectorizer)
   151  		}
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  func (m *Manager) addClassPropertyApplyChanges(ctx context.Context,
   158  	className string, prop *models.Property,
   159  ) error {
   160  	class, err := m.schemaCache.addProperty(className, prop)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	metadata, err := json.Marshal(&class)
   165  	if err != nil {
   166  		return fmt.Errorf("marshal class %s: %w", className, err)
   167  	}
   168  	m.logger.
   169  		WithField("action", "schema.add_property").
   170  		Debug("saving updated schema to configuration store")
   171  	err = m.repo.UpdateClass(ctx, ClassPayload{Name: className, Metadata: metadata})
   172  	if err != nil {
   173  		return err
   174  	}
   175  	m.triggerSchemaUpdateCallbacks()
   176  
   177  	// will result in a mismatch between schema and index if function below fails
   178  	return m.migrator.AddProperty(ctx, className, prop)
   179  }