github.com/dolthub/go-mysql-server@v0.18.0/sql/fulltext/fulltext.go (about)

     1  // Copyright 2023 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 fulltext
    16  
    17  import (
    18  	"crypto/sha256"
    19  	"encoding/binary"
    20  	"encoding/hex"
    21  	"fmt"
    22  	"hash"
    23  	"io"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/shopspring/decimal"
    28  
    29  	"github.com/dolthub/go-mysql-server/sql"
    30  	"github.com/dolthub/go-mysql-server/sql/types"
    31  )
    32  
    33  // IndexTableNames holds all of the names for each pseudo-index table used by a Full-Text index.
    34  type IndexTableNames struct {
    35  	Config      string
    36  	Position    string
    37  	DocCount    string
    38  	GlobalCount string
    39  	RowCount    string
    40  }
    41  
    42  // KeyType refers to the type of key that the columns belong to.
    43  type KeyType byte
    44  
    45  const (
    46  	KeyType_Primary KeyType = iota
    47  	KeyType_Unique
    48  	KeyType_None
    49  )
    50  
    51  // KeyColumns contains all of the information needed to create key columns for each Full-Text table.
    52  type KeyColumns struct {
    53  	// Type refers to the type of key that the columns belong to.
    54  	Type KeyType
    55  	// Name is the name of the key. Only unique keys will have a name.
    56  	Name string
    57  	// Positions represents the schema index positions for primary keys and unique keys.
    58  	Positions []int
    59  }
    60  
    61  // Database allows a database to return a set of unique names that will be used for the pseudo-index tables with
    62  // Full-Text indexes.
    63  type Database interface {
    64  	sql.Database
    65  	sql.TableCreator
    66  	CreateFulltextTableNames(ctx *sql.Context, parentTable string, parentIndexName string) (IndexTableNames, error)
    67  }
    68  
    69  // IndexAlterableTable represents a table that supports the creation of FULLTEXT indexes. Renaming and deleting
    70  // the FULLTEXT index are both handled by RenameIndex and DropIndex respectively.
    71  type IndexAlterableTable interface {
    72  	sql.IndexAlterableTable
    73  	// CreateFulltextIndex creates a FULLTEXT index for this table. The index should not create a backing store, as the
    74  	// names of the tables given have already been created and will be managed by GMS as pseudo-indexes.
    75  	CreateFulltextIndex(ctx *sql.Context, indexDef sql.IndexDef, keyCols KeyColumns, tableNames IndexTableNames) error
    76  }
    77  
    78  // Index contains additional information regarding the FULLTEXT index.
    79  type Index interface {
    80  	sql.Index
    81  	// FullTextTableNames returns the names of the tables that represent the FULLTEXT index.
    82  	FullTextTableNames(ctx *sql.Context) (IndexTableNames, error)
    83  	// FullTextKeyColumns returns the key columns of its parent table that are used with this Full-Text index.
    84  	FullTextKeyColumns(ctx *sql.Context) (KeyColumns, error)
    85  }
    86  
    87  // SearchModifier represents the search modifier when using MATCH ... AGAINST ...
    88  type SearchModifier byte
    89  
    90  const (
    91  	SearchModifier_NaturalLanguage SearchModifier = iota
    92  	SearchModifier_NaturalLangaugeQueryExpansion
    93  	SearchModifier_Boolean
    94  	SearchModifier_QueryExpansion
    95  )
    96  
    97  // HashRow returns a 64 character lowercase hexadecimal hash of the given row. This is intended for use with keyless tables.
    98  func HashRow(row sql.Row) (string, error) {
    99  	h := sha256.New()
   100  	// Since we can't represent a NULL value in binary, we instead append the NULL results to the end, which will
   101  	// give us a unique representation for representing NULL values.
   102  	valIsNull := make([]bool, len(row))
   103  	for i, val := range row {
   104  		var err error
   105  		valIsNull[i], err = writeHashedValue(h, val)
   106  		if err != nil {
   107  			return "", err
   108  		}
   109  	}
   110  
   111  	if err := binary.Write(h, binary.LittleEndian, valIsNull); err != nil {
   112  		return "", err
   113  	}
   114  	// Go's current implementation will always return lowercase hashes, but we'll convert for safety since it's not
   115  	// explicitly stated in the API that it's a lowercase string, therefore it might change in the future (even if highly unlikely).
   116  	return strings.ToLower(hex.EncodeToString(h.Sum(nil))), nil
   117  }
   118  
   119  // writeHashedValue writes the given value into the hash.
   120  func writeHashedValue(h hash.Hash, val interface{}) (valIsNull bool, err error) {
   121  	switch val := val.(type) {
   122  	case int:
   123  		if err := binary.Write(h, binary.LittleEndian, int64(val)); err != nil {
   124  			return false, err
   125  		}
   126  	case uint:
   127  		if err := binary.Write(h, binary.LittleEndian, uint64(val)); err != nil {
   128  			return false, err
   129  		}
   130  	case string:
   131  		if _, err := h.Write([]byte(val)); err != nil {
   132  			return false, err
   133  		}
   134  	case []byte:
   135  		if _, err := h.Write(val); err != nil {
   136  			return false, err
   137  		}
   138  	case decimal.Decimal:
   139  		bytes, err := val.GobEncode()
   140  		if err != nil {
   141  			return false, err
   142  		}
   143  		if _, err := h.Write(bytes); err != nil {
   144  			return false, err
   145  		}
   146  	case decimal.NullDecimal:
   147  		if !val.Valid {
   148  			return true, nil
   149  		} else {
   150  			bytes, err := val.Decimal.GobEncode()
   151  			if err != nil {
   152  				return false, err
   153  			}
   154  			if _, err := h.Write(bytes); err != nil {
   155  				return false, err
   156  			}
   157  		}
   158  	case time.Time:
   159  		bytes, err := val.MarshalBinary()
   160  		if err != nil {
   161  			return false, err
   162  		}
   163  		if _, err := h.Write(bytes); err != nil {
   164  			return false, err
   165  		}
   166  	case types.GeometryValue:
   167  		if _, err := h.Write(val.Serialize()); err != nil {
   168  			return false, err
   169  		}
   170  	case types.JSONDocument:
   171  		str, err := val.JSONString()
   172  		if err != nil {
   173  			return false, err
   174  		}
   175  		if _, err := h.Write([]byte(str)); err != nil {
   176  			return false, err
   177  		}
   178  	case nil:
   179  		return true, nil
   180  	default:
   181  		if err := binary.Write(h, binary.LittleEndian, val); err != nil {
   182  			return false, err
   183  		}
   184  	}
   185  	return false, nil
   186  }
   187  
   188  // GetKeyColumns returns the key columns from the parent table that will be used to uniquely reference any given row on
   189  // the parent table. For many tables, this will be the primary key. For tables that do not have a valid key, the columns
   190  // will be much more important.
   191  func GetKeyColumns(ctx *sql.Context, parent sql.Table) (KeyColumns, []*sql.Column, error) {
   192  	var columns []*sql.Column
   193  	var positions []int
   194  	// Check for a primary key. We'll only check on tables that implement sql.PrimaryKeyTable as we need to replicate
   195  	// the declaration order, and there's no guarantee that the order is sequential with a standard sql.Schema.
   196  	if pkTable, ok := parent.(sql.PrimaryKeyTable); ok {
   197  		sch := pkTable.PrimaryKeySchema()
   198  		if len(sch.PkOrdinals) > 0 {
   199  			positions = make([]int, len(sch.PkOrdinals))
   200  			copy(positions, sch.PkOrdinals)
   201  			nameIdx := 0
   202  			for _, ordinal := range sch.PkOrdinals {
   203  				newCol := sch.Schema[ordinal].Copy()
   204  				newCol.Name = fmt.Sprintf("C%d", nameIdx)
   205  				columns = append(columns, newCol)
   206  				nameIdx++
   207  			}
   208  			return KeyColumns{
   209  				Type:      KeyType_Primary,
   210  				Name:      "",
   211  				Positions: positions,
   212  			}, columns, nil
   213  		}
   214  	}
   215  	// Check for a unique key (we'll just use the first one we find)
   216  	if idxTable, ok := parent.(sql.IndexAddressableTable); ok {
   217  		indexes, err := idxTable.GetIndexes(ctx)
   218  		if err != nil {
   219  			return KeyColumns{}, nil, err
   220  		}
   221  		for _, index := range indexes {
   222  			if !index.IsUnique() {
   223  				continue
   224  			}
   225  
   226  			// Create a map from schema column expression to position
   227  			parentSch := parent.Schema()
   228  			parentColMap := GetParentColumnMap(parentSch)
   229  
   230  			// Map from expression to position
   231  			hasNullableCol := false
   232  			for i, expr := range index.Expressions() {
   233  				parentColPosition, ok := parentColMap[strings.ToLower(expr)]
   234  				if !ok {
   235  					return KeyColumns{}, nil, fmt.Errorf("table `%s` UNIQUE index `%s` references the column `%s` but it could not be found",
   236  						parent.Name(), index.ID(), expr)
   237  				}
   238  				newCol := parentSch[parentColPosition].Copy()
   239  				newCol.Name = fmt.Sprintf("C%d", i)
   240  				newCol.PrimaryKey = true
   241  				columns = append(columns, newCol)
   242  				positions = append(positions, parentColPosition)
   243  				hasNullableCol = hasNullableCol || newCol.Nullable
   244  			}
   245  			// We don't want to consider unique keys that have nullable columns, since they can have duplicate keys
   246  			if hasNullableCol {
   247  				continue
   248  			}
   249  
   250  			return KeyColumns{
   251  				Type:      KeyType_Unique,
   252  				Name:      index.ID(),
   253  				Positions: positions,
   254  			}, columns, nil
   255  		}
   256  	}
   257  	// No applicable keys were found, so we'll hash the row in exchange for a lack of indexing support
   258  	return KeyColumns{
   259  		Type:      KeyType_None,
   260  		Name:      "",
   261  		Positions: nil,
   262  	}, []*sql.Column{SchemaRowCount[0].Copy()}, nil
   263  }
   264  
   265  // GetCollationFromSchema returns the WORD's collation. This assumes that it is only used with schemas from the position,
   266  // doc count, and global count tables.
   267  func GetCollationFromSchema(ctx *sql.Context, sch sql.Schema) sql.CollationID {
   268  	collation, _ := sch[0].Type.CollationCoercibility(ctx)
   269  	return collation
   270  }
   271  
   272  // NewSchema returns a new schema based on the given schema with all collated fields replaced with the given collation,
   273  // the given key columns inserted after the first column, and all columns set to the source given.
   274  func NewSchema(sch sql.Schema, insertCols []*sql.Column, source string, collation sql.CollationID) (newSch sql.Schema, err error) {
   275  	// Create the new schema with the given key columns.
   276  	// Key columns are always applied after the first column (key columns are not always provided).
   277  	newSch = make(sql.Schema, 1, len(sch)+len(insertCols))
   278  	newSch[0] = sch[0].Copy()
   279  	for _, refCol := range insertCols {
   280  		newSch = append(newSch, refCol.Copy())
   281  	}
   282  	for _, col := range sch[1:] {
   283  		newSch = append(newSch, col.Copy())
   284  	}
   285  	// Assign the collation (if applicable) and source
   286  	for _, col := range newSch {
   287  		if collatedType, ok := col.Type.(sql.TypeWithCollation); ok {
   288  			if col.Type, err = collatedType.WithNewCollation(collation); err != nil {
   289  				return nil, err
   290  			}
   291  		}
   292  		col.Source = source
   293  	}
   294  	return newSch, nil
   295  }
   296  
   297  // GetParentColumnMap is used to map index expressions to their source columns. All strings have been lowercased.
   298  func GetParentColumnMap(parentSch sql.Schema) map[string]int {
   299  	parentColMap := make(map[string]int)
   300  	for i, col := range parentSch {
   301  		parentColMap[strings.ToLower(col.Name)] = i
   302  		parentColMap[strings.ToLower(col.Source)+"."+strings.ToLower(col.Name)] = i
   303  	}
   304  	return parentColMap
   305  }
   306  
   307  // DropAllIndexes drops all Full-Text pseudo-index tables and indexes that are declared on the given table.
   308  func DropAllIndexes(ctx *sql.Context, tbl sql.IndexAddressableTable, db Database) error {
   309  	// Check the interfaces on the parameters
   310  	dropper, ok := db.(sql.TableDropper)
   311  	if !ok {
   312  		return sql.ErrIncompleteFullTextIntegration.New()
   313  	}
   314  	idxAlterable, ok := tbl.(sql.IndexAlterableTable)
   315  	if !ok {
   316  		return sql.ErrIncompleteFullTextIntegration.New()
   317  	}
   318  
   319  	// Load the indexes to search for Full-Text indexes
   320  	indexes, err := tbl.GetIndexes(ctx)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	droppedConfig := false
   325  	for _, index := range indexes {
   326  		// We don't have anything to drop on a non-Full-Text index
   327  		if !index.IsFullText() {
   328  			continue
   329  		}
   330  		ftIndex := index.(Index)
   331  		tableNames, err := ftIndex.FullTextTableNames(ctx)
   332  		if err != nil {
   333  			return err
   334  		}
   335  		// We drop the config table only once, since it's shared by all index tables
   336  		if !droppedConfig {
   337  			droppedConfig = true
   338  			if err = dropper.DropTable(ctx, tableNames.Config); err != nil {
   339  				return err
   340  			}
   341  		}
   342  		// We delete all other tables
   343  		if err = dropper.DropTable(ctx, tableNames.Position); err != nil {
   344  			return err
   345  		}
   346  		if err = dropper.DropTable(ctx, tableNames.DocCount); err != nil {
   347  			return err
   348  		}
   349  		if err = dropper.DropTable(ctx, tableNames.GlobalCount); err != nil {
   350  			return err
   351  		}
   352  		if err = dropper.DropTable(ctx, tableNames.RowCount); err != nil {
   353  			return err
   354  		}
   355  		// Finally we'll drop the index
   356  		if err = idxAlterable.DropIndex(ctx, ftIndex.ID()); err != nil {
   357  			return err
   358  		}
   359  	}
   360  	return nil
   361  }
   362  
   363  // RebuildTables rebuilds all Full-Text pseudo-index tables that are declared on the given table.
   364  func RebuildTables(ctx *sql.Context, tbl sql.IndexAddressableTable, db Database) error {
   365  	// Check the interfaces on the parameters
   366  	dropper, ok := db.(sql.TableDropper)
   367  	if !ok {
   368  		return sql.ErrIncompleteFullTextIntegration.New()
   369  	}
   370  	idxAlterable, ok := tbl.(sql.IndexAlterableTable)
   371  	if !ok {
   372  		return sql.ErrIncompleteFullTextIntegration.New()
   373  	}
   374  
   375  	predeterminedNames := make(map[string]IndexTableNames)
   376  	var indexDefs []sql.IndexDef
   377  
   378  	// Load the indexes to search for Full-Text indexes
   379  	indexes, err := tbl.GetIndexes(ctx)
   380  	if err != nil {
   381  		return err
   382  	}
   383  	for _, index := range indexes {
   384  		// Skip all non-Full-Text indexes
   385  		if !index.IsFullText() {
   386  			continue
   387  		}
   388  		// Store the index definition so that we may recreate it below
   389  		ftIndex := index.(Index)
   390  		exprs := ftIndex.Expressions()
   391  		indexCols := make([]sql.IndexColumn, len(exprs))
   392  		for i, expr := range exprs {
   393  			indexCols[i] = sql.IndexColumn{
   394  				Name:   strings.TrimPrefix(expr, ftIndex.Table()+"."),
   395  				Length: 0,
   396  			}
   397  		}
   398  		indexDefs = append(indexDefs, sql.IndexDef{
   399  			Name:       ftIndex.ID(),
   400  			Columns:    indexCols,
   401  			Constraint: sql.IndexConstraint_Fulltext,
   402  			Storage:    sql.IndexUsing_Default,
   403  			Comment:    ftIndex.Comment(),
   404  		})
   405  		tableNames, err := ftIndex.FullTextTableNames(ctx)
   406  		if err != nil {
   407  			return err
   408  		}
   409  		predeterminedNames[ftIndex.ID()] = tableNames
   410  		// We delete all tables besides the config table
   411  		if err = dropper.DropTable(ctx, tableNames.Position); err != nil {
   412  			return err
   413  		}
   414  		if err = dropper.DropTable(ctx, tableNames.DocCount); err != nil {
   415  			return err
   416  		}
   417  		if err = dropper.DropTable(ctx, tableNames.GlobalCount); err != nil {
   418  			return err
   419  		}
   420  		if err = dropper.DropTable(ctx, tableNames.RowCount); err != nil {
   421  			return err
   422  		}
   423  		// Finally we'll drop the index
   424  		if err = idxAlterable.DropIndex(ctx, ftIndex.ID()); err != nil {
   425  			return err
   426  		}
   427  	}
   428  	return CreateFulltextIndexes(ctx, db, tbl, predeterminedNames, indexDefs...)
   429  }
   430  
   431  // DropColumnFromTables removes the given column from all of the Full-Text indexes, which will trigger a rebuild if the
   432  // index spans multiple columns, but will trigger a deletion if the index spans that single column. The column name is
   433  // case-insensitive.
   434  func DropColumnFromTables(ctx *sql.Context, tbl sql.IndexAddressableTable, db Database, colName string) error {
   435  	// Check the interfaces on the parameters
   436  	dropper, ok := db.(sql.TableDropper)
   437  	if !ok {
   438  		return sql.ErrIncompleteFullTextIntegration.New()
   439  	}
   440  	idxAlterable, ok := tbl.(sql.IndexAlterableTable)
   441  	if !ok {
   442  		return sql.ErrIncompleteFullTextIntegration.New()
   443  	}
   444  
   445  	lowercaseColName := strings.ToLower(colName)
   446  	configTableReuse := make(map[string]bool)
   447  	predeterminedNames := make(map[string]IndexTableNames)
   448  	var indexDefs []sql.IndexDef
   449  
   450  	// Load the indexes to search for Full-Text indexes
   451  	indexes, err := tbl.GetIndexes(ctx)
   452  	if err != nil {
   453  		return err
   454  	}
   455  	for _, index := range indexes {
   456  		// Skip all non-Full-Text indexes
   457  		if !index.IsFullText() {
   458  			continue
   459  		}
   460  		// Store the index definition so that we may recreate it below
   461  		ftIndex := index.(Index)
   462  		tableNames, err := ftIndex.FullTextTableNames(ctx)
   463  		if err != nil {
   464  			return err
   465  		}
   466  		predeterminedNames[ftIndex.ID()] = tableNames
   467  		// Iterate over the columns to search for the given column
   468  		exprs := ftIndex.Expressions()
   469  		var indexCols []sql.IndexColumn
   470  		for _, expr := range exprs {
   471  			exprColName := strings.TrimPrefix(expr, ftIndex.Table()+".")
   472  			// Skip this column if it matches our given column
   473  			if strings.ToLower(exprColName) == lowercaseColName {
   474  				continue
   475  			}
   476  			indexCols = append(indexCols, sql.IndexColumn{
   477  				Name:   exprColName,
   478  				Length: 0,
   479  			})
   480  		}
   481  		if len(indexCols) > 0 {
   482  			// This index will continue to exist, so we want to preserve the config table
   483  			indexDefs = append(indexDefs, sql.IndexDef{
   484  				Name:       ftIndex.ID(),
   485  				Columns:    indexCols,
   486  				Constraint: sql.IndexConstraint_Fulltext,
   487  				Storage:    sql.IndexUsing_Default,
   488  				Comment:    ftIndex.Comment(),
   489  			})
   490  			configTableReuse[tableNames.Config] = true
   491  		} else {
   492  			// This index will be deleted, so we should delete the config table if no other indexes will reuse the table
   493  			if _, ok := configTableReuse[tableNames.Config]; !ok {
   494  				configTableReuse[tableNames.Config] = false
   495  			}
   496  		}
   497  		// We delete all tables besides the config table
   498  		if err = dropper.DropTable(ctx, tableNames.Position); err != nil {
   499  			return err
   500  		}
   501  		if err = dropper.DropTable(ctx, tableNames.DocCount); err != nil {
   502  			return err
   503  		}
   504  		if err = dropper.DropTable(ctx, tableNames.GlobalCount); err != nil {
   505  			return err
   506  		}
   507  		if err = dropper.DropTable(ctx, tableNames.RowCount); err != nil {
   508  			return err
   509  		}
   510  		// Finally we'll drop the index
   511  		if err = idxAlterable.DropIndex(ctx, ftIndex.ID()); err != nil {
   512  			return err
   513  		}
   514  	}
   515  	// Delete all orphaned config tables
   516  	for configTableName, reused := range configTableReuse {
   517  		if !reused {
   518  			if err = dropper.DropTable(ctx, configTableName); err != nil {
   519  				return err
   520  			}
   521  		}
   522  	}
   523  	return CreateFulltextIndexes(ctx, db, tbl, predeterminedNames, indexDefs...)
   524  }
   525  
   526  // CreateFulltextIndexes creates and populates Full-Text indexes on the target table.
   527  func CreateFulltextIndexes(ctx *sql.Context, database Database, parent sql.Table,
   528  	predeterminedNames map[string]IndexTableNames, indexes ...sql.IndexDef) error {
   529  	fulltextIndexes := make([]sql.IndexDef, 0, len(indexes))
   530  	for _, index := range indexes {
   531  		if index.Constraint == sql.IndexConstraint_Fulltext {
   532  			fulltextIndexes = append(fulltextIndexes, index)
   533  		}
   534  	}
   535  	if len(fulltextIndexes) == 0 {
   536  		return nil
   537  	}
   538  
   539  	// Ensure that the needed interfaces have been implemented
   540  	fulltextAlterable, ok := parent.(IndexAlterableTable)
   541  	if !ok {
   542  		return sql.ErrFullTextNotSupported.New()
   543  	}
   544  	if _, ok = fulltextAlterable.(sql.IndexAddressableTable); !ok {
   545  		return sql.ErrFullTextNotSupported.New()
   546  	}
   547  	if _, ok = fulltextAlterable.(sql.StatisticsTable); !ok {
   548  		return sql.ErrFullTextNotSupported.New()
   549  	}
   550  	tblSch := parent.Schema()
   551  
   552  	// Grab the key columns, which we will share among all indexes
   553  	keyCols, insertCols, err := GetKeyColumns(ctx, fulltextAlterable)
   554  	if err != nil {
   555  		return err
   556  	}
   557  
   558  	// Create unique tables for each index
   559  	for _, fulltextIndex := range fulltextIndexes {
   560  		// Get the collation that will be used, while checking for duplicate columns and ensuring they have valid types
   561  		collation := sql.Collation_Unspecified
   562  		exists := make(map[string]struct{})
   563  		for _, indexCol := range fulltextIndex.Columns {
   564  			indexColNameLower := strings.ToLower(indexCol.Name)
   565  			if _, ok = exists[indexColNameLower]; ok {
   566  				return sql.ErrFullTextDuplicateColumn.New(fulltextIndex.Name)
   567  			}
   568  			found := false
   569  			for _, tblCol := range tblSch {
   570  				if indexColNameLower == strings.ToLower(tblCol.Name) {
   571  					if !types.IsTextOnly(tblCol.Type) {
   572  						return sql.ErrFullTextInvalidColumnType.New()
   573  					}
   574  					colCollation, _ := tblCol.Type.CollationCoercibility(ctx)
   575  					if collation == sql.Collation_Unspecified {
   576  						collation = colCollation
   577  					} else if collation != colCollation {
   578  						return sql.ErrFullTextDifferentCollations.New()
   579  					}
   580  					found = true
   581  					break
   582  				}
   583  			}
   584  			if !found {
   585  				return sql.ErrFullTextMissingColumn.New(indexCol.Name)
   586  			}
   587  			exists[indexColNameLower] = struct{}{}
   588  		}
   589  
   590  		// Grab the table names that we'll use
   591  		var tableNames IndexTableNames
   592  		if predeterminedName, ok := predeterminedNames[fulltextIndex.Name]; ok {
   593  			tableNames = predeterminedName
   594  		} else {
   595  			tableNames, err = database.CreateFulltextTableNames(ctx, fulltextAlterable.Name(), fulltextIndex.Name)
   596  			if err != nil {
   597  				return err
   598  			}
   599  		}
   600  
   601  		// We'll only create the config table if it doesn't already exist, since it is shared between all indexes on the table
   602  		_, ok, err = database.GetTableInsensitive(ctx, tableNames.Config)
   603  		if err != nil {
   604  			return err
   605  		}
   606  		if !ok {
   607  			// We create the config table first since it will be shared between all Full-Text indexes for this table
   608  			configSch, err := NewSchema(SchemaConfig, nil, tableNames.Config, sql.Collation_Default)
   609  			if err != nil {
   610  				return err
   611  			}
   612  			err = database.CreateTable(ctx, tableNames.Config, sql.NewPrimaryKeySchema(configSch), sql.Collation_Default, "")
   613  			if err != nil {
   614  				return err
   615  			}
   616  		}
   617  
   618  		// Create the additional tables
   619  		positionSch, err := NewSchema(SchemaPosition, insertCols, tableNames.Position, collation)
   620  		if err != nil {
   621  			return err
   622  		}
   623  		err = database.CreateTable(ctx, tableNames.Position, sql.NewPrimaryKeySchema(positionSch), sql.Collation_Default, "")
   624  		if err != nil {
   625  			return err
   626  		}
   627  		docCountSch, err := NewSchema(SchemaDocCount, insertCols, tableNames.DocCount, collation)
   628  		if err != nil {
   629  			return err
   630  		}
   631  		err = database.CreateTable(ctx, tableNames.DocCount, sql.NewPrimaryKeySchema(docCountSch), sql.Collation_Default, "")
   632  		if err != nil {
   633  			return err
   634  		}
   635  		globalCountSch, err := NewSchema(SchemaGlobalCount, nil, tableNames.GlobalCount, collation)
   636  		if err != nil {
   637  			return err
   638  		}
   639  		err = database.CreateTable(ctx, tableNames.GlobalCount, sql.NewPrimaryKeySchema(globalCountSch), sql.Collation_Default, "")
   640  		if err != nil {
   641  			return err
   642  		}
   643  		rowCountSch, err := NewSchema(SchemaRowCount, nil, tableNames.RowCount, collation)
   644  		if err != nil {
   645  			return err
   646  		}
   647  		err = database.CreateTable(ctx, tableNames.RowCount, sql.NewPrimaryKeySchema(rowCountSch), sql.Collation_Default, "")
   648  		if err != nil {
   649  			return err
   650  		}
   651  
   652  		// Create the Full-Text index
   653  		err = fulltextAlterable.CreateFulltextIndex(ctx, fulltextIndex, keyCols, tableNames)
   654  		if err != nil {
   655  			return err
   656  		}
   657  	}
   658  
   659  	// We'll populate all of the new tables, so we're grabbing the row iter of the parent table
   660  	tblPartIter, err := parent.Partitions(ctx)
   661  	if err != nil {
   662  		return err
   663  	}
   664  	rowIter := sql.NewTableRowIter(ctx, parent, tblPartIter)
   665  	defer rowIter.Close(ctx)
   666  
   667  	// Next we'll get the "official" indexes and create table sets
   668  	var configTbl EditableTable
   669  	officialIndexes, err := parent.(sql.IndexAddressableTable).GetIndexes(ctx)
   670  	if err != nil {
   671  		return err
   672  	}
   673  	tableSets := make([]TableSet, 0, len(fulltextIndexes))
   674  	for _, idx := range officialIndexes {
   675  		if !idx.IsFullText() {
   676  			continue
   677  		}
   678  		ftIdx, ok := idx.(Index)
   679  		if !ok { // This should never happen
   680  			panic("index returns true for FULLTEXT, but does not implement interface")
   681  		}
   682  		ftTableNames, err := ftIdx.FullTextTableNames(ctx)
   683  		if err != nil { // This should never happen
   684  			panic(err.Error())
   685  		}
   686  
   687  		if configTbl == nil {
   688  			tbl, ok, err := database.GetTableInsensitive(ctx, ftTableNames.Config)
   689  			if err != nil {
   690  				panic(err)
   691  			}
   692  			if !ok {
   693  				return fmt.Errorf("index `%s` declares the table `%s` as a FULLTEXT config table, but it could not be found", idx.ID(), ftTableNames.Config)
   694  			}
   695  			// We'll only do the check once, since we can fairly safely assume that the other tables will also implement the interface
   696  			configTbl, ok = tbl.(EditableTable)
   697  			if !ok {
   698  				return fmt.Errorf("index `%s` declares the table `%s` as a FULLTEXT config table, however it does not implement EditableTable", idx.ID(), ftTableNames.Config)
   699  			}
   700  		}
   701  		positionTbl, ok, err := database.GetTableInsensitive(ctx, ftTableNames.Position)
   702  		if err != nil {
   703  			panic(err)
   704  		}
   705  		if !ok {
   706  			return fmt.Errorf("index `%s` declares the table `%s` as a FULLTEXT position table, but it could not be found", idx.ID(), ftTableNames.Position)
   707  		}
   708  		docCountTbl, ok, err := database.GetTableInsensitive(ctx, ftTableNames.DocCount)
   709  		if err != nil {
   710  			panic(err)
   711  		}
   712  		if !ok {
   713  			return fmt.Errorf("index `%s` declares the table `%s` as a FULLTEXT doc count table, but it could not be found", idx.ID(), ftTableNames.DocCount)
   714  		}
   715  		globalCountTbl, ok, err := database.GetTableInsensitive(ctx, ftTableNames.GlobalCount)
   716  		if err != nil {
   717  			panic(err)
   718  		}
   719  		if !ok {
   720  			return fmt.Errorf("index `%s` declares the table `%s` as a FULLTEXT global count table, but it could not be found", idx.ID(), ftTableNames.GlobalCount)
   721  		}
   722  		rowCountTbl, ok, err := database.GetTableInsensitive(ctx, ftTableNames.RowCount)
   723  		if err != nil {
   724  			panic(err)
   725  		}
   726  		if !ok {
   727  			return fmt.Errorf("index `%s` declares the table `%s` as a FULLTEXT row count table, but it could not be found", idx.ID(), ftTableNames.RowCount)
   728  		}
   729  
   730  		tableSets = append(tableSets, TableSet{
   731  			Index:       ftIdx,
   732  			Position:    positionTbl.(EditableTable),
   733  			DocCount:    docCountTbl.(EditableTable),
   734  			GlobalCount: globalCountTbl.(EditableTable),
   735  			RowCount:    rowCountTbl.(EditableTable),
   736  		})
   737  	}
   738  
   739  	// Create the editor with the sets
   740  	editor, err := CreateEditor(ctx, parent, configTbl, tableSets...)
   741  	if err != nil {
   742  		return err
   743  	}
   744  	defer editor.Close(ctx)
   745  
   746  	// Finally, loop over all of the rows and write them to the tables
   747  	editor.StatementBegin(ctx)
   748  	row, err := rowIter.Next(ctx)
   749  	for ; err == nil; row, err = rowIter.Next(ctx) {
   750  		if err = editor.Insert(ctx, row); err != nil {
   751  			return err
   752  		}
   753  	}
   754  	if err == io.EOF {
   755  		return editor.StatementComplete(ctx)
   756  	} else if err != nil {
   757  		_ = editor.DiscardChanges(ctx, err)
   758  		return err
   759  	}
   760  	return editor.StatementComplete(ctx)
   761  }
   762  
   763  // validateSchema compares two schemas to make sure that they're compatible. This is used to verify that the
   764  // given Full-Text tables have the correct schemas. In practice, this shouldn't fail unless the integrator allows the
   765  // user to modify the tables' schemas.
   766  func validateSchema(ftTblName string, parentSch sql.Schema, sch sql.Schema, expected sql.Schema, keyCols KeyColumns) (err error) {
   767  	var revisedExpected sql.Schema
   768  	if keyCols.Type != KeyType_None {
   769  		// This will still work for tables that do not have key columns
   770  		if len(expected)+len(keyCols.Positions) != len(sch) {
   771  			return fmt.Errorf("Full-Text table `%s` has an unexpected number of columns", ftTblName)
   772  		}
   773  		revisedExpected = make(sql.Schema, 1, len(expected)+len(keyCols.Positions))
   774  		revisedExpected[0] = expected[0]
   775  		for i, pos := range keyCols.Positions {
   776  			newKeyCol := *parentSch[pos]
   777  			newKeyCol.Name = fmt.Sprintf("C%d", i)
   778  			newKeyCol.PrimaryKey = true
   779  			revisedExpected = append(revisedExpected, &newKeyCol)
   780  		}
   781  		revisedExpected = append(revisedExpected, expected[1:]...)
   782  	} else {
   783  		if len(expected)+1 != len(sch) {
   784  			return fmt.Errorf("Full-Text table `%s` has an unexpected number of columns", ftTblName)
   785  		}
   786  		revisedExpected = make(sql.Schema, 2, len(expected)+1)
   787  		revisedExpected[0] = expected[0]
   788  		revisedExpected[1] = SchemaRowCount[0].Copy()
   789  		revisedExpected = append(revisedExpected, expected[1:]...)
   790  	}
   791  	for i := range sch {
   792  		col := *sch[i]
   793  		expectedCol := *revisedExpected[i]
   794  		if col.Generated != nil || expectedCol.Generated != nil {
   795  			// It might be fine for Full-Text to reference generated columns, but we aren't completely sure of any
   796  			// potential implementation issues, so it's disabled for now.
   797  			return fmt.Errorf("Full-Text does not currently support generated columns")
   798  		}
   799  		// The expected schemas use the default collation, so we set them to the given column's collation for comparison
   800  		if expectedColStrType, ok := expectedCol.Type.(sql.TypeWithCollation); ok {
   801  			colStrType, ok := col.Type.(sql.TypeWithCollation)
   802  			if !ok {
   803  				return fmt.Errorf("Full-Text table `%s` has an incorrect type for the column `%s`", ftTblName, col.Name)
   804  			}
   805  			expectedCol.Type, err = expectedColStrType.WithNewCollation(colStrType.Collation())
   806  			if err != nil {
   807  				return err
   808  			}
   809  		}
   810  		// We can't just use the Equals() function on the columns as they care about fields that we do not.
   811  		if col.Name != expectedCol.Name || !col.Type.Equals(expectedCol.Type) || col.PrimaryKey != expectedCol.PrimaryKey || col.Nullable != expectedCol.Nullable ||
   812  			col.AutoIncrement != expectedCol.AutoIncrement {
   813  			return fmt.Errorf("Full-Text table `%s` column `%s` has an incorrect definition", ftTblName, col.Name)
   814  		}
   815  	}
   816  	return nil
   817  }