Skip to content

Commit 91bd7f5

Browse files
authored
Merge pull request #401 from jcreedcmu/jcreed/pagination
Implement pagination for BQRS results.
2 parents 109c875 + c90dae8 commit 91bd7f5

File tree

9 files changed

+322
-55
lines changed

9 files changed

+322
-55
lines changed

extensions/ql-vscode/src/adapt.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,34 @@ export function adaptBqrs(schema: AdaptedSchema, page: DecodedBqrsChunk): RawRes
101101
rows: page.tuples.map(adaptRow),
102102
};
103103
}
104+
105+
/**
106+
* This type has two branches; we are in the process of changing from
107+
* one to the other. The old way is to parse them inside the webview,
108+
* the new way is to parse them in the extension. The main motivation
109+
* for this transition is to make pagination possible in such a way
110+
* that only one page needs to be sent from the extension to the webview.
111+
*/
112+
export type ParsedResultSets = ExtensionParsedResultSets | WebviewParsedResultSets;
113+
114+
/**
115+
* The old method doesn't require any nontrivial information to be included here,
116+
* just a tag to indicate that it is being used.
117+
*/
118+
export interface WebviewParsedResultSets {
119+
t: 'WebviewParsed';
120+
selectedTable?: string; // when undefined, means 'show default table'
121+
}
122+
123+
/**
124+
* The new method includes which bqrs page is being sent, and the
125+
* actual results parsed on the extension side.
126+
*/
127+
export interface ExtensionParsedResultSets {
128+
t: 'ExtensionParsed';
129+
pageNumber: number;
130+
numPages: number;
131+
selectedTable?: string; // when undefined, means 'show default table'
132+
resultSetNames: string[];
133+
resultSet: RawResultSet;
134+
}

extensions/ql-vscode/src/interface-types.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as sarif from 'sarif';
22
import { ResolvableLocationValue } from 'semmle-bqrs';
3-
import { RawResultSet } from './adapt';
3+
import { ParsedResultSets } from './adapt';
44

55
/**
66
* Only ever show this many results per run in interpreted results.
@@ -12,6 +12,11 @@ export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
1212
*/
1313
export const RAW_RESULTS_LIMIT = 10000;
1414

