D1: limitedBoundParameters workaround misses drafts queries (parent IN (...)), breaking bulk publish above ~100 docs

Open
#17,493 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
67/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
sqlite, typescript
Domain
backend, databases

Research direction

Start in packages/drizzle/src/queries/parseParams.ts, then trace the id-to-parent rewrite in packages/payload/src/versions/drafts/appendVersionToQueryKey.ts. Review the affected paths in packages/payload/src/collections/operations/update.ts and packages/next/src/views/List/enrichDocsWithVersionStatus.ts, and reproduce the D1 query with about 100 IDs. Done means the parent-based IN query uses the existing workaround without the bound-parameter failure or silent status omission.

Written by the indexing model from the issue text.

Description

Describe the Bug

On D1, the limitedBoundParameters workaround that inlines id IN (...) as raw SQL is gated on the query path being literally 'id'. Every drafts-related query rewrites id to parent before it reaches that check, so the workaround silently does not apply and the query falls back to inArray() with one bound parameter per ID — hitting D1's hard limit of 100 bound parameters per query.

The guard is here:

https://github.com/payloadcms/payload/blob/main/packages/drizzle/src/queries/parseParams.ts — in the in / not_in branch:

if (
  adapter.limitedBoundParameters &&
  (operator === 'in' || operator === 'not_in') &&
  relationOrPath === 'id' &&          // <-- only 'id'
  Array.isArray(queryValue)
) {
  // ... inlines the values as raw SQL, avoiding bound parameters
}

But appendVersionToQueryKey (packages/payload/src/versions/drafts/appendVersionToQueryKey.ts) maps id to parent:

if (key !== 'id') {
  return { ...res, [`version.${key}`]: val }
}
return { ...res, parent: val }

So by the time parseParams sees it, relationOrPath is 'parent', not 'id', and the workaround is skipped.

Affected call sites

1. Bulk publish / unpublish from the list view (hard failure)

updateOperation takes the queryDrafts path when the collection has drafts enabled, after running the incoming where through appendVersionToQueryKey:

// packages/payload/src/collections/operations/update.ts
if (hasDraftsEnabled(collectionConfig) && (shouldSaveDraft || isTrashAttempt)) {
  const versionsWhere = appendVersionToQueryKey(fullWhere)
  const query = await payload.db.queryDrafts({ ..., where: versionsWhere })

The admin's PublishMany sends where: { and: [{ _status: { not_equals: 'published' } }, { id: { in: selectedIDs } }] }, which becomes parent IN (...) with N bound parameters. Selecting ~100 documents (the list view's max page size is 100 — admin.pagination.limits defaults to [5, 10, 25, 50, 100]) exceeds the limit and the whole request fails with too many SQL variables before a single document is published.

2. List view "changed" status (silent failure)

enrichDocsWithVersionStatus (packages/next/src/views/List/enrichDocsWithVersionStatus.ts) queries:

where: {
  and: [
    { parent: { in: draftDocIds } },
    { 'version._status': { equals: 'published' } },
  ],
}

Same parent path, same missed workaround. With 100 drafts on one page that is 100 + 1 = 101 bound parameters. This one is wrapped in a try/catch that only logs, so it fails silently — the user just sees documents that never get the "changed" badge, with no visible error.

Steps to Reproduce
  1. Deploy a Payload app on Cloudflare Workers with @payloadcms/db-d1-sqlite.
  2. Add a collection with versions: { drafts: true }.
  3. Create ~120 draft documents in it.
  4. In the admin list view, set per-page to 100, select all 100 on the page, and click Publish.

Expected: all 100 documents are published.
Actual: the request fails with too many SQL variables and nothing is published.

Selecting a smaller number (e.g. 50) works, which is what makes this look like a flaky/size-dependent bug from the user's side.

Proposed fix

Widen the guard to cover the drafts path, e.g.:

(relationOrPath === 'id' || relationOrPath === 'parent')

or, more robustly, key the decision off the resolved column being an integer PK/FK rather than off the path string.

Note that the parent values are the same document IDs that already pass the isValidStringID validation in the existing branch, so the same inlining logic applies unchanged.

Environment
  • Payload Version: 3.85.2
  • Database Adapter: @payloadcms/db-d1-sqlite 3.85.2
  • Deployment: Cloudflare Workers (Paid) + D1
  • Node Version: 24.x
Related but different

#14766 is also a too many SQL variables report on D1, but that one is about wide UPDATE statements on collections with many columns (batching applies to INSERT but not UPDATE). This issue is about the IN (...) query path and the idparent rewrite, which is a separate root cause.

Workaround

Splitting the bulk publish into chunks of ~20 IDs client-side avoids both limits. Worth noting that D1's other limit — 1000 queries per Worker invocation — is hit independently at roughly 50–60 documents, because updateOperation runs all matched documents in one request via Promise.all and each publish costs 15–20 queries. So chunking is needed regardless of the fix above; the fix here removes the hard 100-document wall and the silent list-view failure.

Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from payloadcms/payload

All issues in payloadcms/payload

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.