Skip to content

Commit fc68d8e

Browse files
authored
Merge pull request #84 from paustint/bug-83
DISTANCE with GEOLOCATION throw errors #83
2 parents 1747dcd + ff5e49c commit fc68d8e

File tree

10 files changed

+337
-59
lines changed

10 files changed

+337
-59
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
# Changelog
22

3+
## 2.2.0
4+
5+
Nov 6, 2019
6+
7+
1. `DISTANCE` and `GEOLOCATION` functions failed to parse when used in a `WHERE` clauses and `ORDER BY` clauses.
8+
39
## 2.1.0
410

11+
Oct 28, 2019
12+
513
1. The method signature for `getFlattenedFields` has changed to allow `Query | Subquery | FieldSubquery` to be passed in. this is not being considered a breaking change because it is fully backwards compatible.
614
2. A new helper method `isFieldSubquery(value: any)` was added to allow determining if a Field is a FieldSubquery. This is used internally for `getFlattenedFields()`.
715

816
## 2.0.0
917

18+
Oct 6, 2019
19+
1020
### Summary
1121

1222
Version 2.0 brings some significant bundle size and performance improvements. This library now uses [Chevrotain](https://github.com/SAP/chevrotain) instead of [antlr4](https://github.com/antlr/antlr4). With this change, everything related to parsing had to be re-written from scratch. Chevrotain uses pure javascript to handle lexing, parsing, and visiting the generated ast/cst as opposed to using a grammar file and generating a javascript parser based on the grammar.

docs/src/components/sample-queries-json.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,8 @@
8585
"SELECT Id, Amount FROM Opportunity WHERE Amount IN (usd500.01, usd600)",
8686
"SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 0 AND (Name LIKE '%testing%' OR Name LIKE '%123%')",
8787
"SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 0 AND (Name IN ('4/30 testing account', 'amendment quote doc testing', null))",
88-
"SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 0 AND (NOT Name IN ('4/30 testing account', 'amendment quote doc testing'))"
88+
"SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 0 AND (NOT Name IN ('4/30 testing account', 'amendment quote doc testing'))",
89+
"SELECT Name, Location__c FROM Warehouse__c WHERE DISTANCE(Location__c, GEOLOCATION(37.775, -122.418), 'mi') < 20",
90+
"SELECT Name, StreetAddress__c FROM Warehouse__c WHERE DISTANCE(Location__c, GEOLOCATION(37.775, -122.418), 'mi') < 20 ORDER BY DISTANCE(Location__c, GEOLOCATION(37.775, -122.418), 'mi') LIMIT 10",
91+
"SELECT Id, Name, Location, DISTANCE(Location, GEOLOCATION(10, 10), 'mi') FROM CONTACT"
8992
]

src/composer/composer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export class Compose {
248248
if (field.parameters) {
249249
params = field.parameters
250250
.map(param => (utils.isString(param) ? param : this.parseFields([param as FieldFunctionExpression])))
251-
.join(',');
251+
.join(', ');
252252
}
253253
return `${field.functionName}(${params})${field.alias ? ` ${field.alias}` : ''}`;
254254
}

