github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/alterschema.go (about) 1 // Copyright 2022 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package sqle 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "strings" 22 23 "github.com/dolthub/go-mysql-server/sql" 24 25 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 26 "github.com/dolthub/dolt/go/libraries/doltcore/schema" 27 "github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo" 28 ) 29 30 // renameTable renames a table with in a RootValue and returns the updated root. 31 func renameTable(ctx context.Context, root doltdb.RootValue, oldName, newName string) (doltdb.RootValue, error) { 32 if newName == oldName { 33 return root, nil 34 } else if root == nil { 35 panic("invalid parameters") 36 } 37 38 return root.RenameTable(ctx, oldName, newName) 39 } 40 41 // Nullable represents whether a column can have a null value. 42 type Nullable bool 43 44 const ( 45 NotNull Nullable = false 46 Null Nullable = true 47 ) 48 49 // addColumnToTable adds a new column to the schema given and returns the new table value. Non-null column additions 50 // rewrite the entire table, since we must write a value for each row. If the column is not nullable, a default value 51 // must be provided. 52 // 53 // Returns an error if the column added conflicts with the existing schema in tag or name. 54 func addColumnToTable( 55 ctx context.Context, 56 root doltdb.RootValue, 57 tbl *doltdb.Table, 58 tblName string, 59 tag uint64, 60 newColName string, 61 typeInfo typeinfo.TypeInfo, 62 nullable Nullable, 63 defaultVal *sql.ColumnDefaultValue, 64 comment string, 65 order *sql.ColumnOrder, 66 ) (*doltdb.Table, error) { 67 oldSchema, err := tbl.GetSchema(ctx) 68 if err != nil { 69 return nil, err 70 } 71 72 if err := validateNewColumn(ctx, root, tbl, tblName, tag, newColName, typeInfo); err != nil { 73 return nil, err 74 } 75 76 newCol, err := createColumn(nullable, newColName, tag, typeInfo, defaultVal.String(), comment) 77 if err != nil { 78 return nil, err 79 } 80 81 newSchema, err := oldSchema.AddColumn(newCol, orderToOrder(order)) 82 if err != nil { 83 return nil, err 84 } 85 86 newTable, err := tbl.UpdateSchema(ctx, newSchema) 87 if err != nil { 88 return nil, err 89 } 90 91 // TODO: we do a second pass in the engine to set a default if there is one. We should only do a single table scan. 92 return newTable.AddColumnToRows(ctx, newColName, newSchema) 93 } 94 95 func orderToOrder(order *sql.ColumnOrder) *schema.ColumnOrder { 96 if order == nil { 97 return nil 98 } 99 return &schema.ColumnOrder{ 100 First: order.First, 101 AfterColumn: order.AfterColumn, 102 } 103 } 104 105 func createColumn(nullable Nullable, newColName string, tag uint64, typeInfo typeinfo.TypeInfo, defaultVal, comment string) (schema.Column, error) { 106 if nullable { 107 return schema.NewColumnWithTypeInfo(newColName, tag, typeInfo, false, defaultVal, false, comment) 108 } else { 109 return schema.NewColumnWithTypeInfo(newColName, tag, typeInfo, false, defaultVal, false, comment, schema.NotNullConstraint{}) 110 } 111 } 112 113 // ValidateNewColumn returns an error if the column as specified cannot be added to the schema given. 114 func validateNewColumn( 115 ctx context.Context, 116 root doltdb.RootValue, 117 tbl *doltdb.Table, 118 tblName string, 119 tag uint64, 120 newColName string, 121 typeInfo typeinfo.TypeInfo, 122 ) error { 123 if typeInfo == nil { 124 return fmt.Errorf(`typeinfo may not be nil`) 125 } 126 127 sch, err := tbl.GetSchema(ctx) 128 129 if err != nil { 130 return err 131 } 132 133 cols := sch.GetAllCols() 134 err = cols.Iter(func(currColTag uint64, currCol schema.Column) (stop bool, err error) { 135 if currColTag == tag { 136 return false, schema.ErrTagPrevUsed(tag, newColName, tblName, tblName) 137 } else if strings.ToLower(currCol.Name) == strings.ToLower(newColName) { 138 return true, fmt.Errorf("A column with the name %s already exists in table %s.", newColName, tblName) 139 } 140 141 return false, nil 142 }) 143 144 if err != nil { 145 return err 146 } 147 148 _, oldTblName, found, err := doltdb.GetTableByColTag(ctx, root, tag) 149 if err != nil { 150 return err 151 } 152 if found { 153 return schema.ErrTagPrevUsed(tag, newColName, tblName, oldTblName) 154 } 155 156 return nil 157 } 158 159 var ErrPrimaryKeySetsIncompatible = errors.New("primary key sets incompatible") 160 161 // modifyColumn modifies the column with the name given, replacing it with the new definition provided. A column with 162 // the name given must exist in the schema of the table. 163 func modifyColumn( 164 ctx context.Context, 165 tbl *doltdb.Table, 166 existingCol schema.Column, 167 newCol schema.Column, 168 order *sql.ColumnOrder, 169 ) (*doltdb.Table, error) { 170 sch, err := tbl.GetSchema(ctx) 171 if err != nil { 172 return nil, err 173 } 174 175 // TODO: write test of changing column case 176 177 // Modify statements won't include key info, so fill it in from the old column 178 // TODO: fix this in GMS 179 if existingCol.IsPartOfPK { 180 newCol.IsPartOfPK = true 181 if schema.IsColSpatialType(newCol) { 182 return nil, fmt.Errorf("can't use Spatial Types as Primary Key for table") 183 } 184 foundNotNullConstraint := false 185 for _, constraint := range newCol.Constraints { 186 if _, ok := constraint.(schema.NotNullConstraint); ok { 187 foundNotNullConstraint = true 188 break 189 } 190 } 191 if !foundNotNullConstraint { 192 newCol.Constraints = append(newCol.Constraints, schema.NotNullConstraint{}) 193 } 194 } 195 196 newSchema, err := replaceColumnInSchema(sch, existingCol, newCol, order) 197 if err != nil { 198 return nil, err 199 } 200 201 return tbl.UpdateSchema(ctx, newSchema) 202 } 203 204 // replaceColumnInSchema replaces the column with the name given with its new definition, optionally reordering it. 205 // TODO: make this a schema API? 206 func replaceColumnInSchema(sch schema.Schema, oldCol schema.Column, newCol schema.Column, order *sql.ColumnOrder) (schema.Schema, error) { 207 // If no order is specified, insert in the same place as the existing column 208 prevColumn := "" 209 for _, col := range sch.GetAllCols().GetColumns() { 210 if col.Name == oldCol.Name { 211 if prevColumn == "" { 212 if order == nil { 213 order = &sql.ColumnOrder{First: true} 214 } 215 } 216 break 217 } else { 218 prevColumn = col.Name 219 } 220 } 221 222 if order == nil { 223 if prevColumn != "" { 224 order = &sql.ColumnOrder{AfterColumn: prevColumn} 225 } else { 226 return nil, fmt.Errorf("Couldn't find column %s", oldCol.Name) 227 } 228 } 229 230 var newCols []schema.Column 231 if order.First { 232 newCols = append(newCols, newCol) 233 } 234 235 for _, col := range sch.GetAllCols().GetColumns() { 236 if col.Name != oldCol.Name { 237 newCols = append(newCols, col) 238 } 239 240 if order.AfterColumn == col.Name { 241 newCols = append(newCols, newCol) 242 } 243 } 244 245 collection := schema.NewColCollection(newCols...) 246 247 err := schema.ValidateForInsert(collection) 248 if err != nil { 249 return nil, err 250 } 251 252 newSch, err := schema.SchemaFromCols(collection) 253 if err != nil { 254 return nil, err 255 } 256 for _, index := range sch.Indexes().AllIndexes() { 257 tags := index.IndexedColumnTags() 258 for i := range tags { 259 if tags[i] == oldCol.Tag { 260 tags[i] = newCol.Tag 261 } 262 } 263 _, err = newSch.Indexes().AddIndexByColTags( 264 index.Name(), 265 tags, 266 index.PrefixLengths(), 267 schema.IndexProperties{ 268 IsUnique: index.IsUnique(), 269 IsSpatial: index.IsSpatial(), 270 IsFullText: index.IsFullText(), 271 IsUserDefined: index.IsUserDefined(), 272 Comment: index.Comment(), 273 FullTextProperties: index.FullTextProperties(), 274 }) 275 if err != nil { 276 return nil, err 277 } 278 } 279 280 // Copy over all checks from the old schema 281 for _, check := range sch.Checks().AllChecks() { 282 _, err := newSch.Checks().AddCheck(check.Name(), check.Expression(), check.Enforced()) 283 if err != nil { 284 return nil, err 285 } 286 } 287 288 pkOrds, err := modifyPkOrdinals(sch, newSch) 289 if err != nil { 290 return nil, err 291 } 292 err = newSch.SetPkOrdinals(pkOrds) 293 if err != nil { 294 return nil, err 295 } 296 return newSch, nil 297 } 298 299 // modifyPkOrdinals tries to create primary key ordinals for a newSch maintaining 300 // the relative positions of PKs from the oldSch. Return an ErrPrimaryKeySetsIncompatible 301 // error if the two schemas have a different number of primary keys, or a primary 302 // key column's tag changed between the two sets. 303 // TODO: move this to schema package 304 func modifyPkOrdinals(oldSch, newSch schema.Schema) ([]int, error) { 305 if newSch.GetPKCols().Size() != oldSch.GetPKCols().Size() { 306 return nil, ErrPrimaryKeySetsIncompatible 307 } 308 309 newPkOrdinals := make([]int, len(newSch.GetPkOrdinals())) 310 for _, newCol := range newSch.GetPKCols().GetColumns() { 311 // ordIdx is the relative primary key order (that stays the same) 312 ordIdx, ok := oldSch.GetPKCols().TagToIdx[newCol.Tag] 313 if !ok { 314 // if pk tag changed, use name to find the new newCol tag 315 oldCol, ok := oldSch.GetPKCols().NameToCol[newCol.Name] 316 if !ok { 317 return nil, ErrPrimaryKeySetsIncompatible 318 } 319 ordIdx = oldSch.GetPKCols().TagToIdx[oldCol.Tag] 320 } 321 322 // ord is the schema ordering index, which may have changed in newSch 323 ord := newSch.GetAllCols().TagToIdx[newCol.Tag] 324 newPkOrdinals[ordIdx] = ord 325 } 326 327 return newPkOrdinals, nil 328 } 329 330 // backupFkcIndexesForKeyDrop finds backup indexes to cover foreign key references during a primary 331 // key drop. If multiple indexes are valid, we sort by unique and select the first. 332 // This will not work with a non-pk index drop without an additional index filter argument. 333 func backupFkcIndexesForPkDrop(ctx *sql.Context, tbl string, sch schema.Schema, fkc *doltdb.ForeignKeyCollection) ([]doltdb.FkIndexUpdate, error) { 334 fkUpdates := make([]doltdb.FkIndexUpdate, 0) 335 336 declared, referenced := fkc.KeysForTable(tbl) 337 for _, fk := range declared { 338 if fk.TableIndex == "" { 339 // pk used in fk definition on |tbl| 340 return nil, sql.ErrCantDropIndex.New("PRIMARY", fk.Name) 341 } 342 } 343 for _, fk := range referenced { 344 if fk.ReferencedTableIndex != "" { 345 // if an index doesn't reference primary key, it is unaffected 346 continue 347 } 348 // pk reference by fk definition on |fk.TableName| 349 350 // get column names from tags in foreign key 351 fkParentCols := make([]string, len(fk.ReferencedTableColumns)) 352 for i, colTag := range fk.ReferencedTableColumns { 353 col, _ := sch.GetPKCols().GetByTag(colTag) 354 fkParentCols[i] = col.Name 355 } 356 357 // find suitable secondary index 358 newIdx, ok, err := findIndexWithPrefix(sch, sch.GetPKCols().GetColumnNames()) 359 if err != nil { 360 return nil, err 361 } else if !ok { 362 return nil, sql.ErrCantDropIndex.New("PRIMARY", fk.Name) 363 } 364 365 fkUpdates = append(fkUpdates, doltdb.FkIndexUpdate{FkName: fk.Name, FromIdx: fk.ReferencedTableIndex, ToIdx: newIdx.Name()}) 366 } 367 return fkUpdates, nil 368 }