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  }