Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ where `parse_result` is a [ParseResults](https://pyparsing-docs.readthedocs.io/e

- Subqueries (e.g., `SELECT Name, (SELECT LastName FROM Contacts) FROM Account`)
- Aggregate queries
- SOQL specific WHERE-clause tokens (e.g., `LAST_N_DAYS:<integer>`)
- Some SOQL specific WHERE-clause tokens like Polymorphic Relationship Fields. [Date literals](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_dateformats.htm) _are_ supported (e.g., `LAST_N_DAYS:<integer>`).

## Partially supported

Expand Down
136 changes: 76 additions & 60 deletions python_soql_parser/core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# stolen from https://github.com/pyparsing/pyparsing/blob/master/examples/simpleSQL.py

from typing import Any, TypedDict
from functools import cache

from pyparsing import (
CaselessKeyword,
Literal,
Forward,
Group,
Optional,
Expand All @@ -21,65 +23,79 @@
)

from python_soql_parser.binops import EQ, GT, GTE, LT, LTE, NEQ

ParserElement.enablePackrat()

select_statement = Forward()
SELECT, FROM, WHERE, AND, OR, IN, NULL, TRUE, FALSE, LIMIT, OFFSET, ORDER, BY, DESC, ASC = map(
CaselessKeyword,
"select from where and or in null true false limit offset order by desc asc".split(),
)

identifier = Word(alphas, alphanums + "_" + ".").setName("identifier")
field_name = delimitedList(identifier).setName("field name")
field_name_list = Group(delimitedList(field_name))
sobject_name = identifier.setName("sobject name")


binop = oneOf(f"{EQ} {NEQ} {LT} {LTE} {GT} {GTE}")
real_num = pyparsing_common.real()
int_num = pyparsing_common.signed_integer()
date_time = pyparsing_common.iso8601_datetime()

field_right_value = date_time | real_num | int_num | quotedString | field_name
where_condition = Group(
(field_name + binop + field_right_value)
| (field_name + IN + Group("(" + delimitedList(field_right_value) + ")"))
)

where_expression = infixNotation(
where_condition,
[
(AND, 2, opAssoc.LEFT),
(OR, 2, opAssoc.LEFT),
],
)

where_clause = Optional(Suppress(WHERE) + where_expression, None)

limit_clause = Optional(Suppress(LIMIT) + int_num, None)

offset_clause = Optional(Suppress(OFFSET) + int_num, None)

ordering_term = Group(field_name + Optional(ASC | DESC)("direction"))

order_clause = Optional(
Suppress(ORDER) + Suppress(BY) + Group(delimitedList(ordering_term))
)

# define the grammar
select_statement <<= (
SELECT
+ field_name_list("fields")
+ FROM
+ sobject_name("sobject")
+ where_clause("where")
+ order_clause("order_by")
+ limit_clause("limit")
+ offset_clause("offset")
)

soql = select_statement
from python_soql_parser.date_literals import CONSTANT_DATE_KEYWORDS, PARAMETERIZED_DATE_KEYWORDS


@cache
def create_soql_parser() -> ParserElement:
ParserElement.enablePackrat()

select_statement = Forward()
SELECT, FROM, WHERE, AND, OR, IN, NULL, TRUE, FALSE, LIMIT, OFFSET, ORDER, BY, DESC, ASC = map(
CaselessKeyword,
"select from where and or in null true false limit offset order by desc asc".split(),
)

identifier = Word(alphas, alphanums + "_" + ".").setName("identifier")
field_name = delimitedList(identifier).setName("field name")
field_name_list = Group(delimitedList(field_name))
sobject_name = identifier.setName("sobject name")

binop = oneOf(f"{EQ} {NEQ} {LT} {LTE} {GT} {GTE}")
real_num = pyparsing_common.real()
int_num = pyparsing_common.signed_integer()
date_time = pyparsing_common.iso8601_datetime()
# See https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_dateformats.htm
constant_date_literal = oneOf(CONSTANT_DATE_KEYWORDS)("constant date literal")
parameterized_date_literal = Group(
oneOf(PARAMETERIZED_DATE_KEYWORDS) + Suppress(Literal(":")) + int_num
)("parameterized date literal")

field_right_value = (
date_time | real_num | int_num | quotedString
| constant_date_literal | parameterized_date_literal
| field_name
)
where_condition = Group(
(field_name + binop + field_right_value)
| (field_name + IN + Group("(" + delimitedList(field_right_value) + ")"))
)

where_expression = infixNotation(
where_condition,
[
(AND, 2, opAssoc.LEFT),
(OR, 2, opAssoc.LEFT),
],
)

where_clause = Optional(Suppress(WHERE) + where_expression, None)

limit_clause = Optional(Suppress(LIMIT) + int_num, None)

offset_clause = Optional(Suppress(OFFSET) + int_num, None)

ordering_term = Group(field_name + Optional(ASC | DESC)("direction"))

order_clause = Optional(
Suppress(ORDER) + Suppress(BY) + Group(delimitedList(ordering_term))
)

# define the grammar
select_statement <<= (
SELECT
+ field_name_list("fields")
+ FROM
+ sobject_name("sobject")
+ where_clause("where")
+ order_clause("order_by")
+ limit_clause("limit")
+ offset_clause("offset")
)

soql: ParserElement = select_statement

return soql


class SoqlQuery(TypedDict):
Expand All @@ -92,4 +108,4 @@ class SoqlQuery(TypedDict):


def parse(soql_query: str) -> SoqlQuery:
return soql.parseString(soql_query)
return create_soql_parser().parseString(soql_query)
49 changes: 49 additions & 0 deletions python_soql_parser/date_literals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
CONSTANT_DATE_KEYWORDS = [
"YESTERDAY",
"TODAY",
"TOMORROW",
"LAST_WEEK",
"THIS_WEEK",
"NEXT_WEEK",
"LAST_MONTH",
"THIS_MONTH",
"NEXT_MONTH",
"LAST_90_DAYS",
"NEXT_90_DAYS",
"THIS_QUARTER",
"LAST_QUARTER",
"NEXT_QUARTER",
"THIS_YEAR",
"LAST_YEAR",
"NEXT_YEAR",
"THIS_FISCAL_QUARTER",
"LAST_FISCAL_QUARTER",
"NEXT_FISCAL_QUARTER",
"THIS_FISCAL_YEAR",
"LAST_FISCAL_YEAR",
"NEXT_FISCAL_YEAR",
]

PARAMETERIZED_DATE_KEYWORDS = [
"LAST_N_DAYS",
"NEXT_N_DAYS",
"N_DAYS_AGO",
"NEXT_N_WEEKS",
"LAST_N_WEEKS",
"N_WEEKS_AGO",
"NEXT_N_MONTHS",
"LAST_N_MONTHS",
"N_MONTHS_AGO",
"NEXT_N_QUARTERS",
"LAST_N_QUARTERS",
"N_QUARTERS_AGO",
"NEXT_N_YEARS",
"LAST_N_YEARS",
"N_YEARS_AGO",
"NEXT_N_FISCAL_​QUARTERS",
"LAST_N_FISCAL_​QUARTERS",
"N_FISCAL_QUARTERS_AGO",
"NEXT_N_FISCAL_​YEARS",
"LAST_N_FISCAL_​YEARS",
"N_FISCAL_YEARS_AGO",
]
37 changes: 0 additions & 37 deletions python_soql_parser/tokens.py

This file was deleted.