Skip to content

fix(postgrest-js): correctly format array values in not() with 'in' operator - #2631

Open
diptomistry wants to merge 2 commits into
supabase:masterfrom
diptomistry:fix/not-in-array-formatting
Open

fix(postgrest-js): correctly format array values in not() with 'in' operator#2631
diptomistry wants to merge 2 commits into
supabase:masterfrom
diptomistry:fix/not-in-array-formatting

Conversation

@diptomistry

Copy link
Copy Markdown

Problem

.not(column, 'in', arrayValue) produces a malformed PostgREST URL because
unlike .in(), .not() has no array-aware formatting — it relies on JS's
default array-to-string coercion, which omits the required wrapping
parentheses. PostgREST rejects the request with a PGRST100 parse error.

Originally reported at supabase/postgrest-js#105 (repo archived; filing here
per its migration notice).

Fix

Extracted the array-cleaning logic already used by .in() (and the existing
.notIn() helper) into a shared cleanFilterValues function, and taught
.not() to use it when operator === 'in' and value is an array.

Why not just use .notIn()?

.notIn() already worked correctly — this doesn't touch its behavior beyond
routing it through the same shared helper. The bug is specifically in the
generic .not(column, operator, value) escape hatch, which docs and examples
still teach as .not('id', 'in', '(5,6,7)'). Callers reasonably try passing
a plain array there too, the same way they would to .in(). This fix closes
that footgun without deprecating or changing .notIn().

This mirrors the same fix already shipped in postgrest-dart
(supabase/postgrest-dart#32), for consistency across SDKs.

Testing

Added 4 URL-level unit tests: array input, reserved-character quoting,
pre-formatted string input (no regression), and a non-in operator call
(no regression). Full Docker-based integration suite not run locally
(unavailable in this environment) — CI should cover it.

… values

- Introduced a new function `cleanFilterValues` to deduplicate and format array values for the 'in' operator, ensuring proper quoting of reserved characters.
- Updated the `not` method to utilize `cleanFilterValues` for array inputs, maintaining the original format for pre-formatted strings.
- Added tests to verify the correct formatting of array values and handling of reserved characters in the 'not' operator.
@diptomistry
diptomistry requested review from a team as code owners August 27, 2026 10:46
@coderabbitai

coderabbitai Bot commented Aug 27, 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: 6e17559d-768d-4b4f-81cc-f87fc2572524

📥 Commits

Reviewing files that changed from the base of the PR and between 7b2b803 and c5a1fe7.

📒 Files selected for processing (2)
  • packages/core/postgrest-js/src/PostgrestFilterBuilder.ts
  • packages/core/postgrest-js/test/filters.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved not filters using in with array values.
    • Correctly formats duplicate values and values containing reserved characters.
    • Escapes embedded quotes and backslashes to preserve valid filter values.
    • Preserves already formatted values and leaves other operators unchanged.
  • Documentation

    • Clarified automatic array formatting behavior and corrected documentation wording.

Walkthrough

The change adds a shared cleanFilterValues helper for deduplication, reserved-character quoting, and escaping within quoted values. The in() and notIn() methods use this helper. The not() method now formats array values when the operator is in, while preserving pre-formatted strings and values for other operators. Tests cover standard arrays, reserved characters, escaping, pre-formatted strings, and non-in operators.

Merge Risk: ⚪ Minimal · up to c5a1f

This change fixes array formatting for the generic negated in filter while preserving existing string and non-in behavior; no actionable merge-blocking risk remains after normal checks and review.


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.

this {
this.url.searchParams.append(column, `not.${operator}.${value}`)
if (operator === 'in' && Array.isArray(value)) {
this.url.searchParams.append(column, `not.in.(${cleanFilterValues(value)})`)

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.

Severity: LOW

An attacker-controlled array element reaches not.in through cleanFilterValues, but embedded " and \ are not escaped, while current PostgREST reserved ., :, and * are not quoted. The resulting URL can split or reinterpret values, changing the predicate and incorrectly scoping rows when this filter protects a privileged read.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: The root cause is in the cleanFilterValues function (lines 44–53) and the PostgrestReservedCharsRegexp pattern (line 36), not at the not.in call site itself. Two changes are needed:

  1. Expand PostgrestReservedCharsRegexp at line 36 to include the additional PostgREST reserved characters (., :, *, !, |, &) that can also alter filter semantics when unquoted:
const PostgrestReservedCharsRegexp = new RegExp('[,().:*!|&]')
  1. Escape embedded \ and " before double-quote-wrapping in cleanFilterValues (line 49). Currently "${s}" is emitted without escaping, so a value like a",b becomes "a",b" — PostgREST splits it into two elements. The corrected map callback:
if (typeof s === 'string' && PostgrestReservedCharsRegexp.test(s))
  return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`

These two changes together ensure that any string value is both correctly quoted when it contains reserved characters and that internal quotes/backslashes cannot escape the quoting, preventing filter-predicate splitting or reinterpretation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — cleanFilterValues now escapes embedded \ and " before
quote-wrapping, so values like a",b can no longer break out of their
quoting and split the filter list.

Deliberately left PostgrestReservedCharsRegexp unchanged: widening it to
add . : * ! | & would change output for a large share of existing
.in()/.notIn() calls (emails, UUIDs, decimals, etc. all contain .),
which is a broader behavioral change than this PR's scope. Filed as a
separate follow-up for a maintainer to weigh in on: ##2633

- Updated the `cleanFilterValues` function to escape embedded double quotes and backslashes when quoting reserved characters.
- Added tests to ensure proper escaping behavior for the 'in' operator and the 'not' operator with embedded quotes and backslashes.
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.

1 participant