Project

General

Profile

9305a_h2.patch

Stefanel Pezamosca, 10/02/2026 09:01 AM

Download (18.5 KB)

View differences:

new/src/main/org/h2/expression/condition/CompareLike.java 2026-09-17 07:55:20 +0000
5 5
 */
6 6
package org.h2.expression.condition;
7 7

  
8
import java.util.Objects;
8 9
import java.util.regex.Pattern;
9 10
import java.util.regex.PatternSyntaxException;
10 11
import org.h2.api.ErrorCode;
......
60 61
    /** indicates that we can shortcut the comparison and use contains */
61 62
    private boolean shortcutToContains;
62 63

  
64
    /** pattern the compiled state above was built from, valid only if patternCached */
65
    private String cachedPattern;
66
    /** escape character the compiled state above was built from */
67
    private Character cachedEscape;
68
    /** indicates that initPattern completed, so the two fields above describe its input */
69
    private boolean patternCached;
70

  
63 71
    public CompareLike(Database db, Expression left, Expression right,
64 72
            Expression escape, boolean regexp) {
65 73
        this(db.getCompareMode(), db.getSettings().defaultEscape, left, right,
......
128 136
                return TypedValueExpression.getUnknown();
129 137
            }
130 138
            String p = r.getString();
131
            initPattern(p, getEscapeChar(e));
139
            initPatternIfChanged(p, getEscapeChar(e));
132 140
            if (invalidPattern) {
133 141
                return TypedValueExpression.getUnknown();
134 142
            }
......
136 144
                // optimization for X LIKE '%': convert to X IS NOT NULL
137 145
                return new NullPredicate(left, true).optimize(session);
138 146
            }
139
            if (isFullMatch()) {
147
            // A right-trimming compare mode makes '=' ignore trailing blanks, which LIKE never
148
            // does, so the rewrite below stops preserving the answer: 'abc ' = 'abc' is then true
149
            // where 'abc ' LIKE 'abc' is false. Matching the pattern out is the only correct
150
            // route in that mode.
151
            if (isFullMatch() && !compareMode.isRtrim()) {
140 152
                // optimization for X LIKE 'Hello': convert to X = 'Hello'
141 153
                Value value = ValueString.get(patternString);
142 154
                Expression expr = ValueExpression.get(value);
......
197 209
                // should already be optimized
198 210
                DbException.throwInternalError();
199 211
            }
200
            initPattern(p, getEscapeChar(e));
212
            initPatternIfChanged(p, getEscapeChar(e));
201 213
        }
202 214
        if (invalidPattern) {
203 215
            return;
......
261 273
            if (e == ValueNull.INSTANCE) {
262 274
                return ValueNull.INSTANCE;
263 275
            }
264
            initPattern(p, getEscapeChar(e));
276
            initPatternIfChanged(p, getEscapeChar(e));
265 277
        }
266 278
        if (invalidPattern) {
267 279
            return ValueNull.INSTANCE;
......
360 372
     * @return true if the value matches
361 373
     */
362 374
    public boolean test(String testPattern, String value, char escapeChar) {
363
        initPattern(testPattern, escapeChar);
375
        initPatternIfChanged(testPattern, escapeChar);
364 376
        if (invalidPattern) {
365 377
            return false;
366 378
        }
367 379
        return compareAt(value, 0, 0, value.length(), patternChars, patternTypes);
368 380
    }
369 381

  
382
    /**
383
     * Compile the pattern unless the compiled state already describes it.  A pattern bound as a
384
     * parameter is never folded to a constant by optimize(), so without this guard the callers
385
     * below recompile the same pattern for every row they evaluate.
386
     *
387
     * @param p the pattern
388
     * @param escapeChar the escape character, or null for none
389
     */
390
    private void initPatternIfChanged(String p, Character escapeChar) {
391
        if (patternCached
392
                && Objects.equals(p, cachedPattern)
393
                && Objects.equals(escapeChar, cachedEscape)) {
394
            return;
395
        }
396
        // recorded only once initPattern returns, so a pattern which throws caches nothing
397
        initPattern(p, escapeChar);
398
        cachedPattern = p;
399
        cachedEscape = escapeChar;
400
        patternCached = true;
401
    }
402

  
370 403
    private void initPattern(String p, Character escapeChar) {
371 404
        if (compareMode.getName().equals(CompareMode.OFF) && !ignoreCase) {
372 405
            fastCompare = true;
......
385 418
            return;
386 419
        }
387 420
        patternLength = 0;
421
        // cleared per call:  a pattern which sets it below must not condemn the next one
422
        invalidPattern = false;
423
        // likewise cleared up front, so the early returns below cannot leave a shortcut armed
424
        // against the previous pattern's patternString
425
        shortcutToStartsWith = false;
426
        shortcutToEndsWith = false;
427
        shortcutToContains = false;
388 428
        if (p == null) {
389 429
            patternTypes = null;
390 430
            patternChars = null;
......
428 468
        }
429 469
        patternString = new String(patternChars, 0, patternLength);
430 470

  
431
        // Clear optimizations
432
        shortcutToStartsWith = false;
433
        shortcutToEndsWith = false;
434
        shortcutToContains = false;
435

  
436 471
        // optimizes the common case of LIKE 'foo%'
437 472
        if (compareMode.getName().equals(CompareMode.OFF) && patternLength > 1) {
438 473
            int maxMatch = 0;
new/src/main/org/h2/value/CompareMode.java 2026-09-15 14:22:15 +0000
365 365
        return uuidUnsigned;
366 366
    }
367 367

  
368
    public boolean isRtrim() {
369
        return shouldRtrim;
370
    }
371

  
368 372
    @Override
369 373
    public boolean equals(Object obj) {
370 374
        if (obj == this) {
new/src/test/org/h2/test/TestAll.java 2026-09-17 08:40:35 +0000
183 183
import org.h2.test.unit.TestCharsetCollator;
184 184
import org.h2.test.unit.TestClearReferences;
185 185
import org.h2.test.unit.TestCollation;
186
import org.h2.test.unit.TestCompareLike;
186 187
import org.h2.test.unit.TestCompress;
187 188
import org.h2.test.unit.TestConnectionInfo;
188 189
import org.h2.test.unit.TestDataPage;
......
768 769
        addTest(new TestBigResult());
769 770
        addTest(new TestCases());
770 771
        addTest(new TestCheckpoint());
772
        addTest(new TestCompareLike());
771 773
        addTest(new TestCompatibility());
772 774
        addTest(new TestCompatibilityOracle());
773 775
        addTest(new TestCompatibilitySQLServer());
new/src/test/org/h2/test/unit/TestCompareLike.java 2026-09-17 07:40:11 +0000
1
/*
2
 * Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
3
 * and the EPL 1.0 (https://h2database.com/html/license.html).
4
 * Initial Developer: H2 Group
5
 */
6
package org.h2.test.unit;
7

  
8
import java.sql.Connection;
9
import java.sql.DriverManager;
10
import java.sql.ResultSet;
11
import java.sql.SQLException;
12
import java.sql.Statement;
13
import java.util.ArrayList;
14
import java.util.Random;
15
import org.h2.expression.condition.CompareLike;
16
import org.h2.test.TestBase;
17
import org.h2.value.CompareMode;
18

  
19
/**
20
 * Tests that a reused CompareLike instance never answers from state left behind by an earlier
21
 * pattern. One instance serves every row of a query, and when the pattern is a column
22
 * (WHERE v LIKE p) it is recompiled per row, so anything initPattern() forgets to reset shows
23
 * up as a wrong answer on a later row rather than as an obvious failure.
24
 */
25
public class TestCompareLike extends TestBase {
26

  
27
    private static final int MATCH = 0, ONE = 1, ANY = 2;
28

  
29
    private static final char ESC = '\\';
30

  
31
    /**
32
     * Run just this test.
33
     *
34
     * @param a ignored
35
     */
36
    public static void main(String... a) throws Exception {
37
        TestBase.createCaller().init().test();
38
    }
39

  
40
    @Override
41
    public void test() throws Exception {
42
        testInvalidPatternIsNotSticky();
43
        testEscapeCharIsPartOfTheKey();
44
        testPatternLengthShrinks();
45
        testRandomPatternSequence();
46
        testColumnPattern();
47
        testConstantPatternShortcuts();
48
        testRtrimDoesNotLeakIntoLike();
49
    }
50

  
51
    private static CompareLike newLike() {
52
        return new CompareLike(CompareMode.getInstance(null, 0, false), "\\", null, null, null, false);
53
    }
54

  
55
    /**
56
     * A pattern ending in a bare escape character is invalid, and before the invalidPattern reset
57
     * it stayed invalid for the life of the instance - poisoning every later row.
58
     */
59
    private void testInvalidPatternIsNotSticky() {
60
        CompareLike comp = newLike();
61
        assertFalse(comp.test("ab\\", "ab", ESC));
62
        assertTrue(comp.test("abc", "abc", ESC));
63
        assertTrue(comp.test("a%", "abc", ESC));
64
        // and the other way round, so a valid pattern does not mask a following invalid one
65
        assertTrue(comp.test("abc", "abc", ESC));
66
        assertFalse(comp.test("ab\\", "ab", ESC));
67
        assertTrue(comp.test("ab_", "abc", ESC));
68
    }
69

  
70
    /**
71
     * Same pattern text, different escape character: the compiled state must be rebuilt, because
72
     * the escape decides whether the next character is a wildcard or a literal.
73
     */
74
    private void testEscapeCharIsPartOfTheKey() {
75
        CompareLike comp = newLike();
76
        // '#' escapes the '_', so the pattern is the literal two characters "a_"
77
        assertTrue(comp.test("a#_", "a_", '#'));
78
        assertFalse(comp.test("a#_", "ab", '#'));
79
        // with '\' as the escape the '#' is a literal and the '_' is the ONE wildcard
80
        assertFalse(comp.test("a#_", "a_", ESC));
81
        assertTrue(comp.test("a#_", "a#x", ESC));
82
    }
83

  
84
    /**
85
     * Feeds patterns of decreasing length through one instance. The compiled arrays are sized
86
     * from the raw pattern, and escapes make the used prefix shorter than the array, so a reader
87
     * that trusts the array length rather than patternLength sees the previous pattern's tail.
88
     */
89
    private void testPatternLengthShrinks() {
90
        CompareLike comp = newLike();
91
        assertTrue(comp.test("abcdef", "abcdef", ESC));
92
        assertTrue(comp.test("ab", "ab", ESC));
93
        assertFalse(comp.test("ab", "abcdef", ESC));
94
        assertTrue(comp.test("a%", "abcdef", ESC));
95
        // escapes collapse two pattern characters into one compiled position
96
        assertTrue(comp.test("a\\%b", "a%b", ESC));
97
        assertFalse(comp.test("a\\%b", "axb", ESC));
98
        assertTrue(comp.test("a\\%b", "a%b", ESC));
99
        // consecutive wildcards collapse too
100
        assertTrue(comp.test("a%%%b", "axyzb", ESC));
101
        assertTrue(comp.test("a", "a", ESC));
102
    }
103

  
104
    /**
105
     * Drives one instance through a long sequence of unrelated patterns - the shape of
106
     * WHERE v LIKE p - and cross-checks every answer against an independent matcher.
107
     */
108
    private void testRandomPatternSequence() {
109
        CompareLike comp = newLike();
110
        Random random = new Random(1);
111
        String alphabet = "ab%_\\";
112
        for (int i = 0; i < 20000; i++) {
113
            String pattern = randomString(random, alphabet, 5);
114
            String value = randomString(random, "ab%_\\", 6);
115
            Boolean expected = referenceMatch(pattern, value);
116
            boolean actual = comp.test(pattern, value, ESC);
117
            // an invalid pattern is reported as "no match" by CompareLike.test()
118
            boolean expectedFlag = expected != null && expected;
119
            if (expectedFlag != actual) {
120
                fail("pattern=<" + pattern + "> value=<" + value + "> expected=" + expectedFlag
121
                        + " actual=" + actual);
122
            }
123
        }
124
    }
125

  
126
    private static String randomString(Random random, String alphabet, int maxLength) {
127
        int len = random.nextInt(maxLength + 1);
128
        StringBuilder buff = new StringBuilder(len);
129
        for (int i = 0; i < len; i++) {
130
            buff.append(alphabet.charAt(random.nextInt(alphabet.length())));
131
        }
132
        return buff.toString();
133
    }
134

  
135
    /**
136
     * The column-pattern case end to end: one CompareLike, a different pattern per row, and an
137
     * invalid pattern in the middle of the scan.
138
     */
139
    private void testColumnPattern() throws SQLException {
140
        try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:testCompareLikeColumn")) {
141
            Statement stat = conn.createStatement();
142
            stat.execute("CREATE TABLE t(id INT PRIMARY KEY, v VARCHAR, p VARCHAR)");
143
            stat.execute("INSERT INTO t VALUES"
144
                    + "(1, 'abc', 'ab\\')," // invalid: trailing escape, yields NULL
145
                    + "(2, 'abc', 'abc')," // full match
146
                    + "(3, 'abc', 'a%')," // startsWith shortcut
147
                    + "(4, 'abc', '%bc')," // endsWith shortcut
148
                    + "(5, 'abcdef', '%cd%')," // contains shortcut
149
                    + "(6, 'abc', 'zzz')," // no match
150
                    + "(7, 'abc', 'a_c')," // single character wildcard
151
                    + "(8, 'abc', NULL)," // NULL pattern, yields NULL
152
                    + "(9, 'a%c', 'a\\%c')," // escaped wildcard, literal percent
153
                    + "(10, 'abc', 'a\\%c')"); // same pattern, must not match
154
            assertEquals("2,3,4,5,7,9", queryIds(stat, "SELECT id FROM t WHERE v LIKE p ORDER BY id"));
155
            // the same rows, evaluated in the opposite order, to rule out order-dependent state
156
            assertEquals("9,7,5,4,3,2",
157
                    queryIds(stat, "SELECT id FROM t WHERE v LIKE p ORDER BY id DESC"));
158
        }
159
    }
160

  
161
    /**
162
     * Constant patterns take the compile-once path and the startsWith / endsWith / contains
163
     * shortcuts in getValue(). Each shortcut is checked against the same pattern spelled so that
164
     * it cannot take a shortcut, so the two paths have to agree.
165
     */
166
    private void testConstantPatternShortcuts() throws SQLException {
167
        try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:testCompareLikeConst")) {
168
            Statement stat = conn.createStatement();
169
            stat.execute("CREATE TABLE t(id INT PRIMARY KEY, v VARCHAR)");
170
            stat.execute("INSERT INTO t VALUES(1, 'abcdef'), (2, 'abc'), (3, 'cdef'), (4, 'xyz'),"
171
                    + "(5, ''), (6, 'cd'), (7, 'ABCDEF')");
172
            // contains
173
            assertEquals("1,3,6", queryIds(stat, "SELECT id FROM t WHERE v LIKE '%cd%' ORDER BY id"));
174
            assertEquals("1,3,6", queryIds(stat, "SELECT id FROM t WHERE v LIKE '%c_%' ORDER BY id"));
175
            // startsWith
176
            assertEquals("1,2", queryIds(stat, "SELECT id FROM t WHERE v LIKE 'abc%' ORDER BY id"));
177
            // endsWith
178
            assertEquals("1,3", queryIds(stat, "SELECT id FROM t WHERE v LIKE '%def' ORDER BY id"));
179
            // full match, rewritten to an equality comparison
180
            assertEquals("2", queryIds(stat, "SELECT id FROM t WHERE v LIKE 'abc' ORDER BY id"));
181
            // a pattern that matches everything becomes IS NOT NULL
182
            assertEquals("1,2,3,4,5,6,7",
183
                    queryIds(stat, "SELECT id FROM t WHERE v LIKE '%' ORDER BY id"));
184
        }
185
    }
186

  
187
    /**
188
     * Under RTRIM the '=' comparison ignores trailing blanks but LIKE never does, so the
189
     * LIKE 'literal' to '= literal' rewrite must not fire.
190
     */
191
    private void testRtrimDoesNotLeakIntoLike() throws SQLException {
192
        try (Connection conn = DriverManager.getConnection(
193
                "jdbc:h2:mem:testCompareLikeRtrim;RTRIM=TRUE")) {
194
            Statement stat = conn.createStatement();
195
            stat.execute("CREATE TABLE t(id INT PRIMARY KEY, v VARCHAR)");
196
            stat.execute("INSERT INTO t VALUES(1, 'abc'), (2, 'abc '), (3, 'abc  ')");
197
            // '=' ignores the trailing blanks under RTRIM
198
            assertEquals("1,2,3", queryIds(stat, "SELECT id FROM t WHERE v = 'abc' ORDER BY id"));
199
            // LIKE does not, so only the exact value matches
200
            assertEquals("1", queryIds(stat, "SELECT id FROM t WHERE v LIKE 'abc' ORDER BY id"));
201
            assertEquals("2", queryIds(stat, "SELECT id FROM t WHERE v LIKE 'abc ' ORDER BY id"));
202
            assertEquals("1,2,3", queryIds(stat, "SELECT id FROM t WHERE v LIKE 'abc%' ORDER BY id"));
203
            // and the same holds when the pattern arrives as a column instead of a literal
204
            assertEquals("1", queryIds(stat, "SELECT id FROM (SELECT id, v, 'abc' AS p FROM t) "
205
                    + "WHERE v LIKE p ORDER BY id"));
206
        }
207
    }
208

  
209
    private String queryIds(Statement stat, String sql) throws SQLException {
210
        StringBuilder buff = new StringBuilder();
211
        try (ResultSet rs = stat.executeQuery(sql)) {
212
            while (rs.next()) {
213
                if (buff.length() > 0) {
214
                    buff.append(',');
215
                }
216
                buff.append(rs.getInt(1));
217
            }
218
        }
219
        return buff.toString();
220
    }
221

  
222
    /**
223
     * An independent, deliberately naive LIKE matcher for the default compare mode. Returns null
224
     * when the pattern is invalid.
225
     */
226
    private static Boolean referenceMatch(String p, String value) {
227
        ArrayList<int[]> tokens = new ArrayList<>();
228
        for (int i = 0; i < p.length(); i++) {
229
            char c = p.charAt(i);
230
            if (c == ESC) {
231
                if (i == p.length() - 1) {
232
                    return null;
233
                }
234
                tokens.add(new int[] { MATCH, p.charAt(++i) });
235
            } else if (c == '%') {
236
                tokens.add(new int[] { ANY, 0 });
237
            } else if (c == '_') {
238
                tokens.add(new int[] { ONE, 0 });
239
            } else {
240
                tokens.add(new int[] { MATCH, c });
241
            }
242
        }
243
        return referenceMatch(tokens, 0, value, 0);
244
    }
245

  
246
    private static boolean referenceMatch(ArrayList<int[]> tokens, int ti, String v, int vi) {
247
        if (ti == tokens.size()) {
248
            return vi == v.length();
249
        }
250
        int[] token = tokens.get(ti);
251
        switch (token[0]) {
252
        case MATCH:
253
            return vi < v.length() && v.charAt(vi) == token[1]
254
                    && referenceMatch(tokens, ti + 1, v, vi + 1);
255
        case ONE:
256
            return vi < v.length() && referenceMatch(tokens, ti + 1, v, vi + 1);
257
        default:
258
            for (int k = vi; k <= v.length(); k++) {
259
                if (referenceMatch(tokens, ti + 1, v, k)) {
260
                    return true;
261
                }
262
            }
263
            return false;
264
        }
265
    }
266

  
267
}