|
| 1 | +import chalk from "chalk"; |
| 2 | +import { Context } from "../../bundler/context.js"; |
| 3 | +import { |
| 4 | + changeSpinner, |
| 5 | + logFinishedStep, |
| 6 | + logMessage, |
| 7 | + stopSpinner, |
| 8 | +} from "../../bundler/log.js"; |
| 9 | +import { formatIndex } from "./indexes.js"; |
| 10 | +import { promptYesNo } from "./utils/prompts.js"; |
| 11 | +import { Span } from "./tracing.js"; |
| 12 | +import { StartPushRequest } from "./deployApi/startPush.js"; |
| 13 | +import { evaluatePush } from "./deploy2.js"; |
| 14 | +import { DeveloperIndexConfig, IndexDiff } from "./deployApi/finishPush.js"; |
| 15 | +import { runSystemQuery } from "./run.js"; |
| 16 | + |
| 17 | +const MIN_DOCUMENTS_FOR_INDEX_DELETE_WARNING = 10_000_000; |
| 18 | + |
| 19 | +export async function checkForLargeIndexDeletion({ |
| 20 | + ctx, |
| 21 | + span, |
| 22 | + request, |
| 23 | + options, |
| 24 | + askForConfirmation, |
| 25 | +}: { |
| 26 | + ctx: Context; |
| 27 | + span: Span; |
| 28 | + request: StartPushRequest; |
| 29 | + options: { |
| 30 | + url: string; |
| 31 | + deploymentName: string | null; |
| 32 | + adminKey: string; |
| 33 | + }; |
| 34 | + askForConfirmation: boolean; |
| 35 | +}): Promise<void> { |
| 36 | + changeSpinner("Verifying that the push isn’t deleting large indexes..."); |
| 37 | + |
| 38 | + const { schemaChange } = await evaluatePush(ctx, span, request, options); |
| 39 | + |
| 40 | + const indexDiffs = schemaChange.indexDiffs ?? {}; |
| 41 | + const deletedIndexes = Object.entries(indexDiffs).flatMap( |
| 42 | + ([componentDefinitionPath, indexDiff]) => |
| 43 | + indexDiff.removed_indexes.map((index) => ({ |
| 44 | + componentDefinitionPath, |
| 45 | + index, |
| 46 | + })), |
| 47 | + ); |
| 48 | + |
| 49 | + if (deletedIndexes.length === 0) { |
| 50 | + logFinishedStep("No indexes are deleted by this push"); |
| 51 | + return; |
| 52 | + } |
| 53 | + |
| 54 | + const tablesWithDeletedIndexes = [ |
| 55 | + ...new Set( |
| 56 | + deletedIndexes.map( |
| 57 | + ({ componentDefinitionPath, index }) => |
| 58 | + `${componentDefinitionPath}:${getTableName(index)}`, |
| 59 | + ), |
| 60 | + ), |
| 61 | + ].map((str) => { |
| 62 | + const [componentDefinitionPath, table] = str.split(":"); |
| 63 | + return { componentDefinitionPath, table }; |
| 64 | + }); |
| 65 | + changeSpinner("Checking whether the deleted indexes are on large tables..."); |
| 66 | + const documentCounts = await Promise.all( |
| 67 | + tablesWithDeletedIndexes.map( |
| 68 | + async ({ componentDefinitionPath, table }) => ({ |
| 69 | + componentDefinitionPath, |
| 70 | + table, |
| 71 | + count: (await runSystemQuery(ctx, { |
| 72 | + deploymentUrl: options.url, |
| 73 | + adminKey: options.adminKey, |
| 74 | + functionName: "_system/cli/tableSize:default", |
| 75 | + componentPath: componentDefinitionPath, |
| 76 | + args: { tableName: table }, |
| 77 | + })) as number, |
| 78 | + }), |
| 79 | + ), |
| 80 | + ); |
| 81 | + const deletedIndexesWithDocumentsCount = deletedIndexes.map( |
| 82 | + ({ componentDefinitionPath, index }) => ({ |
| 83 | + componentDefinitionPath, |
| 84 | + index, |
| 85 | + count: documentCounts.find( |
| 86 | + (count) => |
| 87 | + count.table === getTableName(index) && |
| 88 | + count.componentDefinitionPath === componentDefinitionPath, |
| 89 | + )!.count, |
| 90 | + }), |
| 91 | + ); |
| 92 | + |
| 93 | + if ( |
| 94 | + !deletedIndexesWithDocumentsCount.some( |
| 95 | + ({ count }) => count >= MIN_DOCUMENTS_FOR_INDEX_DELETE_WARNING, |
| 96 | + ) |
| 97 | + ) { |
| 98 | + logFinishedStep("No large indexes are deleted by this push"); |
| 99 | + return; |
| 100 | + } |
| 101 | + |
| 102 | + logMessage(`⚠️ This code push will ${chalk.bold("delete")} the following ${deletedIndexesWithDocumentsCount.length === 1 ? "index" : "indexes"} |
| 103 | +from your production deployment (${options.url}): |
| 104 | +
|
| 105 | +${deletedIndexesWithDocumentsCount |
| 106 | + .map(({ componentDefinitionPath, index, count }) => |
| 107 | + formatDeletedIndex( |
| 108 | + componentDefinitionPath, |
| 109 | + index, |
| 110 | + indexDiffs[componentDefinitionPath], |
| 111 | + count, |
| 112 | + ), |
| 113 | + ) |
| 114 | + .join("\n")} |
| 115 | +
|
| 116 | +The documents that are in the index won’t be deleted, but the index will need |
| 117 | +to be backfilled again if you want to restore it later. |
| 118 | +`); |
| 119 | + |
| 120 | + if (!askForConfirmation) { |
| 121 | + logFinishedStep( |
| 122 | + "Proceeding with push since --allow-deleting-large-indexes is set", |
| 123 | + ); |
| 124 | + return; |
| 125 | + } |
| 126 | + |
| 127 | + if (!process.stdin.isTTY) { |
| 128 | + return ctx.crash({ |
| 129 | + exitCode: 1, |
| 130 | + errorType: "fatal", |
| 131 | + printedMessage: `To confirm the push: |
| 132 | +• run the deploy command in an ${chalk.bold("interactive terminal")} |
| 133 | +• or run the deploy command with the ${chalk.bold("--allow-deleting-large-indexes")} flag`, |
| 134 | + }); |
| 135 | + } |
| 136 | + |
| 137 | + stopSpinner(); |
| 138 | + if ( |
| 139 | + !(await promptYesNo(ctx, { |
| 140 | + message: `Delete these ${deletedIndexesWithDocumentsCount.length === 1 ? "index" : "indexes"}?`, |
| 141 | + default: false, |
| 142 | + })) |
| 143 | + ) { |
| 144 | + return ctx.crash({ |
| 145 | + exitCode: 1, |
| 146 | + errorType: "fatal", |
| 147 | + printedMessage: `Canceling push`, |
| 148 | + }); |
| 149 | + } |
| 150 | + |
| 151 | + logFinishedStep("Proceeding with push."); |
| 152 | +} |
| 153 | + |
| 154 | +function formatDeletedIndex( |
| 155 | + componentDefinitionPath: string, |
| 156 | + index: DeveloperIndexConfig, |
| 157 | + indexDiff: IndexDiff, |
| 158 | + documentsCount: number, |
| 159 | +) { |
| 160 | + const componentNameFormatted = |
| 161 | + componentDefinitionPath !== "" |
| 162 | + ? `${chalk.gray(componentDefinitionPath)}:` |
| 163 | + : ""; |
| 164 | + |
| 165 | + const documentsCountFormatted = |
| 166 | + documentsCount >= MIN_DOCUMENTS_FOR_INDEX_DELETE_WARNING |
| 167 | + ? ` ${chalk.yellowBright(`⚠️ ${documentsCount.toLocaleString()} documents`)}` |
| 168 | + : ` ${documentsCount.toLocaleString()} ${documentsCount === 1 ? "document" : "documents"}`; |
| 169 | + |
| 170 | + const replacedBy = indexDiff.added_indexes.find((i) => i.name === index.name); |
| 171 | + const replacedByFormatted = replacedBy |
| 172 | + ? `\n ${chalk.green("→ replaced by:")} ${formatIndex(replacedBy)}` |
| 173 | + : ""; |
| 174 | + |
| 175 | + return ( |
| 176 | + "⛔ " + |
| 177 | + componentNameFormatted + |
| 178 | + formatIndex(index) + |
| 179 | + documentsCountFormatted + |
| 180 | + replacedByFormatted |
| 181 | + ); |
| 182 | +} |
| 183 | + |
| 184 | +function getTableName(index: DeveloperIndexConfig) { |
| 185 | + const [tableName, _indexName] = index.name.split("."); |
| 186 | + return tableName; |
| 187 | +} |
0 commit comments