Skip to content

🐛 Fixed filters losing values that contain ^ or $ - #213

Merged
rob-ghost merged 1 commit into
mainfrom
fix/regex-anchor-detection
Aug 18, 2026
Merged

🐛 Fixed filters losing values that contain ^ or $#213
rob-ghost merged 1 commit into
mainfrom
fix/regex-anchor-detection

Conversation

@rob-ghost

Copy link
Copy Markdown
Contributor

Problem

A filter value can contain the very characters a regex uses for its anchors. Searching for a price that ends in a dollar, or a title that starts with a caret, are ordinary things to ask for.

Those characters arrive escaped, to mark them as part of the value rather than as anchors. But the conversion to a LIKE pattern removed that escaping before it looked for the anchors, so by the time it checked, a literal character and an anchor were the same character.

The result changed both the value and the question. Asking for names containing 5$ was answered as if it asked for names ending in 5: the dollar was dropped and the operator silently became a different one. Asking for names containing ^caret was answered as if it asked for names starting with caret.

Nothing surfaced this. Both filters still returned rows, just the wrong ones, so a saved Ghost segment could quietly change who it was about without anyone seeing an error.

A pattern anchored at both ends was mishandled too. Only one anchor was ever considered, so anchoring both ends left the trailing character sitting in the pattern as a literal and matched almost nothing.

Solution

Read the anchors before removing the escaping, rather than after. At that point an escaped character is still visibly escaped, so it can be kept as part of the value while a genuine anchor is recognised as one.

A trailing anchor character counts as an anchor only when an even number of backslashes precedes it, so a value that itself ends in a backslash is still told apart from an escaped dollar.

Both anchors are now handled independently, which makes anchoring both ends an exact match with no wildcard on either side.

Six tests cover the cases. Three of them fail without the change: the two values holding a literal anchor character, and the both-ends match. The rest guard the boundaries the parity counting exists for.

Verified against Ghost with the package linked locally: its members filter suite and full integration suite pass, and the two filters above now select the members they name. The behaviour only changes for filters that were already wrong, so there is nothing for a saved filter to have depended on.

`^` and `$` are both characters a value can contain and the way a regex
writes its anchors. A value carrying one arrives escaped — `5\$`, `\^caret`
— but `processRegExp` removed the escaping before it looked for the anchors,
so by the time it tested `startsWith('^')` and `endsWith('$')` a literal
character and an anchor were indistinguishable.

That corrupted the value and the operator together. `contains '5$'` compiled
to `LIKE '%5'`, which is `endsWith '5'`: the dollar was dropped and the
question changed. `contains '^caret'` compiled to `LIKE 'caret%'` the same
way. Both still returned rows, just the wrong ones, so nothing surfaced the
mistake — a saved Ghost segment silently changed who it was about.

Reading the anchors off the escaped source fixes both. A trailing `$` is an
anchor only when an even number of backslashes precedes it, so a value
ending in a backslash is still told apart from an escaped dollar.

A pattern anchored at both ends was also mishandled: the old branch was an
`else if`, so `/^exact$/` took the start-anchor path and left the `$` in the
pattern. It is now an exact match with no wildcard on either end.
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ae52d44-3571-4bf1-9acb-fbd413208b4e

📥 Commits

Reviewing files that changed from the base of the PR and between def27b0 and 6c21356.

📒 Files selected for processing (2)
  • packages/mongo-knex/lib/convertor.js
  • packages/mongo-knex/test/unit/convertor.test.js

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


Walkthrough

The regex-to-SQL conversion now detects start and end anchors before unescaping characters. It distinguishes literal escaped ^ and $ characters from regex anchors. It adds SQL LIKE wildcards independently for each missing anchor. Regression tests cover escaped characters, anchored patterns, wildcard placement, and trailing backslashes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 6c213

This localized change preserves literal ^ and $ characters in filters and correctly handles matches anchored at both ends. It is merge-ready after normal checks and review, with no actionable merge-blocking risk remaining.

