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

Laravel Batch Laravel Package

mavinoo/laravel-batch

Laravel Batch provides bulk insert and update helpers for Laravel models. Use Batch::update with a key column or updateMultipleCondition for per-row conditions, making large data changes faster and cleaner without repetitive queries. Includes a facade and batch() helper.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Supersedes the v3.1.0 assessment. This release is v3.3.0; we skipped v3.2.0 and have not reviewed its notes. The release states 3.2 → 3.3 is additive with no code changes, which implies the 3.1 → 3.3 hop is also no-code-change — but the 3.2.0 changelog is an unreviewed gap and should be read before approval.

Architecture Fit

  • Strengths:

    • Performance-Centric, and now materially faster (unchanged from 3.1.0): The UPDATE ... CASE statement cap of 100 rows removed the super-linear degradation large statements suffered. The 50,000-row benchmark remains the headline:

      • MySQL 8.4: 30.5s → 3.1s
      • MariaDB 10.11: 25.3s → 2.4s
      • PostgreSQL 16: over 15 minutes → 2.5s
      • SQLite 3.45: 3.7s → 3.4s The PostgreSQL result remains a qualitative change: bulk-update workloads there were unusable at scale and would have been routed elsewhere. That option stays open.
    • Bulk import is now a first-class capability (new): Batch::import($model, $path, ['fast' => true]) uses COPY on PostgreSQL and LOAD DATA LOCAL INFILE on MySQL/MariaDB. The published 1,000,000-row CSV numbers:

      1M-row CSV import fast
      PostgreSQL 18.7s 6.6s
      MySQL 8.4 18.7s 8.4s
      MariaDB 12.6s 6.4s

      This is an order-of-magnitude change in the shape of what the package is for. Previously it was "batch writes from PHP arrays". Now it is a credible ingestion path for file-based feeds — a workload we currently solve with bespoke scripts, \copy invocations, or ETL tooling. That is the most strategically significant item in this release, and also the one with the most operational prerequisites.

    • Import validation with per-row error reporting (new): ['rules' => [...]] returns ['totalRows' => n, 'skipped' => n, 'errors' => [rowIndex => [field => messages]]]. This is the first time the package offers row-level diagnostics — a gap flagged in every prior assessment. It applies to imports only, not to update()/upsert(), but it materially reduces the "which row failed?" support burden for the ingestion use case.

    • Fast-import data fidelity (new, needs verification): The release claims bad values that MySQL would otherwise store with a warning now throw and write nothing. If it holds under our MySQL strict-mode settings, it closes the biggest objection to LOAD DATA LOCAL INFILE — silent truncation. Verify before relying on it.

    • Counter upserts (new): ['qty' => ['+']], plus ['-'], ['max'], ['min'], on every database. Replaces hand-rolled ON DUPLICATE KEY UPDATE qty = qty + VALUES(qty) raw SQL — a common source of per-driver divergence. Directly applicable to inventory, counters, quotas and usage-metering tables.

    • Conditional upserts (new): ['onlyIf' => ['updated_at' => '>']] updates only when the new value wins, while new rows always insert. This is last-writer-wins-with-guard semantics — the primitive most hand-rolled sync jobs implement badly. Note it guards updates only; it does not prevent an insert.

    • upsertReturning() (new): Returns the affected rows, giving us IDs and generated values without a follow-up query. Closes a real workflow gap (upsert-then-need-the-PK). Implementation is almost certainly native RETURNING on PostgreSQL and an emulation elsewhere — see risk notes; treat cross-driver semantics as unverified until we test.

    • insertOrIgnoreRows() with skipped-duplicate reporting (new): Returns ['inserted' => 98, 'skipped' => [...]], reporting which rows were skipped. Release notes state this was validated with 8 concurrent writers on MySQL 8.4, MariaDB and PostgreSQL, with correct per-connection attribution. If that claim holds, it turns "insert and hope" into a reconcilable operation.

    • Export (new): User::where(...)->exportTo($path, ['maxRows' => 100000]) writes CSV/TSV/JSON Lines, optionally gzipped, chunked at constant memory, splitting into numbered parts. New capability, but it is a data egress surface first and a feature second — see governance questions.

    • withoutTimestamps() documented and tested for every method (new): Closes a long-standing documentation hole. Timestamp behaviour on upsert() was previously implicit; it is now a documented, testable contract.

    • Upsert / composite keys / row-shaped inserts / pretend() (from 3.1.0, still central): updateByKeys()/deleteByKeys() for composite and tenant-scoped keys, insertRows() for heterogeneous rows, and pretend() — which returns SQL and bindings without executing or connecting. pretend() remains the single highest-leverage operational feature in the package: SQL review in code review, CI assertions, and pre-flight validation of destructive batches.

    • Eloquent Integration: Unchanged — native Eloquent models, facade and helper patterns, plus static HasBatch methods for every operation. Discoverability is now very high, which is a double-edged sword (see Gaps).

    • Security Posture (3.x baseline): v3.0.0 eliminated the SQL-injection class present in all 2.x releases by binding all values and delegating quoting to Laravel's grammar. This release does not weaken that for the parameterised paths — but the fast import path is a different mechanism: file contents are parsed by the database server, not bound as parameters. Column names, table names, file paths and options are still composed into SQL and are the remaining interpolation surface. Review the wrapper's handling of those inputs.

    • Correctness Signals: CI matrix across MySQL, MariaDB, PostgreSQL and SQLite, plus PHPStan level 9 with Larastan. The concurrency testing described for insertOrIgnoreRows() is a genuine maturity marker.

    • Database Agnostic (partially eroded): The core CRUD paths remain CI-verified on four drivers. The new features are increasingly driver-specific in implementation (COPY, LOAD DATA, RETURNING), so "database agnostic" is now true of the API shape but less true of the execution path.

  • Gaps (post-v3.3.0 reassessment):

    • Event System: Still no integration with Laravel's event system. Unchanged and now broader — upsert(), upsertReturning(), insertOrIgnoreRows(), counter/conditional upserts and import() all bypass observers, validation hooks and audit hooks. deleteByKeys() soft deletes correctly but fires no deleting/deleted. This remains the single largest "surprise" risk and the surface it covers has roughly doubled.
    • Soft Deletes on Update Paths: Unchanged — update(), updateByKeys() and upsert() do not filter deleted_at. Callers must exclude soft-deleted rows.
    • Validation Parity: Row validation exists for imports only. upsert(), updateByKeys() and insertRows() still bypass model validation entirely. The asymmetry is a trap: developers who learn "the package validates rows" from import() will assume it applies everywhere.
    • New Surface Area (significantly worse): Five methods were pilot-tier at 3.1.0. This release adds approximately eight more public entry points (import(), fast mode, exportTo(), upsertReturning(), insertOrIgnoreRows(), counter upserts, conditional upserts, withoutTimestamps() coverage). All tested, but with very little production mileage. The package is now growing faster than our ability to validate it.
    • Per-Row Failure Analysis: Partially closed for imports (errors keyed by row), still absent for mid-batch driver rejections on update/upsert paths.
    • Transaction Isolation: Unchanged. Per-call atomicity is good; sequences of batch calls still require explicit DB::transaction(). import() raises the stakes: a large import inside one transaction means a large rollback log and long-held locks — verify the transaction strategy for big files.
    • Concurrency: Still no row locking or optimistic-concurrency primitive. Counter upserts are atomic at row level, but read-modify-write sequences spanning calls remain unsafe.
    • Forward-Compatibility Debt: Instance updateMultipleCondition() remains deprecated and removed in 4.0. Unchanged by this release; still a guaranteed future fatal.
    • Operational Prerequisites Not Encoded in the API: local_infile must be enabled client- and server-side; the CSV file must exist on the filesystem of whichever process runs the job (worker, not web node); SQLite has neither COPY nor LOAD DATA. None of this is discoverable from the method signature.
    • Documentation Depth: Feature docs are expanding fast; failure-mode docs (what fast does when local_infile is off, what onlyIf does with NULLs, what insertOrIgnoreRows() does with a non-duplicate error) are the likely thin spots.

