Skip to content

Commit a25db96

Browse files
alexetaeisenberg
authored andcommitted
QueryServer: Use non-destructive upgrades where possible.
1 parent cb4d6f2 commit a25db96

4 files changed

Lines changed: 246 additions & 211 deletions

File tree

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

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ import {
1212
} from 'vscode';
1313
import * as fs from 'fs-extra';
1414

15-
import * as cli from './cli';
1615
import {
1716
DatabaseChangedEvent,
1817
DatabaseItem,
1918
DatabaseManager,
20-
getUpgradesDirectories,
2119
} from './databases';
2220
import {
2321
commandRunner,
@@ -33,7 +31,7 @@ import {
3331
import { logger } from './logging';
3432
import { clearCacheInDatabase } from './run-queries';
3533
import * as qsClient from './queryserver-client';
36-
import { upgradeDatabase } from './upgrades';
34+
import { upgradeDatabaseExplicit } from './upgrades';
3735
import {
3836
importArchiveDatabase,
3937
promptImportInternetDatabase,
@@ -218,7 +216,6 @@ export class DatabaseUI extends DisposableObject {
218216
private treeDataProvider: DatabaseTreeDataProvider;
219217

220218
public constructor(
221-
private cliserver: cli.CodeQLCliServer,
222219
private databaseManager: DatabaseManager,
223220
private readonly queryServer: qsClient.QueryServerClient | undefined,
224221
private readonly storagePath: string,
@@ -540,25 +537,10 @@ export class DatabaseUI extends DisposableObject {
540537
}
541538

542539
// Search for upgrade scripts in any workspace folders available
543-
const searchPath: string[] = getOnDiskWorkspaceFolders();
544540

545-
const upgradeInfo = await this.cliserver.resolveUpgrades(
546-
databaseItem.contents.dbSchemeUri.fsPath,
547-
searchPath
548-
);
549-
550-
const { scripts, finalDbscheme } = upgradeInfo;
551-
552-
if (finalDbscheme === undefined) {
553-
throw new Error('Could not determine target dbscheme to upgrade to.');
554-
}
555-
const targetDbSchemeUri = Uri.file(finalDbscheme);
556-
557-
await upgradeDatabase(
541+
await upgradeDatabaseExplicit(
558542
this.queryServer,
559543
databaseItem,
560-
targetDbSchemeUri,
561-
getUpgradesDirectories(scripts),
562544
progress,
563545
token
564546
);

extensions/ql-vscode/src/extension.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,6 @@ async function activateWithInstalledDistribution(
369369
ctx.subscriptions.push(dbm);
370370
logger.log('Initializing database panel.');
371371
const databaseUI = new DatabaseUI(
372-
cliServer,
373372
dbm,
374373
qs,
375374
getContextStoragePath(ctx),

extensions/ql-vscode/src/run-queries.ts

Lines changed: 105 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ import { ErrorCodes, ResponseError } from 'vscode-languageclient';
1414

1515
import * as cli from './cli';
1616
import * as config from './config';
17-
import { DatabaseItem, getUpgradesDirectories } from './databases';
17+
import { DatabaseItem } from './databases';
1818
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
1919
import { ProgressCallback, UserCancellationException } from './commandRunner';
20+
import * as helpers from './helpers';
2021
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './pure/interface-types';
2122
import { logger } from './logging';
2223
import * as messages from './pure/messages';
2324
import { QueryHistoryItemOptions } from './query-history';
2425
import * as qsClient from './queryserver-client';
2526
import { isQuickQueryPath } from './quick-query';
26-
import { upgradeDatabase } from './upgrades';
27+
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
2728

2829
/**
2930
* run-queries.ts
@@ -80,6 +81,7 @@ export class QueryInfo {
8081

8182
async run(
8283
qs: qsClient.QueryServerClient,
84+
upgradeQlo: string | undefined,
8385
progress: ProgressCallback,
8486
token: CancellationToken,
8587
): Promise<messages.EvaluationResult> {
@@ -90,6 +92,7 @@ export class QueryInfo {
9092
const queryToRun: messages.QueryToRun = {
9193
resultsPath: this.resultsPaths.resultsPath,
9294
qlo: Uri.file(this.compiledQueryPath).toString(),
95+
compiledUpgrade: upgradeQlo && Uri.file(upgradeQlo).toString(),
9396
allowUnknownTemplates: true,
9497
templateValues: this.templates,
9598
id: callbackId,
@@ -292,7 +295,7 @@ async function checkDbschemeCompatibility(
292295
const searchPath = getOnDiskWorkspaceFolders();
293296

294297
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
295-
const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
298+
const { finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
296299
const hash = async function(filename: string): Promise<string> {
297300
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
298301
};
@@ -311,25 +314,60 @@ async function checkDbschemeCompatibility(
311314
const upgradableTo = await hash(finalDbscheme);
312315

313316
if (upgradableTo != dbschemeOfLib) {
314-
logger.log(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but database has scheme ${query.program.dbschemePath}, and no upgrade path found`);
315-
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace. Please try using a newer version of the query libraries.`);
317+
reportNoUpgradePath(query);
316318
}
317319

318320
if (upgradableTo == dbschemeOfLib &&
319321
dbschemeOfDb != dbschemeOfLib) {
320322
// Try to upgrade the database
321-
await upgradeDatabase(
323+
await upgradeDatabaseExplicit(
322324
qs,
323325
query.dbItem,
324-
Uri.file(finalDbscheme),
325-
getUpgradesDirectories(scripts),
326326
progress,
327327
token
328328
);
329329
}
330330
}
331331
}
332332

333+
334+
function reportNoUpgradePath(query: QueryInfo) {
335+
logger.log(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but database has scheme ${query.program.dbschemePath}, and no upgrade path found`);
336+
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace. Please try using a newer version of the query libraries.`);
337+
}
338+
339+
/**
340+
* Compile a non-destructive upgrade.
341+
*/
342+
async function compileNonDestructiveUpgrade(
343+
qs: qsClient.QueryServerClient,
344+
upgradeTemp: tmp.DirResult,
345+
query: QueryInfo,
346+
progress: ProgressCallback,
347+
token: CancellationToken,
348+
): Promise<string> {
349+
const searchPath = helpers.getOnDiskWorkspaceFolders();
350+
351+
if (query.dbItem.contents === undefined || query.dbItem.contents.dbSchemeUri === undefined) {
352+
throw new Error('Database is invalid, and cannot be upgraded.');
353+
}
354+
const { scripts, matchesTarget } = await qs.cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath, query.queryDbscheme);
355+
356+
if (!matchesTarget) {
357+
reportNoUpgradePath(query);
358+
}
359+
const result = await compileDatabaseUpgradeSequence(qs, query.dbItem, query.queryDbscheme, scripts, upgradeTemp, progress, token);
360+
if (result.compiledUpgrades === undefined) {
361+
const error = result.error || '[no error message available]';
362+
throw new Error(error);
363+
}
364+
// We can upgrade to the actual target
365+
query.program.dbschemePath = query.queryDbscheme;
366+
// We are new enough that we will always support single file upgrades.
367+
return result.compiledUpgrades.compiledUpgradeFile!;
368+
369+
}
370+
333371
/**
334372
* Prompts the user to save `document` if it has unsaved changes.
335373
*
@@ -516,64 +554,73 @@ export async function compileAndRunQueryAgainstDatabase(
516554
}
517555

518556
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
519-
await checkDbschemeCompatibility(cliServer, qs, query, progress, token);
520557

521-
let errors;
558+
const upgradeDir = tmp.dirSync({ dir: upgradesTmpDir.name });
522559
try {
523-
errors = await query.compile(qs, progress, token);
524-
} catch (e) {
525-
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
526-
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
560+
let upgradeQlo;
561+
if (await hasNondestructiveUpgradeCapabilities(qs)) {
562+
upgradeQlo = await compileNonDestructiveUpgrade(qs, upgradeDir, query, progress, token);
527563
} else {
528-
throw e;
529-
}
530-
}
531-
532-
if (errors.length == 0) {
533-
const result = await query.run(qs, progress, token);
534-
if (result.resultType !== messages.QueryResultType.SUCCESS) {
535-
const message = result.message || 'Failed to run query';
536-
logger.log(message);
537-
showAndLogErrorMessage(message);
564+
await checkDbschemeCompatibility(cliServer, qs, query, progress, token);
538565
}
539-
return {
540-
query,
541-
result,
542-
database: {
543-
name: db.name,
544-
databaseUri: db.databaseUri.toString(true)
545-
},
546-
options: historyItemOptions,
547-
logFileLocation: result.logFileLocation,
548-
dispose: () => {
549-
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
566+
let errors;
567+
try {
568+
errors = await query.compile(qs, progress, token);
569+
} catch (e) {
570+
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
571+
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
572+
} else {
573+
throw e;
550574
}
551-
};
552-
} else {
553-
// Error dialogs are limited in size and scrollability,
554-
// so we include a general description of the problem,
555-
// and direct the user to the output window for the detailed compilation messages.
556-
// However we don't show quick eval errors there so we need to display them anyway.
557-
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
558-
559-
const formattedMessages: string[] = [];
560-
561-
for (const error of errors) {
562-
const message = error.message || '[no error message available]';
563-
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
564-
formattedMessages.push(formatted);
565-
qs.logger.log(formatted);
566575
}
567-
if (quickEval && formattedMessages.length <= 3) {
568-
showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n'));
576+
if (errors.length == 0) {
577+
const result = await query.run(qs, upgradeQlo, progress, token);
578+
if (result.resultType !== messages.QueryResultType.SUCCESS) {
579+
const message = result.message || 'Failed to run query';
580+
logger.log(message);
581+
helpers.showAndLogErrorMessage(message);
582+
}
583+
return {
584+
query,
585+
result,
586+
database: {
587+
name: db.name,
588+
databaseUri: db.databaseUri.toString(true)
589+
},
590+
options: historyItemOptions,
591+
logFileLocation: result.logFileLocation,
592+
dispose: () => {
593+
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
594+
}
595+
};
569596
} else {
570-
showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') +
571-
' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
572-
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
573-
' and choose CodeQL Query Server from the dropdown.');
574-
}
597+
// Error dialogs are limited in size and scrollability,
598+
// so we include a general description of the problem,
599+
// and direct the user to the output window for the detailed compilation messages.
600+
// However we don't show quick eval errors there so we need to display them anyway.
601+
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
602+
603+
const formattedMessages: string[] = [];
604+
605+
for (const error of errors) {
606+
const message = error.message || '[no error message available]';
607+
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
608+
formattedMessages.push(formatted);
609+
qs.logger.log(formatted);
610+
}
611+
if (quickEval && formattedMessages.length <= 3) {
612+
showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n'));
613+
} else {
614+
showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') +
615+
' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
616+
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
617+
' and choose CodeQL Query Server from the dropdown.');
618+
}
575619

576-
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
620+
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
621+
}
622+
} finally {
623+
upgradeDir.removeCallback();
577624
}
578625
}
579626

0 commit comments

Comments
 (0)