src/models.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,12 @@ export interface OrderByExpressionContext extends WithIdentifier {
129129

130130
export interface OrderByFunctionExpressionContext extends WithIdentifier {
131131
fn: IToken[];
132-
alias?: IToken[];
132+
order?: IToken[];
133+
nulls?: IToken[];
134+
}
135+
136+
export interface OrderByLocationExpressionContext {
137+
locationFunction: CstNode[];
133138
order?: IToken[];
134139
nulls?: IToken[];
135140
}
@@ -161,6 +166,17 @@ export interface FieldFunctionContext {
161166
fn: IToken[];
162167
}
163168

169+
export interface LocationFunctionContext {
170+
location1: IToken[];
171+
location2: IToken[] | CstNode[];
172+
unit: IToken[];
173+
}
174+
175+
export interface GeoLocationFunctionContext {
176+
latitude: IToken[];
177+
longitude: IToken[];
178+
}
179+
164180
export interface ExpressionContext {
165181
logicalPrefix?: IToken[];
166182
lhs: IToken[] | CstNode[];
@@ -183,8 +199,8 @@ export interface FromClauseContext extends WithIdentifier {
183199
alias?: IToken[];
184200
}
185201

186-
export interface FunctionExpressionContext extends WithIdentifier {
187-
fn?: CstNode[];
202+
export interface FunctionExpressionContext {
203+
params?: (CstNode | IToken)[];
188204
}
189205

190206
export interface AtomicExpressionContext {

src/parser/lexer.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ export const Distance = createToken({
385385
longer_alt: Identifier,
386386
categories: [LocationFunction, Identifier],
387387
});
388+
388389
export const Geolocation = createToken({
389390
name: 'GEOLOCATION',
390391
pattern: /GEOLOCATION/i,
@@ -746,6 +747,13 @@ export const SignedInteger = createToken({
746747
pattern: /(\-|\+)[0-9]+/,
747748
categories: [NumberIdentifier, IntegerNumberIdentifier],
748749
});
750+
export const GeolocationUnit = createToken({
751+
name: 'GEOLOCATION_UNIT',
752+
pattern: /'(mi|km)'/,
753+
longer_alt: Identifier,
754+
categories: [Identifier],
755+
});
756+
749757
export const UnsignedInteger = createToken({
750758
name: 'UNSIGNED_INTEGER',
751759
pattern: /0|[1-9]\d*/,
@@ -949,6 +957,8 @@ export const allTokens = [
949957
LastNFiscalYears,
950958
NFiscalYearsAgo,
951959

960+
GeolocationUnit,
961+
952962
In,
953963
NotIn,
954964
For,

src/parser/parser.ts

Lines changed: 78 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export class SoqlParser extends CstParser {
201201
const parenCount = this.getParenCount();
202202
this.AT_LEAST_ONE({
203203
DEF: () => {
204-
this.SUBRULE(this.conditionExpression, { ARGS: [parenCount, false, true] });
204+
this.SUBRULE(this.conditionExpression, { ARGS: [parenCount, false, true, true] });
205205
},
206206
});
207207

@@ -220,7 +220,7 @@ export class SoqlParser extends CstParser {
220220

221221
private conditionExpression = this.RULE(
222222
'conditionExpression',
223-
(parenCount?: ParenCount, allowSubquery?: boolean, alowAggregateFn?: boolean) => {
223+
(parenCount?: ParenCount, allowSubquery?: boolean, alowAggregateFn?: boolean, allowLocationFn?: boolean) => {
224224
// argument is undefined during self-analysis, need to initialize to avoid exception
225225
parenCount = this.getParenCount(parenCount);
226226
this.OPTION(() => {
@@ -229,7 +229,7 @@ export class SoqlParser extends CstParser {
229229
{ ALT: () => this.CONSUME(lexer.Or, { LABEL: 'logicalOperator' }) },
230230
]);
231231
});
232-
this.SUBRULE(this.expression, { ARGS: [parenCount, allowSubquery, alowAggregateFn] });
232+
this.SUBRULE(this.expression, { ARGS: [parenCount, allowSubquery, alowAggregateFn, allowLocationFn] });
233233
},
234234
);
235235

@@ -314,6 +314,7 @@ export class SoqlParser extends CstParser {
314314
DEF: () => {
315315
this.OR([
316316
{ ALT: () => this.SUBRULE(this.orderByFunctionExpression, { LABEL: 'orderByExpressionOrFn' }) },
317+
{ ALT: () => this.SUBRULE(this.orderByLocationExpression, { LABEL: 'orderByExpressionOrFn' }) },
317318
{ ALT: () => this.SUBRULE(this.orderByExpression, { LABEL: 'orderByExpressionOrFn' }) },
318319
]);
319320
},
@@ -336,6 +337,17 @@ export class SoqlParser extends CstParser {
336337
this.SUBRULE(this.functionExpression);
337338
});
338339

340+
private orderByLocationExpression = this.RULE('orderByLocationExpression', () => {
341+
this.SUBRULE(this.locationFunction);
342+
this.OPTION(() => {
343+
this.OR([{ ALT: () => this.CONSUME(lexer.Asc, { LABEL: 'order' }) }, { ALT: () => this.CONSUME(lexer.Desc, { LABEL: 'order' }) }]);
344+
});
345+
this.OPTION1(() => {
346+
this.CONSUME(lexer.Nulls);
347+
this.OR1([{ ALT: () => this.CONSUME(lexer.First, { LABEL: 'nulls' }) }, { ALT: () => this.CONSUME(lexer.Last, { LABEL: 'nulls' }) }]);
348+
});
349+
});
350+
339351
private limitClause = this.RULE('limitClause', () => {
340352
this.CONSUME(lexer.Limit);
341353
this.CONSUME(lexer.UnsignedInteger, { LABEL: 'value' });
@@ -385,11 +397,6 @@ export class SoqlParser extends CstParser {
385397
this.SUBRULE(this.functionExpression, { ARGS: [true] });
386398
});
387399

388-
private locationFunction = this.RULE('locationFunction', () => {
389-
this.OR([{ ALT: () => this.CONSUME(lexer.Distance) }, { ALT: () => this.CONSUME(lexer.Geolocation) }]);
390-
this.SUBRULE(this.functionExpression);
391-
});
392-
393400
private otherFunction = this.RULE('otherFunction', () => {
394401
this.OR(
395402
this.$_otherFunction ||
@@ -420,53 +427,81 @@ export class SoqlParser extends CstParser {
420427
SEP: lexer.Comma,
421428
DEF: () => {
422429
this.OR([
423-
{ GATE: () => !skipAggregate, ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'fn' }) },
424-
{ ALT: () => this.SUBRULE(this.locationFunction, { LABEL: 'fn' }) },
425-
{ ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'fn' }) },
426-
{ ALT: () => this.CONSUME(lexer.Identifier) },
430+
{ GATE: () => !skipAggregate, ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'params' }) },
431+
{ ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'params' }) },
432+
{ ALT: () => this.CONSUME(lexer.StringIdentifier, { LABEL: 'params' }) },
433+
{ ALT: () => this.CONSUME(lexer.NumberIdentifier, { LABEL: 'params' }) },
434+
{ ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'params' }) },
427435
]);
428436
},
429437
});
430438
this.CONSUME(lexer.RParen);
431439
});
432440

433-
private expression = this.RULE('expression', (parenCount?: ParenCount, allowSubquery?: boolean, alowAggregateFn?: boolean) => {
434-
this.OPTION(() => {
435-
this.MANY(() => {
436-
this.CONSUME(lexer.LParen);
437-
if (parenCount) {
438-
parenCount.left++;
439-
}
440-
});
441-
});
442-
443-
this.OPTION1(() => {
444-
this.CONSUME(lexer.Not, { LABEL: 'logicalPrefix' });
445-
});
446-
447-
this.OR1([
448-
{ GATE: () => alowAggregateFn, ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'lhs' }) },
449-
{ ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'lhs' }) },
450-
{ ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'lhs' }) },
441+
private locationFunction = this.RULE('locationFunction', () => {
442+
this.CONSUME(lexer.Distance);
443+
this.CONSUME(lexer.LParen);
444+
this.CONSUME(lexer.Identifier, { LABEL: 'location1' });
445+
this.CONSUME(lexer.Comma);
446+
this.OR([
447+
{ ALT: () => this.SUBRULE(this.geolocationFunction, { LABEL: 'location2' }) },
448+
{ ALT: () => this.CONSUME1(lexer.Identifier, { LABEL: 'location2' }) },
451449
]);
450+
this.CONSUME1(lexer.Comma);
451+
this.CONSUME(lexer.GeolocationUnit, { LABEL: 'unit' });
452+
this.CONSUME(lexer.RParen);
453+
});
452454

