Skip to content

Commit 361fed6

Browse files
committed
Ask user if they want to re-import outdated testproj dbs
Before running a query now, do the following: 1. Check if the selected database is imported from a testproj 2. If so, check the last modified time of the `codeql-datase.yml` file of the imported database with that of its origin. 3. If the origin database has a file that is newer, assume that the database has been recreated since the last time it was imported. 4. If newer, then ask the user if they want to re-import before running the query. Also, this change appends the `(test)` label to all test databases in the database list.
1 parent ca21ed1 commit 361fed6

6 files changed

Lines changed: 136 additions & 20 deletions

File tree

extensions/ql-vscode/src/databases/local-databases-ui.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ class DatabaseTreeDataProvider
146146
item.iconPath = new ThemeIcon("error", new ThemeColor("errorForeground"));
147147
}
148148
item.tooltip = element.databaseUri.fsPath;
149-
item.description = element.language;
149+
item.description =
150+
element.language + (element.origin?.type === "testproj" ? " (test)" : "");
150151
return item;
151152
}
152153

extensions/ql-vscode/src/databases/local-databases/database-manager.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import {
1818
import { join } from "path";
1919
import type { FullDatabaseOptions } from "./database-options";
2020
import { DatabaseItemImpl } from "./database-item-impl";
21-
import { showNeverAskAgainDialog } from "../../common/vscode/dialog";
21+
import {
22+
showBinaryChoiceDialog,
23+
showNeverAskAgainDialog,
24+
} from "../../common/vscode/dialog";
2225
import {
2326
getFirstWorkspaceFolder,
2427
isFolderAlreadyInWorkspace,
@@ -32,7 +35,7 @@ import { QlPackGenerator } from "../../local-queries/qlpack-generator";
3235
import { asError, getErrorMessage } from "../../common/helpers-pure";
3336
import type { DatabaseItem, PersistedDatabaseItem } from "./database-item";
3437
import { redactableError } from "../../common/errors";
35-
import { remove } from "fs-extra";
38+
import { copy, remove, stat } from "fs-extra";
3639
import { containsPath } from "../../common/files";
3740
import type { DatabaseChangedEvent } from "./database-events";
3841
import { DatabaseEventKind } from "./database-events";
@@ -116,6 +119,7 @@ export class DatabaseManager extends DisposableObject {
116119
super();
117120

118121
qs.onStart(this.reregisterDatabases.bind(this));
122+
qs.onQueryRunStarting(this.maybeReimportTestDatabase.bind(this));
119123

120124
this.push(
121125
this.languageContext.onLanguageContextChanged(async () => {
@@ -170,12 +174,82 @@ export class DatabaseManager extends DisposableObject {
170174
const originPath = uri.fsPath;
171175
for (const item of this._databaseItems) {
172176
if (item.origin?.type === "testproj" && item.origin.path === originPath) {
173-
return item
177+
return item;
174178
}
175179
}
176180
return undefined;
177181
}
178182

183+
public async maybeReimportTestDatabase(
184+
databaseUri: vscode.Uri,
185+
forceImport = false,
186+
): Promise<void> {
187+
const res = await this.isTestDatabaseOutdated(databaseUri);
188+
if (!res) {
189+
return;
190+
}
191+
const doit =
192+
forceImport ||
193+
(await showBinaryChoiceDialog(
194+
"This test database is outdated. Do you want to reimport it?",
195+
));
196+
197+
if (doit) {
198+
await this.reimportTestDatabase(databaseUri);
199+
}
200+
}
201+
202+
/**
203+
* Checks if the origin of the imported database is newer.
204+
* The imported database must be a test database.
205+
* @param databaseUri the URI of the imported database to check
206+
* @returns true if both databases exist and the origin database is newer.
207+
*/
208+
private async isTestDatabaseOutdated(
209+
databaseUri: vscode.Uri,
210+
): Promise<boolean> {
211+
const dbItem = this.findDatabaseItem(databaseUri);
212+
if (dbItem === undefined || dbItem.origin?.type !== "testproj") {
213+
return false;
214+
}
215+
216+
// Compare timestmps of the codeql-database.yml files of the original and the
217+
// imported databases.
218+
const originDbYml = join(dbItem.origin.path, "codeql-database.yml");
219+
const importedDbYml = join(
220+
dbItem.databaseUri.fsPath,
221+
"codeql-database.yml",
222+
);
223+
224+
// TODO add error handling if one does not exist.
225+
const originStat = await stat(originDbYml);
226+
const importedStat = await stat(importedDbYml);
227+
return originStat.mtimeMs > importedStat.mtimeMs;
228+
}
229+
230+
/**
231+
* Reimport the specified imported database from its origin.
232+
* The imported databsae must be a testproj database.
233+
*
234+
* @param databaseUri the URI of the imported database to reimport
235+
*/
236+
private async reimportTestDatabase(databaseUri: vscode.Uri): Promise<void> {
237+
const dbItem = this.findDatabaseItem(databaseUri);
238+
if (dbItem === undefined || dbItem.origin?.type !== "testproj") {
239+
throw new Error(`Database ${databaseUri} is not a testproj.`);
240+
}
241+
242+
await this.removeDatabaseItem(dbItem);
243+
await copy(dbItem.origin.path, databaseUri.fsPath);
244+
const newDbItem = new DatabaseItemImpl(databaseUri, dbItem.contents, {
245+
dateAdded: Date.now(),
246+
language: dbItem.language,
247+
origin: dbItem.origin,
248+
});
249+
await this.addDatabaseItem(newDbItem);
250+
await this.setCurrentDatabaseItem(newDbItem);
251+
}
252+
179253
/**
180254
* Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on
181255
* the list.

extensions/ql-vscode/src/query-server/query-runner.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { window } from "vscode";
1+
import { window, Uri } from "vscode";
22
import type { CancellationToken, MessageItem } from "vscode";
33
import type { CodeQLCliServer } from "../codeql-cli/cli";
44
import type { ProgressCallback } from "../common/vscode/progress";
@@ -63,9 +63,22 @@ export interface CoreQueryRun {
6363
export type CoreCompletedQuery = CoreQueryResults &
6464
Omit<CoreQueryRun, "evaluate">;
6565

66+
type OnQueryRunStargingListener = (dbPath: Uri) => Promise<void>;
6667
export class QueryRunner {
6768
constructor(public readonly qs: QueryServerClient) {}
6869

70+
// Event handlers that get notified whenever a query is about to start running.
71+
// Can't use vscode EventEmitters since they are not asynchronous.
72+
private readonly onQueryRunStartingListeners: OnQueryRunStargingListener[] =
73+
[];
74+
public onQueryRunStarting(listener: OnQueryRunStargingListener) {
75+
this.onQueryRunStartingListeners.push(listener);
76+
}
77+
78+
private async fireQueryRunStarting(dbPath: Uri) {
79+
await Promise.all(this.onQueryRunStartingListeners.map((l) => l(dbPath)));
80+
}
81+
6982
get cliServer(): CodeQLCliServer {
7083
return this.qs.cliServer;
7184
}
@@ -138,6 +151,8 @@ export class QueryRunner {
138151
templates: Record<string, string> | undefined,
139152
logger: BaseLogger,
140153
): Promise<CoreQueryResults> {
154+
await this.fireQueryRunStarting(Uri.file(dbPath));
155+
141156
return await compileAndRunQueryAgainstDatabaseCore(
142157
this.qs,
143158
dbPath,

extensions/ql-vscode/test/vscode-tests/cli-integration/databases/database-fetcher.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
testprojLoc,
1717
} from "../../global.helper";
1818
import { createMockCommandManager } from "../../../__mocks__/commandsMock";
19-
import { remove } from "fs-extra";
19+
import { utimesSync } from "fs";
20+
import { remove, existsSync } from "fs-extra";
2021

2122
/**
2223
* Run various integration tests for databases
@@ -80,7 +81,26 @@ describe("database-fetcher", () => {
8081
expect(dbItem).toBeDefined();
8182
dbItem = dbItem!;
8283
expect(dbItem.name).toBe("db");
83-
expect(dbItem.databaseUri.fsPath).toBe(join(storagePath, "db", "db"));
84+
expect(dbItem.databaseUri.fsPath).toBe(join(storagePath, "db"));
85+
86+
// Now that we have fetched it. Check for re-importing
87+
// Delete a file in the imported database and we can check if the file is recreated
88+
const srczip = join(dbItem.databaseUri.fsPath, "src.zip");
89+
await remove(srczip);
90+
91+
// Attempt 1: re-import database should be a no-op since timestamp of imported database is newer
92+
await databaseManager.maybeReimportTestDatabase(dbItem.databaseUri);
93+
expect(existsSync(srczip)).toBeFalsy();
94+
95+
// Attempt 3: re-import database should re-import the database after updating modified time
96+
utimesSync(
97+
join(testprojLoc, "codeql-database.yml"),
98+
new Date(),
99+
new Date(),
100+
);
101+
102+
await databaseManager.maybeReimportTestDatabase(dbItem.databaseUri, true);
103+
expect(existsSync(srczip)).toBeTruthy();
84104
});
85105
});
86106

extensions/ql-vscode/test/vscode-tests/cli-integration/jest.setup.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import {
66
beforeEachAction,
77
} from "../jest.activated-extension.setup";
88
import { createWriteStream, existsSync, mkdirpSync } from "fs-extra";
9-
import { dirname } from "path";
9+
import { dirname, join } from "path";
1010
import { DB_URL, dbLoc, testprojLoc } from "../global.helper";
1111
import fetch from "node-fetch";
1212
import { createReadStream, renameSync } from "fs";
1313
import { Extract } from "unzipper";
1414

1515
beforeAll(async () => {
1616
// ensure the test database is downloaded
17-
mkdirpSync(dirname(dbLoc));
17+
const dbParentDir = dirname(dbLoc);
18+
mkdirpSync(dbParentDir);
1819
if (!existsSync(dbLoc)) {
1920
console.log(`Downloading test database to ${dbLoc}`);
2021

@@ -30,18 +31,23 @@ beforeAll(async () => {
3031
});
3132
});
3233
});
34+
}
35+
36+
// unzip the database from dbLoc to testprojLoc
37+
if (!existsSync(testprojLoc)) {
38+
console.log(`Unzipping test database to ${testprojLoc}`);
3339

34-
// unzip the database from dbLoc to testprojLoc
35-
if (!existsSync(testprojLoc)) {
36-
console.log(`Unzipping test database to ${testprojLoc}`);
37-
const dbDir = dirname(testprojLoc);
38-
mkdirpSync(dbDir);
39-
console.log(`Unzipping test database to ${testprojLoc}`);
40+
await new Promise((resolve, reject) => {
4041
createReadStream(dbLoc)
41-
.pipe(Extract({ path: dirname(dbDir) }))
42-
.on("close", () => console.log("Unzip completed."));
43-
}
44-
renameSync(dbLoc, testprojLoc);
42+
.pipe(Extract({ path: dbParentDir }))
43+
.on("close", () => {
44+
console.log("Unzip completed.");
45+
resolve(undefined);
46+
})
47+
.on("error", (e) => reject(e));
48+
});
49+
50+
renameSync(join(dbParentDir, "db"), testprojLoc);
4551
}
4652

4753
await beforeAllAction();

extensions/ql-vscode/test/vscode-tests/global.helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const dbLoc = join(
2424

2525
export const testprojLoc = join(
2626
realpathSync(join(__dirname, "../../../")),
27-
"build/tests/db.zip",
27+
"build/tests/db.testproj",
2828
);
2929

3030
// eslint-disable-next-line import/no-mutable-exports

0 commit comments

Comments
 (0)