Skip to content

Commit 731012d

Browse files
authored
Merge pull request #32 from paustint/bug-23-24
where fn where subqueries #23 #24
2 parents 1858e5b + 0b44c7e commit 731012d

File tree

7 files changed

+236
-19
lines changed

7 files changed

+236
-19
lines changed

debug/test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
var soqlParserJs = require('../dist');
22

33
const query = `
4-
SELECT Title FROM FAQ__kav WHERE PublishStatus='online' and Language = 'en_US' and KnowledgeArticleVersion = 'ka230000000PCiy' UPDATE VIEWSTAT
4+
SELECT Company, toLabel(Status) FROM Lead WHERE toLabel(Status) = 'le Draft'
55
`;
66

77
const parsedQuery = soqlParserJs.parseQuery(query, { logging: true });
88

99
console.log(JSON.stringify(parsedQuery, null, 2));
1010

11-
// SELECT amount, FORMAT(amount) Amt, convertCurrency(amount) editDate, FORMAT(convertCurrency(amount)) convertedCurrency FROM Opportunity where id = '12345'
12-
// SELECT FORMAT(MIN(closedate)) Amt FROM opportunity
11+
// SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity WHERE StageName = 'Closed Lost')
12+
// SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Opportunity WHERE IsClosed = false)
13+
// SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE LastName LIKE 'apple%') AND Id IN (SELECT AccountId FROM Opportunity WHERE isClosed = false)

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"predeploy": "npm run build",
2222
"deploy": "gh-pages -d build",
2323
"gh-pages": "gh-pages --help",
24-
"update": "npm install soql-parser-js@latest && git add package*.json && git commit -m \"Updated docs version\" && git push"
24+
"update": "npm install soql-parser-js@latest && git add package*.json && git commit -m \"Updated docs version\" && git push && npm run deploy"
2525
},
2626
"devDependencies": {
2727
"@types/jest": "^23.3.5",

docs/src/components/sample-queries.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,22 @@ export default class SampleQueries extends React.Component<ISampleQueriesProps,
166166
soql: `SELECT amount, FORMAT(amount) Amt, convertCurrency(amount) editDate, FORMAT(convertCurrency(amount)) convertedCurrency FROM Opportunity where id = '12345'`,
167167
},
168168
{ key: 39, num: 39, soql: `SELECT FORMAT(MIN(closedate)) Amt FROM opportunity` },
169+
{ key: 40, num: 40, soql: `SELECT Company, toLabel(Status) FROM Lead WHERE toLabel(Status) = 'le Draft'` },
170+
{
171+
key: 41,
172+
num: 41,
173+
soql: `SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity WHERE StageName = 'Closed Lost')`,
174+
},
175+
{
176+
key: 42,
177+
num: 42,
178+
soql: `SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Opportunity WHERE IsClosed = false)`,
179+
},
180+
{
181+
key: 43,
182+
num: 43,
183+
soql: `SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE LastName LIKE 'apple%') AND Id IN (SELECT AccountId FROM Opportunity WHERE isClosed = false)`,
184+
},
169185
];
170186
};
171187

