jetstreamapp / soql-parser-js

Javascript SOQL parser
https://jetstreamapp.github.io/soql-parser-js/
MIT License
79 stars 20 forks source link

Improve apex bind variable parsing #155

Closed paustint closed 3 years ago

paustint commented 3 years ago

Carried over from #153

Description

Apex bind variables only support the most basic syntax, which should be improved to support additional common use-cases.

paustint commented 3 years ago

@pgonzaleznetwork - I have an initial version of this developed on #156, but was hoping you could provide some additional example apex queries that I can add test-cases for - or at least provide your feedback on what I have implemented so far to see if changes should be made.

Right now, the general logic is like this for apex variables after a :

Known gaps:

Here are some test cases I have written. You can see #113 has invalid apex and that WHERE clause is stripped off, but all the other queries are parsed correctly.

{
    testCase: 112,
    options: { allowApexBindVariables: true, ignoreParseErrors: false },
    soql: `SELECT Id, (SELECT Id FROM Contacts WHERE Id IN :contactMap.keySet()) FROM Account WHERE Id IN :accountMap.keySet()`,
    output: {
      fields: [
        {
          type: 'Field',
          field: 'Id',
        },
        {
          type: 'FieldSubquery',
          subquery: {
            fields: [{ type: 'Field', field: 'Id' }],
            relationshipName: 'Contacts',
            where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'contactMap.keySet()' } },
          },
        },
      ],
      sObject: 'Account',
      where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'accountMap.keySet()' } },
    },
  },
  {
    testCase: 113,
    options: { allowApexBindVariables: true, ignoreParseErrors: true },
    soql: `SELECT Id, (SELECT Id FROM Contacts WHERE Id IN :contact_900Map.keySet()) FROM Account WHERE Id IN :acco INVALID untMap.keySet()`,
    soqlComposed: `SELECT Id, (SELECT Id FROM Contacts WHERE Id IN :contact_900Map.keySet()) FROM Account`,
    output: {
      fields: [
        {
          type: 'Field',
          field: 'Id',
        },
        {
          type: 'FieldSubquery',
          subquery: {
            fields: [{ type: 'Field', field: 'Id' }],
            relationshipName: 'Contacts',
            where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'contact_900Map.keySet()' } },
          },
        },
      ],
      sObject: 'Account',
    },
  },
  {
    testCase: 114,
    options: { allowApexBindVariables: true, ignoreParseErrors: false },
    soql: `SELECT Id FROM Account WHERE Id IN :new Map<Id, SObject>(someVar).keySet()`,
    output: {
      fields: [
        {
          type: 'Field',
          field: 'Id',
        },
      ],
      sObject: 'Account',
      where: { left: { field: 'Id', literalType: 'APEX_BIND_VARIABLE', operator: 'IN', value: 'new Map<Id, SObject>(someVar).keySet()' } },
    },
  },
  {
    testCase: 115,
    options: { allowApexBindVariables: true, ignoreParseErrors: false },
    soql: `SELECT Id FROM Account WHERE Id IN :new Map<Id, SObject>(someVar).getSomeClass().records`,
    output: {
      fields: [
        {
          type: 'Field',
          field: 'Id',
        },
      ],
      sObject: 'Account',
      where: {
        left: {
          field: 'Id',
          literalType: 'APEX_BIND_VARIABLE',
          operator: 'IN',
          value: 'new Map<Id, SObject>(someVar).getSomeClass().records',
        },
      },
    },
  },
paustint commented 3 years ago

Added parsing support for accessing variables by index

Ex: SELECT Id FROM Opportunity WHERE SBQQ__StandardTerm__c = :quotes[3].SBQQ__QuoteLine__r[0].Term__c

paustint commented 3 years ago

@pgonzaleznetwork - I am going to merge and publish #156 - if you encounter additional apex expressions that are not parsed correctly, please open a new ticket.

Thank you!

pgonzaleznetwork commented 3 years ago

@paustint sorry for the late reply on this. This looks great and I think it covers the use cases we care about. When it comes to HappySoup.io, the most important part is to be able to get the fields in the SELECT or WHERE clause, but we don't care too much about what the actual filter value is i.e

WHERE SomeField = :myExpression() Here, we care about SomeField and not really about the value of the expression. For our use case, what matters is that the parseQuery function doesn't fail all together, and what you've done so far satisfies that use case.

Thank you so much!