github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/drop_table.go (about) 1 // Copyright 2015 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package sql 12 13 import ( 14 "context" 15 "fmt" 16 "strings" 17 18 "github.com/cockroachdb/cockroach/pkg/roachpb" 19 "github.com/cockroachdb/cockroach/pkg/security" 20 "github.com/cockroachdb/cockroach/pkg/server/telemetry" 21 "github.com/cockroachdb/cockroach/pkg/sql/catalog/resolver" 22 "github.com/cockroachdb/cockroach/pkg/sql/privilege" 23 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 24 "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" 25 "github.com/cockroachdb/cockroach/pkg/sql/sqltelemetry" 26 "github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented" 27 "github.com/cockroachdb/cockroach/pkg/util/hlc" 28 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 29 "github.com/cockroachdb/errors" 30 ) 31 32 type dropTableNode struct { 33 n *tree.DropTable 34 // td is a map from table descriptor to toDelete struct, indicating which 35 // tables this operation should delete. 36 td map[sqlbase.ID]toDelete 37 } 38 39 type toDelete struct { 40 tn *tree.TableName 41 desc *sqlbase.MutableTableDescriptor 42 } 43 44 // DropTable drops a table. 45 // Privileges: DROP on table. 46 // Notes: postgres allows only the table owner to DROP a table. 47 // mysql requires the DROP privilege on the table. 48 func (p *planner) DropTable(ctx context.Context, n *tree.DropTable) (planNode, error) { 49 td := make(map[sqlbase.ID]toDelete, len(n.Names)) 50 for i := range n.Names { 51 tn := &n.Names[i] 52 droppedDesc, err := p.prepareDrop(ctx, tn, !n.IfExists, resolver.ResolveRequireTableDesc) 53 if err != nil { 54 return nil, err 55 } 56 if droppedDesc == nil { 57 continue 58 } 59 60 td[droppedDesc.ID] = toDelete{tn, droppedDesc} 61 } 62 63 for _, toDel := range td { 64 droppedDesc := toDel.desc 65 for i := range droppedDesc.InboundFKs { 66 ref := &droppedDesc.InboundFKs[i] 67 if _, ok := td[ref.OriginTableID]; !ok { 68 if err := p.canRemoveFKBackreference(ctx, droppedDesc.Name, ref, n.DropBehavior); err != nil { 69 return nil, err 70 } 71 } 72 } 73 for _, idx := range droppedDesc.AllNonDropIndexes() { 74 for _, ref := range idx.InterleavedBy { 75 if _, ok := td[ref.Table]; !ok { 76 if err := p.canRemoveInterleave(ctx, droppedDesc.Name, ref, n.DropBehavior); err != nil { 77 return nil, err 78 } 79 } 80 } 81 } 82 for _, ref := range droppedDesc.DependedOnBy { 83 if _, ok := td[ref.ID]; !ok { 84 if err := p.canRemoveDependentView(ctx, droppedDesc, ref, n.DropBehavior); err != nil { 85 return nil, err 86 } 87 } 88 } 89 if err := p.canRemoveAllTableOwnedSequences(ctx, droppedDesc, n.DropBehavior); err != nil { 90 return nil, err 91 } 92 93 } 94 95 if len(td) == 0 { 96 return newZeroNode(nil /* columns */), nil 97 } 98 return &dropTableNode{n: n, td: td}, nil 99 } 100 101 // ReadingOwnWrites implements the planNodeReadingOwnWrites interface. 102 // This is because DROP TABLE performs multiple KV operations on descriptors 103 // and expects to see its own writes. 104 func (n *dropTableNode) ReadingOwnWrites() {} 105 106 func (n *dropTableNode) startExec(params runParams) error { 107 telemetry.Inc(sqltelemetry.SchemaChangeDropCounter("table")) 108 109 ctx := params.ctx 110 for _, toDel := range n.td { 111 droppedDesc := toDel.desc 112 if droppedDesc == nil { 113 continue 114 } 115 116 droppedViews, err := params.p.dropTableImpl(ctx, droppedDesc, true /* queueJob */, tree.AsStringWithFQNames(n.n, params.Ann())) 117 if err != nil { 118 return err 119 } 120 // Log a Drop Table event for this table. This is an auditable log event 121 // and is recorded in the same transaction as the table descriptor 122 // update. 123 if err := MakeEventLogger(params.extendedEvalCtx.ExecCfg).InsertEventRecord( 124 ctx, 125 params.p.txn, 126 EventLogDropTable, 127 int32(droppedDesc.ID), 128 int32(params.extendedEvalCtx.NodeID.SQLInstanceID()), 129 struct { 130 TableName string 131 Statement string 132 User string 133 CascadeDroppedViews []string 134 }{toDel.tn.FQString(), n.n.String(), 135 params.SessionData().User, droppedViews}, 136 ); err != nil { 137 return err 138 } 139 } 140 return nil 141 } 142 143 func (*dropTableNode) Next(runParams) (bool, error) { return false, nil } 144 func (*dropTableNode) Values() tree.Datums { return tree.Datums{} } 145 func (*dropTableNode) Close(context.Context) {} 146 147 // prepareDrop/dropTableImpl is used to drop a single table by 148 // name, which can result from a DROP TABLE, DROP VIEW, DROP SEQUENCE, 149 // or DROP DATABASE statement. This method returns the dropped table 150 // descriptor, to be used for the purpose of logging the event. The table 151 // is not actually truncated or deleted synchronously. Instead, it is marked 152 // as deleted (meaning up_version is set and deleted is set) and the 153 // actual deletion happens async in a schema changer. Note that, 154 // courtesy of up_version, the actual truncation and dropping will 155 // only happen once every node ACKs the version of the descriptor with 156 // the deleted bit set, meaning the lease manager will not hand out 157 // new leases for it and existing leases are released). 158 // If the table does not exist, this function returns a nil descriptor. 159 func (p *planner) prepareDrop( 160 ctx context.Context, 161 name *tree.TableName, 162 required bool, 163 requiredType resolver.ResolveRequiredType, 164 ) (*sqlbase.MutableTableDescriptor, error) { 165 tableDesc, err := p.ResolveMutableTableDescriptor(ctx, name, required, requiredType) 166 if err != nil { 167 return nil, err 168 } 169 if tableDesc == nil { 170 return nil, err 171 } 172 if err := p.prepareDropWithTableDesc(ctx, tableDesc); err != nil { 173 return nil, err 174 } 175 return tableDesc, nil 176 } 177 178 // prepareDropWithTableDesc behaves as prepareDrop, except it assumes the 179 // table descriptor is already fetched. This is useful for DropDatabase, 180 // as prepareDrop requires resolving a TableName when DropDatabase already 181 // has it resolved. 182 func (p *planner) prepareDropWithTableDesc( 183 ctx context.Context, tableDesc *sqlbase.MutableTableDescriptor, 184 ) error { 185 return p.CheckPrivilege(ctx, tableDesc, privilege.DROP) 186 } 187 188 // canRemoveFKBackReference returns an error if the input backreference isn't 189 // allowed to be removed. 190 func (p *planner) canRemoveFKBackreference( 191 ctx context.Context, from string, ref *sqlbase.ForeignKeyConstraint, behavior tree.DropBehavior, 192 ) error { 193 table, err := p.Tables().GetMutableTableVersionByID(ctx, ref.OriginTableID, p.txn) 194 if err != nil { 195 return err 196 } 197 if behavior != tree.DropCascade { 198 return fmt.Errorf("%q is referenced by foreign key from table %q", from, table.Name) 199 } 200 // Check to see whether we're allowed to edit the table that has a 201 // foreign key constraint on the table that we're dropping right now. 202 return p.CheckPrivilege(ctx, table, privilege.CREATE) 203 } 204 205 func (p *planner) canRemoveInterleave( 206 ctx context.Context, from string, ref sqlbase.ForeignKeyReference, behavior tree.DropBehavior, 207 ) error { 208 table, err := p.Tables().GetMutableTableVersionByID(ctx, ref.Table, p.txn) 209 if err != nil { 210 return err 211 } 212 // TODO(dan): It's possible to DROP a table that has a child interleave, but 213 // some loose ends would have to be addressed. The zone would have to be 214 // kept and deleted when the last table in it is removed. Also, the dropped 215 // table's descriptor would have to be kept around in some Dropped but 216 // non-public state for referential integrity of the `InterleaveDescriptor` 217 // pointers. 218 if behavior != tree.DropCascade { 219 return unimplemented.NewWithIssuef( 220 8036, "%q is interleaved by table %q", from, table.Name) 221 } 222 return p.CheckPrivilege(ctx, table, privilege.CREATE) 223 } 224 225 func (p *planner) removeInterleave(ctx context.Context, ref sqlbase.ForeignKeyReference) error { 226 table, err := p.Tables().GetMutableTableVersionByID(ctx, ref.Table, p.txn) 227 if err != nil { 228 return err 229 } 230 if table.Dropped() { 231 // The referenced table is being dropped. No need to modify it further. 232 return nil 233 } 234 idx, err := table.FindIndexByID(ref.Index) 235 if err != nil { 236 return err 237 } 238 idx.Interleave.Ancestors = nil 239 // No job description, since this is presumably part of some larger schema change. 240 return p.writeSchemaChange(ctx, table, sqlbase.InvalidMutationID, "") 241 } 242 243 // dropTableImpl does the work of dropping a table (and everything that depends 244 // on it if `cascade` is enabled). It returns a list of view names that were 245 // dropped due to `cascade` behavior. 246 func (p *planner) dropTableImpl( 247 ctx context.Context, tableDesc *sqlbase.MutableTableDescriptor, queueJob bool, jobDesc string, 248 ) ([]string, error) { 249 var droppedViews []string 250 251 // Remove foreign key back references from tables that this table has foreign 252 // keys to. 253 for i := range tableDesc.OutboundFKs { 254 ref := &tableDesc.OutboundFKs[i] 255 if err := p.removeFKBackReference(ctx, tableDesc, ref); err != nil { 256 return droppedViews, err 257 } 258 } 259 tableDesc.OutboundFKs = nil 260 261 // Remove foreign key forward references from tables that have foreign keys 262 // to this table. 263 for i := range tableDesc.InboundFKs { 264 ref := &tableDesc.InboundFKs[i] 265 if err := p.removeFKForBackReference(ctx, tableDesc, ref); err != nil { 266 return droppedViews, err 267 } 268 } 269 tableDesc.InboundFKs = nil 270 271 // Remove interleave relationships. 272 for _, idx := range tableDesc.AllNonDropIndexes() { 273 if len(idx.Interleave.Ancestors) > 0 { 274 if err := p.removeInterleaveBackReference(ctx, tableDesc, idx); err != nil { 275 return droppedViews, err 276 } 277 } 278 for _, ref := range idx.InterleavedBy { 279 if err := p.removeInterleave(ctx, ref); err != nil { 280 return droppedViews, err 281 } 282 } 283 } 284 285 // Remove sequence dependencies. 286 for i := range tableDesc.Columns { 287 if err := p.removeSequenceDependencies(ctx, tableDesc, &tableDesc.Columns[i]); err != nil { 288 return droppedViews, err 289 } 290 } 291 292 // Drop sequences that the columns of the table own 293 for _, col := range tableDesc.Columns { 294 if err := p.dropSequencesOwnedByCol(ctx, &col); err != nil { 295 return droppedViews, err 296 } 297 } 298 299 // Drop all views that depend on this table, assuming that we wouldn't have 300 // made it to this point if `cascade` wasn't enabled. 301 for _, ref := range tableDesc.DependedOnBy { 302 viewDesc, err := p.getViewDescForCascade( 303 ctx, tableDesc.TypeName(), tableDesc.Name, tableDesc.ParentID, ref.ID, tree.DropCascade, 304 ) 305 if err != nil { 306 return droppedViews, err 307 } 308 // This view is already getting dropped. Don't do it twice. 309 if viewDesc.Dropped() { 310 continue 311 } 312 // TODO (lucy): Have more consistent/informative names for dependent jobs. 313 cascadedViews, err := p.dropViewImpl(ctx, viewDesc, queueJob, "dropping dependent view", tree.DropCascade) 314 if err != nil { 315 return droppedViews, err 316 } 317 droppedViews = append(droppedViews, cascadedViews...) 318 droppedViews = append(droppedViews, viewDesc.Name) 319 } 320 321 err := p.removeTableComments(ctx, tableDesc) 322 if err != nil { 323 return droppedViews, err 324 } 325 326 err = p.initiateDropTable(ctx, tableDesc, queueJob, jobDesc, true /* drain name */) 327 return droppedViews, err 328 } 329 330 // drainName when set implies that the name needs to go through the draining 331 // names process. This parameter is always passed in as true except from 332 // TRUNCATE which directly deletes the old name to id map and doesn't need 333 // drain the old map. 334 func (p *planner) initiateDropTable( 335 ctx context.Context, 336 tableDesc *sqlbase.MutableTableDescriptor, 337 queueJob bool, 338 jobDesc string, 339 drainName bool, 340 ) error { 341 if tableDesc.Dropped() { 342 return fmt.Errorf("table %q is being dropped", tableDesc.Name) 343 } 344 345 // If the table is not interleaved , use the delayed GC mechanism to 346 // schedule usage of the more efficient ClearRange pathway. ClearRange will 347 // only work if the entire hierarchy of interleaved tables are dropped at 348 // once, as with ON DELETE CASCADE where the top-level "root" table is 349 // dropped. 350 // 351 // TODO(bram): If interleaved and ON DELETE CASCADE, we will be able to use 352 // this faster mechanism. 353 if tableDesc.IsTable() && !tableDesc.IsInterleaved() { 354 // Get the zone config applying to this table in order to 355 // ensure there is a GC TTL. 356 _, _, _, err := GetZoneConfigInTxn( 357 ctx, p.txn, uint32(tableDesc.ID), &sqlbase.IndexDescriptor{}, "", false, /* getInheritedDefault */ 358 ) 359 if err != nil { 360 return err 361 } 362 363 tableDesc.DropTime = timeutil.Now().UnixNano() 364 } 365 366 // Unsplit all manually split ranges in the table so they can be 367 // automatically merged by the merge queue. 368 ranges, err := ScanMetaKVs(ctx, p.txn, tableDesc.TableSpan(p.ExecCfg().Codec)) 369 if err != nil { 370 return err 371 } 372 for _, r := range ranges { 373 var desc roachpb.RangeDescriptor 374 if err := r.ValueProto(&desc); err != nil { 375 return err 376 } 377 if (desc.GetStickyBit() != hlc.Timestamp{}) { 378 // Swallow "key is not the start of a range" errors because it would mean 379 // that the sticky bit was removed and merged concurrently. DROP TABLE 380 // should not fail because of this. 381 if err := p.ExecCfg().DB.AdminUnsplit(ctx, desc.StartKey); err != nil && !strings.Contains(err.Error(), "is not the start of a range") { 382 return err 383 } 384 } 385 } 386 387 tableDesc.State = sqlbase.TableDescriptor_DROP 388 if drainName { 389 parentSchemaID := tableDesc.GetParentSchemaID() 390 391 // Queue up name for draining. 392 nameDetails := sqlbase.TableDescriptor_NameInfo{ 393 ParentID: tableDesc.ParentID, 394 ParentSchemaID: parentSchemaID, 395 Name: tableDesc.Name} 396 tableDesc.DrainingNames = append(tableDesc.DrainingNames, nameDetails) 397 } 398 399 // Mark all jobs scheduled for schema changes as successful. 400 jobIDs := make(map[int64]struct{}) 401 var id sqlbase.MutationID 402 for _, m := range tableDesc.Mutations { 403 if id != m.MutationID { 404 id = m.MutationID 405 jobID, err := getJobIDForMutationWithDescriptor(ctx, tableDesc.TableDesc(), id) 406 if err != nil { 407 return err 408 } 409 jobIDs[jobID] = struct{}{} 410 } 411 } 412 for jobID := range jobIDs { 413 if err := p.ExecCfg().JobRegistry.Succeeded(ctx, p.txn, jobID); err != nil { 414 return errors.Wrapf(err, 415 "failed to mark job %d as as successful", errors.Safe(jobID)) 416 } 417 } 418 // Initiate an immediate schema change. When dropping a table 419 // in a session, the data and the descriptor are not deleted. 420 // Instead, that is taken care of asynchronously by the schema 421 // change manager, which is notified via a system config gossip. 422 // The schema change manager will properly schedule deletion of 423 // the underlying data when the GC deadline expires. 424 return p.writeDropTable(ctx, tableDesc, queueJob, jobDesc) 425 } 426 427 func (p *planner) removeFKForBackReference( 428 ctx context.Context, tableDesc *sqlbase.MutableTableDescriptor, ref *sqlbase.ForeignKeyConstraint, 429 ) error { 430 var originTableDesc *sqlbase.MutableTableDescriptor 431 // We don't want to lookup/edit a second copy of the same table. 432 if tableDesc.ID == ref.OriginTableID { 433 originTableDesc = tableDesc 434 } else { 435 lookup, err := p.Tables().GetMutableTableVersionByID(ctx, ref.OriginTableID, p.txn) 436 if err != nil { 437 return errors.Errorf("error resolving origin table ID %d: %v", ref.OriginTableID, err) 438 } 439 originTableDesc = lookup 440 } 441 if originTableDesc.Dropped() { 442 // The origin table is being dropped. No need to modify it further. 443 return nil 444 } 445 446 if err := removeFKForBackReferenceFromTable(originTableDesc, ref, tableDesc.TableDesc()); err != nil { 447 return err 448 } 449 // No job description, since this is presumably part of some larger schema change. 450 return p.writeSchemaChange(ctx, originTableDesc, sqlbase.InvalidMutationID, "") 451 } 452 453 // removeFKBackReferenceFromTable edits the supplied originTableDesc to 454 // remove the foreign key constraint that corresponds to the supplied 455 // backreference, which is a member of the supplied referencedTableDesc. 456 func removeFKForBackReferenceFromTable( 457 originTableDesc *sqlbase.MutableTableDescriptor, 458 backref *sqlbase.ForeignKeyConstraint, 459 referencedTableDesc *sqlbase.TableDescriptor, 460 ) error { 461 matchIdx := -1 462 for i, fk := range originTableDesc.OutboundFKs { 463 if fk.ReferencedTableID == referencedTableDesc.ID && fk.Name == backref.Name { 464 // We found a match! We want to delete it from the list now. 465 matchIdx = i 466 break 467 } 468 } 469 if matchIdx == -1 { 470 // There was no match: no back reference in the referenced table that 471 // matched the foreign key constraint that we were trying to delete. 472 // This really shouldn't happen... 473 return errors.AssertionFailedf("there was no foreign key constraint "+ 474 "for backreference %v on table %q", backref, originTableDesc.Name) 475 } 476 // Delete our match. 477 originTableDesc.OutboundFKs = append( 478 originTableDesc.OutboundFKs[:matchIdx], 479 originTableDesc.OutboundFKs[matchIdx+1:]...) 480 return nil 481 } 482 483 // removeFKBackReference removes the FK back reference from the table that is 484 // referenced by the input constraint. 485 func (p *planner) removeFKBackReference( 486 ctx context.Context, tableDesc *sqlbase.MutableTableDescriptor, ref *sqlbase.ForeignKeyConstraint, 487 ) error { 488 var referencedTableDesc *sqlbase.MutableTableDescriptor 489 // We don't want to lookup/edit a second copy of the same table. 490 if tableDesc.ID == ref.ReferencedTableID { 491 referencedTableDesc = tableDesc 492 } else { 493 lookup, err := p.Tables().GetMutableTableVersionByID(ctx, ref.ReferencedTableID, p.txn) 494 if err != nil { 495 return errors.Errorf("error resolving referenced table ID %d: %v", ref.ReferencedTableID, err) 496 } 497 referencedTableDesc = lookup 498 } 499 if referencedTableDesc.Dropped() { 500 // The referenced table is being dropped. No need to modify it further. 501 return nil 502 } 503 504 if err := removeFKBackReferenceFromTable(referencedTableDesc, ref.Name, tableDesc.TableDesc()); err != nil { 505 return err 506 } 507 // No job description, since this is presumably part of some larger schema change. 508 return p.writeSchemaChange(ctx, referencedTableDesc, sqlbase.InvalidMutationID, "") 509 } 510 511 // removeFKBackReferenceFromTable edits the supplied referencedTableDesc to 512 // remove the foreign key backreference that corresponds to the supplied fk, 513 // which is a member of the supplied originTableDesc. 514 func removeFKBackReferenceFromTable( 515 referencedTableDesc *sqlbase.MutableTableDescriptor, 516 fkName string, 517 originTableDesc *sqlbase.TableDescriptor, 518 ) error { 519 matchIdx := -1 520 for i, backref := range referencedTableDesc.InboundFKs { 521 if backref.OriginTableID == originTableDesc.ID && backref.Name == fkName { 522 // We found a match! We want to delete it from the list now. 523 matchIdx = i 524 break 525 } 526 } 527 if matchIdx == -1 { 528 // There was no match: no back reference in the referenced table that 529 // matched the foreign key constraint that we were trying to delete. 530 // This really shouldn't happen... 531 return errors.AssertionFailedf("there was no foreign key backreference "+ 532 "for constraint %q on table %q", fkName, originTableDesc.Name) 533 } 534 // Delete our match. 535 referencedTableDesc.InboundFKs = append(referencedTableDesc.InboundFKs[:matchIdx], referencedTableDesc.InboundFKs[matchIdx+1:]...) 536 return nil 537 } 538 539 func (p *planner) removeInterleaveBackReference( 540 ctx context.Context, tableDesc *sqlbase.MutableTableDescriptor, idx *sqlbase.IndexDescriptor, 541 ) error { 542 if len(idx.Interleave.Ancestors) == 0 { 543 return nil 544 } 545 ancestor := idx.Interleave.Ancestors[len(idx.Interleave.Ancestors)-1] 546 var t *sqlbase.MutableTableDescriptor 547 if ancestor.TableID == tableDesc.ID { 548 t = tableDesc 549 } else { 550 lookup, err := p.Tables().GetMutableTableVersionByID(ctx, ancestor.TableID, p.txn) 551 if err != nil { 552 return errors.Errorf("error resolving referenced table ID %d: %v", ancestor.TableID, err) 553 } 554 t = lookup 555 } 556 if t.Dropped() { 557 // The referenced table is being dropped. No need to modify it further. 558 return nil 559 } 560 targetIdx, err := t.FindIndexByID(ancestor.IndexID) 561 if err != nil { 562 return err 563 } 564 foundAncestor := false 565 for k, ref := range targetIdx.InterleavedBy { 566 if ref.Table == tableDesc.ID && ref.Index == idx.ID { 567 if foundAncestor { 568 return errors.AssertionFailedf( 569 "ancestor entry in %s for %s@%s found more than once", t.Name, tableDesc.Name, idx.Name) 570 } 571 targetIdx.InterleavedBy = append(targetIdx.InterleavedBy[:k], targetIdx.InterleavedBy[k+1:]...) 572 foundAncestor = true 573 } 574 } 575 if t != tableDesc { 576 // TODO (lucy): Have more consistent/informative names for dependent jobs. 577 return p.writeSchemaChange( 578 ctx, t, sqlbase.InvalidMutationID, "removing reference for interleaved table", 579 ) 580 } 581 return nil 582 } 583 584 // removeMatchingReferences removes all refs from the provided slice that 585 // match the provided ID, returning the modified slice. 586 func removeMatchingReferences( 587 refs []sqlbase.TableDescriptor_Reference, id sqlbase.ID, 588 ) []sqlbase.TableDescriptor_Reference { 589 updatedRefs := refs[:0] 590 for _, ref := range refs { 591 if ref.ID != id { 592 updatedRefs = append(updatedRefs, ref) 593 } 594 } 595 return updatedRefs 596 } 597 598 func (p *planner) removeTableComments( 599 ctx context.Context, tableDesc *sqlbase.MutableTableDescriptor, 600 ) error { 601 _, err := p.ExtendedEvalContext().ExecCfg.InternalExecutor.ExecEx( 602 ctx, 603 "delete-table-comments", 604 p.txn, 605 sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser}, 606 "DELETE FROM system.comments WHERE object_id=$1", 607 tableDesc.ID) 608 if err != nil { 609 return err 610 } 611 return err 612 }