lib/SoqlComposer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,11 @@ export class Compose {
166166
? new Array(where.left.openParen).fill('(').join('')
167167
: '';
168168
output += `${utils.get(where.left.logicalPrefix, ' ')}`;
169-
output += `${where.left.field} ${where.left.operator} ${utils.getAsArrayStr(where.left.value)}`;
169+
output += where.left.fn ? this.parseFn(where.left.fn) : where.left.field;
170+
output += ` ${where.left.operator} `;
171+
output += where.left.valueQuery
172+
? `(${this.parseQuery(where.left.valueQuery)})`
173+
: utils.getAsArrayStr(where.left.value);
170174
output +=
171175
utils.isNumber(where.left.closeParen) && where.left.closeParen > 0
172176
? new Array(where.left.closeParen).fill(')').join('')

lib/SoqlListener.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ export type currItem = 'field' | 'typeof' | 'from' | 'where' | 'groupby' | 'orde
2222

2323
export interface Context {
2424
isSubQuery: boolean;
25+
isWhereSubQuery: boolean;
26+
whereSubquery: Query;
2527
currSubqueryIdx: number;
2628
currWhereConditionGroupIdx: number;
2729
currentItem: currItem;
2830
inWhereClauseGroup: boolean;
2931
tempData: any;
32+
tempDataBackup: any; // used to store tempDate while in WHILE subquery
3033
}
3134

3235
export class SoqlQuery implements Query {
@@ -50,11 +53,14 @@ export class SoqlQuery implements Query {
5053
export class Listener implements SOQLListener {
5154
context: Context = {
5255
isSubQuery: false,
56+
isWhereSubQuery: false,
57+
whereSubquery: null,
5358
currSubqueryIdx: -1,
5459
currWhereConditionGroupIdx: 0,
5560
currentItem: null,
5661
inWhereClauseGroup: false,
5762
tempData: null,
63+
tempDataBackup: null,
5864
};
5965

6066
soqlQuery: SoqlQuery;
@@ -101,7 +107,13 @@ export class Listener implements SOQLListener {
101107
}
102108

103109
getSoqlQuery(): Query {
104-
return this.context.isSubQuery ? this.soqlQuery.subqueries[this.context.currSubqueryIdx] : this.soqlQuery;
110+
if (this.context.isSubQuery) {
111+
return this.soqlQuery.subqueries[this.context.currSubqueryIdx];
112+
}
113+
if (this.context.isWhereSubQuery) {
114+
return this.context.whereSubquery;
115+
}
116+
return this.soqlQuery;
105117
}
106118

107119
enterKeywords_alias_allowed(ctx: Parser.Keywords_alias_allowedContext) {
@@ -214,6 +226,9 @@ export class Listener implements SOQLListener {
214226
if (this.context.currentItem === 'field') {
215227
this.context.tempData.alias = ctx.text;
216228
}
229+
if (this.context.currentItem === 'where') {
230+
this.context.tempData.currConditionOperation.left.fn.alias = ctx.text;
231+
}
217232
if (this.context.currentItem === 'having') {
218233
this.context.tempData.currConditionOperation.left.fn.alias = ctx.text;
219234
}
@@ -363,9 +378,11 @@ export class Listener implements SOQLListener {
363378
const currFn: FunctionExp = this.context.tempData.fn || this.context.tempData;
364379
currFn.name = ctx.text;
365380
}
366-
if (this.context.currentItem === 'having') {
381+
if (this.context.currentItem === 'where' || this.context.currentItem === 'having') {
367382
this.context.tempData.currConditionOperation.left.fn.name = ctx.text;
368-
// this.context.tempData.fn.name = ctx.text;
383+
if (this.context.tempData.currConditionOperation.left.field) {
384+
delete this.context.tempData.currConditionOperation.left.field;
385+
}
369386
}
370387
if (this.context.currentItem === 'orderby') {
371388
this.context.tempData.fn.name = ctx.text;
@@ -572,16 +589,30 @@ export class Listener implements SOQLListener {
572589
if (this.config.logging) {
573590
console.log('enterSoql_subquery:', ctx.text);
574591
}
575-
this.context.isSubQuery = true;
576-
this.soqlQuery.subqueries.push(new SoqlQuery());
577-
this.context.currSubqueryIdx = this.soqlQuery.subqueries.length - 1;
592+
if (this.context.currentItem === 'where') {
593+
this.context.isWhereSubQuery = true;
594+
this.context.whereSubquery = new SoqlQuery();
595+
delete this.context.tempData.currConditionOperation.left.value;
596+
this.context.tempDataBackup = this.context.tempData;
597+
this.context.tempData = null;
598+
} else {
599+
this.context.isSubQuery = true;
600+
this.soqlQuery.subqueries.push(new SoqlQuery());
601+
this.context.currSubqueryIdx = this.soqlQuery.subqueries.length - 1;
602+
}
578603
}
579604
exitSoql_subquery(ctx: Parser.Soql_subqueryContext) {
580605
if (this.config.logging) {
581606
console.log('exitSoql_subquery:', ctx.text);
582607
}
583-
this.context.isSubQuery = false;
584-
this.context.currWhereConditionGroupIdx = 0; // ensure reset for base query or next subquery
608+
if (this.context.isWhereSubQuery) {
609+
this.context.isWhereSubQuery = false;
610+
this.context.tempData = this.context.tempDataBackup;
611+
this.context.tempData.currConditionOperation.left.valueQuery = this.context.whereSubquery;
612+
} else {
613+
this.context.isSubQuery = false;
614+
this.context.currWhereConditionGroupIdx = 0; // ensure reset for base query or next subquery
615+
}
585616
}
586617
enterSubquery_select_clause(ctx: Parser.Subquery_select_clauseContext) {
587618
if (this.config.logging) {
@@ -673,7 +704,7 @@ export class Listener implements SOQLListener {
673704
const currFn: FunctionExp = this.context.tempData.fn || this.context.tempData;
674705
currFn.text = ctx.text;
675706
}
676-
if (this.context.currentItem === 'having') {
707+
if (this.context.currentItem === 'where' || this.context.currentItem === 'having') {
677708
this.context.tempData.currConditionOperation.left.fn = {
678709
text: ctx.text,
679710
};
@@ -705,6 +736,7 @@ export class Listener implements SOQLListener {
705736
// Get correct fn object based on what is set in tempData (set differently for field vs having)
706737
if (
707738
this.context.currentItem === 'field' ||
739+
this.context.currentItem === 'where' ||
708740
this.context.currentItem === 'having' ||
709741
this.context.currentItem === 'orderby'
710742
) {
@@ -914,9 +946,6 @@ export class Listener implements SOQLListener {
914946
if (this.config.logging) {
915947
console.log('enterField_based_condition:', ctx.text);
916948
}
917-
if (this.config.logging) {
918-
console.log('enterLike_based_condition:', ctx.text);
919-
}
920949
if (this.context.currentItem === 'where') {
921950
const currItem: any = {};
922951
if (!this.context.tempData.currConditionOperation.left) {

lib/models/SoqlQuery.model.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ export interface Condition {
5757
openParen?: number;
5858
closeParen?: number;
5959
logicalPrefix?: LogicalPrefix;
60-
field: string;
60+
field?: string;
61+
fn?: FunctionExp;
6162
operator: Operator;
62-
value: string | string[];
63+
value?: string | string[];
64+
valueQuery?: Query;
6365
}
6466

6567
export interface OrderByClause {

test/TestCases.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,5 +1275,170 @@ export const testCases: TestCase[] = [
12751275
sObject: 'Opportunity',
12761276
},
12771277
},
1278+
{
1279+
testCase: 46,
1280+
soql: `SELECT Company, toLabel(Status) FROM Lead WHERE toLabel(Status) = 'le Draft'`,
1281+
output: {
1282+
fields: [
1283+
{
1284+
text: 'Company',
1285+
},
1286+
{
1287+
fn: {
1288+
text: 'toLabel(Status)',
1289+
name: 'toLabel',
1290+
parameter: 'Status',
1291+
},
1292+
},
1293+
],
1294+
subqueries: [],
1295+
sObject: 'Lead',
1296+
where: {
1297+
left: {
1298+
operator: '=',
1299+
value: "'le Draft'",
1300+
fn: {
1301+
text: 'toLabel(Status)',
1302+
name: 'toLabel',
1303+
parameter: 'Status',
1304+
},
1305+
},
1306+
},
1307+
},
1308+
},
1309+
{
1310+
testCase: 47,
1311+
soql: `SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Opportunity WHERE StageName = 'Closed Lost')`,
1312+
output: {
1313+
fields: [
1314+
{
1315+
text: 'Id',
1316+
},
1317+
{
1318+
text: 'Name',
1319+
},
1320+
],
1321+
subqueries: [],
1322+
sObject: 'Account',
1323+
where: {
1324+
left: {
1325+
field: 'Id',
1326+
operator: 'IN',
1327+
valueQuery: {
1328+
fields: [
1329+
{
1330+
text: 'AccountId',
1331+
},
1332+
],
1333+
subqueries: [],
1334+
sObject: 'Opportunity',
1335+
where: {
1336+
left: {
1337+
field: 'StageName',
1338+
operator: '=',
1339+
value: "'Closed Lost'",
1340+
},
1341+
},
1342+
},
1343+
},
1344+
},
1345+
},
1346+
},
1347+
{
1348+
testCase: 48,
1349+
soql: `SELECT Id FROM Account WHERE Id NOT IN (SELECT AccountId FROM Opportunity WHERE IsClosed = false)`,
1350+
output: {
1351+
fields: [
1352+
{
1353+
text: 'Id',
1354+
},
1355+
],
1356+
subqueries: [],
1357+
sObject: 'Account',
1358+
where: {
1359+
left: {
1360+
field: 'Id',
1361+
operator: 'NOT IN',
1362+
valueQuery: {
1363+
fields: [
1364+
{
1365+
text: 'AccountId',
1366+
},
1367+
],
1368+
subqueries: [],
1369+
sObject: 'Opportunity',
1370+
where: {
1371+
left: {
1372+
field: 'IsClosed',
1373+
operator: '=',
1374+
value: 'false',
1375+
},
1376+
},
1377+
},
1378+
},
1379+
},
1380+
},
1381+
},
1382+
{
1383+
testCase: 49,
1384+
soql: `SELECT Id, Name FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE LastName LIKE 'apple%') AND Id IN (SELECT AccountId FROM Opportunity WHERE isClosed = false)`,
1385+
output: {
1386+
fields: [
1387+
{
1388+
text: 'Id',
1389+
},
1390+
{
1391+
text: 'Name',
1392+
},
1393+
],
1394+
subqueries: [],
1395+
sObject: 'Account',
1396+
where: {
1397+
left: {
1398+
field: 'Id',
1399+
operator: 'IN',
1400+
valueQuery: {
1401+
fields: [
1402+
{
1403+
text: 'AccountId',
1404+
},
1405+
],
1406+
subqueries: [],
1407+
sObject: 'Contact',
1408+
where: {
1409+
left: {
1410+
field: 'LastName',
1411+
operator: 'LIKE',
1412+
value: "'apple%'",
1413+
},
1414+
},
1415+
},
1416+
},
1417+
operator: 'AND',
1418+
right: {
1419+
left: {
1420+
field: 'Id',
1421+
operator: 'IN',
1422+
valueQuery: {
1423+
fields: [
1424+
{
1425+
text: 'AccountId',
1426+
},
1427+
],
1428+
subqueries: [],
1429+
sObject: 'Opportunity',
1430+
where: {
1431+
left: {
1432+
field: 'isClosed',
1433+
operator: '=',
1434+
value: 'false',
1435+
},
1436+
},
1437+
},
1438+
},
1439+
},
1440+
},
1441+
},
1442+
},
12781443
];
12791444
export default testCases;

0 commit comments

Comments
 (0)