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 }