Skip to content

Commit 474ec19

Browse files
committed
Add generation of Ruby models
This adds the ability to generate Ruby models from a database. It uses the `GenerateModel.ql` query to do this. The query will essentially return data in the data extensions format, so this will just parse it and return it as `ModeledMethod` objects.
1 parent 135bce8 commit 474ec19

File tree

6 files changed

+411
-4
lines changed

6 files changed

+411
-4
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import tk from "tree-kill";
1010
import { promisify } from "util";
1111
import { CancellationToken, Disposable, Uri } from "vscode";
1212

13-
import { BQRSInfo, DecodedBqrsChunk } from "../common/bqrs-cli-types";
13+
import {
14+
BQRSInfo,
15+
DecodedBqrs,
16+
DecodedBqrsChunk,
17+
} from "../common/bqrs-cli-types";
1418
import { allowCanaryQueryServer, CliConfig } from "../config";
1519
import {
1620
DistributionProvider,
@@ -1040,6 +1044,18 @@ export class CodeQLCliServer implements Disposable {
10401044
);
10411045
}
10421046

1047+
/**
1048+
* Gets all results from a bqrs.
1049+
* @param bqrsPath The path to the bqrs.
1050+
*/
1051+
async bqrsDecodeAll(bqrsPath: string): Promise<DecodedBqrs> {
1052+
return await this.runJsonCodeQlCliCommand<DecodedBqrs>(
1053+
["bqrs", "decode"],
1054+
[bqrsPath],
1055+
"Reading all bqrs data",
1056+
);
1057+
}
1058+
10431059
async runInterpretCommand(
10441060
format: string,
10451061
additonalArgs: string[],

extensions/ql-vscode/src/common/bqrs-cli-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,5 @@ export interface DecodedBqrsChunk {
121121
next?: number;
122122
columns: BqrsColumn[];
123123
}
124+
125+
export type DecodedBqrs = Record<string, DecodedBqrsChunk>;

extensions/ql-vscode/src/local-queries/query-resolver.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface QueryConstraints {
3131
kind?: string;
3232
"tags contain"?: string[];
3333
"tags contain all"?: string[];
34+
"query filename"?: string;
35+
"query path"?: string;
3436
}
3537

3638
/**
@@ -132,6 +134,14 @@ export async function resolveQueries(
132134
`tagged all of "${constraints["tags contain all"].join(" ")}"`,
133135
);
134136
}
137+
if (constraints["query filename"] !== undefined) {
138+
humanConstraints.push(
139+
`with query filename "${constraints["query filename"]}"`,
140+
);
141+
}
142+
if (constraints["query path"] !== undefined) {
143+
humanConstraints.push(`with query path "${constraints["query path"]}"`);
144+
}
135145

136146
const joinedPacksToSearch = packsToSearch.join(", ");
137147
const error = redactableError`No ${name} queries (${humanConstraints.join(
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { CancellationToken } from "vscode";
2+
import { DatabaseItem } from "../databases/local-databases";
3+
import { QueryRunner } from "../query-server";
4+
import { CodeQLCliServer } from "../codeql-cli/cli";
5+
import {
6+
BaseLogger,
7+
showAndLogExceptionWithTelemetry,
8+
} from "../common/logging";
9+
import { extLogger } from "../common/logging/vscode";
10+
import { getModelsAsDataLanguage } from "./languages";
11+
import { ProgressCallback } from "../common/vscode/progress";
12+
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
13+
import { ModeledMethod } from "./modeled-method";
14+
import { redactableError } from "../common/errors";
15+
import { telemetryListener } from "../common/vscode/telemetry";
16+
import { runQuery } from "../local-queries/run-query";
17+
import { resolveQueries } from "../local-queries";
18+
import { QueryLanguage } from "../common/query-language";
19+
import { DataTuple } from "./model-extension-file";
20+
21+
const GENERATE_MODEL_SUPPORTED_LANGUAGES = [QueryLanguage.Ruby];
22+
23+
export function isGenerateModelSupported(language: QueryLanguage): boolean {
24+
return GENERATE_MODEL_SUPPORTED_LANGUAGES.includes(language);
25+
}
26+
27+
type GenerateModelOptions = {
28+
cliServer: CodeQLCliServer;
29+
queryRunner: QueryRunner;
30+
logger: BaseLogger;
31+
queryStorageDir: string;
32+
databaseItem: DatabaseItem;
33+
language: QueryLanguage;
34+
progress: ProgressCallback;
35+
token: CancellationToken;
36+
};
37+
38+
// resolve (100) + query (1000) + interpret (100)
39+
const maxStep = 1200;
40+
41+
export async function runGenerateModelQuery({
42+
cliServer,
43+
queryRunner,
44+
logger,
45+
queryStorageDir,
46+
databaseItem,
47+
language,
48+
progress,
49+
token,
50+
}: GenerateModelOptions): Promise<ModeledMethod[]> {
51+
progress({
52+
message: "Resolving generate model query",
53+
step: 100,
54+
maxStep,
55+
});
56+
57+
const queryPath = await resolveGenerateModelQuery(cliServer, databaseItem);
58+
if (queryPath === undefined) {
59+
return [];
60+
}
61+
62+
// Run the query
63+
const completedQuery = await runQuery({
64+
queryRunner,
65+
databaseItem,
66+
queryPath,
67+
queryStorageDir,
68+
additionalPacks: getOnDiskWorkspaceFolders(),
69+
extensionPacks: undefined,
70+
progress: ({ step, message }) =>
71+
progress({
72+
message: `Generating models: ${message}`,
73+
step: 100 + step,
74+
maxStep,
75+
}),
76+
token,
77+
});
78+
79+
if (!completedQuery) {
80+
return [];
81+
}
82+
83+
progress({
84+
message: "Decoding results",
85+
step: 1100,
86+
maxStep,
87+
});
88+
89+
const decodedBqrs = await cliServer.bqrsDecodeAll(
90+
completedQuery.outputDir.bqrsPath,
91+
);
92+
93+
const modelsAsDataLanguage = getModelsAsDataLanguage(language);
94+
95+
const modeledMethods: ModeledMethod[] = [];
96+
97+
for (const resultSetName in decodedBqrs) {
98+
const definition = Object.values(modelsAsDataLanguage.predicates).find(
99+
(definition) => definition.extensiblePredicate === resultSetName,
100+
);
101+
if (definition === undefined) {
102+
void logger.log(`No predicate found for ${resultSetName}`);
103+
104+
continue;
105+
}
106+
107+
const resultSet = decodedBqrs[resultSetName];
108+
109+
if (
110+
resultSet.tuples.some((tuple) =>
111+
tuple.some((value) => typeof value === "object"),
112+
)
113+
) {
114+
void logger.log(
115+
`Skipping ${resultSetName} because it contains undefined values`,
116+
);
117+
continue;
118+
}
119+
120+
modeledMethods.push(
121+
...resultSet.tuples.map((tuple) => {
122+
const row = tuple.filter(
123+
(value): value is DataTuple => typeof value !== "object",
124+
);
125+
126+
return definition.readModeledMethod(row);
127+
}),
128+
);
129+
}
130+
131+
return modeledMethods;
132+
}
133+
134+
async function resolveGenerateModelQuery(
135+
cliServer: CodeQLCliServer,
136+
databaseItem: DatabaseItem,
137+
): Promise<string | undefined> {
138+
const packsToSearch = [`codeql/${databaseItem.language}-queries`];
139+
140+
const queries = await resolveQueries(
141+
cliServer,
142+
packsToSearch,
143+
"generate model",
144+
{
145+
"query path": "queries/modeling/GenerateModel.ql",
146+
},
147+
);
148+
if (queries.length !== 1) {
149+
void showAndLogExceptionWithTelemetry(
150+
extLogger,
151+
telemetryListener,
152+
redactableError`Expected exactly one generate model query, got ${queries.length}`,
153+
);
154+
return undefined;
155+
}
156+
157+
return queries[0];
158+
}

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ import { ModelingStore } from "./modeling-store";
5151
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
5252
import { ModelingEvents } from "./modeling-events";
5353
import { getModelsAsDataLanguage, ModelsAsDataLanguage } from "./languages";
54+
import {
55+
isGenerateModelSupported,
56+
runGenerateModelQuery,
57+
} from "./generate-model-queries";
5458

5559
export class ModelEditorView extends AbstractWebview<
5660
ToModelEditorMessage,
@@ -266,7 +270,11 @@ export class ModelEditorView extends AbstractWebview<
266270

267271
break;
268272
case "generateMethod":
269-
await this.generateModeledMethods();
273+
if (isFlowModelGenerationSupported(this.language)) {
274+
await this.generateModeledMethodsFromFlow();
275+
} else if (isGenerateModelSupported(this.language)) {
276+
await this.generateModeledMethodsFromGenerateModel();
277+
}
270278
void telemetryListener?.sendUIInteraction(
271279
"model-editor-generate-modeled-methods",
272280
);
@@ -371,7 +379,8 @@ export class ModelEditorView extends AbstractWebview<
371379
private async setViewState(): Promise<void> {
372380
const showFlowGeneration =
373381
this.modelConfig.flowGeneration &&
374-
isFlowModelGenerationSupported(this.language);
382+
(isFlowModelGenerationSupported(this.language) ||
383+
isGenerateModelSupported(this.language));
375384

376385
const showLlmButton =
377386
this.databaseItem.language === "java" && this.modelConfig.llmGeneration;
@@ -464,7 +473,7 @@ export class ModelEditorView extends AbstractWebview<
464473
}
465474
}
466475

467-
protected async generateModeledMethods(): Promise<void> {
476+
protected async generateModeledMethodsFromFlow(): Promise<void> {
468477
await withProgress(
469478
async (progress) => {
470479
const tokenSource = new CancellationTokenSource();
@@ -537,6 +546,48 @@ export class ModelEditorView extends AbstractWebview<
537546
);
538547
}
539548

549+
protected async generateModeledMethodsFromGenerateModel(): Promise<void> {
550+
await withProgress(
551+
async (progress) => {
552+
const tokenSource = new CancellationTokenSource();
553+
554+
try {
555+
const modeledMethods = await runGenerateModelQuery({
556+
cliServer: this.cliServer,
557+
queryRunner: this.queryRunner,
558+
logger: this.app.logger,
559+
queryStorageDir: this.queryStorageDir,
560+
databaseItem: this.databaseItem,
561+
language: this.language,
562+
progress,
563+
token: tokenSource.token,
564+
});
565+
566+
const modeledMethodsByName: Record<string, ModeledMethod[]> = {};
567+
568+
for (const modeledMethod of modeledMethods) {
569+
if (!(modeledMethod.signature in modeledMethodsByName)) {
570+
modeledMethodsByName[modeledMethod.signature] = [];
571+
}
572+
573+
modeledMethodsByName[modeledMethod.signature].push(modeledMethod);
574+
}
575+
576+
this.addModeledMethods(modeledMethodsByName);
577+
} catch (e: unknown) {
578+
void showAndLogExceptionWithTelemetry(
579+
this.app.logger,
580+
this.app.telemetry,
581+
redactableError(
582+
asError(e),
583+
)`Failed to generate models: ${getErrorMessage(e)}`,
584+
);
585+
}
586+
},
587+
{ cancellable: false },
588+
);
589+
}
590+
540591
private async generateModeledMethodsFromLlm(
541592
packageName: string,
542593
methodSignatures: string[],

0 commit comments

Comments
 (0)