Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: modify large queries use query more to fetch all needed records #338

Merged
merged 12 commits into from
Dec 8, 2023
3 changes: 2 additions & 1 deletion src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,6 @@ export const messages = {
covIdFormatErr: 'Cannot specify code coverage with a TestRunId result',
startHandshake: 'Attempting StreamingClient handshake',
finishHandshake: 'Finished StreamingClient handshake',
subscribeStarted: 'Subscribing to ApexLog events'
subscribeStarted: 'Subscribing to ApexLog events',
invalidCountQueryResult: 'Invalid query result for count query ("%s")'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a user facing error? what will the user do if one sees this error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is customer facing, but unlikely to fail. I imagine the error would become part of an issue report.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this message is not needed anymore I believe?

};
32 changes: 28 additions & 4 deletions src/tests/asyncTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ import {
TestResult,
TestRunIdResult
} from './types';
import { calculatePercentage, isValidTestRunID } from './utils';
import {
calculatePercentage,
isValidTestRunID,
verifyCountQueries
} from './utils';
import * as util from 'util';
import { QUERY_RECORD_LIMIT } from './constants';
import { CodeCoverage } from './codeCoverage';
import { HttpRequest } from 'jsforce';
import { OrgConfigProperties } from '@salesforce/core/lib/org/orgConfigProperties';

