Skip to content

fix(drizzle): ignore empty in/not_in operands instead of passing them to typed columns - #17839

Open
creativoma wants to merge 1 commit into
payloadcms:mainfrom
creativoma:fix/drizzle-empty-in-not-in-operands
Open

fix(drizzle): ignore empty in/not_in operands instead of passing them to typed columns#17839
creativoma wants to merge 1 commit into
payloadcms:mainfrom
creativoma:fix/drizzle-empty-in-not-in-operands

Conversation

@creativoma

Copy link
Copy Markdown

What?

sanitizeQueryValue only coerced in / not_in / all operands when the incoming value was a comma-delineated string. When the same value arrives as an array — which is what an indexed query string produces — the coercion was skipped entirely, so an operand that cannot be cast to the column's type reached the driver as-is.

This moves the existing numeric coercion below the array normalisation so it applies in both cases, drops operands that are not numbers, and adds the equivalent for uuid id columns.

Why?

Payload's own relationship field asks for its options with the already-selected ids excluded, and it still sends the parameter when nothing is selected yet:

GET /api/roles?where[and][0][id][not_in][0]=

qs parses that into not_in: ['']. Because the value is an array, no coercion ran, and the empty string was bound straight to the id column:

select count(*) from "roles" where "roles"."id" not in ($1)   -- params: ['']
error: invalid input syntax for type integer: ""

The endpoint 500s and the field spins on "Loading…" with nothing surfaced to the user. On a uuid adapter the same query fails with invalid input syntax for type uuid: "". Every collection is affected, not only the one where we hit it.

Where it actually shows up is an install whose users table is empty while the data to select already exists — the create-first-user screen — because that is when nothing is selected yet. Local development rarely reproduces it, since those databases already have users.

Related issues:

How?

The coercion moved below the Array.isArray normalisation and gained a filter:

if (field.type === 'number') {
  formattedValue = formattedValue
    .map((arrayVal) => parseFloat(arrayVal))
    .filter((arrayVal) => !Number.isNaN(arrayVal))
} else if (isUUID) {
  formattedValue = formattedValue.filter((arrayVal) => uuidValidate(arrayVal))
}

Dropping the operand is semantics-preserving rather than lenient: a value that cannot be cast to the column's type can never equal any row, so removing it leaves the result set identical for both operators. Filtering down to an empty list stays correct as well — drizzle compiles inArray(col, []) to false (matches nothing) and notInArray(col, []) to true (excludes nothing), which is exactly what an in / not_in of nothing means.

isUUID is only true when the column really is a PgUUID, so the uuid filter cannot narrow a plain text id column.

Tests

  • packages/drizzle/src/queries/sanitizeQueryValue.spec.ts — new unit spec covering both id types, including that valid operands are left untouched.
  • test/database/int.spec.ts — one integration test under describe('id type'), tagged { db: 'drizzle' }. It asserts the semantics rather than just the absence of a throw: not_in: [''] excludes nothing, in: [''] matches nothing.

Without this change that integration test fails with the driver error above: invalid input syntax for type integer: "" on postgres, and invalid input syntax for type uuid: "" on postgres-uuid.

Verified locally: test:unit, test:types, build:core, and the database int suite on postgres, postgres-uuid and sqlite.


This targets main (v4). The 3.x branch carries the identical block, so the fix applies there unchanged if you would like a backport.

… to typed columns

`in`/`not_in` operands were only coerced when the value arrived as a
comma-delineated string. An indexed query string is parsed into an array
instead, so the coercion was skipped and an operand that cannot be cast to
the column's type reached the driver as-is.

Payload's own relationship field sends `?where[id][not_in][0]=` when nothing
is selected yet, which bound an empty string to the id column and failed with
`invalid input syntax for type integer: ""` (or `for type uuid: ""`).

An operand that cannot be cast to the column type can never match a row, so
dropping it leaves the result set unchanged; drizzle compiles an empty `in` to
`false` and an empty `not in` to `true`.
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