Skip to content

Commit 00161f1

Browse files
committed
Use bqrs diff in when comparing query results
1 parent 7903201 commit 00161f1

File tree

3 files changed

+97
-31
lines changed

3 files changed

+97
-31
lines changed

extensions/ql-vscode/src/codeql-cli/cli.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SemVer } from "semver";
99
import type { Readable } from "stream";
1010
import tk from "tree-kill";
1111
import type { CancellationToken, Disposable, Uri } from "vscode";
12+
import { dir } from "tmp-promise";
1213

1314
import type {
1415
BqrsInfo,
@@ -1250,6 +1251,34 @@ export class CodeQLCliServer implements Disposable {
12501251
);
12511252
}
12521253

1254+
/** Gets the difference between two bqrs files. */
1255+
async bqrsDiff(
1256+
bqrsPath1: string,
1257+
bqrsPath2: string,
1258+
): Promise<{
1259+
uniquePath1: string;
1260+
uniquePath2: string;
1261+
cleanup: () => Promise<void>;
1262+
}> {
1263+
const { path, cleanup } = await dir({ unsafeCleanup: true });
1264+
const uniquePath1 = join(path, "left.bqrs");
1265+
const uniquePath2 = join(path, "right.bqrs");
1266+
await this.runCodeQlCliCommand(
1267+
["bqrs", "diff"],
1268+
[
1269+
"--left",
1270+
uniquePath1,
1271+
"--right",
1272+
uniquePath2,
1273+
"--retain-result-sets=''",
1274+
bqrsPath1,
1275+
bqrsPath2,
1276+
],
1277+
"Diffing bqrs files",
1278+
);
1279+
return { uniquePath1, uniquePath2, cleanup };
1280+
}
1281+
12531282
async runInterpretCommand(
12541283
format: string,
12551284
additonalArgs: string[],

extensions/ql-vscode/src/compare/compare-view.ts

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ interface ComparePair {
4545
commonResultSetNames: readonly string[];
4646
}
4747

48+
function findSchema(info: BqrsInfo, name: string) {
49+
const schema = info["result-sets"].find((schema) => schema.name === name);
50+
if (schema === undefined) {
51+
throw new Error(`Schema ${name} not found.`);
52+
}
53+
return schema;
54+
}
55+
4856
export class CompareView extends AbstractWebview<
4957
ToCompareViewMessage,
5058
FromCompareViewMessage
@@ -370,12 +378,7 @@ export class CompareView extends AbstractWebview<
370378
resultSetName: string,
371379
resultsPath: string,
372380
): Promise<RawResultSet> {
373-
const schema = bqrsInfo["result-sets"].find(
374-
(schema) => schema.name === resultSetName,
375-
);
376-
if (!schema) {
377-
throw new Error(`Schema ${resultSetName} not found.`);
378-
}
381+
const schema = findSchema(bqrsInfo, resultSetName);
379382
const chunk = await this.cliServer.bqrsDecode(resultsPath, resultSetName);
380383
return bqrsToResultSet(schema, chunk);
381384
}
@@ -385,20 +388,66 @@ export class CompareView extends AbstractWebview<
385388
fromResultSetName: string,
386389
toResultSetName: string,
387390
): Promise<RawQueryCompareResult> {
388-
const [fromResultSet, toResultSet] = await Promise.all([
389-
this.getResultSet(
390-
fromInfo.schemas,
391-
fromResultSetName,
392-
from.completedQuery.query.resultsPath,
393-
),
394-
this.getResultSet(
395-
toInfo.schemas,
396-
toResultSetName,
397-
to.completedQuery.query.resultsPath,
398-
),
399-
]);
391+
const fromPath = from.completedQuery.query.resultsPath;
392+
const toPath = to.completedQuery.query.resultsPath;
393+
394+
const fromSchema = findSchema(fromInfo.schemas, fromResultSetName);
395+
const toSchema = findSchema(toInfo.schemas, toResultSetName);
400396

401-
return resultsDiff(fromResultSet, toResultSet);
397+
if (fromSchema.columns.length !== toSchema.columns.length) {
398+
throw new Error("CodeQL Compare: Columns do not match.");
399+
}
400+
if (fromSchema.rows === 0) {
401+
throw new Error("CodeQL Compare: Source query has no results.");
402+
}
403+
if (toSchema.rows === 0) {
404+
throw new Error("CodeQL Compare: Target query has no results.");
405+
}
406+
407+
// If the result set names are the same, we use `bqrs diff`. This is more
408+
// efficient, but we can't use it in general as it does not support
409+
// comparing different result sets.
410+
if (fromResultSetName === toResultSetName) {
411+
const { uniquePath1, uniquePath2, cleanup } =
412+
await this.cliServer.bqrsDiff(fromPath, toPath);
413+
try {
414+
const info1 = await this.cliServer.bqrsInfo(uniquePath1);
415+
const info2 = await this.cliServer.bqrsInfo(uniquePath2);
416+
417+
// We avoid decoding the results sets if there is no overlap
418+
if (
419+
fromSchema.rows === findSchema(info1, fromResultSetName).rows &&
420+
toSchema.rows === findSchema(info2, toResultSetName).rows
421+
) {
422+
throw new Error(
423+
"CodeQL Compare: No overlap between the selected queries.",
424+
);
425+
}
426+
427+
const fromUniqueResults = bqrsToResultSet(
428+
fromSchema,
429+
await this.cliServer.bqrsDecode(uniquePath1, fromResultSetName),
430+
);
431+
const toUniqueResults = bqrsToResultSet(
432+
fromSchema,
433+
await this.cliServer.bqrsDecode(uniquePath2, toResultSetName),
434+
);
435+
return {
436+
kind: "raw",
437+
columns: fromUniqueResults.columns,
438+
from: fromUniqueResults.rows,
439+
to: toUniqueResults.rows,
440+
};
441+
} finally {
442+
await cleanup();
443+
}
444+
} else {
445+
const [fromResultSet, toResultSet] = await Promise.all([
446+
this.getResultSet(fromInfo.schemas, fromResultSetName, fromPath),
447+
this.getResultSet(toInfo.schemas, toResultSetName, toPath),
448+
]);
449+
return resultsDiff(fromResultSet, toResultSet);
450+
}
402451
}
403452

404453
private async compareInterpretedResults({

extensions/ql-vscode/src/compare/resultsDiff.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,6 @@ export default function resultsDiff(
2323
fromResults: RawResultSet,
2424
toResults: RawResultSet,
2525
): RawQueryCompareResult {
26-
if (fromResults.columns.length !== toResults.columns.length) {
27-
throw new Error("CodeQL Compare: Columns do not match.");
28-
}
29-
30-
if (!fromResults.rows.length) {
31-
throw new Error("CodeQL Compare: Source query has no results.");
32-
}
33-
34-
if (!toResults.rows.length) {
35-
throw new Error("CodeQL Compare: Target query has no results.");
36-
}
37-
3826
const results: RawQueryCompareResult = {
3927
kind: "raw",
4028
columns: fromResults.columns,

0 commit comments

Comments
 (0)