Integration Feasibility

  • Laravel Compatibility: v3.x requires Laravel 10–13. Laravel 8/9 cannot take any 3.x release and remain stranded on a known-vulnerable 2.x.
  • PHP Compatibility: 8.1+ required (8.1–8.5 tested).
  • Dependency Lightweight: Still a single Composer package with no runtime dependencies beyond Laravel/Eloquent.
  • Configuration Overhead: Minimal for the core package. fast import adds infrastructure configuration: a MySQL/MariaDB parameter-group change (local_infile=1) and a filesystem-placement constraint for the CSV on queue workers. This is the first release where adoption has a non-PHP prerequisite.
  • Database Schema: No schema changes for any feature, including import and export.
  • Migration Path: 3.1 → 3.3 is a drop-in composer update — additive. The 3.0 → 3.1 hop was likewise additive. The 2.x → 3.x pass remains unchanged and still required wherever 2.x remains. The instance updateMultipleCondition() → static migration task is still outstanding and still cheaper now than later.

Technical Risk

  • Critical (2.x only, unchanged):
    • Known SQL Injection in 2.x: Every 2.x release is vulnerable. PostgreSQL unconditionally; all databases via index values and JSON keys. Mitigation: upgrade to ^3.3, or remove the package.
  • High:
    • 2.x → 3.x Behavioral Breakage (unchanged): Return types (int/array/0 on empty instead of false), $raw = true now throws, and typed BatchInterface signatures that fatal in any in-house decorator or mock. Silent business-logic failure for falsy-return guards.
    • Fast Import Bypasses the Parameterised Path (new): COPY and LOAD DATA LOCAL INFILE hand the file to the database server. Values are no longer bound parameters; the safety of the operation depends on correct CSV escaping, correct file handling, and the server's own parsing. This is not a regression in the injection class — but it is a different execution model and deserves its own review, not an assumption of inherited safety. Combined with server-side file reading, local_infile is a known data-exfiltration vector if the client ever connects to an untrusted or spoofed MySQL server. Enforce TLS verification and restrict the setting to application connections pointing at our own DB.
  • Medium:
    • New-Method Immaturity (worsened): Eight additional entry points at one release old. Highest-risk semantics: upsertReturning() (how is it emulated off PostgreSQL?), insertOrIgnoreRows() (how is skip attribution computed?), conditional upserts (NULL and type-coercion comparisons), and counter upserts (unsigned underflow, overflow, decimal vs. integer coercion). Mitigation: pilot on non-critical tables, assert generated SQL with pretend(), and write integration tests per driver we actually run.
    • insertOrIgnoreRows() Error Conflation (new): On MySQL, INSERT IGNORE downgrades more than duplicate-key errors — data truncation, FK violations and NOT NULL issues become warnings. If the implementation derives "skipped" from the ignore path, a row rejected for a FK violation could be reported as a duplicate. The release's concurrency claim addresses attribution, not classification. Verify explicitly with non-duplicate failure cases.
    • upsertReturning() Read-After-Write (new): Unless it is native RETURNING, the returned rows are likely a follow-up read. Under concurrency that read can see rows written by another transaction, or miss rows changed in between. Ask whether the operation is a single statement per driver and whether it is transaction-wrapped.
    • Conditional Upsert Semantics (new): ['onlyIf' => ['updated_at' => '>']] is evaluated against the stored value — correct only if NULL handling, timezone normalisation and type coercion match the column's semantics. New rows always insert, so this is not an idempotency guard. Test with NULLs, equal values and out-of-order timestamps.
    • Export as a Data-Governance Surface (new): exportTo() writes PII to arbitrary paths, chunked, gzipped, multi-part. There is no access control, no audit trail and no retention policy in the API. This is a compliance review item before any customer-data export ships.
    • Import File Handling (new): File path must resolve on the worker's filesystem, be readable by the PHP user, and be cleaned up — imports may involve PII at rest in temp CSVs. Multi-node or ephemeral-disk worker fleets need shared storage or an on-node download step.
    • Driver Capability Divergence (new): fast has no SQLite equivalent and likely none on SQL Server; RETURNING is native only on PostgreSQL. Code that runs on CI's SQLite will not exercise the production path for these features. Test on the production driver or don't ship the feature.
    • Partial Failures Across Multiple Calls: Unchanged. Multi-call workflows need DB::transaction().
    • Performance Tradeoffs (unchanged): The 100-row statement cap trades statement size for statement count; remote/high-latency links see more round trips. The 1M-row import numbers assume a fast local link to the DB and a fast disk. Re-benchmark on our network path.
    • Observer Bypass: Unchanged, broader.
    • API Surface Growth vs. Team Knowledge: With roughly a dozen write primitives, the probability that a developer picks an inappropriate one (e.g. upsert() where observers matter) rises faster than documentation can compensate.
  • Low:
    • Signature Changes: upsert() gains an optional fifth $options argument; import() gains an errors key. Both additive at the end of the argument list and in the return array. Only in-house wrappers that override these methods need touching.
    • Stability: Actively maintained, MIT, CI matrix, PHPStan 9, and now documented concurrency testing. Single-maintainer bus factor remains.
    • Backward Compatibility: Additive across 3.1 → 3.3. The next real event is 4.0 (removal of instance updateMultipleCondition()).