15+
/**
16+
* Show this many rows in a raw result table at a time.
17+
*/
18+
export const RAW_RESULTS_PAGE_SIZE = 100;
19+
1520
export interface DatabaseInfo {
1621
name: string;
1722
databaseUri: string;
@@ -81,9 +86,10 @@ export interface SetStateMsg {
8186

8287
/**
8388
* An experimental way of providing results from the extension.
84-
* Should be undefined unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
89+
* Should be in the WebviewParsedResultSets branch of the type
90+
* unless config.EXPERIMENTAL_BQRS_SETTING is set to true.
8591
*/
86-
resultSets?: RawResultSet[];
92+
parsedResultSets: ParsedResultSets;
8793
}
8894

8995
/** Advance to the next or previous path no in the path viewer */
@@ -101,7 +107,8 @@ export type FromResultsViewMsg =
101107
| ToggleDiagnostics
102108
| ChangeRawResultsSortMsg
103109
| ChangeInterpretedResultsSortMsg
104-
| ResultViewLoaded;
110+
| ResultViewLoaded
111+
| ChangePage;
105112

106113
interface ViewSourceFileMsg {
107114
t: 'viewSourceFile';
@@ -122,6 +129,12 @@ interface ResultViewLoaded {
122129
t: 'resultViewLoaded';
123130
}
124131

132+
interface ChangePage {
133+
t: 'changePage';
134+
pageNumber: number; // 0-indexed, displayed to the user as 1-indexed
135+
selectedTable: string;
136+
}
137+
125138
export enum SortDirection {
126139
asc, desc
127140
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { RawResultSet } from "./adapt";
2+
import { ResultSetSchema } from "semmle-bqrs";
3+
import { Interpretation } from "./interface-types";
4+
5+
export const SELECT_TABLE_NAME = '#select';
6+
export const ALERTS_TABLE_NAME = 'alerts';
7+
8+
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
9+
export type PathTableResultSet = { t: 'SarifResultSet'; readonly schema: ResultSetSchema; name: string } & Interpretation;
10+
11+
export type ResultSet =
12+
| RawTableResultSet
13+
| PathTableResultSet;
14+
15+
export function getDefaultResultSet(resultSets: readonly ResultSet[]): string {
16+
return getDefaultResultSetName(resultSets.map(resultSet => resultSet.schema.name));
17+
}
18+
19+
export function getDefaultResultSetName(resultSetNames: readonly string[]): string {
20+
// Choose first available result set from the array
21+
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSetNames[0]].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
22+
}

extensions/ql-vscode/src/interface.ts

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import { CodeQLCliServer } from './cli';
1010
import { DatabaseItem, DatabaseManager } from './databases';
1111
import { showAndLogErrorMessage } from './helpers';
1212
import { assertNever } from './helpers-pure';
13-
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection } from './interface-types';
13+
import { FromResultsViewMsg, Interpretation, INTERPRETED_RESULTS_PER_RUN_LIMIT, IntoResultsViewMsg, QueryMetadata, ResultsPaths, SortedResultSetInfo, SortedResultsMap, InterpretedResultsSortState, SortDirection, RAW_RESULTS_PAGE_SIZE } from './interface-types';
1414
import { Logger } from './logging';
1515
import * as messages from './messages';
1616
import { CompletedQuery, interpretResults } from './query-results';
1717
import { QueryInfo, tmpDir } from './run-queries';
1818
import { parseSarifLocation, parseSarifPlainTextMessage } from './sarif-utils';
19-
import { adaptSchema, adaptBqrs, RawResultSet } from './adapt';
19+
import { adaptSchema, adaptBqrs, RawResultSet, ParsedResultSets } from './adapt';
2020
import { EXPERIMENTAL_BQRS_SETTING } from './config';
21+
import { getDefaultResultSetName } from './interface-utils';
2122

2223
/**
2324
* interface.ts
@@ -115,8 +116,13 @@ function sortInterpretedResults(results: Sarif.Result[], sortState: InterpretedR
115116
}
116117
}
117118

119+
function numPagesOfResultSet(resultSet: RawResultSet): number {
120+
return Math.ceil(resultSet.schema.tupleCount / RAW_RESULTS_PAGE_SIZE);
121+
}
122+
118123
export class InterfaceManager extends DisposableObject {
119124
private _displayedQuery?: CompletedQuery;
125+
private _interpretation?: Interpretation;
120126
private _panel: vscode.WebviewPanel | undefined;
121127
private _panelLoaded = false;
122128
private _panelLoadedCallBacks: (() => void)[] = [];
@@ -288,6 +294,9 @@ export class InterfaceManager extends DisposableObject {
288294
query.updateInterpretedSortState(this.cliServer, msg.sortState)
289295
);
290296
break;
297+
case "changePage":
298+
await this.showPageOfResults(msg.selectedTable, msg.pageNumber);
299+
break;
291300
default:
292301
assertNever(msg);
293302
}
@@ -339,6 +348,7 @@ export class InterfaceManager extends DisposableObject {
339348
);
340349

341350
this._displayedQuery = results;
351+
this._interpretation = interpretation;
342352

343353
const panel = this.getPanel();
344354
await this.waitForPanelLoaded();
@@ -364,18 +374,37 @@ export class InterfaceManager extends DisposableObject {
364374
});
365375
}
366376

367-
let resultSets: RawResultSet[] | undefined;
377+
const getParsedResultSets = async (): Promise<ParsedResultSets> => {
378+
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
379+
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
380+
381+
const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
382+
383+
// This may not wind up being the page we actually show, if there are interpreted results,
384+
// but speculatively send it anyway.
385+
const selectedTable = getDefaultResultSetName(resultSetNames);
386+
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
387+
if (schema === undefined) {
388+
return { t: 'WebviewParsed' };
389+
}
368390

369-
if (EXPERIMENTAL_BQRS_SETTING.getValue()) {
370-
resultSets = [];
371-
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath);
372-
for (const schema of schemas["result-sets"]) {
373-
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name);
391+
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[0]);
374392
const adaptedSchema = adaptSchema(schema);
375393
const resultSet = adaptBqrs(adaptedSchema, chunk);
376-
resultSets.push(resultSet);
394+
395+
return {
396+
t: 'ExtensionParsed',
397+
pageNumber: 0,
398+
numPages: numPagesOfResultSet(resultSet),
399+
resultSet,
400+
selectedTable: undefined,
401+
resultSetNames
402+
};
377403
}
378-
}
404+
else {
405+
return { t: 'WebviewParsed' };
406+
}
407+
};
379408

380409
await this.postMessage({
381410
t: "setState",
@@ -384,14 +413,67 @@ export class InterfaceManager extends DisposableObject {
384413
resultsPath: this.convertPathToWebviewUri(
385414
results.query.resultsPaths.resultsPath
386415
),
387-
resultSets,
416+
parsedResultSets: await getParsedResultSets(),
388417
sortedResultsMap,
389418
database: results.database,
390419
shouldKeepOldResultsWhileRendering,
391420
metadata: results.query.metadata
392421
});
393422
}
394423

424+
/**
425+
* Show a page of raw results from the chosen table.
426+
*/
427+
public async showPageOfResults(selectedTable: string, pageNumber: number): Promise<void> {
428+
const results = this._displayedQuery;
429+
if (results === undefined) {
430+
throw new Error('trying to view a page of a query that is not loaded');
431+
}
432+
433+
const sortedResultsMap: SortedResultsMap = {};
434+
results.sortedResultsInfo.forEach(
435+
(v, k) =>
436+
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
437+
v
438+
))
439+
);
440+
441+
const schemas = await this.cliServer.bqrsInfo(results.query.resultsPaths.resultsPath, RAW_RESULTS_PAGE_SIZE);
442+
443+
const resultSetNames = schemas["result-sets"].map(resultSet => resultSet.name);
444+
445+
const schema = schemas["result-sets"].find(resultSet => resultSet.name == selectedTable)!;
446+
if (schema === undefined)
447+
throw new Error(`Query result set '${selectedTable}' not found.`);
448+
449+
const chunk = await this.cliServer.bqrsDecode(results.query.resultsPaths.resultsPath, schema.name, RAW_RESULTS_PAGE_SIZE, schema.pagination?.offsets[pageNumber]);
450+
const adaptedSchema = adaptSchema(schema);
451+
const resultSet = adaptBqrs(adaptedSchema, chunk);
452+
453+
const parsedResultSets: ParsedResultSets = {
454+
t: 'ExtensionParsed',
455+
pageNumber,
456+
resultSet,
457+
numPages: numPagesOfResultSet(resultSet),
458+
selectedTable: selectedTable,
459+
resultSetNames
460+
};
461+
462+
await this.postMessage({
463+
t: "setState",
464+
interpretation: this._interpretation,
465+
origResultsPaths: results.query.resultsPaths,
466+
resultsPath: this.convertPathToWebviewUri(
467+
results.query.resultsPaths.resultsPath
468+
),
469+
parsedResultSets,
470+
sortedResultsMap,
471+
database: results.database,
472+
shouldKeepOldResultsWhileRendering: false,
473+
metadata: results.query.metadata
474+
});
475+
}
476+
395477
private async getTruncatedResults(
396478
metadata: QueryMetadata | undefined,
397479
resultsPaths: ResultsPaths,

extensions/ql-vscode/src/view/alert-table.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import * as Keys from '../result-keys';
55
import { LocationStyle } from 'semmle-bqrs';
66
import * as octicons from './octicons';
77
import { className, renderLocation, ResultTableProps, zebraStripe, selectableZebraStripe, jumpToLocation, nextSortDirection } from './result-table-utils';
8-
import { PathTableResultSet, onNavigation, NavigationEvent, vscode } from './results';
8+
import { onNavigation, NavigationEvent, vscode } from './results';
99
import { parseSarifPlainTextMessage, parseSarifLocation } from '../sarif-utils';
1010
import { InterpretedResultsSortColumn, SortDirection, InterpretedResultsSortState } from '../interface-types';
11+
import { PathTableResultSet } from '../interface-utils';
1112

1213
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
1314
export interface PathTableState {

extensions/ql-vscode/src/view/raw-results-table.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as React from "react";
22
import { renderLocation, ResultTableProps, zebraStripe, className, nextSortDirection } from "./result-table-utils";
3-
import { RawTableResultSet, vscode } from "./results";
3+
import { vscode } from "./results";
44
import { ResultValue } from "../adapt";
55
import { SortDirection, RAW_RESULTS_LIMIT, RawResultsSortState } from "../interface-types";
6+
import { RawTableResultSet } from "../interface-utils";
67

78
export type RawTableProps = ResultTableProps & {
89
resultSet: RawTableResultSet;
910
sortState?: RawResultsSortState;
11+
offset: number;
1012
};
1113

1214
export class RawTable extends React.Component<RawTableProps, {}> {
@@ -28,7 +30,7 @@ export class RawTable extends React.Component<RawTableProps, {}> {
2830
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
2931
{
3032
[
31-
<td key={-1}>{rowIndex + 1}</td>,
33+
<td key={-1}>{rowIndex + 1 + this.props.offset}</td>,
3234
...row.map((value, columnIndex) =>
3335
<td key={columnIndex}>
3436
{

extensions/ql-vscode/src/view/result-table-utils.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import * as React from 'react';
22
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
33
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
4-
import { ResultSet, vscode } from './results';
4+
import { vscode } from './results';
55
import { assertNever } from '../helpers-pure';
6+
import { ResultSet } from '../interface-utils';
67

78
export interface ResultTableProps {
89
resultSet: ResultSet;
910
databaseUri: string;
1011
metadata?: QueryMetadata;
1112
resultsPath: string | undefined;
1213
sortState?: RawResultsSortState;
14+
offset: number;
1315

1416
/**
1517
* Holds if there are any raw results. When that is the case, we

0 commit comments

Comments
 (0)