Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eloquent Param Limit Fix Laravel Package

staudenmeir/eloquent-param-limit-fix

Laravel package that fixes "too many SQL variables" errors by automatically splitting large whereIn, insert, and update queries into batches that stay under your database's parameter limit. Works with SQLite, MySQL, PostgreSQL, and SQL Server.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm it's live.

composer require staudenmeir/eloquent-param-limit-fix

There is nothing to publish, no service provider to register, no .env key. Laravel auto-discovery picks up the provider from the package's extra.laravel.providers. Your first job is to prove it actually loaded, because a silently-unregistered provider looks identical to a working one until a customer hits the ceiling:

// tinker or a throwaway route
DB::listen(fn ($q) => logger(count($q->bindings) . ' bindings'));

Then run a query you know is large and confirm the package split it (you'll see several statements logged where you wrote one). If you still see an error at >999 bindings on SQLite, discovery didn't happen — check composer show, then php artisan about for the provider list.

Check the compatibility matrix first. The package declares Laravel/PHP constraints in composer.json; verify them against your app before you spend time on reproduction. v1.1.2 (2024-05-26) is the release that added the PostgreSQL limit — on older versions you get SQLite/SQL Server coverage only, which matters if Postgres is your production database.

First use case: reproduce the failure, then let it heal.

Don't adopt this speculatively in a codebase where nothing can produce a large whereIn. The canonical reproduction is eager loading a relationship off a big collection, because Eloquent turns that into where user_id in (?, ?, …) with one binding per parent row:

// 2,000 users in SQLite → "too many SQL variables"
$users = User::with('posts')->get();

Before installing, run that against your real CI database and watch it fail. After installing, run it again and watch it pass with N chunked statements in the log. That before/after pair is your whole justification and your regression test in one artifact.

Where to look when something's off. The package has essentially no surface area: no config file, no facade, no published migrations. Everything you need to debug lives in (a) DB::listen / DB::getQueryLog() to see the chunking, and (b) your own test suite to confirm the results are still correct. Budget for the second one — chunking changes SQL shape, and error monitoring will never tell you if ordering drifted.

Implementation Patterns

Make it a base-level dependency, not a per-feature one. The value is that no squad has to remember to chunk. Put the composer require in its own commit so a regression is unambiguously attributable, then delete any array_chunk($ids, 500) hacks you find — otherwise you're paying the observability cost of invisible rewriting and keeping the manual workaround, with two code paths to reason about.

Inventory the paths that can grow unbounded. Enumerate before you ship, because the package protects you and hides from you at the same time:

  • "Select all" admin actions and multi-select report filters (IDs straight from a request payload).
  • Bulk export / mass reassignment / tenant-wide notifications fanning out from a parent table.
  • Model::with(...) or Model::load(...) over large collections.
  • Model::findMany($ids), whereIntegerInRaw, and import-reconciliation matching.

Those are the call sites to write tests around. Everything else in the app is untouched.

Don't restructure your code. No call-site changes are needed and none should be made. Keep whereIn('id', $ids). The one exception is where you were already hand-chunking for memory reasons (chunkById) — that's a different concern; keep it. This package splits a single oversized statement into a handful; it does not stream results.

Write a functional test that survives the package. Treat this as a correctness test for your query, not for the library:

public function test_large_eager_load_is_correct(): void
{
    $users = User::factory()->count(1500)->create();

    $loaded = User::with('posts')->orderBy('id')->get();

    $this->assertCount(1500, $loaded);
    $this->assertSame($users->pluck('id')->all(), $loaded->pluck('id')->all());
}

Assert order and count, not just "no exception thrown." Add a paginated variant (->paginate(50)) and an aggregate variant (->withCount('posts')), since those are the query shapes where chunking has the most room to change meaning.

Test on the driver that actually breaks. SQLite in CI at 999 bindings is your cheap reproduction; SQL Server's 2,100 is the enterprise case; PostgreSQL's 65,535 needs tens of thousands of IDs to reach, so exercise it with a deliberately large fixture or a staging check rather than pretending a 200-row factory proves anything.

Use it as a safety net, not a licence to forget limits. Because the split is silent, pair it with a cheap guard so you retain visibility:

// AppServiceProvider::boot()
DB::listen(function ($query) {
    if (count($query->bindings) > 500) {
        Log::warning('Large parameter set', [
            'sql' => Str::limit($query->sql, 200),
            'bindings' => count($query->bindings),
        ]);
    }
});

That gives you the observability the package deliberately omits: which paths are generating huge parameter lists, and how often.

Gotchas and Tips

The emitted SQL is not the SQL you wrote. Query logs, EXPLAIN output, APM traces and slow-query dashboards will show several statements where a developer wrote one. Brief whoever owns slow-query alerting before you deploy, or a performance investigation will chase phantom query-count regressions that are actually correct behaviour.

whereNotIn is the one to test hardest. Naively chunking an IN list and combining with OR is correct; doing the same to NOT IN is catastrophically wrong (you'd get the union of everything not in each chunk). The package handles the combinatorics, but write an explicit test asserting that a chunked whereNotIn returns exactly what the unchunked query returned on a small dataset. This is the class of silent regression your error monitor cannot see.

limit / offset / cursor() interact with chunking. Ordering across chunks is the fragile spot, and limit applied to a split query is not obviously limit applied to the union. Always orderBy an indexed unique column on large-whereIn paths, and add a focused test for anything paginated or aggregated over a big ID list. If a query is order-sensitive and you can't easily test it, that's a candidate for explicit chunkById instead.

The binding budget is per-statement, not per-whereIn. A query with 900 IDs plus 150 other bindings still blows the SQLite ceiling. When sizing tests, count all bindings in the statement, not just the array you're worried about.

The SQLite number is not a constant. Older builds compile with SQLITE_MAX_VARIABLE_NUMBER=999; SQLite 3.32+ defaults to 32,766. Your CI's actual value depends on the PHP/SQLite build in the image, so don't hardcode 999 into a test threshold — construct the fixture large enough to exceed whatever the driver reports, and verify against the image you actually run.

Raw queries and non-whereIn constructs are out of scope. DB::select('… where id in (?)', [$ids]), DB::raw fragments, and insert batches are not the documented target — the package's job is whereIn/whereNotIn value lists as generated by Eloquent and the query builder. If your bulk-insert path is a real risk, verify with a reproduction rather than assuming coverage, and chunk it explicitly if not.

Subquery whereIn needs nothing. whereIn('id', fn ($q) => ...) binds no list, so there's nothing to split. Don't "fix" it.

Beware other builder-hooking packages, not the database. Tenancy, read/write splitting, query caching and audit logging are the realistic collision points — all of them wrap or intercept the query builder the same way this package does. If you run any of them, test the combination explicitly on a staging branch before rolling out broadly.

Empty arrays are unaffected. whereIn('id', []) already compiles to a falsey constant; no chunk is generated. No need to guard it.

Pin the version and write down why it's there. This is a small, single-maintainer dependency with no commercial support. Note in your dependency register what it does, which query paths rely on it, and that removal is a single composer remove. Its invisibility is exactly what makes it easy for a future maintainer to delete as "unused" — and the resulting failure will only surface at customer data volumes, not in CI.

You can roll back in one line. If a semantic edge case bites you, composer remove is a complete, symmetric reversal. That low blast radius is the reason to ship it ahead of the bulk-data features it protects rather than as part of them.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource