From a09de6914e2ed0b2e3472f1a1e274a546f87ca51 Mon Sep 17 00:00:00 2001 From: Daniel Weeks Date: Sat, 21 Oct 2023 15:38:00 -0700 Subject: [PATCH] Update like statements to reflect sql behaviors (#91) * Update like statements to reflect sql behaciors * Codestyle * Codestyle * Handle NotStartsWith * Update pyiceberg/expressions/parser.py Co-authored-by: Fokko Driesprong * Update tests/expressions/test_parser.py Co-authored-by: Fokko Driesprong --------- Co-authored-by: Fokko Driesprong --- pyiceberg/expressions/parser.py | 21 ++++++++++++++++++--- tests/expressions/test_parser.py | 22 ++++++++++++++++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/pyiceberg/expressions/parser.py b/pyiceberg/expressions/parser.py index 45805331be..8873907813 100644 --- a/pyiceberg/expressions/parser.py +++ b/pyiceberg/expressions/parser.py @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +import re from decimal import Decimal from pyparsing import ( @@ -51,7 +52,6 @@ NotIn, NotNaN, NotNull, - NotStartsWith, Or, Reference, StartsWith, @@ -78,6 +78,8 @@ identifier = Word(alphas, alphanums + "_$").set_results_name("identifier") column = DelimitedList(identifier, delim=".", combine=False).set_results_name("column") +like_regex = r'(?P(?(? Reference: @@ -217,12 +219,25 @@ def _(result: ParseResults) -> BooleanExpression: @starts_with.set_parse_action def _(result: ParseResults) -> BooleanExpression: - return StartsWith(result.column, result.raw_quoted_string) + return _evaluate_like_statement(result) @not_starts_with.set_parse_action def _(result: ParseResults) -> BooleanExpression: - return NotStartsWith(result.column, result.raw_quoted_string) + return ~_evaluate_like_statement(result) + + +def _evaluate_like_statement(result: ParseResults) -> BooleanExpression: + literal_like: StringLiteral = result.raw_quoted_string + + match = re.search(like_regex, literal_like.value) + + if match and match.groupdict()['invalid_wildcard']: + raise ValueError("LIKE expressions only supports wildcard, '%', at the end of a string") + elif match and match.groupdict()['valid_wildcard']: + return StartsWith(result.column, StringLiteral(literal_like.value[:-1].replace('\\%', '%'))) + else: + return EqualTo(result.column, StringLiteral(literal_like.value.replace('\\%', '%'))) predicate = (comparison | in_check | null_check | nan_check | starts_check | boolean).set_results_name("predicate") diff --git a/tests/expressions/test_parser.py b/tests/expressions/test_parser.py index 65415f2e9a..8257710f66 100644 --- a/tests/expressions/test_parser.py +++ b/tests/expressions/test_parser.py @@ -168,12 +168,30 @@ def test_multiple_and_or() -> None: ) == parser.parse("foo is not null and foo < 5 or (foo > 10 and foo < 100 and bar is null)") +def test_like_equality() -> None: + assert EqualTo("foo", "data") == parser.parse("foo LIKE 'data'") + assert EqualTo("foo", "data%") == parser.parse("foo LIKE 'data\\%'") + + def test_starts_with() -> None: - assert StartsWith("foo", "data") == parser.parse("foo LIKE 'data'") + assert StartsWith("foo", "data") == parser.parse("foo LIKE 'data%'") + assert StartsWith("foo", "some % data") == parser.parse("foo LIKE 'some \\% data%'") + assert StartsWith("foo", "some data%") == parser.parse("foo LIKE 'some data\\%%'") + + +def test_invalid_likes() -> None: + invalid_statements = ["foo LIKE '%data%'", "foo LIKE 'da%ta'", "foo LIKE '%data'"] + + for statement in invalid_statements: + with pytest.raises(ValueError) as exc_info: + parser.parse(statement) + + assert "LIKE expressions only supports wildcard, '%', at the end of a string" in str(exc_info) def test_not_starts_with() -> None: - assert NotStartsWith("foo", "data") == parser.parse("foo NOT LIKE 'data'") + assert NotEqualTo("foo", "data") == parser.parse("foo NOT LIKE 'data'") + assert NotStartsWith("foo", "data") == parser.parse("foo NOT LIKE 'data%'") def test_with_function() -> None: