Skip to content

Commit 6dfa726

Browse files
committed
Merge remote-tracking branch 'origin/main' into koesie10/data-extension-editor-cli-tests
2 parents 9f3baad + ad3a728 commit 6dfa726

20 files changed

+658
-79
lines changed

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { DatabaseManager } from "../local-databases";
77
import { extLogger } from "../common";
88
import { ensureDir } from "fs-extra";
99
import { join } from "path";
10+
import { App } from "../common/app";
1011

1112
export class DataExtensionsEditorModule {
1213
private readonly queryStorageDir: string;
1314

1415
private constructor(
1516
private readonly ctx: ExtensionContext,
17+
private readonly app: App,
1618
private readonly databaseManager: DatabaseManager,
1719
private readonly cliServer: CodeQLCliServer,
1820
private readonly queryRunner: QueryRunner,
@@ -26,13 +28,15 @@ export class DataExtensionsEditorModule {
2628

2729
public static async initialize(
2830
ctx: ExtensionContext,
31+
app: App,
2932
databaseManager: DatabaseManager,
3033
cliServer: CodeQLCliServer,
3134
queryRunner: QueryRunner,
3235
queryStorageDir: string,
3336
): Promise<DataExtensionsEditorModule> {
3437
const dataExtensionsEditorModule = new DataExtensionsEditorModule(
3538
ctx,
39+
app,
3640
databaseManager,
3741
cliServer,
3842
queryRunner,
@@ -54,6 +58,8 @@ export class DataExtensionsEditorModule {
5458

5559
const view = new DataExtensionsEditorView(
5660
this.ctx,
61+
this.app,
62+
this.databaseManager,
5763
this.cliServer,
5864
this.queryRunner,
5965
this.queryStorageDir,

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ViewColumn,
66
window,
77
workspace,
8+
WorkspaceFolder,
89
} from "vscode";
910
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
1011
import {
@@ -20,9 +21,12 @@ import {
2021
import { extLogger } from "../common";
2122
import { readFile, writeFile } from "fs-extra";
2223
import { load as loadYaml } from "js-yaml";
23-
import { DatabaseItem } from "../local-databases";
24+
import { DatabaseItem, DatabaseManager } from "../local-databases";
2425
import { CodeQLCliServer } from "../cli";
2526
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
27+
import { generateFlowModel } from "./generate-flow-model";
28+
import { promptImportGithubDatabase } from "../databaseFetcher";
29+
import { App } from "../common/app";
2630
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
2731
import { showResolvableLocation } from "../interface-utils";
2832
import { decodeBqrsToExternalApiUsages } from "./bqrs";
@@ -32,12 +36,27 @@ import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
3236
import { ExternalApiUsage } from "./external-api-usage";
3337
import { ModeledMethod } from "./modeled-method";
3438

39+
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
40+
const workspaceFolder = workspace.workspaceFolders?.find(
41+
(folder) => folder.name === "ql",
42+
);
43+
if (!workspaceFolder) {
44+
void extLogger.log("No workspace folder 'ql' found");
45+
46+
return;
47+
}
48+
49+
return workspaceFolder;
50+
}
51+
3552
export class DataExtensionsEditorView extends AbstractWebview<
3653
ToDataExtensionsEditorMessage,
3754
FromDataExtensionsEditorMessage
3855
> {
3956
public constructor(
4057
ctx: ExtensionContext,
58+
private readonly app: App,
59+
private readonly databaseManager: DatabaseManager,
4160
private readonly cliServer: CodeQLCliServer,
4261
private readonly queryRunner: QueryRunner,
4362
private readonly queryStorageDir: string,
@@ -86,6 +105,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
86105
);
87106
await this.loadExternalApiUsages();
88107

108+
break;
109+
case "generateExternalApi":
110+
await this.generateModeledMethods();
111+
89112
break;
90113
default:
91114
assertNever(msg);
@@ -158,8 +181,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
158181
}
159182

160183
await this.postMessage({
161-
t: "setExistingModeledMethods",
162-
existingModeledMethods,
184+
t: "addModeledMethods",
185+
modeledMethods: existingModeledMethods,
163186
});
164187
} catch (e: unknown) {
165188
void extLogger.log(`Unable to read data extension YAML: ${e}`);
@@ -225,13 +248,106 @@ export class DataExtensionsEditorView extends AbstractWebview<
225248
}
226249
}
227250

251+
protected async generateModeledMethods(): Promise<void> {
252+
const tokenSource = new CancellationTokenSource();
253+
254+
const selectedDatabase = this.databaseManager.currentDatabaseItem;
255+
256+
// The external API methods are in the library source code, so we need to ask
257+
// the user to import the library database. We need to have the database
258+
// imported to the query server, so we need to register it to our workspace.
259+
const database = await promptImportGithubDatabase(
260+
this.app.commands,
261+
this.databaseManager,
262+
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
263+
this.app.credentials,
264+
(update) => this.showProgress(update),
265+
tokenSource.token,
266+
this.cliServer,
267+
);
268+
if (!database) {
269+
await this.clearProgress();
270+
void extLogger.log("No database chosen");
271+
272+
return;
273+
}
274+
275+
// The library database was set as the current database by importing it,
276+
// but we need to set it back to the originally selected database.
277+
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
278+
279+
const workspaceFolder = getQlSubmoduleFolder();
280+
if (!workspaceFolder) {
281+
return;
282+
}
283+
284+
await this.showProgress({
285+
step: 0,
286+
maxStep: 4000,
287+
message: "Generating modeled methods for library",
288+
});
289+
290+
try {
291+
await generateFlowModel({
292+
cliServer: this.cliServer,
293+
queryRunner: this.queryRunner,
294+
queryStorageDir: this.queryStorageDir,
295+
qlDir: workspaceFolder.uri.fsPath,
296+
databaseItem: database,
297+
onResults: async (results) => {
298+
const modeledMethodsByName: Record<string, ModeledMethod> = {};
299+
300+
for (const result of results) {
301+
modeledMethodsByName[result.signature] = result.modeledMethod;
302+
}
303+
304+
await this.postMessage({
305+
t: "addModeledMethods",
306+
modeledMethods: modeledMethodsByName,
307+
overrideNone: true,
308+
});
309+
},
310+
progress: (update) => this.showProgress(update),
311+
token: tokenSource.token,
312+
});
313+
} catch (e: unknown) {
314+
void showAndLogExceptionWithTelemetry(
315+
redactableError(
316+
asError(e),
317+
)`Failed to generate flow model: ${getErrorMessage(e)}`,
318+
);
319+
}
320+
321+
// After the flow model has been generated, we can remove the temporary database
322+
// which we used for generating the flow model.
323+
await this.databaseManager.removeDatabaseItem(
324+
() =>
325+
this.showProgress({
326+
step: 3900,
327+
maxStep: 4000,
328+
message: "Removing temporary database",
329+
}),
330+
tokenSource.token,
331+
database,
332+
);
333+
334+
await this.clearProgress();
335+
}
336+
228337
/*
229338
* Progress in this class is a bit weird. Most of the progress is based on running the query.
230339
* Query progress is always between 0 and 1000. However, we still have some steps that need
231340
* to be done after the query has finished. Therefore, the maximum step is 1500. This captures
232341
* that there's 1000 steps of the query progress since that takes the most time, and then
233342
* an additional 500 steps for the rest of the work. The progress doesn't need to be 100%
234343
* accurate, so this is just a rough estimate.
344+
*
345+
* For generating the modeled methods for an external library, the max step is 4000. This is
346+
* based on the following steps:
347+
* - 1000 for the summary model
348+
* - 1000 for the sink model
349+
* - 1000 for the source model
350+
* - 1000 for the neutral model
235351
*/
236352
private async showProgress(update: ProgressUpdate, maxStep?: number) {
237353
await this.postMessage({
@@ -251,12 +367,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
251367
}
252368

253369
private calculateModelFilename(): string | undefined {
254-
const workspaceFolder = workspace.workspaceFolders?.find(
255-
(folder) => folder.name === "ql",
256-
);
370+
const workspaceFolder = getQlSubmoduleFolder();
257371
if (!workspaceFolder) {
258-
void extLogger.log("No workspace folder 'ql' found");
259-
260372
return;
261373
}
262374

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { CancellationToken } from "vscode";
2+
import { DatabaseItem } from "../local-databases";
3+
import { join } from "path";
4+
import { QueryRunner } from "../queryRunner";
5+
import { CodeQLCliServer } from "../cli";
6+
import { TeeLogger } from "../common";
7+
import { extensiblePredicateDefinitions } from "./yaml";
8+
import { ProgressCallback } from "../progress";
9+
import { getOnDiskWorkspaceFolders } from "../helpers";
10+
import {
11+
ModeledMethodType,
12+
ModeledMethodWithSignature,
13+
} from "./modeled-method";
14+
15+
type FlowModelOptions = {
16+
cliServer: CodeQLCliServer;
17+
queryRunner: QueryRunner;
18+
queryStorageDir: string;
19+
qlDir: string;
20+
databaseItem: DatabaseItem;
21+
progress: ProgressCallback;
22+
token: CancellationToken;
23+
onResults: (results: ModeledMethodWithSignature[]) => void | Promise<void>;
24+
};
25+
26+
async function getModeledMethodsFromFlow(
27+
type: Exclude<ModeledMethodType, "none">,
28+
queryName: string,
29+
queryStep: number,
30+
{
31+
cliServer,
32+
queryRunner,
33+
queryStorageDir,
34+
qlDir,
35+
databaseItem,
36+
progress,
37+
token,
38+
}: Omit<FlowModelOptions, "onResults">,
39+
): Promise<ModeledMethodWithSignature[]> {
40+
const definition = extensiblePredicateDefinitions[type];
41+
42+
const query = join(
43+
qlDir,
44+
databaseItem.language,
45+
"ql/src/utils/modelgenerator",
46+
queryName,
47+
);
48+
49+
const queryRun = queryRunner.createQueryRun(
50+
databaseItem.databaseUri.fsPath,
51+
{ queryPath: query, quickEvalPosition: undefined },
52+
false,
53+
getOnDiskWorkspaceFolders(),
54+
undefined,
55+
queryStorageDir,
56+
undefined,
57+
undefined,
58+
);
59+
60+
const queryResult = await queryRun.evaluate(
61+
({ step, message }) =>
62+
progress({
63+
message: `Generating ${type} model: ${message}`,
64+
step: queryStep * 1000 + step,
65+
maxStep: 4000,
66+
}),
67+
token,
68+
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
69+
);
70+
71+
const bqrsPath = queryResult.outputDir.bqrsPath;
72+
73+
const bqrsInfo = await cliServer.bqrsInfo(bqrsPath);
74+
if (bqrsInfo["result-sets"].length !== 1) {
75+
throw new Error(
76+
`Expected exactly one result set, got ${bqrsInfo["result-sets"].length}`,
77+
);
78+
}
79+
80+
const resultSet = bqrsInfo["result-sets"][0];
81+
82+
const decodedResults = await cliServer.bqrsDecode(bqrsPath, resultSet.name);
83+
84+
const results = decodedResults.tuples;
85+
86+
return (
87+
results
88+
// This is just a sanity check. The query should only return strings.
89+
.filter((result) => typeof result[0] === "string")
90+
.map((result) => {
91+
const row = result[0] as string;
92+
93+
return definition.readModeledMethod(row.split(";"));
94+
})
95+
);
96+
}
97+
98+
export async function generateFlowModel({
99+
onResults,
100+
...options
101+
}: FlowModelOptions) {
102+
const summaryResults = await getModeledMethodsFromFlow(
103+
"summary",
104+
"CaptureSummaryModels.ql",
105+
0,
106+
options,
107+
);
108+
if (summaryResults) {
109+
await onResults(summaryResults);
110+
}
111+
112+
const sinkResults = await getModeledMethodsFromFlow(
113+
"sink",
114+
"CaptureSinkModels.ql",
115+
1,
116+
options,
117+
);
118+
if (sinkResults) {
119+
await onResults(sinkResults);
120+
}
121+
122+
const sourceResults = await getModeledMethodsFromFlow(
123+
"source",
124+
"CaptureSourceModels.ql",
125+
2,
126+
options,
127+
);
128+
if (sourceResults) {
129+
await onResults(sourceResults);
130+
}
131+
132+
const neutralResults = await getModeledMethodsFromFlow(
133+
"neutral",
134+
"CaptureNeutralModels.ql",
135+
3,
136+
options,
137+
);
138+
if (neutralResults) {
139+
await onResults(neutralResults);
140+
}
141+
}

extensions/ql-vscode/src/data-extensions-editor/modeled-method.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ export type ModeledMethod = {
1111
output: string;
1212
kind: string;
1313
};
14+
15+
export type ModeledMethodWithSignature = {
16+
signature: string;
17+
modeledMethod: ModeledMethod;
18+
};

0 commit comments

Comments
 (0)