export class AsyncTests {
public readonly connection: Connection;
Expand Down Expand Up @@ -298,11 +303,13 @@ export class AsyncTests {
apexTestResultQuery +=
'ApexClass.Id, ApexClass.Name, ApexClass.NamespacePrefix ';
apexTestResultQuery += 'FROM ApexTestResult WHERE QueueItemId IN (%s)';

const apexTestResultQueryCount =
'Select count(id) from ApexTestResult where QueueItemId IN (%s)';
const apexResultIds = testQueueResult.records.map(record => record.Id);

// iterate thru ids, create query with id, & compare query length to char limit
const queries: string[] = [];
const countQueries: string[] = [];
for (let i = 0; i < apexResultIds.length; i += QUERY_RECORD_LIMIT) {
const recordSet: string[] = apexResultIds
.slice(i, i + QUERY_RECORD_LIMIT)
Expand All @@ -311,12 +318,29 @@ export class AsyncTests {
apexTestResultQuery,
recordSet.join(',')
);
const countQuery: string = util.format(
apexTestResultQueryCount,
recordSet.join(',')
);
countQueries.push(countQuery);
queries.push(query);
}

const queryPromises = queries.map(query => {
const countQueryPromises = countQueries.map(query => {
return this.connection.singleRecordQuery<{
expr0: number;
tooling: true;
}>(query);
});

const countQueryResult = await Promise.all(countQueryPromises);

verifyCountQueries(countQueryResult, countQueries);

const queryPromises = queries.map((query, index) => {
return this.connection.tooling.query<ApexTestResultRecord>(query, {
autoFetch: true
autoFetch: true,
maxFetch: countQueryResult[index].expr0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to add some run time logging , for e.g. found total 'x' records, etc?

});
});
const apexTestResults = await Promise.all(queryPromises);
Expand Down
43 changes: 36 additions & 7 deletions src/tests/codeCoverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { Connection } from '@salesforce/core';
import { ConfigAggregator, Connection } from '@salesforce/core';
import {
ApexCodeCoverage,
ApexCodeCoverageAggregate,
Expand All @@ -16,8 +16,10 @@ import {
PerClassCoverage
} from './types';
import * as util from 'util';
import { calculatePercentage } from './utils';
import { calculatePercentage, verifyCountQueries } from './utils';
import { QUERY_RECORD_LIMIT } from './constants';
import { OrgConfigProperties } from '@salesforce/core/lib/org/orgConfigProperties';
import { nls } from '../i18n';

export class CodeCoverage {
public readonly connection: Connection;
Expand Down Expand Up @@ -159,31 +161,58 @@ export class CodeCoverage {
): Promise<ApexCodeCoverage[]> {
const perClassCodeCovQuery =
'SELECT ApexTestClassId, ApexClassOrTrigger.Id, ApexClassOrTrigger.Name, TestMethodName, NumLinesCovered, NumLinesUncovered, Coverage FROM ApexCodeCoverage WHERE ApexTestClassId IN (%s)';
return this.fetchResults(apexTestClassSet, perClassCodeCovQuery);
const countPerClassCodeCovQuery =
'SELECT count(ApexTestClassId) FROM ApexCodeCoverage WHERE ApexTestClassId IN (%s)';
return this.fetchResults(
apexTestClassSet,
perClassCodeCovQuery,
countPerClassCodeCovQuery
);
}

private async queryAggregateCodeCov(
apexClassIdSet: Set<string>
): Promise<ApexCodeCoverageAggregate[]> {
const codeCoverageQuery =
'SELECT ApexClassOrTrigger.Id, ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered, Coverage FROM ApexCodeCoverageAggregate WHERE ApexClassorTriggerId IN (%s)';
return this.fetchResults(apexClassIdSet, codeCoverageQuery);
const countCodeCoverageQuery =
'SELECT count(ApexClassOrTrigger.Id) FROM ApexCodeCoverageAggregate WHERE ApexClassorTriggerId IN (%s)';
return this.fetchResults(
apexClassIdSet,
codeCoverageQuery,
countCodeCoverageQuery
);
}

private async fetchResults<
T extends ApexCodeCoverage | ApexCodeCoverageAggregate
>(idSet: Set<string>, selectQuery: string): Promise<T[]> {
>(idSet: Set<string>, selectQuery: string, countQuery: string): Promise<T[]> {
const config = await ConfigAggregator.create();
const countQueries = this.createQueries(countQuery, idSet);
const queries = this.createQueries(selectQuery, idSet);

const queryPromises = queries.map(query => {
const countQueryPromises = countQueries.map(query =>
this.connection.singleRecordQuery<{ expr0: number }>(query, {
tooling: true
})
);

const countQueryResult = await Promise.all(countQueryPromises);

verifyCountQueries(countQueryResult, countQueries);

const queryPromises = queries.map((query, index) => {
// The query method returns a type QueryResult from jsforce
// that has takes a type that extends the jsforce Record.
// ApexCodeCoverageRecord and ApexCodeCoverageAggregateRecord
// are the Records compatible types defined in this project.
return this.connection.tooling.query<
ApexCodeCoverageRecord | ApexCodeCoverageAggregateRecord
>(query, {
autoFetch: true
autoFetch: true,
maxFetch:
countQueryResult[index]?.expr0 ??
config.getPropertyValue(OrgConfigProperties.ORG_MAX_QUERY_LIMIT)
});
});

Expand Down
21 changes: 21 additions & 0 deletions src/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Connection } from '@salesforce/core';
import { CLASS_ID_PREFIX, TEST_RUN_ID_PREFIX } from './constants';
import { NamespaceInfo } from './types';
import { nls } from '../i18n';

export function isValidTestRunID(testRunId: string): boolean {
return (
Expand Down Expand Up @@ -54,3 +55,23 @@ export async function queryNamespaces(

return [...orgNamespaces, ...installedNamespaces];
}

export function verifyCountQueries(
countQueryResult: { expr0: number }[],
countQueries: string[]
) {
// check count query results to ensure that there are results that can be used to set maxFetch
countQueryResult.forEach((result, index) => {
if (!result?.expr0 || result?.expr0 <= 0) {
throw new Error(
nls.localize(
'invalidCountQueryResult',
countQueries[index].substring(
0,
countQueries[index].indexOf('IN (') + 30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is number 30 here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brings in the 30 characters following IN ( so the user will have a bit of the set if ides being fetched. With out some sort of clipping, the message can be very large, up to ~10.5K

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this method verifyCountQueries still needed?

)
)
);
}
});
}
Loading