Payload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.
fix(drizzle): error when using contains operator on hasMany select fields (#15865)
Fixes `contains` operator on hasMany `select` fields in PostgreSQL. This
was a regression introduced in #15671.
## The problem
Given a collection with a hasMany `select` field:
```typescript
{
slug: 'users',
fields: [
{
name: 'roles',
type: 'select',
hasMany: true,
options: ['user', 'admin', 'editor'],
},
],
}
```
Payload stores this across two PostgreSQL tables. For a user created
with `roles: ['admin', 'editor']`:
**`users`**
| id | created_at | updated_at |
|----|------------|------------|
| 1 | 2026-03-05 | 2026-03-05 |
**`users_roles`**
| id | parent_id | value | order |
|----|-----------|----------|-------|
| 1 | 1 | `admin` | 1 |
| 2 | 1 | `editor` | 2 |
The `value` column is a PostgreSQL **enum type**, not a `varchar`.
When querying `{ roles: { contains: 'admin' } }`, Payload generated:
```sql
SELECT DISTINCT "users"."id"
FROM "users"
LEFT JOIN "users_roles"
ON "users"."id" = "users_roles"."parent_id"
WHERE "users_roles"."value" ILIKE '%admin%'
-- ❌ PostgreSQL error: operator does not exist: enum_users_roles ~~* unknown
```
`ILIKE` does not work on PostgreSQL enum types.
## The fix
The LEFT JOIN already flattens the array into individual rows:
| users.id | users_roles.value |
|----------|-------------------|
| 1 | `admin` |
| 1 | `editor` |
So "contains admin" just means "has a row where value equals admin". The
fix converts `contains` to `equals` for hasMany `select` fields, the
same way it already works for hasMany `relationship` and `upload`
fields:
```sql
SELECT DISTINCT "users"."id"
FROM "users"
LEFT JOIN "users_roles"
ON "users"."id" = "users_roles"."parent_id"
WHERE "users_roles"."value" = 'admin'
-- ✅ Matches row 1, returns user id=1
```
## Why hasMany `text` fields are different
HasMany `text` fields store values as `varchar`, not as an enum. `ILIKE`
works fine on `varchar` columns, so substring matching (`contains:
'adm'` matching `'admin'`) is valid there. Whether it's _desirable_ is a
problem separate from this PR, being discussed internally
[here](https://payloadcms.slack.com/archives/C049BR3QBHC/p1772680331359859) A
Alessio Gravili committed
fba24380578f513209a5f4811e2836a2317c33d2
Parent: 17a0d19
Committed by GitHub <noreply@github.com>
on 3/6/2026, 6:02:30 PM