453-
this.OR2([
454-
{ ALT: () => this.SUBRULE(this.expressionWithRelationalOperator, { LABEL: 'operator' }) },
455-
{ ALT: () => this.SUBRULE(this.expressionWithSetOperator, { LABEL: 'operator', ARGS: [allowSubquery] }) },
456-
]);
455+
private geolocationFunction = this.RULE('geolocationFunction', () => {
456+
this.CONSUME(lexer.Geolocation);
457+
this.CONSUME(lexer.LParen);
458+
this.CONSUME(lexer.NumberIdentifier, { LABEL: 'latitude' });
459+
this.CONSUME(lexer.Comma);
460+
this.CONSUME1(lexer.NumberIdentifier, { LABEL: 'longitude' });
461+
this.CONSUME(lexer.RParen);
462+
});
457463

458-
this.OPTION4(() => {
459-
this.MANY1({
460-
GATE: () => (parenCount ? parenCount.left > parenCount.right : true),
461-
DEF: () => {
462-
this.CONSUME(lexer.RParen);
464+
private expression = this.RULE(
465+
'expression',
466+
(parenCount?: ParenCount, allowSubquery?: boolean, alowAggregateFn?: boolean, allowLocationFn?: boolean) => {
467+
this.OPTION(() => {
468+
this.MANY(() => {
469+
this.CONSUME(lexer.LParen);
463470
if (parenCount) {
464-
parenCount.right++;
471+
parenCount.left++;
465472
}
466-
},
473+
});
467474
});
468-
});
469-
});
475+
476+
this.OPTION1(() => {
477+
this.CONSUME(lexer.Not, { LABEL: 'logicalPrefix' });
478+
});
479+
480+
this.OR1([
481+
{ GATE: () => alowAggregateFn, ALT: () => this.SUBRULE(this.aggregateFunction, { LABEL: 'lhs' }) },
482+
{ GATE: () => allowLocationFn, ALT: () => this.SUBRULE(this.locationFunction, { LABEL: 'lhs' }) },
483+
{ ALT: () => this.SUBRULE(this.otherFunction, { LABEL: 'lhs' }) },
484+
{ ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'lhs' }) },
485+
]);
486+
487+
this.OR2([
488+
{ ALT: () => this.SUBRULE(this.expressionWithRelationalOperator, { LABEL: 'operator' }) },
489+
{ ALT: () => this.SUBRULE(this.expressionWithSetOperator, { LABEL: 'operator', ARGS: [allowSubquery] }) },
490+
]);
491+
492+
this.OPTION4(() => {
493+
this.MANY1({
494+
GATE: () => (parenCount ? parenCount.left > parenCount.right : true),
495+
DEF: () => {
496+
this.CONSUME(lexer.RParen);
497+
if (parenCount) {
498+
parenCount.right++;
499+
}
500+
},
501+
});
502+
});
503+
},
504+
);
470505

471506
private expressionWithRelationalOperator = this.RULE('expressionWithRelationalOperator', () => {
472507
this.SUBRULE(this.relationalOperator);

0 commit comments

Comments
 (0)