Skip to content

Commit 43d298a

Browse files
Allow exporting of results for non-alert queries
1 parent 3d647f6 commit 43d298a

File tree

4 files changed

+69
-3
lines changed

4 files changed

+69
-3
lines changed

extensions/ql-vscode/package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,13 +461,17 @@
461461
"command": "codeQLQueryHistory.showQueryText",
462462
"title": "Show Query Text"
463463
},
464+
{
465+
"command": "codeQLQueryHistory.exportCsvResults",
466+
"title": "Export Results (CSV)"
467+
},
464468
{
465469
"command": "codeQLQueryHistory.viewCsvResults",
466-
"title": "View Results (CSV)"
470+
"title": "View Alerts (CSV)"
467471
},
468472
{
469473
"command": "codeQLQueryHistory.viewSarifResults",
470-
"title": "View Results (SARIF)"
474+
"title": "View Alerts (SARIF)"
471475
},
472476
{
473477
"command": "codeQLQueryHistory.viewDil",
@@ -643,6 +647,11 @@
643647
"group": "9_qlCommands",
644648
"when": "view == codeQLQueryHistory"
645649
},
650+
{
651+
"command": "codeQLQueryHistory.exportCsvResults",
652+
"group": "9_qlCommands",
653+
"when": "view == codeQLQueryHistory && viewItem != interpretedResultsItem"
654+
},
646655
{
647656
"command": "codeQLQueryHistory.viewCsvResults",
648657
"group": "9_qlCommands",

extensions/ql-vscode/src/contextual/locationFinder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ProgressCallback } from '../commandRunner';
1212
import { KeyType } from './keyType';
1313
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
1414

15-
const SELECT_QUERY_NAME = '#select';
15+
export const SELECT_QUERY_NAME = '#select';
1616
export const TEMPLATE_NAME = 'selectedSourceFile';
1717

1818
export interface FullLocationLink extends vscode.LocationLink {

extensions/ql-vscode/src/query-history.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,12 @@ export class QueryHistoryManager extends DisposableObject {
304304
this.handleShowQueryText.bind(this)
305305
)
306306
);
307+
this.push(
308+
commandRunner(
309+
'codeQLQueryHistory.exportCsvResults',
310+
this.handleExportCsvResults.bind(this)
311+
)
312+
);
307313
this.push(
308314
commandRunner(
309315
'codeQLQueryHistory.viewCsvResults',
@@ -571,6 +577,32 @@ export class QueryHistoryManager extends DisposableObject {
571577
}
572578
}
573579

580+
async handleExportCsvResults(
581+
singleItem: CompletedQuery,
582+
multiSelect: CompletedQuery[]
583+
) {
584+
if (!this.assertSingleQuery(multiSelect)) {
585+
return;
586+
}
587+
588+
const saveLocation = await vscode.window.showSaveDialog({
589+
title: 'CSV Results',
590+
saveLabel: 'Export',
591+
filters: {
592+
'Comma-separated values': ['csv'],
593+
}
594+
});
595+
if (!saveLocation) {
596+
void showAndLogErrorMessage('No save location selected for CSV export!');
597+
return;
598+
}
599+
await singleItem.query.exportCsvResults(this.qs, saveLocation.fsPath, () => {
600+
void this.tryOpenExternalFile(
601+
saveLocation.fsPath
602+
);
603+
});
604+
}
605+
574606
async handleViewCsvResults(
575607
singleItem: CompletedQuery,
576608
multiSelect: CompletedQuery[]

extensions/ql-vscode/src/run-queries.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import * as qsClient from './queryserver-client';
2525
import { isQuickQueryPath } from './quick-query';
2626
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
2727
import { ensureMetadataIsComplete } from './query-results';
28+
import { SELECT_QUERY_NAME } from './contextual/locationFinder';
29+
import { DecodedBqrsChunk } from './pure/bqrs-cli-types';
2830

2931
/**
3032
* run-queries.ts
@@ -216,6 +218,29 @@ export class QueryInfo {
216218
return this.dilPath;
217219
}
218220

221+
async exportCsvResults(qs: qsClient.QueryServerClient, csvPath: string, onFinish: () => void): Promise<void> {
222+
let stopDecoding = false;
223+
const out = fs.createWriteStream(csvPath);
224+
out.on('finish', onFinish);
225+
out.on('error', () => {
226+
if (!stopDecoding) {
227+
stopDecoding = true;
228+
void showAndLogErrorMessage(`Failed to write CSV results to ${csvPath}`);
229+
}
230+
});
231+
let nextOffset: number | undefined = 0;
232+
while (nextOffset !== undefined && !stopDecoding) {
233+
const chunk: DecodedBqrsChunk = await qs.cliServer.bqrsDecode(this.resultsPaths.resultsPath, SELECT_QUERY_NAME, {
234+
pageSize: 100,
235+
offset: nextOffset,
236+
});
237+
for (const tuple of chunk.tuples)
238+
out.write(tuple.join(',') + '\n');
239+
nextOffset = chunk.next;
240+
}
241+
out.end();
242+
}
243+
219244
async ensureCsvProduced(qs: qsClient.QueryServerClient): Promise<string> {
220245
if (await this.hasCsv()) {
221246
return this.csvPath;

0 commit comments

Comments
 (0)