Merge pull request #1143 from github/aeisenberg/refactor-query-history-info

Refactor query history to handle remote and local
This commit is contained in:
Andrew Eisenberg
2022-02-22 09:51:13 -08:00
committed by GitHub
10 changed files with 294 additions and 219 deletions

View File

@@ -20,11 +20,11 @@ import { DatabaseManager } from '../databases';
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli-types';
import resultsDiff from './resultsDiff';
import { FullCompletedQueryInfo } from '../query-results';
import { CompletedLocalQueryInfo } from '../query-results';
interface ComparePair {
from: FullCompletedQueryInfo;
to: FullCompletedQueryInfo;
from: CompletedLocalQueryInfo;
to: CompletedLocalQueryInfo;
}
export class CompareInterfaceManager extends DisposableObject {
@@ -39,15 +39,15 @@ export class CompareInterfaceManager extends DisposableObject {
private cliServer: CodeQLCliServer,
private logger: Logger,
private showQueryResultsCallback: (
item: FullCompletedQueryInfo
item: CompletedLocalQueryInfo
) => Promise<void>
) {
super();
}
async showResults(
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo,
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo,
selectedResultSetName?: string
) {
this.comparePair = { from, to };
@@ -188,8 +188,8 @@ export class CompareInterfaceManager extends DisposableObject {
}
private async findCommonResultSetNames(
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo,
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo,
selectedResultSetName: string | undefined
): Promise<[string[], string, RawResultSet, RawResultSet]> {
const fromSchemas = await this.cliServer.bqrsInfo(

View File

@@ -70,7 +70,7 @@ import { InterfaceManager } from './interface';
import { WebviewReveal } from './interface-utils';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { QueryHistoryManager } from './query-history';
import { FullCompletedQueryInfo, FullQueryInfo } from './query-results';
import { CompletedLocalQueryInfo, LocalQueryInfo } from './query-results';
import * as qsClient from './queryserver-client';
import { displayQuickQuery } from './quick-query';
import { compileAndRunQueryAgainstDatabase, createInitialQueryInfo } from './run-queries';
@@ -443,7 +443,7 @@ async function activateWithInstalledDistribution(
void logger.log('Initializing query history manager.');
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();
ctx.subscriptions.push(queryHistoryConfigurationListener);
const showResults = async (item: FullCompletedQueryInfo) =>
const showResults = async (item: CompletedLocalQueryInfo) =>
showResultsForCompletedQuery(item, WebviewReveal.Forced);
const queryStorageDir = path.join(ctx.globalStorageUri.fsPath, 'queries');
await fs.ensureDir(queryStorageDir);
@@ -456,7 +456,7 @@ async function activateWithInstalledDistribution(
ctx,
queryHistoryConfigurationListener,
showResults,
async (from: FullCompletedQueryInfo, to: FullCompletedQueryInfo) =>
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
showResultsForComparison(from, to),
);
await qhm.readQueryHistory();
@@ -480,8 +480,8 @@ async function activateWithInstalledDistribution(
archiveFilesystemProvider.activate(ctx);
async function showResultsForComparison(
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo
): Promise<void> {
try {
await cmpm.showResults(from, to);
@@ -491,7 +491,7 @@ async function activateWithInstalledDistribution(
}
async function showResultsForCompletedQuery(
query: FullCompletedQueryInfo,
query: CompletedLocalQueryInfo,
forceReveal: WebviewReveal
): Promise<void> {
await intm.showResults(query, forceReveal, false);
@@ -521,7 +521,7 @@ async function activateWithInstalledDistribution(
token.onCancellationRequested(() => source.cancel());
const initialInfo = await createInitialQueryInfo(selectedQuery, databaseInfo, quickEval, range);
const item = new FullQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
const item = new LocalQueryInfo(initialInfo, queryHistoryConfigurationListener, source);
qhm.addQuery(item);
try {
const completedQueryInfo = await compileAndRunQueryAgainstDatabase(
@@ -535,7 +535,7 @@ async function activateWithInstalledDistribution(
);
item.completeThisQuery(completedQueryInfo);
await qhm.writeQueryHistory();
await showResultsForCompletedQuery(item as FullCompletedQueryInfo, WebviewReveal.NotForced);
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.NotForced);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
} catch (e) {

View File

@@ -47,7 +47,7 @@ import {
import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-types';
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
import { PAGE_SIZE } from './config';
import { FullCompletedQueryInfo } from './query-results';
import { CompletedLocalQueryInfo } from './query-results';
/**
* interface.ts
@@ -97,7 +97,7 @@ function numInterpretedPages(interpretation: Interpretation | undefined): number
}
export class InterfaceManager extends DisposableObject {
private _displayedQuery?: FullCompletedQueryInfo;
private _displayedQuery?: CompletedLocalQueryInfo;
private _interpretation?: Interpretation;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
@@ -357,7 +357,7 @@ export class InterfaceManager extends DisposableObject {
* history entry.
*/
public async showResults(
fullQuery: FullCompletedQueryInfo,
fullQuery: CompletedLocalQueryInfo,
forceReveal: WebviewReveal,
shouldKeepOldResultsWhileRendering = false
): Promise<void> {

View File

@@ -29,9 +29,11 @@ import { QueryServerClient } from './queryserver-client';
import { DisposableObject } from './pure/disposable-object';
import { commandRunner } from './commandRunner';
import { assertNever, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS } from './pure/helpers-pure';
import { FullCompletedQueryInfo, FullQueryInfo, QueryStatus } from './query-results';
import { CompletedLocalQueryInfo, LocalQueryInfo as LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { DatabaseManager } from './databases';
import { registerQueryHistoryScubber } from './query-history-scrubber';
import { QueryStatus } from './query-status';
import { slurpQueryHistory, splatQueryHistory } from './query-serialization';
/**
* query-history.ts
@@ -99,18 +101,18 @@ const WORKSPACE_QUERY_HISTORY_FILE = 'workspace-query-history.json';
export class HistoryTreeDataProvider extends DisposableObject {
private _sortOrder = SortOrder.DateAsc;
private _onDidChangeTreeData = super.push(new EventEmitter<FullQueryInfo | undefined>());
private _onDidChangeTreeData = super.push(new EventEmitter<QueryHistoryInfo | undefined>());
readonly onDidChangeTreeData: Event<FullQueryInfo | undefined> = this
readonly onDidChangeTreeData: Event<QueryHistoryInfo | undefined> = this
._onDidChangeTreeData.event;
private history: FullQueryInfo[] = [];
private history: QueryHistoryInfo[] = [];
private failedIconPath: string;
private localSuccessIconPath: string;
private current: FullQueryInfo | undefined;
private current: QueryHistoryInfo | undefined;
constructor(extensionPath: string) {
super();
@@ -124,7 +126,7 @@ export class HistoryTreeDataProvider extends DisposableObject {
);
}
async getTreeItem(element: FullQueryInfo): Promise<TreeItem> {
async getTreeItem(element: QueryHistoryInfo): Promise<TreeItem> {
const treeItem = new TreeItem(element.label);
treeItem.command = {
@@ -134,36 +136,52 @@ export class HistoryTreeDataProvider extends DisposableObject {
tooltip: element.failureReason || element.label
};
// Populate the icon and the context value. We use the context value to
// control which commands are visible in the context menu.
let hasResults;
switch (element.status) {
case QueryStatus.InProgress:
treeItem.iconPath = new ThemeIcon('sync~spin');
treeItem.contextValue = 'inProgressResultsItem';
break;
case QueryStatus.Completed:
hasResults = await element.completedQuery?.query.hasInterpretedResults();
treeItem.iconPath = this.localSuccessIconPath;
treeItem.contextValue = hasResults
? 'interpretedResultsItem'
: 'rawResultsItem';
break;
case QueryStatus.Failed:
treeItem.iconPath = this.failedIconPath;
treeItem.contextValue = 'cancelledResultsItem';
break;
default:
assertNever(element.status);
if (element.t === 'local') {
// Populate the icon and the context value. We use the context value to
// control which commands are visible in the context menu.
let hasResults;
switch (element.status) {
case QueryStatus.InProgress:
treeItem.iconPath = new ThemeIcon('sync~spin');
treeItem.contextValue = 'inProgressResultsItem';
break;
case QueryStatus.Completed:
hasResults = await element.completedQuery?.query.hasInterpretedResults();
treeItem.iconPath = this.localSuccessIconPath;
treeItem.contextValue = hasResults
? 'interpretedResultsItem'
: 'rawResultsItem';
break;
case QueryStatus.Failed:
treeItem.iconPath = this.failedIconPath;
treeItem.contextValue = 'cancelledResultsItem';
break;
default:
assertNever(element.status);
}
} else {
// TODO remote queries are not implemented yet.
}
return treeItem;
}
getChildren(
element?: FullQueryInfo
): ProviderResult<FullQueryInfo[]> {
element?: QueryHistoryInfo
): ProviderResult<QueryHistoryInfo[]> {
return element ? [] : this.history.sort((h1, h2) => {
// TODO remote queries are not implemented yet.
if (h1.t !== 'local' && h2.t !== 'local') {
return 0;
}
if (h1.t !== 'local') {
return -1;
}
if (h2.t !== 'local') {
return 1;
}
const resultCount1 = h1.completedQuery?.resultCount ?? -1;
const resultCount2 = h2.completedQuery?.resultCount ?? -1;
@@ -192,25 +210,25 @@ export class HistoryTreeDataProvider extends DisposableObject {
});
}
getParent(_element: FullQueryInfo): ProviderResult<FullQueryInfo> {
getParent(_element: QueryHistoryInfo): ProviderResult<QueryHistoryInfo> {
return null;
}
getCurrent(): FullQueryInfo | undefined {
getCurrent(): QueryHistoryInfo | undefined {
return this.current;
}
pushQuery(item: FullQueryInfo): void {
pushQuery(item: QueryHistoryInfo): void {
this.history.push(item);
this.setCurrentItem(item);
this.refresh();
}
setCurrentItem(item?: FullQueryInfo) {
setCurrentItem(item?: QueryHistoryInfo) {
this.current = item;
}
remove(item: FullQueryInfo) {
remove(item: QueryHistoryInfo) {
const isCurrent = this.current === item;
if (isCurrent) {
this.setCurrentItem();
@@ -227,11 +245,11 @@ export class HistoryTreeDataProvider extends DisposableObject {
}
}
get allHistory(): FullQueryInfo[] {
get allHistory(): QueryHistoryInfo[] {
return this.history;
}
set allHistory(history: FullQueryInfo[]) {
set allHistory(history: QueryHistoryInfo[]) {
this.history = history;
this.current = history[0];
this.refresh();
@@ -254,9 +272,9 @@ export class HistoryTreeDataProvider extends DisposableObject {
export class QueryHistoryManager extends DisposableObject {
treeDataProvider: HistoryTreeDataProvider;
treeView: TreeView<FullQueryInfo>;
lastItemClick: { time: Date; item: FullQueryInfo } | undefined;
compareWithItem: FullQueryInfo | undefined;
treeView: TreeView<QueryHistoryInfo>;
lastItemClick: { time: Date; item: QueryHistoryInfo } | undefined;
compareWithItem: LocalQueryInfo | undefined;
queryHistoryScrubber: Disposable | undefined;
private queryMetadataStorageLocation;
@@ -266,10 +284,10 @@ export class QueryHistoryManager extends DisposableObject {
private queryStorageDir: string,
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: FullCompletedQueryInfo) => Promise<void>,
private selectedCallback: (item: CompletedLocalQueryInfo) => Promise<void>,
private doCompareCallback: (
from: FullCompletedQueryInfo,
to: FullCompletedQueryInfo
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo
) => Promise<void>
) {
super();
@@ -303,7 +321,12 @@ export class QueryHistoryManager extends DisposableObject {
} else {
this.treeDataProvider.setCurrentItem(ev.selection[0]);
}
this.updateCompareWith([...ev.selection]);
if (ev.selection.some(item => item.t !== 'local')) {
// Don't allow comparison of non-local items
this.updateCompareWith([]);
} else {
this.updateCompareWith([...ev.selection] as LocalQueryInfo[]);
}
})
);
@@ -395,7 +418,7 @@ export class QueryHistoryManager extends DisposableObject {
this.push(
commandRunner(
'codeQLQueryHistory.itemClicked',
async (item: FullQueryInfo) => {
async (item: LocalQueryInfo) => {
return this.handleItemClicked(item, [item]);
}
)
@@ -449,28 +472,29 @@ export class QueryHistoryManager extends DisposableObject {
async readQueryHistory(): Promise<void> {
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
const history = await FullQueryInfo.slurp(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
this.treeDataProvider.allHistory = history;
}
async writeQueryHistory(): Promise<void> {
const toSave = this.treeDataProvider.allHistory.filter(q => q.isCompleted());
await FullQueryInfo.splat(toSave, this.queryMetadataStorageLocation);
await splatQueryHistory(toSave, this.queryMetadataStorageLocation);
}
async invokeCallbackOn(queryHistoryItem: FullQueryInfo) {
async invokeCallbackOn(queryHistoryItem: QueryHistoryInfo) {
if (this.selectedCallback && queryHistoryItem.isCompleted()) {
const sc = this.selectedCallback;
await sc(queryHistoryItem as FullCompletedQueryInfo);
await sc(queryHistoryItem as CompletedLocalQueryInfo);
}
}
async handleOpenQuery(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
): Promise<void> {
// TODO will support remote queries
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
if (!this.assertSingleQuery(finalMultiSelect) || (finalSingleItem && finalSingleItem.t !== 'local')) {
return;
}
@@ -499,12 +523,17 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleRemoveHistoryItem(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
const toDelete = (finalMultiSelect || [finalSingleItem]);
await Promise.all(toDelete.map(async (item) => {
// TODO Remote queries are not implemented yet
if (item.t !== 'local') {
return;
}
// Removing in progress queries is not supported. They must be cancelled first.
if (item.status !== QueryStatus.InProgress) {
this.treeDataProvider.remove(item);
@@ -548,12 +577,13 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleSetLabel(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
): Promise<void> {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
// TODO will support remote queries
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
return;
}
@@ -571,21 +601,26 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleCompareWith(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
try {
// local queries only
if (finalSingleItem?.t !== 'local') {
throw new Error('Please select a local query.');
}
if (!finalSingleItem.completedQuery?.didRunSuccessfully) {
throw new Error('Please select a successful query.');
throw new Error('Please select a query that has completed successfully.');
}
const from = this.compareWithItem || singleItem;
const to = await this.findOtherQueryToCompare(from, finalMultiSelect);
if (from.isCompleted() && to?.isCompleted()) {
await this.doCompareCallback(from as FullCompletedQueryInfo, to as FullCompletedQueryInfo);
await this.doCompareCallback(from as CompletedLocalQueryInfo, to as CompletedLocalQueryInfo);
}
} catch (e) {
void showAndLogErrorMessage(e.message);
@@ -593,11 +628,12 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleItemClicked(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
// TODO will support remote queries
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
if (!this.assertSingleQuery(finalMultiSelect) || (finalSingleItem && finalSingleItem?.t !== 'local')) {
return;
}
@@ -625,9 +661,10 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleShowQueryLog(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: LocalQueryInfo,
multiSelect: LocalQueryInfo[]
) {
// Local queries only
if (!this.assertSingleQuery(multiSelect)) {
return;
}
@@ -644,25 +681,28 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleCancel(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
// Local queries only
// In the future, we may support cancelling remote queries, but this is not a short term plan.
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
(finalMultiSelect || [finalSingleItem]).forEach((item) => {
if (item.status === QueryStatus.InProgress) {
if (item.status === QueryStatus.InProgress && item.t === 'local') {
item.cancel();
}
});
}
async handleShowQueryText(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
// TODO will support remote queries
if (!this.assertSingleQuery(finalMultiSelect) || (finalSingleItem && finalSingleItem?.t !== 'local')) {
return;
}
@@ -682,12 +722,13 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleViewSarifAlerts(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem.completedQuery) {
// Local queries only
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {
return;
}
@@ -706,15 +747,13 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleViewCsvResults(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
return;
}
if (!finalSingleItem.completedQuery) {
// Local queries only
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {
return;
}
const query = finalSingleItem.completedQuery.query;
@@ -730,12 +769,13 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleViewCsvAlerts(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem.completedQuery) {
// Local queries only
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {
return;
}
@@ -745,15 +785,13 @@ export class QueryHistoryManager extends DisposableObject {
}
async handleViewDil(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[],
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[],
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect)) {
return;
}
if (!finalSingleItem.completedQuery) {
// Local queries only
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'local' || !finalSingleItem.completedQuery) {
return;
}
@@ -762,11 +800,11 @@ export class QueryHistoryManager extends DisposableObject {
);
}
async getQueryText(queryHistoryItem: FullQueryInfo): Promise<string> {
async getQueryText(queryHistoryItem: LocalQueryInfo): Promise<string> {
return queryHistoryItem.initialInfo.queryText;
}
addQuery(item: FullQueryInfo) {
addQuery(item: LocalQueryInfo) {
this.treeDataProvider.pushQuery(item);
this.updateTreeViewSelectionIfVisible();
}
@@ -825,10 +863,12 @@ the file in the file explorer and dragging it into the workspace.`
}
private async findOtherQueryToCompare(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
): Promise<FullQueryInfo | undefined> {
if (!singleItem.completedQuery) {
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
): Promise<CompletedLocalQueryInfo | undefined> {
// Remote queries cannot be compared
if (singleItem.t !== 'local' || multiSelect.some(s => s.t !== 'local') || !singleItem.completedQuery) {
return undefined;
}
const dbName = singleItem.initialInfo.databaseInfo.name;
@@ -837,7 +877,7 @@ the file in the file explorer and dragging it into the workspace.`
if (multiSelect?.length === 2) {
// return the query that is not the first selected one
const otherQuery =
singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0];
(singleItem === multiSelect[0] ? multiSelect[1] : multiSelect[0]) as LocalQueryInfo;
if (!otherQuery.completedQuery) {
throw new Error('Please select a completed query.');
}
@@ -847,10 +887,10 @@ the file in the file explorer and dragging it into the workspace.`
if (otherQuery.initialInfo.databaseInfo.name !== dbName) {
throw new Error('Query databases must be the same.');
}
return otherQuery;
return otherQuery as CompletedLocalQueryInfo;
}
if (multiSelect?.length > 1) {
if (multiSelect?.length > 2) {
throw new Error('Please select no more than 2 queries.');
}
@@ -859,15 +899,16 @@ the file in the file explorer and dragging it into the workspace.`
.filter(
(otherQuery) =>
otherQuery !== singleItem &&
otherQuery.t === 'local' &&
otherQuery.completedQuery &&
otherQuery.completedQuery.didRunSuccessfully &&
otherQuery.initialInfo.databaseInfo.name === dbName
)
.map((item) => ({
label: item.label,
description: item.initialInfo.databaseInfo.name,
detail: item.completedQuery!.statusString,
query: item,
description: (item as CompletedLocalQueryInfo).initialInfo.databaseInfo.name,
detail: (item as CompletedLocalQueryInfo).completedQuery.statusString,
query: item as CompletedLocalQueryInfo,
}));
if (comparableQueryLabels.length < 1) {
throw new Error('No other queries available to compare with.');
@@ -876,7 +917,7 @@ the file in the file explorer and dragging it into the workspace.`
return choice?.query;
}
private assertSingleQuery(multiSelect: FullQueryInfo[] = [], message = 'Please select a single query.') {
private assertSingleQuery(multiSelect: QueryHistoryInfo[] = [], message = 'Please select a single query.') {
if (multiSelect.length > 1) {
void showAndLogErrorMessage(
message
@@ -903,7 +944,7 @@ the file in the file explorer and dragging it into the workspace.`
*
* @param newSelection the new selection after the most recent selection change
*/
private updateCompareWith(newSelection: FullQueryInfo[]) {
private updateCompareWith(newSelection: LocalQueryInfo[]) {
if (newSelection.length === 1) {
this.compareWithItem = newSelection[0];
} else if (
@@ -927,11 +968,11 @@ the file in the file explorer and dragging it into the workspace.`
* @param multiSelect a multi-select or undefined if no items are selected
*/
private determineSelection(
singleItem: FullQueryInfo,
multiSelect: FullQueryInfo[]
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
): {
finalSingleItem: FullQueryInfo;
finalMultiSelect: FullQueryInfo[]
finalSingleItem: QueryHistoryInfo;
finalMultiSelect: QueryHistoryInfo[]
} {
if (!singleItem && !multiSelect?.[0]) {
const selection = this.treeView.selection;

View File

@@ -15,8 +15,8 @@ import {
} from './pure/interface-types';
import { QueryHistoryConfig } from './config';
import { DatabaseInfo } from './pure/interface-types';
import { showAndLogErrorMessage } from './helpers';
import { asyncFilter } from './pure/helpers-pure';
import { QueryStatus } from './query-status';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
/**
* query-results.ts
@@ -42,12 +42,6 @@ export interface InitialQueryInfo {
readonly id: string; // unique id for this query.
}
export enum QueryStatus {
InProgress = 'InProgress',
Completed = 'Completed',
Failed = 'Failed',
}
export class CompletedQueryInfo implements QueryWithResults {
readonly query: QueryEvaluationInfo;
readonly result: messages.EvaluationResult;
@@ -191,88 +185,21 @@ export function ensureMetadataIsComplete(metadata: QueryMetadata | undefined) {
/**
* Used in Interface and Compare-Interface for queries that we know have been complated.
*/
export type FullCompletedQueryInfo = FullQueryInfo & {
export type CompletedLocalQueryInfo = LocalQueryInfo & {
completedQuery: CompletedQueryInfo
};
export class FullQueryInfo {
export type QueryHistoryInfo = LocalQueryInfo | RemoteQueryHistoryItem;
static async slurp(fsPath: string, config: QueryHistoryConfig): Promise<FullQueryInfo[]> {
try {
if (!(await fs.pathExists(fsPath))) {
return [];
}
const data = await fs.readFile(fsPath, 'utf8');
const queries = JSON.parse(data);
const parsedQueries = queries.map((q: FullQueryInfo) => {
// Need to explicitly set prototype since reading in from JSON will not
// do this automatically. Note that we can't call the constructor here since
// the constructor invokes extra logic that we don't want to do.
Object.setPrototypeOf(q, FullQueryInfo.prototype);
// The config object is a global, se we need to set it explicitly
// and ensure it is not serialized to JSON.
q.setConfig(config);
// Date instances are serialized as strings. Need to
// convert them back to Date instances.
(q.initialInfo as any).start = new Date(q.initialInfo.start);
if (q.completedQuery) {
// Again, need to explicitly set prototypes.
Object.setPrototypeOf(q.completedQuery, CompletedQueryInfo.prototype);
Object.setPrototypeOf(q.completedQuery.query, QueryEvaluationInfo.prototype);
// slurped queries do not need to be disposed
q.completedQuery.dispose = () => { /**/ };
}
return q;
});
// filter out queries that have been deleted on disk
// most likely another workspace has deleted them because the
// queries aged out.
return asyncFilter(parsedQueries, async (q) => {
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
return !!resultsPath && await fs.pathExists(resultsPath);
});
} catch (e) {
void showAndLogErrorMessage('Error loading query history.', {
fullMessage: ['Error loading query history.', e.stack].join('\n'),
});
return [];
}
}
/**
* Save the query history to disk. It is not necessary that the parent directory
* exists, but if it does, it must be writable. An existing file will be overwritten.
*
* Any errors will be rethrown.
*
* @param queries the list of queries to save.
* @param fsPath the path to save the queries to.
*/
static async splat(queries: FullQueryInfo[], fsPath: string): Promise<void> {
try {
if (!(await fs.pathExists(fsPath))) {
await fs.mkdir(path.dirname(fsPath), { recursive: true });
}
// remove incomplete queries since they cannot be recreated on restart
const filteredQueries = queries.filter(q => q.completedQuery !== undefined);
const data = JSON.stringify(filteredQueries, null, 2);
await fs.writeFile(fsPath, data);
} catch (e) {
throw new Error(`Error saving query history to ${fsPath}: ${e.message}`);
}
}
export class LocalQueryInfo {
readonly t = 'local';
public failureReason: string | undefined;
public completedQuery: CompletedQueryInfo | undefined;
private config: QueryHistoryConfig | undefined;
/**
* Note that in the {@link FullQueryInfo.slurp} method, we create a FullQueryInfo instance
* Note that in the {@link slurpQueryHistory} method, we create a FullQueryInfo instance
* by explicitly setting the prototype in order to avoid calling this constructor.
*/
constructor(
@@ -401,7 +328,7 @@ export class FullQueryInfo {
*
* @param config the global query history config object
*/
private setConfig(config: QueryHistoryConfig) {
setConfig(config: QueryHistoryConfig) {
// avoid serializing config property
Object.defineProperty(this, 'config', {
enumerable: false,

View File

@@ -0,0 +1,85 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { showAndLogErrorMessage } from './helpers';
import { asyncFilter } from './pure/helpers-pure';
import { CompletedQueryInfo, LocalQueryInfo, QueryHistoryInfo } from './query-results';
import { QueryEvaluationInfo } from './run-queries';
export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConfig): Promise<QueryHistoryInfo[]> {
try {
if (!(await fs.pathExists(fsPath))) {
return [];
}
const data = await fs.readFile(fsPath, 'utf8');
const queries = JSON.parse(data);
const parsedQueries = queries.map((q: QueryHistoryInfo) => {
// Need to explicitly set prototype since reading in from JSON will not
// do this automatically. Note that we can't call the constructor here since
// the constructor invokes extra logic that we don't want to do.
if (q.t === 'local') {
Object.setPrototypeOf(q, LocalQueryInfo.prototype);
// The config object is a global, se we need to set it explicitly
// and ensure it is not serialized to JSON.
q.setConfig(config);
// Date instances are serialized as strings. Need to
// convert them back to Date instances.
(q.initialInfo as any).start = new Date(q.initialInfo.start);
if (q.completedQuery) {
// Again, need to explicitly set prototypes.
Object.setPrototypeOf(q.completedQuery, CompletedQueryInfo.prototype);
Object.setPrototypeOf(q.completedQuery.query, QueryEvaluationInfo.prototype);
// slurped queries do not need to be disposed
q.completedQuery.dispose = () => { /**/ };
}
} else if (q.t === 'remote') {
// TODO Remote queries are not implemented yet.
}
return q;
});
// filter out queries that have been deleted on disk
// most likely another workspace has deleted them because the
// queries aged out.
return asyncFilter(parsedQueries, async (q) => {
if (q.t !== 'local') {
return false;
}
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
return !!resultsPath && await fs.pathExists(resultsPath);
});
} catch (e) {
void showAndLogErrorMessage('Error loading query history.', {
fullMessage: ['Error loading query history.', e.stack].join('\n'),
});
return [];
}
}
/**
* Save the query history to disk. It is not necessary that the parent directory
* exists, but if it does, it must be writable. An existing file will be overwritten.
*
* Any errors will be rethrown.
*
* @param queries the list of queries to save.
* @param fsPath the path to save the queries to.
*/
export async function splatQueryHistory(queries: QueryHistoryInfo[], fsPath: string): Promise<void> {
try {
if (!(await fs.pathExists(fsPath))) {
await fs.mkdir(path.dirname(fsPath), { recursive: true });
}
// remove incomplete queries since they cannot be recreated on restart
const filteredQueries = queries.filter(q => q.t === 'local' && q.completedQuery !== undefined);
const data = JSON.stringify(filteredQueries, null, 2);
await fs.writeFile(fsPath, data);
} catch (e) {
throw new Error(`Error saving query history to ${fsPath}: ${e.message}`);
}
}

View File

@@ -0,0 +1,5 @@
export enum QueryStatus {
InProgress = 'InProgress',
Completed = 'Completed',
Failed = 'Failed',
}

View File

@@ -0,0 +1,15 @@
// TODO This is a stub and will be filled implemented in later PRs.
import { QueryStatus } from '../query-status';
/**
* Information about a remote query.
*/
export interface RemoteQueryHistoryItem {
readonly t: 'remote';
label: string;
failureReason: string | undefined;
status: QueryStatus;
isCompleted(): boolean;
}

View File

@@ -14,7 +14,7 @@ import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
import { QueryHistoryConfigListener } from '../../config';
import * as messages from '../../pure/messages';
import { QueryServerClient } from '../../queryserver-client';
import { FullQueryInfo, InitialQueryInfo } from '../../query-results';
import { LocalQueryInfo, InitialQueryInfo } from '../../query-results';
import { DatabaseManager } from '../../databases';
import * as tmp from 'tmp-promise';
import { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, TWO_HOURS_IN_MS, THREE_HOURS_IN_MS } from '../../pure/helpers-pure';
@@ -107,7 +107,7 @@ describe('query-history', () => {
});
});
let allHistory: FullQueryInfo[];
let allHistory: LocalQueryInfo[];
beforeEach(() => {
allHistory = [
@@ -520,13 +520,14 @@ describe('query-history', () => {
},
completedQuery: {
resultCount,
}
},
t: 'local'
};
}
});
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
const fqi = new FullQueryInfo(
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): LocalQueryInfo {
const fqi = new LocalQueryInfo(
{
databaseInfo: { name: dbName },
start: new Date(),
@@ -736,7 +737,7 @@ describe('query-history', () => {
};
}
async function createMockQueryHistory(allHistory: FullQueryInfo[]) {
async function createMockQueryHistory(allHistory: LocalQueryInfo[]) {
const qhm = new QueryHistoryManager(
{} as QueryServerClient,
{} as DatabaseManager,

View File

@@ -5,7 +5,7 @@ import 'mocha';
import 'sinon-chai';
import * as sinon from 'sinon';
import * as chaiAsPromised from 'chai-as-promised';
import { FullQueryInfo, InitialQueryInfo, interpretResults } from '../../query-results';
import { LocalQueryInfo, InitialQueryInfo, interpretResults } from '../../query-results';
import { QueryEvaluationInfo, QueryWithResults } from '../../run-queries';
import { QueryHistoryConfig } from '../../config';
import { EvaluationResult, QueryResultType } from '../../pure/messages';
@@ -13,6 +13,7 @@ import { DatabaseInfo, SortDirection, SortedResultSetInfo } from '../../pure/int
import { CodeQLCliServer, SourceInfo } from '../../cli';
import { CancellationTokenSource, Uri, env } from 'vscode';
import { tmpDir } from '../../helpers';
import { slurpQueryHistory, splatQueryHistory } from '../../query-serialization';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -277,12 +278,12 @@ describe('query-results', () => {
const allHistoryPath = path.join(tmpDir.name, 'workspace-query-history.json');
// splat and slurp
await FullQueryInfo.splat(allHistory, allHistoryPath);
const allHistoryActual = await FullQueryInfo.slurp(allHistoryPath, mockConfig);
await splatQueryHistory(allHistory, allHistoryPath);
const allHistoryActual = await slurpQueryHistory(allHistoryPath, mockConfig);
// the dispose methods will be different. Ignore them.
allHistoryActual.forEach(info => {
if (info.completedQuery) {
if (info.t === 'local' && info.completedQuery) {
const completedQuery = info.completedQuery;
(completedQuery as any).dispose = undefined;
@@ -355,8 +356,8 @@ describe('query-results', () => {
return result;
}
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): FullQueryInfo {
const fqi = new FullQueryInfo(
function createMockFullQueryInfo(dbName = 'a', queryWitbResults?: QueryWithResults, isFail = false): LocalQueryInfo {
const fqi = new LocalQueryInfo(
{
databaseInfo: {
name: dbName,