Skip to content

Commit ea9f192

Browse files
committed
extend proto decoding in job_info
now shows resumer traces and handles non-utf map keys such as those in backup kms uri details.
1 parent 2405af3 commit ea9f192

File tree

9 files changed

+285
-75
lines changed

9 files changed

+285
-75
lines changed

src/components/EnhancedFileViewer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ function EnhancedFileViewer({ tab }: FileViewerProps) {
2525

2626
// Binary detection state
2727
const [binaryDetected, setBinaryDetected] = useState(false);
28-
const [binaryReason, setBinaryReason] = useState<string>("");
29-
const [fileType, setFileType] = useState<string>("");
28+
const [_binaryReason, setBinaryReason] = useState<string>("");
29+
const [_fileType, setFileType] = useState<string>("");
3030
const [userOverrideBinary, setUserOverrideBinary] = useState(false);
3131

3232
// Filter state
@@ -170,7 +170,7 @@ function EnhancedFileViewer({ tab }: FileViewerProps) {
170170
}
171171

172172
// Prompt user for save location
173-
const handle = await window.showSaveFilePicker({
173+
const handle = await (window as any).showSaveFilePicker({
174174
suggestedName: fileEntry.name,
175175
});
176176

src/components/SqlEditor.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ function SqlEditor({ tab }: SqlEditorProps) {
9696
return <span className="sql-cell-hex">{displayValue}</span>;
9797
}
9898

99+
// Check for DistSQL diagram URLs (check first 1KB, but replace in full string)
100+
const sampleText = strValue.substring(0, 1024);
101+
if (sampleText.includes('cockroachdb.github.io/distsqlplan/decode.html')) {
102+
// Run replacement on the full string to capture the entire URL
103+
const urlMatch = strValue.match(/"(https:\/\/cockroachdb\.github\.io\/distsqlplan\/decode\.html#[^"]+)"/);
104+
if (urlMatch) {
105+
const url = urlMatch[1];
106+
return (
107+
<a
108+
href={url}
109+
target="_blank"
110+
rel="noopener noreferrer"
111+
className="sql-cell-link"
112+
style={{ color: '#0066cc', textDecoration: 'underline' }}
113+
>
114+
Diagram
115+
</a>
116+
);
117+
}
118+
}
119+
99120
// Check if it's JSON (from decoded protobuf or other sources)
100121
if (strValue.startsWith("{") || strValue.startsWith("[")) {
101122
try {

src/components/sidebar/TablesView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ function TablesView() {
365365
if (table.isError) {
366366
const baseName = table.originalName || table.name;
367367
const errorFiles = table.nodeFiles?.filter(f => f.isError) ||
368-
[{ path: table.sourceFile, nodeId: table.nodeId, size: table.size, isError: true }];
368+
[{ path: table.sourceFile, nodeId: table.nodeId ?? 0, size: table.size ?? 0, isError: true }];
369369
const availableFiles = table.nodeFiles?.filter(f => !f.isError) || [];
370370

371371
dispatch({

src/crdb/csvPreprocessor.ts

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ function createFallbackJson(hexValue: string): string {
151151
}
152152

153153
// Main preprocessing function - transforms keys in place
154-
export function preprocessCSV(
154+
export async function preprocessCSV(
155155
content: string,
156156
options: PreprocessOptions,
157-
): string {
157+
): Promise<string> {
158158
const delimiter = options.delimiter || "\t";
159159
const decoder = options.protoDecoder || protoDecoder; // Use provided decoder or fallback to global
160160
const { headers, rows } = parseDelimited(content, delimiter);
@@ -194,15 +194,10 @@ export function preprocessCSV(
194194

195195
// Check for proto columns
196196
if (options.decodeProtos) {
197-
if (
198-
columnName === "config" ||
199-
columnName === "descriptor" ||
200-
columnName === "payload" ||
201-
columnName === "progress" ||
202-
columnName === "value"
203-
) {
204-
const mapping = findProtoType(options.tableName, header);
205-
protoColumns.set(index, mapping?.protoType || null);
197+
const mapping = findProtoType(options.tableName, header);
198+
if (mapping) {
199+
protoColumns.set(index, mapping.protoType);
200+
console.log(`📋 Proto mapping: ${options.tableName}.${header} -> ${mapping.protoType}`);
206201
}
207202
}
208203
});
@@ -214,8 +209,8 @@ export function preprocessCSV(
214209

215210
// Process rows - transform values in place
216211

217-
const processedRows = rows.map((row) => {
218-
return row.map((value, colIndex) => {
212+
const processedRows = await Promise.all(rows.map(async (row, rowIndex) => {
213+
return Promise.all(row.map(async (value, colIndex) => {
219214
// Transform key columns
220215
if (keyColumns.has(colIndex)) {
221216
// Handle null/empty differently from \x (which is a valid empty key)
@@ -230,15 +225,51 @@ export function preprocessCSV(
230225
if (protoType !== undefined && options.decodeProtos) {
231226
// Handle dynamic proto type resolution for job_info table
232227
if (protoType === "dynamic:job_info" && infoKeyColumnIndex >= 0) {
233-
const infoKey = row[infoKeyColumnIndex];
228+
const infoKey = row[infoKeyColumnIndex]?.trim();
234229
if (infoKey === "legacy_payload") {
235230
protoType = "cockroach.sql.jobs.jobspb.Payload";
236231
} else if (infoKey === "legacy_progress") {
237232
protoType = "cockroach.sql.jobs.jobspb.Progress";
233+
} else if (infoKey?.includes("cockroach.sql.jobs.jobspb.TraceData")) {
234+
// Handle TraceData specially
235+
if (infoKey.includes(".binpb#_final")) {
236+
// This is the final chunk - try to decode, but silently fail to hex wrapper
237+
protoType = "cockroach.sql.jobs.jobspb.TraceData";
238+
} else {
239+
// This is a partial chunk - go straight to hex wrapper without trying to decode
240+
if (value && value !== "\\N" && value !== "NULL") {
241+
return createFallbackJson(value);
242+
}
243+
return value;
244+
}
245+
} else if (
246+
infoKey?.startsWith("~dsp-diag-url-") ||
247+
infoKey?.startsWith("~node-processor-progress-")
248+
) {
249+
// These info_key types contain string values, not protobuf
250+
// If hex-encoded, decode and wrap in JSON
251+
if (value && value !== "\\N" && value !== "NULL") {
252+
if (value.startsWith("\\x")) {
253+
return createFallbackJson(value);
254+
}
255+
// If it's already a plain string, wrap it in JSON for consistency
256+
return JSON.stringify({ value: value });
257+
}
258+
return value;
238259
} else {
239-
// Unknown info_key type - use fallback JSON wrapper
260+
// Unknown info_key type
240261
if (value && value !== "\\N" && value !== "NULL") {
241-
return createFallbackJson(value);
262+
// Only warn and use fallback for hex-encoded values
263+
if (value.startsWith("\\x")) {
264+
const rowData = headers.map((h, i) => `${h}=${row[i]?.substring(0, 50)}${row[i]?.length > 50 ? '...' : ''}`).join(', ');
265+
console.warn(
266+
`⚠️ Unknown job_info.info_key type "${infoKey}" with hex value:\n` +
267+
` Row ${rowIndex + 1}: {${rowData}}`
268+
);
269+
return createFallbackJson(value);
270+
}
271+
// If it's not hex, it's probably already a string - return as-is
272+
return value;
242273
}
243274
return value;
244275
}
@@ -254,9 +285,10 @@ export function preprocessCSV(
254285
// If we have an explicit proto mapping, decode it
255286
if (protoType && protoType !== "dynamic:job_info") {
256287
const currentColumnName = headers[colIndex];
288+
const isTraceData = protoType === "cockroach.sql.jobs.jobspb.TraceData";
257289
try {
258290
const bytes = hexToBytes(value);
259-
const decoded = decoder.decode(bytes, protoType);
291+
const decoded = await decoder.decodeAsync(bytes, protoType);
260292

261293
// Don't use fallback for job_info - if the specific proto fails, leave as hex
262294
// The fallback was incorrectly decoding Progress data as SpanConfig
@@ -265,11 +297,56 @@ export function preprocessCSV(
265297
// Return as compact JSON string
266298
return JSON.stringify(decoded.decoded);
267299
} else {
268-
console.warn(`❌ Proto decode failed for ${currentColumnName}:`, decoded.error);
300+
// Decoding failed - use fallback
301+
const hexSample = value.substring(0, 100) + (value.length > 100 ? '...' : '');
302+
const rowData = headers.map((h, i) => `${h}=${row[i]?.substring(0, 50)}${row[i]?.length > 50 ? '...' : ''}`).join(', ');
303+
304+
if (isTraceData) {
305+
// TraceData decode failures - log to understand why
306+
console.warn(
307+
`⚠️ TraceData decode failed for ${currentColumnName}:\n` +
308+
` Row ${rowIndex + 1}: {${rowData}}\n` +
309+
` Hex (first 100 chars): ${hexSample}\n` +
310+
` Error: ${decoded.error}`
311+
);
312+
} else {
313+
// Show more details for debugging for other proto types
314+
console.warn(
315+
`❌ Proto decode failed for ${currentColumnName} (${protoType}):\n` +
316+
` Row ${rowIndex + 1}: {${rowData}}\n` +
317+
` Hex (first 100 chars): ${hexSample}\n` +
318+
` Error details:\n${decoded.error}`
319+
);
320+
}
321+
// Return hex-wrapped JSON for all failed decodes
322+
return createFallbackJson(value);
269323
}
270324
} catch (err) {
271-
console.warn(`❌ Proto decode exception for ${currentColumnName}:`, err);
272-
// Don't let protobuf errors stop processing of subsequent rows
325+
// Decode exception - use fallback
326+
const hexSample = value.substring(0, 100) + (value.length > 100 ? '...' : '');
327+
const rowData = headers.map((h, i) => `${h}=${row[i]?.substring(0, 50)}${row[i]?.length > 50 ? '...' : ''}`).join(', ');
328+
const errorDetails = err instanceof Error
329+
? `${err.name}: ${err.message}\n${err.stack}`
330+
: String(err);
331+
332+
if (isTraceData) {
333+
// TraceData decode exceptions - log to understand why
334+
console.warn(
335+
`⚠️ TraceData decode exception for ${currentColumnName}:\n` +
336+
` Row ${rowIndex + 1}: {${rowData}}\n` +
337+
` Hex (first 100 chars): ${hexSample}\n` +
338+
` Exception: ${errorDetails}`
339+
);
340+
} else {
341+
console.warn(
342+
`❌ Proto decode exception for ${currentColumnName} (${protoType}):\n` +
343+
` Row ${rowIndex + 1}: {${rowData}}\n` +
344+
` Hex (first 100 chars): ${hexSample}\n` +
345+
` Error details:\n${errorDetails}`
346+
);
347+
}
348+
// Return hex-wrapped JSON for all exceptions
349+
return createFallbackJson(value);
273350
}
274351
}
275352
}
@@ -318,8 +395,8 @@ export function preprocessCSV(
318395
}
319396

320397
return value;
321-
});
322-
});
398+
}));
399+
}));
323400

324401
// Reconstruct the CSV with transformed values
325402
const processedLines = [

src/crdb/pb/jobs/jobspb/jobs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1593,7 +1593,10 @@ class EncryptionInfo$Type extends MessageType<EncryptionInfo> {
15931593
let [fieldNo, wireType] = reader.tag();
15941594
switch (fieldNo) {
15951595
case 1:
1596-
key = reader.string();
1596+
// Read as bytes first, then try to decode as UTF-8 with replacement chars
1597+
const keyBytes = reader.bytes();
1598+
const decoder = new TextDecoder('utf-8', { fatal: false });
1599+
key = decoder.decode(keyBytes);
15971600
break;
15981601
case 2:
15991602
val = reader.bytes();

0 commit comments

Comments
 (0)