Skip to content

Commit ad3a728

Browse files
authored
Merge pull request #2267 from github/koesie10/data-extension-editor-generate-flow-model
Add generating of flow model to data extension editor
2 parents 36f7555 + 102976e commit ad3a728

File tree

8 files changed

+353
-47
lines changed

8 files changed

+353
-47
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 {
@@ -23,9 +24,12 @@ import {
2324
showAndLogExceptionWithTelemetry,
2425
showAndLogWarningMessage,
2526
} from "../helpers";
26-
import { DatabaseItem } from "../local-databases";
27+
import { DatabaseItem, DatabaseManager } from "../local-databases";
2728
import { CodeQLCliServer } from "../cli";
2829
import { asError, assertNever, getErrorMessage } from "../pure/helpers-pure";
30+
import { generateFlowModel } from "./generate-flow-model";
31+
import { promptImportGithubDatabase } from "../databaseFetcher";
32+
import { App } from "../common/app";
2933
import { ResolvableLocationValue } from "../pure/bqrs-cli-types";
3034
import { showResolvableLocation } from "../interface-utils";
3135
import { decodeBqrsToExternalApiUsages } from "./bqrs";
@@ -34,12 +38,27 @@ import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
3438
import { ExternalApiUsage } from "./external-api-usage";
3539
import { ModeledMethod } from "./modeled-method";
3640

41+
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
42+
const workspaceFolder = workspace.workspaceFolders?.find(
43+
(folder) => folder.name === "ql",
44+
);
45+
if (!workspaceFolder) {
46+
void extLogger.log("No workspace folder 'ql' found");
47+
48+
return;
49+
}
50+
51+
return workspaceFolder;
52+
}
53+
3754
export class DataExtensionsEditorView extends AbstractWebview<
3855
ToDataExtensionsEditorMessage,
3956
FromDataExtensionsEditorMessage
4057
> {
4158
public constructor(
4259
ctx: ExtensionContext,
60+
private readonly app: App,
61+
private readonly databaseManager: DatabaseManager,
4362
private readonly cliServer: CodeQLCliServer,
4463
private readonly queryRunner: QueryRunner,
4564
private readonly queryStorageDir: string,
@@ -88,6 +107,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
88107
);
89108
await this.loadExternalApiUsages();
90109

110+
break;
111+
case "generateExternalApi":
112+
await this.generateModeledMethods();
113+
91114
break;
92115
default:
93116
assertNever(msg);
@@ -160,8 +183,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
160183
}
161184

162185
await this.postMessage({
163-
t: "setExistingModeledMethods",
164-
existingModeledMethods,
186+
t: "addModeledMethods",
187+
modeledMethods: existingModeledMethods,
165188
});
166189
} catch (e: unknown) {
167190
void extLogger.log(`Unable to read data extension YAML: ${e}`);
@@ -213,6 +236,92 @@ export class DataExtensionsEditorView extends AbstractWebview<
213236
}
214237
}
215238

239+
protected async generateModeledMethods(): Promise<void> {
240+
const tokenSource = new CancellationTokenSource();
241+
242+
const selectedDatabase = this.databaseManager.currentDatabaseItem;
243+
244+
// The external API methods are in the library source code, so we need to ask
245+
// the user to import the library database. We need to have the database
246+
// imported to the query server, so we need to register it to our workspace.
247+
const database = await promptImportGithubDatabase(
248+
this.app.commands,
249+
this.databaseManager,
250+
this.app.workspaceStoragePath ?? this.app.globalStoragePath,
251+
this.app.credentials,
252+
(update) => this.showProgress(update),
253+
tokenSource.token,
254+
this.cliServer,
255+
);
256+
if (!database) {
257+
await this.clearProgress();
258+
void extLogger.log("No database chosen");
259+
260+
return;
261+
}
262+
263+
// The library database was set as the current database by importing it,
264+
// but we need to set it back to the originally selected database.
265+
await this.databaseManager.setCurrentDatabaseItem(selectedDatabase);
266+
267+
const workspaceFolder = getQlSubmoduleFolder();
268+
if (!workspaceFolder) {
269+
return;
270+
}
271+
272+
await this.showProgress({
273+
step: 0,
274+
maxStep: 4000,
275+
message: "Generating modeled methods for library",
276+
});
277+
278+
try {
279+
await generateFlowModel({
280+
cliServer: this.cliServer,
281+
queryRunner: this.queryRunner,
282+
queryStorageDir: this.queryStorageDir,
283+
qlDir: workspaceFolder.uri.fsPath,
284+
databaseItem: database,
285+
onResults: async (results) => {
286+
const modeledMethodsByName: Record<string, ModeledMethod> = {};
287+
288+
for (const result of results) {
289+
modeledMethodsByName[result.signature] = result.modeledMethod;
290+
}
291+
292+
await this.postMessage({
293+
t: "addModeledMethods",
294+
modeledMethods: modeledMethodsByName,
295+
overrideNone: true,
296+
});
297+
},
298+
progress: (update) => this.showProgress(update),
299+
token: tokenSource.token,
300+
});
301+
} catch (e: unknown) {
302+
void showAndLogExceptionWithTelemetry(
303+
redactableError(
304+
asError(e),
305+
)`Failed to generate flow model: ${getErrorMessage(e)}`,
306+
);
307+
}
308+
309+
// After the flow model has been generated, we can remove the temporary database
310+
// which we used for generating the flow model.
311+
await this.databaseManager.removeDatabaseItem(
312+
() =>
313+
this.showProgress({
314+
step: 3900,
315+
maxStep: 4000,
316+
message: "Removing temporary database",
317+
}),
318+
tokenSource.token,
319+
database,
320+
);
321+
322+
await this.clearProgress();
323+
}
324+
216325
private async runQuery(): Promise<CoreCompletedQuery | undefined> {
217326
const qlpacks = await qlpackOfDatabase(this.cliServer, this.databaseItem);
218327

@@ -297,6 +406,13 @@ export class DataExtensionsEditorView extends AbstractWebview<
297406
* that there's 1000 steps of the query progress since that takes the most time, and then
298407
* an additional 500 steps for the rest of the work. The progress doesn't need to be 100%
299408
* accurate, so this is just a rough estimate.
409+
*
410+
* For generating the modeled methods for an external library, the max step is 4000. This is
411+
* based on the following steps:
412+
* - 1000 for the summary model
413+
* - 1000 for the sink model
414+
* - 1000 for the source model
415+
* - 1000 for the neutral model
300416
*/
301417
private async showProgress(update: ProgressUpdate, maxStep?: number) {
302418
await this.postMessage({
@@ -316,12 +432,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
316432
}
317433

318434
private calculateModelFilename(): string | undefined {
319-
const workspaceFolder = workspace.workspaceFolders?.find(
320-
(folder) => folder.name === "ql",
321-
);
435+
const workspaceFolder = getQlSubmoduleFolder();
322436
if (!workspaceFolder) {
323-
void extLogger.log("No workspace folder 'ql' found");
324-
325437
return;
326438
}
327439

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)