From b4376b1fc27612de0d8f08c527674238968c2838 Mon Sep 17 00:00:00 2001 From: chenjunwen <15046437592@139.com> Date: Tue, 23 Jun 2026 00:06:46 +0800 Subject: [PATCH] test: add regression tests for LIKE operator backtracking (#409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #409 reported that v3's OperatorLike.matchPattern used a greedy segment-matching algorithm without backtracking, causing patterns like "a%c" on input "abc" to incorrectly return false. v4 fixed this with a proper backtracking algorithm using sRecall/pRecall pointers in BaseBinaryOperator.matchPattern. However, no regression tests existed for these backtracking edge cases. This commit adds: - Issue409RegressionTest.java: 14 JUnit tests covering backtracking, multi-wildcard, trailing/leading %, empty strings, null handling, and conditional expressions - like.ql: 14 additional QL assertions for backtracking scenarios 🤖 Generated with [Qoder][https://qoder.com] --- .../test/issue/Issue409RegressionTest.java | 195 ++++++++++++++++++ .../testsuite/independent/operator/like.ql | 23 +++ 2 files changed, 218 insertions(+) create mode 100644 src/test/java/com/alibaba/qlexpress4/test/issue/Issue409RegressionTest.java diff --git a/src/test/java/com/alibaba/qlexpress4/test/issue/Issue409RegressionTest.java b/src/test/java/com/alibaba/qlexpress4/test/issue/Issue409RegressionTest.java new file mode 100644 index 000000000..5c6223d40 --- /dev/null +++ b/src/test/java/com/alibaba/qlexpress4/test/issue/Issue409RegressionTest.java @@ -0,0 +1,195 @@ +package com.alibaba.qlexpress4.test.issue; + +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.QLResult; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Regression tests for issue #409: OperatorLike.matchPattern backtracking bug. + * + *

In v3 (3.3.4), {@code OperatorLike.matchPattern} used a greedy segment-matching algorithm + * without backtracking. When a {@code %} wildcard consumed characters and the next literal + * segment failed to match at the greedy position, v3 would immediately return {@code false} + * instead of backtracking.

+ * + *

Example: {@code "abc" like "a%c"} — v3 returned {@code false} because after matching "a" + * and letting {@code %} consume "b", it tried to find "c" at index 2 (past the string end) + * instead of backtracking to let {@code %} consume "b" and match "c" at the correct position.

+ * + *

v4 fixed this by replacing the greedy algorithm with a backtracking matcher that uses + * sRecall/pRecall pointers in {@code BaseBinaryOperator.matchPattern}.

+ * + * @see Issue #409 + */ +public class Issue409RegressionTest { + + private Express4Runner runner; + private Map context; + private QLOptions options; + + @Before + public void setUp() { + runner = new Express4Runner(InitOptions.builder().build()); + context = new HashMap<>(); + options = QLOptions.builder().build(); + } + + // --------------------------------------------------------------- + // Core regression: the v3 bug case + // --------------------------------------------------------------- + + /** + * Core v3 bug case: "abc" like "a%c" must return true. + * v3 returned false because greedy matching consumed 'b' into '%' + * then failed to find 'c' without backtracking. + */ + @Test + public void testBacktrackingBasicCase() { + assertLikeTrue("\"abc\" like \"a%c\""); + } + + @Test + public void testBacktrackingViaVariable() { + context.put("s", "abc"); + context.put("p", "a%c"); + assertLikeTrue("s like p"); + } + + // --------------------------------------------------------------- + // Multi-segment backtracking + // --------------------------------------------------------------- + + @Test + public void testBacktrackingMultipleWildcards() { + assertLikeTrue("\"aXbYc\" like \"a%b%c\""); + assertLikeTrue("\"aab\" like \"a%a%b\""); + assertLikeTrue("\"aaab\" like \"a%a%b\""); + assertLikeTrue("\"hello_world_test\" like \"hello%world%test\""); + } + + @Test + public void testBacktrackingWithRepeatedPrefix() { + // Pattern "a%a" on "aa" — '%' matches empty, second 'a' matches second 'a' + assertLikeTrue("\"aa\" like \"a%a\""); + // Pattern "a%a" on "aXa" — '%' matches "X" + assertLikeTrue("\"aXa\" like \"a%a\""); + // Pattern "a%a" on "aXXa" — '%' matches "XX" + assertLikeTrue("\"aXXa\" like \"a%a\""); + } + + // --------------------------------------------------------------- + // Trailing/leading % edge cases (v3 issue #409 context) + // --------------------------------------------------------------- + + @Test + public void testTrailingPercent() { + // From v3 issue report: "1%1" like "1%" should be true + assertLikeTrue("\"1%1\" like \"1%\""); + assertLikeTrue("\"anything\" like \"%\""); + assertLikeTrue("\"test\" like \"t%\""); + assertLikeTrue("\"1006\" like \"1%\""); + } + + @Test + public void testLeadingPercent() { + assertLikeTrue("\"1006\" like \"%6\""); + assertLikeTrue("\"test\" like \"%t\""); + assertLikeTrue("\"hello\" like \"%lo\""); + } + + // --------------------------------------------------------------- + // Empty string and wildcard-only patterns + // --------------------------------------------------------------- + + @Test + public void testEmptyStringAndWildcards() { + assertLikeTrue("\"\" like \"%\""); + assertLikeTrue("\"\" like \"%%\""); + assertLikeTrue("\"\" like \"\""); + assertLikeFalse("\"a\" like \"\""); + assertLikeFalse("\"\" like \"a\""); + } + + // --------------------------------------------------------------- + // Negative cases (should return false) + // --------------------------------------------------------------- + + @Test + public void testNegativeCases() { + assertLikeFalse("\"abc\" like \"a%d\""); + assertLikeFalse("\"abc\" like \"%x%\""); + assertLikeFalse("\"abc\" like \"a%b%d\""); + assertLikeFalse("\"hello\" like \"h%x\""); + } + + // --------------------------------------------------------------- + // not_like operator (inverse) + // --------------------------------------------------------------- + + @Test + public void testNotLikeInverse() { + // not_like should be the exact inverse of like + assertLikeFalse("\"abc\" not_like \"a%c\""); + assertLikeTrue("\"abc\" not_like \"a%d\""); + assertLikeFalse("\"abc\" not_like \"a%b%c\""); + } + + // --------------------------------------------------------------- + // Null handling + // --------------------------------------------------------------- + + @Test + public void testNullHandling() { + assertLikeTrue("null like null"); + assertLikeFalse("\"a\" like null"); + assertLikeFalse("null like \"a\""); + } + + // --------------------------------------------------------------- + // Complex expressions mixing LIKE with other operators + // --------------------------------------------------------------- + + @Test + public void testLikeInConditionalExpression() { + context.put("name", "HelloWorld"); + QLResult result = runner.execute( + "if (name like \"Hello%\") { \"match\" } else { \"no_match\" }", + context, options); + Assert.assertEquals("match", result.getResult()); + } + + @Test + public void testLikeCombinedWithLogicalOperators() { + context.put("s", "abc123"); + QLResult result = runner.execute( + "s like \"abc%\" && s like \"%123\"", + context, options); + Assert.assertEquals(true, result.getResult()); + } + + // --------------------------------------------------------------- + // Helper methods + // --------------------------------------------------------------- + + private void assertLikeTrue(String expression) { + QLResult result = runner.execute(expression, context, options); + Assert.assertTrue( + "Expected TRUE for: " + expression + ", but got: " + result.getResult(), + (Boolean) result.getResult()); + } + + private void assertLikeFalse(String expression) { + QLResult result = runner.execute(expression, context, options); + Assert.assertFalse( + "Expected FALSE for: " + expression + ", but got: " + result.getResult(), + (Boolean) result.getResult()); + } +} diff --git a/src/test/resources/testsuite/independent/operator/like.ql b/src/test/resources/testsuite/independent/operator/like.ql index f7e9f4b90..676f3eba3 100644 --- a/src/test/resources/testsuite/independent/operator/like.ql +++ b/src/test/resources/testsuite/independent/operator/like.ql @@ -12,6 +12,29 @@ assertFalse("1006" not_like "%6"); assertFalse("1006" not_like "1%"); assertFalse("ABCD" not_like "A%B%D"); +// Issue #409 regression: backtracking scenarios +// v3 OperatorLike used greedy matching without backtracking, +// causing "abc" like "a%c" to return false (should be true). +// v4 uses a proper backtracking algorithm (sRecall/pRecall). +assert("abc" like "a%c"); +assert("abc" not_like "a%c" == false); +assertFalse("abc" not_like "a%c"); + +// More backtracking edge cases +assert("aXbYc" like "a%b%c"); +assert("hello_world_test" like "hello%world%test"); +assert("aab" like "a%a%b"); +assert("aaab" like "a%a%b"); + +// Trailing % edge cases from v3 issue report +assert("1%1" like "1%"); +assert("anything" like "%"); +assert("" like "%"); +assert("" like "%%"); + +assertFalse("abc" like "a%d"); +assertFalse("abc" like "%x%"); + // error code assertErrorCode(() -> {"ABCD" like 200}, "INVALID_BINARY_OPERAND"); assertErrorCode(() -> {200 like "200"}, "INVALID_BINARY_OPERAND");