Key Questions

  1. Security (blocking, unchanged):
    • Which services still pin mavinoo/laravel-batch at 2.x? Is any exposed to user-controlled index values or JSON keys?
    • Can we get to ^3.3 this cycle? If not, what is the compensating control and who accepts the risk?
  2. Version Gap:
    • What shipped in 3.2.0? We are upgrading 3.1 → 3.3 blind to one release. Read the changelog before rollout.
  3. Platform Floor:
    • Which services are on Laravel <10 or PHP <8.1? Framework upgrade, or extract/replace the package there?
  4. Upgrade Blast Radius:
    • Call sites relying on $raw = true, on false returns for empty input, on Common\Common / global str(), or on BatchInterface implementations/mocks?
    • How many call sites still use the deprecated instance updateMultipleCondition()? (Mandatory before 4.0.)
    • Do we override or subclass upsert()/import() in any wrapper? (Signature changes are additive but overrides are not free.)
  5. Import / Fast Path (new — the main review area):
    • Which of our ingestion jobs are file-based and could move to import()? What is the current cost of those jobs?
    • Is local_infile permitted by our security baseline and enabled on our managed MySQL/MariaDB instances? Who owns that change, and does it require a maintenance window?
    • Where does the CSV live at job runtime — web node, worker node, shared storage, object store? Do we need a download-to-worker step first?
    • Does fast fall back silently on unsupported drivers (SQLite, SQL Server), or throw? Silent fallback would make CI and production behave differently.
    • Can rules (row validation) and fast be combined? If validation forces the PHP path, what is the real-world throughput penalty and which do we choose per job?
    • How are import files cleaned up, and do they contain PII at rest? What is the retention policy for failed-import artefacts?
  6. New Upsert Semantics:
    • Is upsertReturning() a single statement per driver (native RETURNING on PostgreSQL) or an emulated follow-up read? Is it transaction-wrapped?
    • How does insertOrIgnoreRows() classify a row as "skipped" — duplicate only, or any ignored error? Test with a FK violation and a truncation.
    • For counter upserts: what happens on unsigned underflow and on integer overflow? Are ['max']/['min'] defined for strings and dates, or numbers only?
    • For conditional upserts: how are NULLs compared, and in what timezone are timestamps evaluated? What happens when onlyIf and an arithmetic operator are combined?
  7. Export:
    • Which teams want exportTo(), and does any of it touch customer PII? What access control, audit trail and retention apply?
    • Where do partial or failed multi-part exports get written, and who cleans them up? Could an unbounded export fill a worker's disk?
  8. Use Case Alignment:
    • Are batch operations a core requirement (ETL, migrations, ingestion) or a nice-to-have? With import() in scope, the answer is now more likely "core" — which justifies the wrapper investment.
    • Do we need atomicity across multiple batch calls?
  9. Data Volume & Latency:
    • Maximum realistic batch and import size (10K / 100K / 1M+), and network latency to the database? Round-trip count matters more than statement size post-3.1.
    • Have we re-benchmarked on our hardware and network?
  10. Behavioral Fidelity:
    • Which batch targets depend on observers, events or validation — including deleting/deleted now that deleteByKeys() exists?
    • Do we rely on soft deletes on any table targeted by batch update or upsert operations?
  11. Error Handling & Observability:
    • How do we log and alert on failed or partially applied batches outside the import path?
    • Can pretend() output be captured into our audit trail for destructive batches?
  12. Concurrency:
    • Do any batch targets sit on hot write paths where unlocked bulk updates could lose updates? Counter upserts are row-atomic, but read-then-write sequences in our code are not.

