发布

  • feat(webapp,database): bound Prisma list filter arity (#4480)

    frostbyte_neo 发布于 2026-08-07 15:39:58 +00:00 | 318 次提交 在此版本后已推送到 main

    Summary

    Prisma expands in / notIn into one bind parameter per element, so
    every distinct list
    length is a separate prepared statement. Where the length tracks data
    volume (a batch size,
    a run-graph fan-out, a prior query's id set) one call site can mint
    hundreds of them. Each
    is used about once, but inserting it evicts an entry that was being
    reused, so the cost
    lands on unrelated queries sharing the pooler's statement cache. An
    unbounded list also
    risks the 65535 bind-parameter ceiling.

    boundedIn() pads a filter list to the next power of two by repeating
    its last element.
    IN and NOT IN ignore duplicates, so results are unchanged, and a
    call site drops from
    one statement per length to at most log2(cap). Applied to all existing
    sites.

    Enforcement

    Two oxlint rules require the helper: a list filter must be an inline
    array literal or a
    boundedIn() call.

    • The first covers filters reached through where / having /
      cursor, and deliberately
      never descends into data, create, update, set or equals. A key
      named in in
      those positions is user data, not a predicate, and rewriting it would
      corrupt what gets
      stored or compared.
    • The second covers bare filter objects passed to where-building
      helpers, which the first
      cannot see. It found five sites in the run-graph batch loaders that were
      otherwise
      invisible.

    Both rules follow filters through the shapes they are actually written
    in: conditional
    expressions, logical-and objects, spread-conditional properties,
    computed keys, and call
    arguments. An array literal only counts as fixed-arity when nothing
    spreads into it, since
    [...new Set(ids)] has a runtime length. Twelve sites were hidden
    behind those shapes
    until the rules handled them.

    Scoped to in and notIn. The scalar-list filters hasSome and
    hasEvery compile to
    && $1 and @> $1, passing the whole array as a single bind parameter,
    so their arity never
    reaches the statement text and there is nothing to bound.

    Both rules are error, so new call sites fail CI. That ratchet has
    already caught four
    sites added by other PRs while this one was in review.

    Notes

    boundedIn pads by repeating rather than with null: x NOT IN (a, b, NULL) is never true,
    so null-padding a notIn filter would silently return no rows. Lists
    above 32768 are
    returned unchanged so padding can never push a query past the parameter
    limit.

    Route modules reach the helper through ~/db.server rather than
    importing the database
    barrel directly, since a value import of that barrel into a module that
    also exports a React
    component is only safe while dead-code elimination prunes it.

    Measured on a local rig: 300 distinct list lengths produce 300 prepared
    statements
    unpadded, 10 padded. Verified end-to-end against a local stack with the
    full task-suite
    sweep, which surfaced no regressions.

    下载附件