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 }