Integration Approach

Stack Fit

  • Laravel Core: Seamless with Eloquent, the query builder and the container. Quoting is delegated to Laravel's grammar, which is why cross-database behaviour improved in 3.0.
  • Framework Versions: Laravel 10, 11, 12, 13.
  • PHP Versions: 8.1–8.5.
  • Database Support:
    • MySQL / MariaDB: Full support, CI-verified. Core CRUD plus fast import via LOAD DATA LOCAL INFILE — requires local_infile=1 on client and server.
    • PostgreSQL: Full support; best-case beneficiary for both the 100-row update cap and fast import via COPY. Most likely to have native RETURNING for upsertReturning().
    • SQLite: Supported for core CRUD; enables local dev and CI. No fast import path — confirm it throws rather than degrading.
    • SQL Server: Historically claimed, not in the CI matrix. New features are the most driver-specific yet; assume unsupported until spiked. pretend() helps review generated SQL but not execution semantics.
  • Infrastructure Prerequisites (new category): MySQL parameter-group change for local_infile; worker-local or shared filesystem access for import files; disk headroom for exports and multi-part output; DB privileges unchanged for client-side COPY FROM STDIN and LOAD DATA LOCAL INFILE (neither requires FILE/superuser, which is a point in favour).
  • Tooling:
    • Artisan: Natural home for batch:import / batch:export commands wrapping the new APIs, with the wrapper enforcing path allowlists and capability checks.
    • Queues: Import and export should be queue jobs with timeout and disk budgets; fast imports of 1M rows complete in seconds but hold a connection and locks for that duration.
    • Testing: Use pretend() to assert generated SQL for destructive paths. Additionally, now test on the production driver — SQLite cannot exercise COPY, LOAD DATA or RETURNING, so CI green is not evidence that import/upsertReturning work.

Migration Path

  1. Phase 0 — Security Remediation (do first): Inventory 2.x pins; upgrade to ^3.3 where Laravel 10+/PHP 8.1+ allows. Where not, upgrade the framework or replace the package. Do not leave 2.x in production.
  2. Phase 0.5 — Breaking-Change Sweep (2.x → 3.x only): Grep for $raw/true, === false/!$result on batch returns, Common\Common, global str(). Migrate instance updateMultipleCondition() to the static form. Update BatchInterface implementations and mocks.
  3. Phase 0.6 — Version Gap Review (new): Read the 3.2.0 changelog before rollout; we have no assessment covering it.
  4. Assessment Phase:
    • Audit existing bulk operations, including previously excluded candidates (composite-key tables, upsert-shaped sync jobs, bulk deletes) and file-based ingestion jobs now addressable by import().
    • Benchmark current performance over the production network path — including the current cost of our existing CSV ingestion jobs, to size the import() opportunity honestly.
  5. Pilot Integration:
    • Start with non-critical batch operations and a non-critical import feed.
    • Build the BatchService wrapper to abstract the package — centralise transaction wrapping, soft-delete filtering, observer-parity hooks, path allowlisting, capability detection for fast, and pretend()-based audit capture.
    • Adopt pretend() first, before any new destructive path ships.
  6. Incremental Rollout:
    • Core writes first (already sequenced in prior assessment): insert() → insertRows() → update() → batchUpdateMultipleCondition() → updateByKeys() → deleteByKeys().
    • Then the 3.3 additions in ascending order of implicit semantics: withoutTimestamps() (documentation only) → insertOrIgnoreRows() → counter upserts → conditional upserts → upsertReturning() → import() → exportTo().
    • import() goes last of the write paths because it carries infrastructure prerequisites (local_infile, worker filesystem), not just semantic ones. exportTo() is gated on a data-governance review, not engineering.
  7. Validation:
    • Existing: performance, data fidelity, empty batches, invalid input, oversized batches, concurrency, deleteByKeys() with and without $force.
    • New: fast import on the production driver with a deliberately malformed CSV (confirm it throws and writes nothing); fast attempted on SQLite (confirm it throws); import() with rules returning correct row-indexed errors; insertOrIgnoreRows() against duplicate and non-duplicate failure rows; upsertReturning() under a concurrent writer; counter upserts at zero, at column max, and on unsigned columns; conditional upserts with NULL, equal and out-of-order values; export of >maxRows records with part-file naming and cleanup.
  8. Production Readiness:
    • Monitor failures/timeouts; retry or circuit-break critical batches.
    • Record in the risk register: 4.0 removes instance updateMultipleCondition(); local_infile is enabled in our DB parameter group and who owns reverting it; exportTo() requires a data-handling approval before customer data is exported.

Compatibility

  • Laravel: 10–13. 8/9 not supported by 3.x.
  • PHP: 8.1+ (8.1–8.5 tested).
  • Database Drivers: MySQL, MariaDB, PostgreSQL, SQLite CI-verified for core CRUD. fast import is verified only on MySQL/MariaDB/PostgreSQL; SQL Server unverified for everything and near-certainly unsupported for fast and RETURNING.
  • Existing Code:
    • None: 3.1 → 3.3 requires no changes. Additive, including the upsert() $options argument and the import() errors key.
    • Low Risk: No breaking changes to models or existing queries.
    • High Risk: 3.x-era breaking changes (return types, removed $raw, removed Common/str()) still affect consuming code.
    • High Risk: Observers, model events and validation do not run — now across a much wider method set.
    • Medium Risk: Deprecated instance updateMultipleCondition() (fatal at 4.0).
    • Medium Risk (new): In-house subclasses or mocks that override upsert() or import() need their signatures updated.
  • Infrastructure Compatibility (new):
    • MySQL/MariaDB local_infile: must be enabled on both client and server; may be blocked by our security baseline; requires a parameter-group change on managed services.
    • File placement: the import path must resolve on the executing process's filesystem — a real constraint for containerised or autoscaled queue workers.
    • PgBouncer / connection poolers: COPY FROM STDIN works in transaction pooling, but verify against our specific pooler mode before assuming the fast path is available in production.
  • Third-Party Packages: No known conflicts. Verify against anything hooking Eloquent events on batch-targeted models.

