Repository: paustint/soql-parser-js (ts)

Issue:
PROD ISSUE - Parser is incorrectly categorizing value as geolocation unit
- [ ] Feature
- [x] Bug

<!--If this is a bug, please include the steps to reproduce the bug-->
## Description
Parsing a soql query that contains `'MI' or 'KM'` as a value is incorrectly being categorized as a geolocation unit instead of a string literal.

Steps - 
- Parse this query `SELECT State_Abbr_c FROM Contact WHERE State_Abbr_c = 'MI'`
- The `MI` token is being categorized as a geolocation unit instead of a string literal which is throwing an error.
![Screenshot from 2022-06-09 15-59-08](https://user-images.githubusercontent.com/59818977/173181826-60d254ab-204c-4d6e-9ac4-6bb3c360a5bb.png)



Pull request:
Fix parser bug with GeoLocationUnit
mi and km could not be used in WHERE clauses
because it was being identified as a GeolocationUnit instead of a string

resolves #188

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d4561e..db70d07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## 4.5
+
+June 11, 2022
+
+- Fixed parser error where `'mi'` and `'km'` could not be used in a WHERE clause because they were being parsed as a GeoLocationUnit (#188)
+  - Thank you @divijklenty for reporting this.
+
 ## 4.4
 
 March 11, 2022
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f035715..3909055 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,7 @@ Contributions of any kind are welcome and appreciated.
 
 # What features are allowed
 
-Any feature that is part of the core project and does not deviate from what the product is will be considered to be accepted.
+Any feature that is part of the core project and does not deviate from what the product is will be considered t obe accepted.
 
 # Making Changes
 
diff --git a/package-lock.json b/package-lock.json
index e98db54..c7346ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "soql-parser-js",
       "version": "4.4.0",
       "license": "MIT",
       "dependencies": {
diff --git a/src/parser/parser.ts b/src/parser/parser.ts
index e18d80e..53dd8b4 100644
--- a/src/parser/parser.ts
+++ b/src/parser/parser.ts
@@ -626,6 +626,9 @@ export class SoqlParser extends CstParser {
           { GATE: () => !isArray, ALT: () => this.SUBRULE(this.booleanValue) },
           { GATE: () => !isArray, ALT: () => this.CONSUME(lexer.DateLiteral) },
           { GATE: () => !isArray, ALT: () => this.SUBRULE(this.dateNLiteral) },
+          // GeolocationUnit is matched even when it is an actual string in a WHERE clause - treat as StringIdentifier
+          // https://github.com/paustint/soql-parser-js/issues/188
+          { GATE: () => !isArray, ALT: () => this.CONSUME(lexer.GeolocationUnit, { LABEL: 'StringIdentifier' }) },
           { GATE: () => !isArray, ALT: () => this.CONSUME(lexer.StringIdentifier) },
         ]),
     );
@@ -725,6 +728,7 @@ export class SoqlParser extends CstParser {
               { ALT: () => this.CONSUME(lexer.False, { LABEL: 'value' }) },
               { ALT: () => this.CONSUME(lexer.DateLiteral, { LABEL: 'value' }) },
               { ALT: () => this.SUBRULE(this.dateNLiteral, { LABEL: 'value' }) },
+              { ALT: () => this.CONSUME(lexer.GeolocationUnit, { LABEL: 'value' }) },
               { ALT: () => this.CONSUME(lexer.StringIdentifier, { LABEL: 'value' }) },
             ]),
         );


Test patch:
diff --git a/test/test-cases.ts b/test/test-cases.ts
index f7e8015..74e0af8 100644
--- a/test/test-cases.ts
+++ b/test/test-cases.ts
@@ -2390,6 +2390,113 @@ export const testCases: TestCase[] = [
       },
     },
   },
+  {
+    testCase: 118,
+    options: { allowApexBindVariables: true },
+    soql: `SELECT State_Abbr_c FROM Contact WHERE State_Abbr_c = 'MI' OR State_Abbr_c = 'km'`,
+    output: {
+      fields: [
+        {
+          type: 'Field',
+          field: 'State_Abbr_c',
+        },
+      ],
+      sObject: 'Contact',
+      where: {
+        left: {
+          field: 'State_Abbr_c',
+          literalType: 'STRING',
+          operator: '=',
+          value: "'MI'",
+        },
+        operator: 'OR',
+        right: {
+          left: {
+            field: 'State_Abbr_c',
+            literalType: 'STRING',
+            operator: '=',
+            value: "'km'",
+          },
+        },
+      },
+    },
+  },
+  {
+    testCase: 119,
+    options: { allowApexBindVariables: true },
+    soql: `SELECT State_Abbr_c FROM Contact WHERE State_Abbr_c = 'KM'`,
+    output: {
+      fields: [
+        {
+          type: 'Field',
+          field: 'State_Abbr_c',
+        },
+      ],
+      sObject: 'Contact',
+      where: {
+        left: {
+          field: 'State_Abbr_c',
+          literalType: 'STRING',
+          operator: '=',
+          value: "'KM'",
+        },
+      },
+    },
+  },
+  {
+    testCase: 120,
+    options: { allowApexBindVariables: true },
+    soql: `SELECT State_Abbr_c FROM Contact WHERE State_Abbr_c IN ('mi', 'KM')`,
+    output: {
+      fields: [
+        {
+          type: 'Field',
+          field: 'State_Abbr_c',
+        },
+      ],
+      sObject: 'Contact',
+      where: {
+        left: {
+          field: 'State_Abbr_c',
+          literalType: 'STRING',
+          operator: 'IN',
+          value: ["'mi'", "'KM'"],
+        },
+      },
+    },
+  },
+  {
+    testCase: 121,
+    soql: "SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource HAVING COUNT(Name) > 100 AND LeadSource > 'km'",
+    output: {
+      fields: [
+        { type: 'Field', field: 'LeadSource' },
+        {
+          type: 'FieldFunctionExpression',
+          functionName: 'COUNT',
+          rawValue: 'COUNT(Name)',
+          isAggregateFn: true,
+          parameters: ['Name'],
+        },
+      ],
+      sObject: 'Lead',
+      groupBy: [
+        {
+          field: 'LeadSource',
+        },
+      ],
+      having: {
+        left: {
+          operator: '>',
+          value: '100',
+          literalType: 'INTEGER',
+          fn: { rawValue: 'COUNT(Name)', functionName: 'COUNT', parameters: ['Name'] },
+        },
+        operator: 'AND',
+        right: { left: { field: 'LeadSource', operator: '>', value: "'km'", literalType: 'STRING' } },
+      },
+    },
+  },
 ];
 
 export default testCases;
diff --git a/test/test.spec.ts b/test/test.spec.ts
index dd41818..1eec9bc 100644
--- a/test/test.spec.ts
+++ b/test/test.spec.ts
@@ -12,7 +12,7 @@ const replacements = [{ matching: / last /i, replace: ' LAST ' }];
 // Uncomment these to easily test one specific query - useful for troubleshooting/bugfixing
 
 // describe.only('parse queries', () => {
-//   const testCase = testCases.find(tc => tc.testCase === 117);
+//   const testCase = testCases.find(tc => tc.testCase === 118);
 //   it(`should correctly parse test case ${testCase.testCase} - ${testCase.soql}`, () => {
 //     const soqlQuery = parseQuery(testCase.soql, testCase.options);
 //     console.log(soqlQuery);