| 29 | } |
| 30 | |
| 31 | func GenerateRestoreSQL(ctx context.Context, rCtx base.RestoreContext, statement string, backupItem *storepb.PriorBackupDetail_Item) (string, error) { |
| 32 | // Nil guard: backupItem and its SourceTable/TargetTable sub-fields are |
| 33 | // dereferenced unconditionally below. Pre-Bug-9 the nil-backupItem check |
| 34 | // lived in extractStatement (called first); the Bug 9 refactor moved |
| 35 | // metadata fetching ahead of extractStatement, bypassing that guard. |
| 36 | // Per Codex P2 catch on PR #20345 — restoring the nil-backupItem check |
| 37 | // AND adding sub-field checks since each is independently dereferenced. |
| 38 | if backupItem == nil { |
| 39 | return "", errors.Errorf("backup item is nil") |
| 40 | } |
| 41 | if backupItem.SourceTable == nil { |
| 42 | return "", errors.Errorf("backup item source table is nil") |
| 43 | } |
| 44 | if backupItem.TargetTable == nil { |
| 45 | return "", errors.Errorf("backup item target table is nil") |
| 46 | } |
| 47 | |
| 48 | _, sourceDatabase, err := common.GetInstanceDatabaseID(backupItem.SourceTable.Database) |
| 49 | if err != nil { |
| 50 | return "", errors.Wrapf(err, errMsgFailedToGetSourceDB, backupItem.SourceTable.Database) |
| 51 | } |
| 52 | |
| 53 | // Fetch the target table's regular column set once. Used by |
| 54 | // updateMutatesTable to resolve unqualified SET columns against the |
| 55 | // target's actual schema — without it, an unqualified SET on a |
| 56 | // non-target-table column would mis-classify the UPDATE as mutating |
| 57 | // the target and produce invalid `... ON DUPLICATE KEY UPDATE ;` |
| 58 | // downstream. Per Codex P1 catch on PR #20345. |
| 59 | targetCols, err := getNormalColumnsLower(ctx, rCtx, sourceDatabase, backupItem.SourceTable.Table) |
| 60 | if err != nil { |
| 61 | return "", errors.Wrap(err, "failed to get target table columns") |
| 62 | } |
| 63 | |
| 64 | originalSQL, err := extractStatement(statement, backupItem, sourceDatabase, targetCols) |
| 65 | if err != nil { |
| 66 | return "", errors.Errorf("failed to extract single SQL: %v", err) |
| 67 | } |
| 68 | |
| 69 | // Find ALL DML nodes referencing the target table — backup.go's single- |
| 70 | // table path bundles >maxMixedDMLCount same-table DMLs into one backup |
| 71 | // item, and rollback must cover every column touched across the bundle |
| 72 | // (not just the first stmt's columns). Per Codex P1 catch on PR #20345. |
| 73 | matchingNodes, err := findMatchingDMLs(originalSQL, sourceDatabase, backupItem.SourceTable.Table, targetCols) |
| 74 | if err != nil { |
| 75 | return "", err |
| 76 | } |
| 77 | if len(matchingNodes) == 0 { |
| 78 | return "", errors.Errorf("no DML statement found in extracted SQL") |
| 79 | } |
| 80 | |
| 81 | sqlForComment, truncated := common.TruncateString(originalSQL, maxCommentLength) |
| 82 | if truncated { |
| 83 | sqlForComment += "..." |
| 84 | } |
| 85 | return doGenerate(ctx, rCtx, sqlForComment, matchingNodes, backupItem) |
| 86 | } |
| 87 | |
| 88 | // getNormalColumnsLower returns a lowercased set of regular (non- |