Suggested reviewers: kevinansfield

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the fix for filter values containing literal ^ or $ characters.
Description check ✅ Passed The description clearly explains the problem, solution, tests, and verification for the regex-to-LIKE conversion fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/regex-anchor-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.52%. Comparing base (def27b0) to head (6c21356).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #213      +/-   ##
==========================================
+ Coverage   85.31%   85.52%   +0.20%     
==========================================
  Files           9        9              
  Lines        2261     2293      +32     
  Branches      479      484       +5     
==========================================
+ Hits         1929     1961      +32     
  Misses        326      326              
  Partials        6        6              

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

rob-ghost added a commit to TryGhost/Ghost that referenced this pull request Aug 17, 2026
ref https://linear.app/ghost/issue/BER-3868

A member's name or email can contain a dollar sign, and a publisher can
reasonably filter on one. But `$` is also how a regex marks the end of a
pattern, and the admin filter engine used the same character for both.

Reading a saved filter back, the engine asked twice whether the pattern
ended in a `$` — once to work out which operator had been used, and again to
strip the anchor off the value. Neither check considered that the character
might have been escaped because it was part of the value, and neither knew
what the other had concluded. So a filter for names containing "5$" came
back as a filter for names ending in "5", with the value itself left mangled
as "5\". Starts-with came back as contains. This hit contains,
does-not-contain, starts-with and does-not-start-with on member name and
email, and on comment filters.

The anchors are now read once, in a single decomposition that also produces
the value, so the operator and the value can no longer disagree about what
the pattern held. That check is escape-aware: a trailing `$` anchors the
pattern only when it is not escaped.

Round-trip coverage was missing, so it is added across every codec and
operator. A browser test covers the step above them, since the reload is
where the filter stops being state the page holds and becomes NQL that has
to be parsed again — the same path a saved view takes, which is how the
misreading became permanent rather than cosmetic.

The query layer has a related but independent fault, where the same
characters are unescaped before the anchors are read, so the server answers
the wrong question even for a correctly written filter. That is fixed in
TryGhost/NQL#213 and reaches Ghost through a dependency bump.
@rob-ghost
rob-ghost merged commit 9f4dde2 into main Aug 18, 2026
9 checks passed
@rob-ghost
rob-ghost deleted the fix/regex-anchor-detection branch August 18, 2026 09:06
rob-ghost added a commit to TryGhost/Ghost that referenced this pull request Aug 18, 2026
ref https://linear.app/ghost/issue/BER-3868

A member's name or email can contain a dollar sign, and a publisher can
reasonably filter on one. But `$` is also how a regex marks the end of a
pattern, and the admin filter engine used the same character for both.

Reading a saved filter back, the engine asked twice whether the pattern
ended in a `$` — once to work out which operator had been used, and again to
strip the anchor off the value. Neither check considered that the character
might have been escaped because it was part of the value, and neither knew
what the other had concluded. So a filter for names containing "5$" came
back as a filter for names ending in "5", with the value itself left mangled
as "5\". Starts-with came back as contains. This hit contains,
does-not-contain, starts-with and does-not-start-with on member name and
email, and on comment filters.

The anchors are now read once, in a single decomposition that also produces
the value, so the operator and the value can no longer disagree about what
the pattern held. That check is escape-aware: a trailing `$` anchors the
pattern only when it is not escaped.

Round-trip coverage was missing, so it is added across every codec and
operator. A browser test covers the step above them, since the reload is
where the filter stops being state the page holds and becomes NQL that has
to be parsed again — the same path a saved view takes, which is how the
misreading became permanent rather than cosmetic.

The query layer has a related but independent fault, where the same
characters are unescaped before the anchors are read, so the server answers
the wrong question even for a correctly written filter. That is fixed in
TryGhost/NQL#213 and reaches Ghost through a dependency bump.
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