Sequencing

  1. Phase 0: Security Upgrade to ^3.3 — highest priority; remediates the 2.x injection class. 3.1 → 3.3 is a drop-in update; take it in the same hop. Blocks all else on affected services.
  2. Phase 1: Breaking-Change Remediation — $raw call sites, falsy return checks, BatchInterface implementations, Common/str() usages, deprecated instance updateMultipleCondition(), and any overrides of upsert()/import().
  3. Phase 1.5: Version Gap & Changelog Review — read 3.2.0 notes; confirm nothing behavioural hides in the skipped release.
  4. Phase 2: Insert Operations — Batch::insert(), insertRows(), then pilot insertOrIgnoreRows() on a deduplicating feed and reconcile the skipped list against source data.
  5. Phase 3: Update Operations — replace WHERE id IN (...) loops; adopt DB::raw() arithmetic; adopt withoutTimestamps() where we currently fight timestamp updates; re-baseline performance against the 100-row cap.
  6. Phase 4: Conditional & Key-Based Updates — static batchUpdateMultipleCondition(); updateByKeys() for composite/tenant-scoped tables.
  7. Phase 5: Upsert Family — in order: plain upsert() (validate created_at-only-on-new-rows and enum handling) → counter upserts (inventory/metering tables) → conditional upserts (sync jobs with stale-write protection) → upsertReturning() (workflows needing the PK back). Gate each behind pretend()-based SQL assertions plus driver-specific integration tests.
  8. Phase 6: Delete — deleteByKeys() on a SoftDeletes model first; $force only behind wrapper-level prohibition and dual review.
  9. Phase 7: Import — enable local_infile in a non-production parameter group; prove the fast path in staging on the production driver with malformed input; then migrate one real ingestion job. Layer rules validation on a job that needs row-level error reporting. Keep the old path behind a feature flag for one cycle.
  10. Phase 8: Export — only after data-governance approval; start with internal, non-PII datasets; verify maxRows part-splitting, gzip, and cleanup on failure.
  11. Phase 9: Wrapper & Guardrails — BatchService with mandatory DB::transaction() for multi-call workflows, soft-delete filtering for update/upsert paths, observer-parity hooks, an allowlist of approved methods (default-deny for anything not yet piloted), path allowlisting for import/export, fast capability detection, and pretend()-based audit logging for destructive batches.

Operational Impact

Maintenance

  • Ownership: Assign a named owner. The 2.x vulnerability is exactly the failure mode of a pinned, forgotten dependency. fast import adds a second owner in infrastructure/DB ops — the local_infile parameter is now part of our production configuration and needs an owner and a documented rationale.
  • Upgrade Cadence: 3.1 → 3.3 is low-risk and high-value — take it promptly. Standing rule: security-motivated majors within one sprint; feature releases assessed but not auto-adopted, because the surface area is growing faster than our validation capacity.
  • Version Pinning: Pin ^3.3 and track the changelog. Single-maintainer bus factor remains a register item — and it matters more now that the package sits on ingestion paths.
  • Approved-Method Allowlist (new): The durable control is not documentation, it's an allowlist in the wrapper. Default-deny for import(), exportTo(), upsertReturning(), insertOrIgnoreRows() and the counter/conditional upsert options until each has a named pilot and test coverage. This is the practical answer to "the API grew by eight methods in one release".
  • Deprecation Watch: Instance updateMultipleCondition() still has a hard 4.0 deadline. Scheduled task, not backlog item.
  • Code Ownership Boundary: BatchService is the durable asset; the package is a swappable implementation detail behind it. With import()/exportTo() in scope, the wrapper must also own file handling, so swapping the package out doesn't mean rewriting ingestion.

Support

  • Documentation: Expanding quickly and generally good; core methods, timestamps and now import/export are covered. Thin spots to expect: fast prerequisite failures, onlyIf NULL semantics, insertOrIgnoreRows() classification of non-duplicate errors, and upsertReturning() behaviour per driver.
  • Internal Enablement: The four-item internal note grows to six: (a) observers do not fire — including on upsert, deleteByKeys, insertOrIgnoreRows and import; (b) update/upsert paths do not filter soft deletes even though delete paths do; (c) return contract is int/array/0 for core methods, plus new structured array shapes for import/insertOrIgnoreRows/upsertReturning; (d) pretend() is the safe way to inspect a batch; (e) row validation applies to imports only — it is not a package-wide guarantee; (f) fast needs local_infile, a local file, and the production driver — it will not work in SQLite CI.
  • Escalation: Per-row diagnostics now exist for imports — a genuine support improvement. Update and upsert paths still lack them; keep reconciliation tooling for money/billing batches.
  • Support Leverage: pretend() for SQL review; import errors output for feed triage; the skipped list from insertOrIgnoreRows() for dedupe reconciliation. Together these turn three previously opaque failure classes into reviewable artefacts — capture all three in the wrapper and attach to deploy/job records.

Scaling

  • Throughput: Strong and further improved. The 100-row statement cap still delivers the bulk-write win; fast import moves 1M rows from ~19s to 6–8s, which changes ingestion from "schedule it" to "run it inline or in a short job" for moderate files.
  • Memory/Timeouts: Core batches remain queue-friendly. Import is now the memory/disk consideration rather than the write path: import() streams from disk, but the file must exist complete, and rules validation will hold per-row state — expect rules to be the throughput limiter, not the DB. Revisit timeout budgets downward for fast jobs and keep conservative values for validated imports.
  • Round-Trip Cost: Unchanged from 3.1 — smaller statements mean more of them; benchmark on the production network path, particularly cross-region.
  • Connection and Lock Pressure: Fast imports hold a connection and table/row locks for the duration of the load; a 1M-row load on a hot table during peak traffic is a different risk profile from a batch update. Schedule imports off-peak for large files.
  • Disk: New resource dimension. Imports need source CSV on the worker's disk; exports need output space for possibly multi-part gzipped files. Autoscaled workers with small ephemeral volumes are a plausible failure point.
  • Verification Economics: pretend() plus SQLite still make core-SQL regressions cheap to catch. That advantage does not extend to import, fast or upsertReturning — those require the production driver, which raises the cost of a proper test suite for the new features. Budget for it explicitly.

Failure Modes

  • Security (2.x only): SQL injection via index values/JSON keys. Critical. Should be zero after ^3.3.
  • local_infile Exfiltration Vector (new): Enabling client-side file loading means a malicious or spoofed MySQL server could request arbitrary client files. Severity depends on our TLS and endpoint trust posture. Mitigation: enable only on connections to our own verified DB endpoints, enforce TLS verification, and document the setting as a security-relevant configuration.
  • Silent Side-Effect Skipping: Observers, events and validation do not execute. Symptom is stale derived data, not an exception — hardest to detect. Widened again in 3.3: import(), insertOrIgnoreRows(), upsertReturning() and the counter/conditional upsert paths all bypass hooks. Mitigation: explicit post-batch hooks in the wrapper; treat "did anything downstream need to know?" as a mandatory review question for every new batch call site.
  • Import Writes Nothing / Throws at Runtime (new): If local_infile is off, the file isn't on the worker, or the driver is SQLite, the fast path fails — hopefully loudly. Symptom: ingestion jobs fail in production only, because CI runs SQLite. Mitigation: capability check at boot/health-check, and a staging run on the production driver before enabling.
  • Silent Data Corruption via LOAD DATA (new, mitigated but verify): Default MySQL LOAD DATA stores out-of-range and malformed values with warnings. The release claims this now throws and writes nothing. Mitigation: prove it with truncated integers, over-length strings, invalid dates and bad encodings before trusting any fast import.
  • Misclassified Skips (new): If insertOrIgnoreRows() reports non-duplicate rejections as duplicates, we silently lose rows while the report says "deduplicated". Symptom: missing records that nobody investigates because the report looked clean. Mitigation: test with FK violations and truncation, not just duplicate keys.
  • upsertReturning() Race (new): If emulated as a follow-up read, returned rows may not match what this call wrote. Symptom: wrong IDs propagated into downstream work. Mitigation: confirm single-statement semantics per driver, or wrap in a transaction and validate against a known key.
  • Counter Upsert Numeric Edge Cases (new): Unsigned underflow and integer overflow behave differently across MySQL/MariaDB/PostgreSQL. Symptom: errors on some drivers, wraparound or clamping on others. Mitigation: test at zero and at column maximum in CI per driver.
  • Conditional Upsert Semantic Drift (new): NULL comparisons, equal values and timezone mismatches can make a "guard" behave as a no-op or as an unconditional overwrite. Symptom: stale rows preserved or fresh rows overwritten. Mitigation: explicit tests for NULL/equal/out-of-order; pretend() to inspect the generated predicate.
  • Export Data Leakage (new): PII written to arbitrary paths with no access control or audit. Symptom: data found where it shouldn't be, or retained past policy. Mitigation: path allowlist in the wrapper, encryption/retention policy, and governance sign-off before non-internal use.
  • Disk Exhaustion (new): Large export parts or leftover import CSVs fill a worker volume. Symptom: unrelated jobs fail on the same node. Mitigation: size limits, post-job cleanup, disk alerts on workers.
  • Unexpected Hard Deletes: deleteByKeys() soft deletes by default; $force = true is a physical delete with no observer interception. Mitigation: forbid $force outside the wrapper, require pretend() evidence and second review.
  • Soft-Deleted Rows Updated/Upserted: Update and upsert paths lack deleted_at filtering. Symptom: mutated or resurrected deleted records. Mitigation: filter in the wrapper.
  • Partial Application Across Calls: Per-call atomicity is good; multi-call workflows half-apply. Mitigation: DB::transaction() in the wrapper — and be careful not to accidentally wrap a 1M-row import in a business transaction.
  • Upgrade-Time Exceptions: $raw = true throws by design; deprecated updateMultipleCondition() becomes fatal at 4.0.
  • Lost Updates Under Concurrency: No row locking. Counter upserts are row-atomic; our read-modify-write sequences are not. Mitigation: SELECT ... FOR UPDATE or version columns on contended paths.
  • Overwriting Good Data with Bad Input: Reduced by pre-write InvalidArgumentException and import validation, but update/upsert paths still bypass model validation. Validate upstream.

