Skip to content

Escape regex metacharacters in Postgres LIKE filter values - #331

Open
sanjulksharma wants to merge 4 commits into
mainfrom
ADP-1966/escape-regex-metacharacters-in-postgres-like
Open

sanjulksharma wants to merge 4 commits into
mainfrom
ADP-1966/escape-regex-metacharacters-in-postgres-like

Conversation

@sanjulksharma

Copy link
Copy Markdown
Collaborator

Fixes ASP-3514: a LIKE filter value containing regex metacharacters (e.g. {{) works on MongoDB but throws invalid regular expression: quantifier operand invalid on Postgres, since Postgres's ~* (POSIX ARE) hard-errors on malformed patterns that MongoDB's PCRE2 tolerates. Adds a validity gate that escapes such values to match literally instead of erroring, applied to both the legacy and v1 LIKE parsers, and drops the now-redundant .* wrapping since ~* is already unanchored.

(Supersedes #330, which was opened from a fork before I had collaborator access and couldn't run CI.)

🤖 Generated with Claude Code

Postgres's `~*` operator (used to implement the LIKE/"contains" filter)
is a case-insensitive, unanchored regex match using POSIX ARE, whereas
MongoDB's $regex uses PCRE2. PCRE2 tolerates malformed patterns (e.g. an
unmatched quantifier brace like "{{") by treating them literally, while
Postgres's ARE engine hard-errors with "invalid regular expression:
quantifier operand invalid" on the same input - breaking LIKE/"contains"
filters on Postgres for values containing regex metacharacters (e.g. API
Inventory domain search values like "{{" or "${${8gs").

Add a validity gate (PostgresUtils#sanitizeRegexValue) that passes a LIKE
value through unchanged if it is already a valid regex (preserving
existing regex-filter semantics, e.g. ".*amp.*", "^Sun"), and otherwise
escapes POSIX ARE metacharacters so the value is matched literally
instead of erroring out. Apply this in both LIKE code paths: the legacy
Filter/Op parser and the v1 expression parser
(PostgresLikeRelationalFilterParser), the latter being the actual path
hit by the reported production stacktrace.

Also drop the now-redundant ".*<value>.*" wrapping previously applied to
LIKE values before the ~* search, since `~*` is already an unanchored
substring regex match.

Note: for a value that is not a valid regex in either engine but IS
syntactically valid Perl/PCRE2 (e.g. "${${8gs", which PCRE2 compiles but
matches 0 documents because "$" is treated as an end-of-line anchor),
this introduces a narrow Mongo/Postgres result divergence (Mongo: 0
matches, Postgres: 1 literal match) - a deliberate, documented trade-off
versus leaving Postgres's LIKE operator broken for such inputs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

AI-Session-Id: f9a3360e-ecb3-4388-beeb-682ed3b1d400
AI-Tool: claude-code
AI-Model: unknown
The LIKE-regex test added a "notes" field to the shared collection_data.json
document (id=8), which broke unrelated tests asserting exact document
equality against that fixture (testQueryV1ForSimpleWhereClause,
testQueryV1ForFilterWithLogicalExpressionAndOr). Upsert a standalone document
scoped to the test instead of mutating shared fixture data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

AI-Session-Id: f9a3360e-ecb3-4388-beeb-682ed3b1d400
AI-Tool: claude-code
AI-Model: unknown
@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.36%. Comparing base (cd897e9) to head (e20a5e6).

Files with missing lines Patch % Lines
...re/documentstore/postgres/utils/PostgresUtils.java 93.33% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #331      +/-   ##
============================================
+ Coverage     81.23%   81.36%   +0.13%     
- Complexity     1698     1703       +5     
============================================
  Files           243      243              
  Lines          7780     7793      +13     
  Branches        771      773       +2     
============================================
+ Hits           6320     6341      +21     
+ Misses          962      955       -7     
+ Partials        498      497       -1     
Flag Coverage Δ
integration 81.36% <93.75%> (+0.13%) ⬆️
unit 58.97% <93.75%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

AI-Session-Id: f9a3360e-ecb3-4388-beeb-682ed3b1d400
AI-Tool: claude-code
AI-Model: unknown
Upserting the notes-only doc into the shared collection changed its total
document count, breaking unrelated tests that assert exact counts
(testFlatVsNestedCollectionConsistency, testNotContains, etc.). Delete it in
a finally block so the shared fixture's state is unchanged afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

AI-Session-Id: f9a3360e-ecb3-4388-beeb-682ed3b1d400
AI-Tool: claude-code
AI-Model: unknown
@github-actions

Copy link
Copy Markdown

Test Results

  129 files  +1    129 suites  +1   41s ⏱️ -2s
  899 tests +5    898 ✅ +5  1 💤 ±0  0 ❌ ±0 
1 271 runs  +7  1 270 ✅ +7  1 💤 ±0  0 ❌ ±0 

Results for commit e20a5e6. ± Comparison against base commit cd897e9.

@suddendust suddendust changed the title ADP-1966 | Escape regex metacharacters in Postgres LIKE filter values Escape regex metacharacters in Postgres LIKE filter values Oct 21, 2026

@suresh-prakash suresh-prakash left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed 3 findings. Posted 3 inline comments.

Severity Count
High 1
Medium 2
Low 0

Reviewed using @claude traceable:traceable-java-code-review


💡 Want this skill on your own PRs? Install the Traceable plugin from the Harness internal marketplace:

/plugin marketplace add https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/claude-code-marketplace.git
/plugin install traceable@harness-internal-claude-plugins

Then run /skill traceable:traceable-java-code-review <pr-url>.

public static String sanitizeRegexValue(final Object value) {
final String stringValue = String.valueOf(value);
try {
Pattern.compile(stringValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] Java regex validation does not validate PostgreSQL ARE or MongoDB PCRE2

`Pattern.compile` validates Java regex syntax, but the value is executed by PostgreSQL's ARE engine while MongoDB uses PCRE2. For example, Java accepts `C++` (possessive quantifier) and `a{256}`, so both pass this gate unchanged, but PostgreSQL rejects `++` and only permits repetition bounds through 255; the query can still fail with `invalid regular expression`. In the opposite direction, PostgreSQL documents `{` followed by a non-digit as a literal brace, so `foo.{bar` is a valid PostgreSQL pattern matching `fooX{bar`; Java rejects it and this code converts the whole pattern to the literal `foo\.\{bar`, changing the match result. Because MongoDB generally accepts the original PCRE pattern, this also creates cross-store incompatibility; please define and validate a common supported regex subset (or consistently treat LIKE as literal search in both backends) instead of using the Java engine as the compatibility gate.

Tip

The same input passes through three different grammars: Java Pattern, PostgreSQL ARE, and MongoDB PCRE2. Validity in one is not evidence of validity or equivalent semantics in either of the others. Compatibility tests should execute representative patterns against both datastore implementations and assert the same results, including C++, a{256}, foo.{bar, ^Sun, and b.*B.

References:

Reviewed using @claude traceable:traceable-java-code-review

Pattern.compile(stringValue);
return stringValue;
} catch (final PatternSyntaxException e) {
return escapeRegexMetacharacters(stringValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] Use PostgreSQL's documented literal-pattern mode for a literal fallback

If the intended fallback is literal matching, PostgreSQL already documents `***=` (or a leading `(?q)`) to quote the rest of an ARE. For example, binding `***=${${8gs` makes `$`, `{`, and the remaining characters ordinary, while `***=C++` safely matches literal `C++`; this avoids maintaining a context-sensitive metacharacter list and correctly handles backslashes and bracket expressions. This only makes the PostgreSQL fallback safer—it does not by itself provide MongoDB parity, because Mongo may successfully interpret the original value as a PCRE regex—so the fallback policy still needs to be shared by both backends or unsupported patterns should be rejected.

Tip

PostgreSQL's ARE metasyntax defines the ***= director as “the rest of the RE is taken to be a literal string, with all characters considered ordinary characters.” Since the pattern is bound through a prepared-statement parameter, the prefix and backslashes are regex content rather than SQL-literal syntax.

References:

Reviewed using @claude traceable:traceable-java-code-review

// which does hit. This divergence is expected, not a bug.
long dollarBraceCount = collection.count(dollarBraceQuery);
if (MONGO_STORE.equals(dataStoreName)) {
assertEquals(0, dollarBraceCount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] Do not encode different MongoDB and PostgreSQL results as expected behavior

This test explicitly accepts MongoDB returning 0 while PostgreSQL returns 1 for the same document and LIKE value. With document text `weird {{bracket}} and ${${8gs} value`, MongoDB/PCRE2 interprets the `$` characters as end-of-string assertions and does not match, whereas the PostgreSQL fallback turns the expression into literal `\$\{\$\{8gs` and finds that substring. If datastore compatibility is required, this should assert the same result for both providers; either quote the value for both backends when the contract is literal substring search, or define a common regex subset and reject/translate expressions that cannot have equivalent semantics.

Tip

A regression test that intentionally branches by datastore makes the incompatibility permanent. Add parity cases for malformed and engine-specific expressions alongside existing valid expressions such as ^Sun, Sha.*, and b.*B, then assert one shared expected result.

References:

Reviewed using @claude traceable:traceable-java-code-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants