Skip to content

fix(postgrest): escape reserved characters in array filter values - #1126

Open
AndroidPoet wants to merge 2 commits into
supabase:mainfrom
AndroidPoet:fix/postgrest-array-literal-escape-2
Open

fix(postgrest): escape reserved characters in array filter values#1126
AndroidPoet wants to merge 2 commits into
supabase:mainfrom
AndroidPoet:fix/postgrest-array-literal-escape-2

Conversation

@AndroidPoet

Copy link
Copy Markdown
Contributor

What

Array's PostgrestFilterValue conformance builds a PostgreSQL array literal by joining the elements' raw values with commas, with no escaping:

extension Array: PostgrestFilterValue where Element: PostgrestFilterValue {
  public var rawValue: String {
    "{\(map(\.rawValue).joined(separator: ","))}"
  }
}

So any element that contains a character with structural meaning inside an array literal is silently corrupted. For example, filtering an array/text[] column with ["a,b"] produces {a,b}, which PostgREST parses as the two values a and b instead of the single value a,b. The same happens for elements containing {, }, ", \, surrounding whitespace, an empty string, or the literal NULL.

This is the array-literal counterpart of the in() filter escaping fixed in #1061.

Fix

Quote and backslash-escape each element that needs it, reusing the same escaping approach as the existing in() filter helper (added alongside it in Helpers):

public var rawValue: String {
  let elements = map { element -> String in
    let raw = element.rawValue
    if raw.hasPrefix("{"), raw.hasSuffix("}") {
      return raw
    }
    return escapePostgRESTArrayLiteralElement(raw)
  }
  return "{\(elements.joined(separator: ","))}"
}

Elements that are already safe (admin, 9:00, numbers) are emitted verbatim, so existing valid usage is unchanged.

Note on nested arrays

An element whose raw value both starts with { and ends with } is treated as an already-formed nested array literal and passed through unquoted, so arrays of arrays ([[1, 2], [3, 4]] -> {{1,2},{3,4}}) keep working. The trade-off is that a String element that is literally {...} will not be quoted — this is inherent to the string-based rawValue design, which can't distinguish a nested [Int] (whose rawValue is {1,2}) from the string "{1,2}". Elements with partial braces (e.g. a{b) are still quoted.

Testing

Added tests covering reserved characters (comma, brace), quote/backslash escaping, whitespace/empty/NULL, nested array literals, the AnyJSON.array path, and that safe/numeric values stay unquoted. They fail on main and pass with this change.

  • swift test --filter PostgRESTTests — 105 passing (existing in() and snapshot tests unchanged)
  • ./scripts/format.sh — clean

Array's PostgrestFilterValue.rawValue joined elements with commas inside
{...} without escaping, so an element containing a comma, brace, quote,
backslash, surrounding whitespace, or the literal NULL corrupted the array
literal — e.g. ["a,b"] became {a,b}, which PostgREST reads as two separate
values. Quote and backslash-escape elements that need it, mirroring the
existing in() filter escaping. Nested array literals ({...}) pass through
unchanged so arrays of arrays keep working.
@AndroidPoet
AndroidPoet requested review from a team and grdsdev as code owners July 13, 2026 19:41
Port the array-literal escaping tests to Swift Testing, which main now
requires.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved PostgreSQL array filter value encoding.
    • Correctly quotes and escapes elements containing reserved characters, quotes, backslashes, whitespace, or empty values.
    • Handles NULL values safely and preserves nested array literals.
    • Applies consistent escaping to JSON array values.
  • Tests

    • Added coverage for special characters, whitespace, numeric values, nested arrays, and NULL handling.

Walkthrough

PostgREST array filter serialization now escapes elements containing reserved characters, quotes, backslashes, whitespace, empty values, or NULL markers. Elements that already represent nested array literals remain unchanged. Tests cover quoting, escaping, numeric values, nested arrays, and AnyJSON.array(...).


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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/PostgREST/PostgrestFilterValue.swift`:
- Around line 71-76: Update the array-element encoding flow around the map
closure to use a typed array-member encoding contract or marker before
converting values to raw strings; do not infer nested-array or NULL semantics
from rawValue alone. Ensure scalar brace-delimited strings remain escaped as
scalar elements, while Optional<Int>.none and AnyJSON.null encode as actual NULL
array members, and add regressions covering both cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd8c80a7-7c02-4206-9012-b9b0477f62a9

📥 Commits

Reviewing files that changed from the base of the PR and between b118484 and f8ea82e.

📒 Files selected for processing (3)
  • Sources/Helpers/PostgRESTFilterValue.swift
  • Sources/PostgREST/PostgrestFilterValue.swift
  • Tests/PostgRESTTests/PostgrestFilterValueTests.swift

Comment on lines +71 to +76
let elements = map { element -> String in
let raw = element.rawValue
if raw.hasPrefix("{"), raw.hasSuffix("}") {
return raw
}
return escapePostgRESTArrayLiteralElement(raw)

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not infer array-member semantics from rawValue.

A scalar ["{a,b}"] becomes {{a,b}} (a nested array), while [Optional<Int>.none] and [AnyJSON.null] become {"NULL"} (a string rather than a NULL element). Add a typed array-member encoding contract/marker before stringification, and regressions for both cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/PostgREST/PostgrestFilterValue.swift` around lines 71 - 76, Update
the array-element encoding flow around the map closure to use a typed
array-member encoding contract or marker before converting values to raw
strings; do not infer nested-array or NULL semantics from rawValue alone. Ensure
scalar brace-delimited strings remain escaped as scalar elements, while
Optional<Int>.none and AnyJSON.null encode as actual NULL array members, and add
regressions covering both cases.

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