Ramp-Up

  • Effort to First Value: Still low for core writes — minutes to convert a loop to Batch::update(). No longer low for the package as a whole: the highest-value new feature (fast import) needs infrastructure work before it can be used at all.
  • Effort for the 3.1 → 3.3 Upgrade: Very low — composer update, no code changes. Only in-house overrides of upsert()/import() need attention. No reason to defer.
  • Effort for the 2.x → 3.x Upgrade: Low-to-moderate, dominated by the breaking-change sweep and fidelity regression testing. Unchanged.
  • Effort for Safe Production Use of the 3.3 Additions: Moderate to high, and higher than 3.1. Roughly:
    • Low: withoutTimestamps(), counter upserts on a non-critical table.
    • Moderate: insertOrIgnoreRows(), conditional upserts, upsertReturning() — all need per-driver tests beyond duplicate/timestamp cases.
    • Moderate-high: fast import — DB parameter change, worker filesystem, malformed-input validation, capability detection, rollback behaviour. It's an infrastructure project with a PHP API attached.
    • Moderate + governance: exportTo() — technically simple, but blocked on data-handling approval.
  • Effort for Safe Production Use Overall: Moderate. The wrapper plus the original three guardrails (transactions, soft-delete filtering, observer parity) and the 3.1 additions (forbid bare $force, pretend() assertions) now gains two more: an approved-method allowlist and production-driver test coverage for anything not exercised by SQLite.
  • Learning Curve: The API is shallower per-method than ever (which is the risk) but the decision space is now genuinely wide — insert vs. insertRows vs. insertOrIgnoreRows vs. upsert vs. upsertReturning vs. import. Developers must internalise two things: (1) batch operations are raw SQL-level writes, not Eloquent writes, and (2) the new import/export features are file-and-infrastructure operations with their own prerequisites. pretend() remains the best teaching tool for (1); a short staging walkthrough of a failing fast import is the best teaching tool for (2).
  • Recommendation: Approve adoption of ^3.3. Treat the 2.x → 3.x upgrade as security remediation with a deadline; take 3.1 → 3.3 immediately as a no-risk, no-code-change win; read the skipped 3.2.0 changelog first. Then sequence deliberately: core writes and withoutTimestamps() now; insertOrIgnoreRows() and the upsert family as named pilots with per-driver tests; fast import as a separate infrastructure-enabled workstream with DB ops; exportTo() gated on data-governance approval. Keep instance updateMultipleCondition() migration on the calendar for 4.0.
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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table