github.com/XiaoMi/Gaea@v1.2.5/parser/model/model.go (about) 1 // Copyright 2015 PingCAP, 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 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package model 15 16 import ( 17 "encoding/json" 18 "strings" 19 "time" 20 21 "github.com/pingcap/errors" 22 tipb "github.com/pingcap/tipb/go-tipb" 23 24 "github.com/XiaoMi/Gaea/mysql" 25 "github.com/XiaoMi/Gaea/parser/auth" 26 "github.com/XiaoMi/Gaea/parser/types" 27 ) 28 29 // SchemaState is the state for schema elements. 30 type SchemaState byte 31 32 const ( 33 // StateNone means this schema element is absent and can't be used. 34 StateNone SchemaState = iota 35 // StateDeleteOnly means we can only delete items for this schema element. 36 StateDeleteOnly 37 // StateWriteOnly means we can use any write operation on this schema element, 38 // but outer can't read the changed data. 39 StateWriteOnly 40 // StateWriteReorganization means we are re-organizing whole data after write only state. 41 StateWriteReorganization 42 // StateDeleteReorganization means we are re-organizing whole data after delete only state. 43 StateDeleteReorganization 44 // StatePublic means this schema element is ok for all write and read operations. 45 StatePublic 46 ) 47 48 // String implements fmt.Stringer interface. 49 func (s SchemaState) String() string { 50 switch s { 51 case StateDeleteOnly: 52 return "delete only" 53 case StateWriteOnly: 54 return "write only" 55 case StateWriteReorganization: 56 return "write reorganization" 57 case StateDeleteReorganization: 58 return "delete reorganization" 59 case StatePublic: 60 return "public" 61 default: 62 return "none" 63 } 64 } 65 66 const ( 67 // ColumnInfoVersion0 means the column info version is 0. 68 ColumnInfoVersion0 = uint64(0) 69 // ColumnInfoVersion1 means the column info version is 1. 70 ColumnInfoVersion1 = uint64(1) 71 ) 72 73 // ColumnInfo provides meta data describing of a table column. 74 type ColumnInfo struct { 75 ID int64 `json:"id"` 76 Name CIStr `json:"name"` 77 Offset int `json:"offset"` 78 OriginDefaultValue interface{} `json:"origin_default"` 79 DefaultValue interface{} `json:"default"` 80 DefaultValueBit []byte `json:"default_bit"` 81 GeneratedExprString string `json:"generated_expr_string"` 82 GeneratedStored bool `json:"generated_stored"` 83 Dependences map[string]struct{} `json:"dependences"` 84 types.FieldType `json:"type"` 85 State SchemaState `json:"state"` 86 Comment string `json:"comment"` 87 // Version means the version of the column info. 88 // Version = 0: For OriginDefaultValue and DefaultValue of timestamp column will stores the default time in system time zone. 89 // That is a bug if multiple TiDB servers in different system time zone. 90 // Version = 1: For OriginDefaultValue and DefaultValue of timestamp column will stores the default time in UTC time zone. 91 // This will fix bug in version 0. For compatibility with version 0, we add version field in column info struct. 92 Version uint64 `json:"version"` 93 } 94 95 // Clone clones ColumnInfo. 96 func (c *ColumnInfo) Clone() *ColumnInfo { 97 nc := *c 98 return &nc 99 } 100 101 // IsGenerated returns true if the column is generated column. 102 func (c *ColumnInfo) IsGenerated() bool { 103 return len(c.GeneratedExprString) != 0 104 } 105 106 // SetDefaultValue sets the default value. 107 func (c *ColumnInfo) SetDefaultValue(value interface{}) error { 108 c.DefaultValue = value 109 if c.Tp == mysql.TypeBit { 110 // For mysql.TypeBit type, the default value storage format must be a string. 111 // Other value such as int must convert to string format first. 112 // The mysql.TypeBit type supports the null default value. 113 if value == nil { 114 return nil 115 } 116 if v, ok := value.(string); ok { 117 c.DefaultValueBit = []byte(v) 118 return nil 119 } 120 return types.ErrInvalidDefault.GenWithStackByArgs(c.Name) 121 } 122 return nil 123 } 124 125 // GetDefaultValue gets the default value of the column. 126 // Default value use to stored in DefaultValue field, but now, 127 // bit type default value will store in DefaultValueBit for fix bit default value decode/encode bug. 128 func (c *ColumnInfo) GetDefaultValue() interface{} { 129 if c.Tp == mysql.TypeBit && c.DefaultValueBit != nil { 130 return string(c.DefaultValueBit) 131 } 132 return c.DefaultValue 133 } 134 135 // FindColumnInfo finds ColumnInfo in cols by name. 136 func FindColumnInfo(cols []*ColumnInfo, name string) *ColumnInfo { 137 name = strings.ToLower(name) 138 for _, col := range cols { 139 if col.Name.L == name { 140 return col 141 } 142 } 143 144 return nil 145 } 146 147 // ExtraHandleID is the column ID of column which we need to append to schema to occupy the handle's position 148 // for use of execution phase. 149 const ExtraHandleID = -1 150 151 const ( 152 // TableInfoVersion0 means the table info version is 0. 153 // Upgrade from v2.1.1 or v2.1.2 to v2.1.3 and later, and then execute a "change/modify column" statement 154 // that does not specify a charset value for column. Then the following error may be reported: 155 // ERROR 1105 (HY000): unsupported modify charset from utf8mb4 to utf8. 156 // To eliminate this error, we will not modify the charset of this column 157 // when executing a change/modify column statement that does not specify a charset value for column. 158 // This behavior is not compatible with MySQL. 159 TableInfoVersion0 = uint16(0) 160 // TableInfoVersion1 ColumnInfoVersion1 means the table info version is 1. 161 // When we execute a change/modify column statement that does not specify a charset value for column, 162 // we set the charset of this column to the charset of table. This behavior is compatible with MySQL. 163 TableInfoVersion1 = uint16(1) 164 // CurrLatestTableInfoVersion means the latest table info in the current TiDB. 165 CurrLatestTableInfoVersion = TableInfoVersion1 166 ) 167 168 // ExtraHandleName is the name of ExtraHandle Column. 169 var ExtraHandleName = NewCIStr("_tidb_rowid") 170 171 // TableInfo provides meta data describing a DB table. 172 type TableInfo struct { 173 ID int64 `json:"id"` 174 Name CIStr `json:"name"` 175 Charset string `json:"charset"` 176 Collate string `json:"collate"` 177 // Columns are listed in the order in which they appear in the schema. 178 Columns []*ColumnInfo `json:"cols"` 179 Indices []*IndexInfo `json:"index_info"` 180 ForeignKeys []*FKInfo `json:"fk_info"` 181 State SchemaState `json:"state"` 182 PKIsHandle bool `json:"pk_is_handle"` 183 Comment string `json:"comment"` 184 AutoIncID int64 `json:"auto_inc_id"` 185 MaxColumnID int64 `json:"max_col_id"` 186 MaxIndexID int64 `json:"max_idx_id"` 187 // UpdateTS is used to record the timestamp of updating the table's schema information. 188 // These changing schema operations don't include 'truncate table' and 'rename table'. 189 UpdateTS uint64 `json:"update_timestamp"` 190 // OldSchemaID : 191 // Because auto increment ID has schemaID as prefix, 192 // We need to save original schemaID to keep autoID unchanged 193 // while renaming a table from one database to another. 194 // TODO: Remove it. 195 // Now it only uses for compatibility with the old version that already uses this field. 196 OldSchemaID int64 `json:"old_schema_id,omitempty"` 197 198 // ShardRowIDBits specify if the implicit row ID is sharded. 199 ShardRowIDBits uint64 200 201 Partition *PartitionInfo `json:"partition"` 202 203 Compression string `json:"compression"` 204 205 View *ViewInfo `json:"view"` 206 207 // Version means the version of the table info. 208 Version uint16 `json:"version"` 209 } 210 211 // GetPartitionInfo returns the partition information. 212 func (t *TableInfo) GetPartitionInfo() *PartitionInfo { 213 if t.Partition != nil && t.Partition.Enable { 214 return t.Partition 215 } 216 return nil 217 } 218 219 // GetUpdateTime gets the table's updating time. 220 func (t *TableInfo) GetUpdateTime() time.Time { 221 return TSConvert2Time(t.UpdateTS) 222 } 223 224 // GetDBID returns the schema ID that is used to create an allocator. 225 // TODO: Remove it after removing OldSchemaID. 226 func (t *TableInfo) GetDBID(dbID int64) int64 { 227 if t.OldSchemaID != 0 { 228 return t.OldSchemaID 229 } 230 return dbID 231 } 232 233 // Clone clones TableInfo. 234 func (t *TableInfo) Clone() *TableInfo { 235 nt := *t 236 nt.Columns = make([]*ColumnInfo, len(t.Columns)) 237 nt.Indices = make([]*IndexInfo, len(t.Indices)) 238 nt.ForeignKeys = make([]*FKInfo, len(t.ForeignKeys)) 239 240 for i := range t.Columns { 241 nt.Columns[i] = t.Columns[i].Clone() 242 } 243 244 for i := range t.Indices { 245 nt.Indices[i] = t.Indices[i].Clone() 246 } 247 248 for i := range t.ForeignKeys { 249 nt.ForeignKeys[i] = t.ForeignKeys[i].Clone() 250 } 251 252 return &nt 253 } 254 255 // GetPkName will return the pk name if pk exists. 256 func (t *TableInfo) GetPkName() CIStr { 257 if t.PKIsHandle { 258 for _, colInfo := range t.Columns { 259 if mysql.HasPriKeyFlag(colInfo.Flag) { 260 return colInfo.Name 261 } 262 } 263 } 264 return CIStr{} 265 } 266 267 // GetPkColInfo gets the ColumnInfo of pk if exists. 268 // Make sure PkIsHandle checked before call this method. 269 func (t *TableInfo) GetPkColInfo() *ColumnInfo { 270 for _, colInfo := range t.Columns { 271 if mysql.HasPriKeyFlag(colInfo.Flag) { 272 return colInfo 273 } 274 } 275 return nil 276 } 277 278 // GetAutoIncrementColInfo return auto increment column info 279 func (t *TableInfo) GetAutoIncrementColInfo() *ColumnInfo { 280 for _, colInfo := range t.Columns { 281 if mysql.HasAutoIncrementFlag(colInfo.Flag) { 282 return colInfo 283 } 284 } 285 return nil 286 } 287 288 // IsAutoIncColUnsigned check if auto increment column unsigned 289 func (t *TableInfo) IsAutoIncColUnsigned() bool { 290 col := t.GetAutoIncrementColInfo() 291 if col == nil { 292 return false 293 } 294 return mysql.HasUnsignedFlag(col.Flag) 295 } 296 297 // Cols returns the columns of the table in public state. 298 func (t *TableInfo) Cols() []*ColumnInfo { 299 publicColumns := make([]*ColumnInfo, len(t.Columns)) 300 maxOffset := -1 301 for _, col := range t.Columns { 302 if col.State != StatePublic { 303 continue 304 } 305 publicColumns[col.Offset] = col 306 if maxOffset < col.Offset { 307 maxOffset = col.Offset 308 } 309 } 310 return publicColumns[0 : maxOffset+1] 311 } 312 313 // NewExtraHandleColInfo mocks a column info for extra handle column. 314 func NewExtraHandleColInfo() *ColumnInfo { 315 colInfo := &ColumnInfo{ 316 ID: ExtraHandleID, 317 Name: ExtraHandleName, 318 } 319 colInfo.Flag = mysql.PriKeyFlag 320 colInfo.Tp = mysql.TypeLonglong 321 colInfo.Flen, colInfo.Decimal = mysql.GetDefaultFieldLengthAndDecimal(mysql.TypeLonglong) 322 return colInfo 323 } 324 325 // ColumnIsInIndex checks whether c is included in any indices of t. 326 func (t *TableInfo) ColumnIsInIndex(c *ColumnInfo) bool { 327 for _, index := range t.Indices { 328 for _, column := range index.Columns { 329 if column.Name.L == c.Name.L { 330 return true 331 } 332 } 333 } 334 return false 335 } 336 337 // IsView checks if tableinfo is a view 338 func (t *TableInfo) IsView() bool { 339 return t.View != nil 340 } 341 342 // ViewAlgorithm is VIEW's SQL AlGORITHM characteristic. 343 // See https://dev.mysql.com/doc/refman/5.7/en/view-algorithms.html 344 type ViewAlgorithm int 345 346 const ( 347 // AlgorithmUndefined undefined algorithm 348 AlgorithmUndefined ViewAlgorithm = iota 349 // AlgorithmMerge merge algorithm 350 AlgorithmMerge 351 // AlgorithmTemptable temptable algorithm 352 AlgorithmTemptable 353 ) 354 355 func (v *ViewAlgorithm) String() string { 356 switch *v { 357 case AlgorithmMerge: 358 return "MERGE" 359 case AlgorithmTemptable: 360 return "TEMPTABLE" 361 case AlgorithmUndefined: 362 return "UNDEFINED" 363 default: 364 return "UNDEFINED" 365 } 366 } 367 368 // ViewSecurity is VIEW's SQL SECURITY characteristic. 369 // See https://dev.mysql.com/doc/refman/5.7/en/create-view.html 370 type ViewSecurity int 371 372 // view security 373 const ( 374 SecurityDefiner ViewSecurity = iota 375 SecurityInvoker 376 ) 377 378 func (v *ViewSecurity) String() string { 379 switch *v { 380 case SecurityInvoker: 381 return "INVOKER" 382 case SecurityDefiner: 383 return "DEFINER" 384 default: 385 return "DEFINER" 386 } 387 } 388 389 // ViewCheckOption is VIEW's WITH CHECK OPTION clause part. 390 // See https://dev.mysql.com/doc/refman/5.7/en/view-check-option.html 391 type ViewCheckOption int 392 393 // view check option 394 const ( 395 CheckOptionLocal ViewCheckOption = iota 396 CheckOptionCascaded 397 ) 398 399 func (v *ViewCheckOption) String() string { 400 switch *v { 401 case CheckOptionLocal: 402 return "LOCAL" 403 case CheckOptionCascaded: 404 return "CASCADED" 405 default: 406 return "CASCADED" 407 } 408 } 409 410 // ViewInfo provides meta data describing a DB view. 411 type ViewInfo struct { 412 Algorithm ViewAlgorithm `json:"view_algorithm"` 413 Definer *auth.UserIdentity `json:"view_definer"` 414 Security ViewSecurity `json:"view_security"` 415 SelectStmt string `json:"view_select"` 416 CheckOption ViewCheckOption `json:"view_checkoption"` 417 Cols []CIStr `json:"view_cols"` 418 } 419 420 // PartitionType is the type for PartitionInfo 421 type PartitionType int 422 423 // Partition types. 424 const ( 425 PartitionTypeRange PartitionType = 1 426 PartitionTypeHash PartitionType = 2 427 PartitionTypeList PartitionType = 3 428 ) 429 430 func (p PartitionType) String() string { 431 switch p { 432 case PartitionTypeRange: 433 return "RANGE" 434 case PartitionTypeHash: 435 return "HASH" 436 case PartitionTypeList: 437 return "LIST" 438 default: 439 return "" 440 } 441 442 } 443 444 // PartitionInfo provides table partition info. 445 type PartitionInfo struct { 446 Type PartitionType `json:"type"` 447 Expr string `json:"expr"` 448 Columns []CIStr `json:"columns"` 449 450 // User may already creates table with partition but table partition is not 451 // yet supported back then. When Enable is true, write/read need use tid 452 // rather than pid. 453 Enable bool `json:"enable"` 454 455 Definitions []PartitionDefinition `json:"definitions"` 456 Num uint64 `json:"num"` 457 } 458 459 // GetNameByID gets the partition name by ID. 460 func (pi *PartitionInfo) GetNameByID(id int64) string { 461 for _, def := range pi.Definitions { 462 if id == def.ID { 463 return def.Name.L 464 } 465 } 466 return "" 467 } 468 469 // PartitionDefinition defines a single partition. 470 type PartitionDefinition struct { 471 ID int64 `json:"id"` 472 Name CIStr `json:"name"` 473 LessThan []string `json:"less_than"` 474 Comment string `json:"comment,omitempty"` 475 } 476 477 // IndexColumn provides index column info. 478 type IndexColumn struct { 479 Name CIStr `json:"name"` // Index name 480 Offset int `json:"offset"` // Index offset 481 // Length of prefix when using column prefix 482 // for indexing; 483 // UnspecifedLength if not using prefix indexing 484 Length int `json:"length"` 485 } 486 487 // Clone clones IndexColumn. 488 func (i *IndexColumn) Clone() *IndexColumn { 489 ni := *i 490 return &ni 491 } 492 493 // IndexType is the type of index 494 type IndexType int 495 496 // String implements Stringer interface. 497 func (t IndexType) String() string { 498 switch t { 499 case IndexTypeBtree: 500 return "BTREE" 501 case IndexTypeHash: 502 return "HASH" 503 default: 504 return "" 505 } 506 } 507 508 // IndexTypes 509 const ( 510 IndexTypeInvalid IndexType = iota 511 IndexTypeBtree 512 IndexTypeHash 513 ) 514 515 // IndexInfo provides meta data describing a DB index. 516 // It corresponds to the statement `CREATE INDEX Name ON Table (Column);` 517 // See https://dev.mysql.com/doc/refman/5.7/en/create-index.html 518 type IndexInfo struct { 519 ID int64 `json:"id"` 520 Name CIStr `json:"idx_name"` // Index name. 521 Table CIStr `json:"tbl_name"` // Table name. 522 Columns []*IndexColumn `json:"idx_cols"` // Index columns. 523 Unique bool `json:"is_unique"` // Whether the index is unique. 524 Primary bool `json:"is_primary"` // Whether the index is primary key. 525 State SchemaState `json:"state"` 526 Comment string `json:"comment"` // Comment 527 Tp IndexType `json:"index_type"` // Index type: Btree or Hash 528 } 529 530 // Clone clones IndexInfo. 531 func (index *IndexInfo) Clone() *IndexInfo { 532 ni := *index 533 ni.Columns = make([]*IndexColumn, len(index.Columns)) 534 for i := range index.Columns { 535 ni.Columns[i] = index.Columns[i].Clone() 536 } 537 return &ni 538 } 539 540 // HasPrefixIndex returns whether any columns of this index uses prefix length. 541 func (index *IndexInfo) HasPrefixIndex() bool { 542 for _, ic := range index.Columns { 543 if ic.Length != types.UnspecifiedLength { 544 return true 545 } 546 } 547 return false 548 } 549 550 // FKInfo provides meta data describing a foreign key constraint. 551 type FKInfo struct { 552 ID int64 `json:"id"` 553 Name CIStr `json:"fk_name"` 554 RefTable CIStr `json:"ref_table"` 555 RefCols []CIStr `json:"ref_cols"` 556 Cols []CIStr `json:"cols"` 557 OnDelete int `json:"on_delete"` 558 OnUpdate int `json:"on_update"` 559 State SchemaState `json:"state"` 560 } 561 562 // Clone clones FKInfo. 563 func (fk *FKInfo) Clone() *FKInfo { 564 nfk := *fk 565 566 nfk.RefCols = make([]CIStr, len(fk.RefCols)) 567 nfk.Cols = make([]CIStr, len(fk.Cols)) 568 copy(nfk.RefCols, fk.RefCols) 569 copy(nfk.Cols, fk.Cols) 570 571 return &nfk 572 } 573 574 // DBInfo provides meta data describing a DB. 575 type DBInfo struct { 576 ID int64 `json:"id"` // Database ID 577 Name CIStr `json:"db_name"` // DB name. 578 Charset string `json:"charset"` 579 Collate string `json:"collate"` 580 Tables []*TableInfo `json:"-"` // Tables in the DB. 581 State SchemaState `json:"state"` 582 } 583 584 // Clone clones DBInfo. 585 func (db *DBInfo) Clone() *DBInfo { 586 newInfo := *db 587 newInfo.Tables = make([]*TableInfo, len(db.Tables)) 588 for i := range db.Tables { 589 newInfo.Tables[i] = db.Tables[i].Clone() 590 } 591 return &newInfo 592 } 593 594 // Copy shallow copies DBInfo. 595 func (db *DBInfo) Copy() *DBInfo { 596 newInfo := *db 597 newInfo.Tables = make([]*TableInfo, len(db.Tables)) 598 copy(newInfo.Tables, db.Tables) 599 return &newInfo 600 } 601 602 // CIStr is case insensitive string. 603 type CIStr struct { 604 O string `json:"O"` // Original string. 605 L string `json:"L"` // Lower case string. 606 } 607 608 // String implements fmt.Stringer interface. 609 func (cis CIStr) String() string { 610 return cis.O 611 } 612 613 // NewCIStr creates a new CIStr. 614 func NewCIStr(s string) (cs CIStr) { 615 cs.O = s 616 cs.L = strings.ToLower(s) 617 return 618 } 619 620 // UnmarshalJSON implements the user defined unmarshal method. 621 // CIStr can be unmarshaled from a single string, so PartitionDefinition.Name 622 // in this change https://github.com/pingcap/tidb/pull/6460/files would be 623 // compatible during TiDB upgrading. 624 func (cis *CIStr) UnmarshalJSON(b []byte) error { 625 type T CIStr 626 if err := json.Unmarshal(b, (*T)(cis)); err == nil { 627 return nil 628 } 629 630 // Unmarshal CIStr from a single string. 631 err := json.Unmarshal(b, &cis.O) 632 if err != nil { 633 return errors.Trace(err) 634 } 635 cis.L = strings.ToLower(cis.O) 636 return nil 637 } 638 639 // ColumnsToProto converts a slice of model.ColumnInfo to a slice of tipb.ColumnInfo. 640 func ColumnsToProto(columns []*ColumnInfo, pkIsHandle bool) []*tipb.ColumnInfo { 641 cols := make([]*tipb.ColumnInfo, 0, len(columns)) 642 for _, c := range columns { 643 col := ColumnToProto(c) 644 // TODO: Here `PkHandle`'s meaning is changed, we will change it to `IsHandle` when tikv's old select logic 645 // is abandoned. 646 if (pkIsHandle && mysql.HasPriKeyFlag(c.Flag)) || c.ID == ExtraHandleID { 647 col.PkHandle = true 648 } else { 649 col.PkHandle = false 650 } 651 cols = append(cols, col) 652 } 653 return cols 654 } 655 656 // IndexToProto converts a model.IndexInfo to a tipb.IndexInfo. 657 func IndexToProto(t *TableInfo, idx *IndexInfo) *tipb.IndexInfo { 658 pi := &tipb.IndexInfo{ 659 TableId: t.ID, 660 IndexId: idx.ID, 661 Unique: idx.Unique, 662 } 663 cols := make([]*tipb.ColumnInfo, 0, len(idx.Columns)+1) 664 for _, c := range idx.Columns { 665 cols = append(cols, ColumnToProto(t.Columns[c.Offset])) 666 } 667 if t.PKIsHandle { 668 // Coprocessor needs to know PKHandle column info, so we need to append it. 669 for _, col := range t.Columns { 670 if mysql.HasPriKeyFlag(col.Flag) { 671 colPB := ColumnToProto(col) 672 colPB.PkHandle = true 673 cols = append(cols, colPB) 674 break 675 } 676 } 677 } 678 pi.Columns = cols 679 return pi 680 } 681 682 // ColumnToProto converts model.ColumnInfo to tipb.ColumnInfo. 683 func ColumnToProto(c *ColumnInfo) *tipb.ColumnInfo { 684 pc := &tipb.ColumnInfo{ 685 ColumnId: c.ID, 686 Collation: collationToProto(c.FieldType.Collate), 687 ColumnLen: int32(c.FieldType.Flen), 688 Decimal: int32(c.FieldType.Decimal), 689 Flag: int32(c.Flag), 690 Elems: c.Elems, 691 } 692 pc.Tp = int32(c.FieldType.Tp) 693 return pc 694 } 695 696 // TODO: update it when more collate is supported. 697 func collationToProto(c string) int32 { 698 v := mysql.CollationNames[c] 699 if v == mysql.BinaryCollationID { 700 return int32(mysql.BinaryCollationID) 701 } 702 // We only support binary and utf8_bin collation. 703 // Setting other collations to utf8_bin for old data compatibility. 704 // For the data created when we didn't enforce utf8_bin collation in create table. 705 return int32(mysql.DefaultCollationID) 706 } 707 708 // TableColumnID is composed by table ID and column ID. 709 type TableColumnID struct { 710 TableID int64 711 ColumnID int64 712 }