patrick-hanna / database-crud-buffer
Buffer and batch Laravel database inserts, upserts, updates, and deletes into a minimal set of merged SQL statements.
Package info
github.com/patrickjames242/database-crud-buffer
pkg:composer/patrick-hanna/database-crud-buffer
Requires
- php: ^8.2
- illuminate/database: ^11.0 || ^12.0
- illuminate/support: ^11.0 || ^12.0
- php-ds/php-ds: ^1.4
Requires (Dev)
- orchestra/testbench: ^9.0 || ^10.0
- pestphp/pest: ^3.0
README
Buffer Laravel database writes — inserts, upserts, updates, and deletes — and flush
them as a minimal set of merged SQL statements. Rows for the same table collapse
into single multi-row statements, repeated writes to the same key merge, and
compatible deletes fold into where … in (…) clauses.
Built on Laravel's query builder (illuminate/database) and the DB facade.
Installation
composer require patrick-hanna/database-crud-buffer
Requires PHP 8.2+ and Laravel 11 or 12. The php-ds
polyfill is pulled in automatically; installing the ds PHP extension improves
performance but is optional.
Usage
use PatrickHanna\DatabaseCrudBuffer\DatabaseCrudBuffer; $buffer = new DatabaseCrudBuffer(); $buffer->insert('accounts', ['id' => '1', 'legal_name' => 'Acme']); $buffer->insert(Account::class, ['id' => '2', 'legal_name' => 'Globex']); $buffer->flush();
Every write target accepts either a literal table name or an Eloquent model class — the model's table name is resolved automatically.
By default the buffer auto-flushes once ~50,000 buffered items accumulate, inside a database transaction. Both are configurable:
$buffer = new DatabaseCrudBuffer( maxSize: 10000, // null to disable auto-flush useTransaction: false, // flush without opening a transaction );
Insert
$buffer->insert('accounts', ['id' => '1', 'name' => 'Acme']);
Rows are buffered per table and emitted as one multi-row insert at flush time.
Column sets are normalized (missing columns become null) and rows that normalize
to the same shape are deduplicated.
Upsert
$buffer->upsert( tableOrModelClass: 'accounts', uniqueBy: ['id'], row: ['id' => '1', 'name' => 'New'], updateColumns: ['name'], // optional; defaults to the buffered columns originalValues: ['id' => '1', 'name' => 'Old', 'email' => 'old@example.com'], );
Writes bucket by table, unique key, and update-column list. Repeated writes to the
same unique row merge before flush. originalValues supplies fallback data to build
a complete insert row without widening the on-conflict update set.
Update
$buffer->update('accounts', 'id', 'account-1', ['name' => 'Acme']); $buffer->update('accounts', ['status', 'active'], ['email' => 'a@example.com']); $buffer->update('accounts', [['status', 'active'], ['id', 'a']], ['name' => 'Acme']); $buffer->update('accounts', ['id', 'in', ['a', 'b']], ['status' => 'active']);
Updates sharing a normalized filter set merge into a single statement, regardless of the order the filters were supplied.
Delete
$buffer->delete('accounts', 'id', 'account-1'); $buffer->delete('accounts', [['status', 'active'], ['id', 'a']]); $buffer->delete('accounts', ['id', 'in', ['a', 'b']]);
Deletes that differ in exactly one =/in filter fold into a single
where … in (…) statement at flush time.
Flushing
$buffer->flush(); // wraps the work in DB::transaction(...) $buffer->flushWithoutTransaction(); // caller owns the transaction boundary
Flush order is always inserts → upserts → updates → deletes. On success the buffer is cleared and its size counter resets.
Testing
composer install
composer test
License
MIT