eloquent_flutter 0.1.2
eloquent_flutter: ^0.1.2 copied to clipboard
Laravel Eloquent-style ORM for Flutter and Dart on top of Drift. Chainable queries, casts, soft deletes, relationships, observers, pagination, and a schema DSL.
Changelog #
0.1.2 — 2026-09-03 #
Trim pubspec.yaml description to 164 chars so it lands inside pub.flutter-io.cn's
60–180 char ideal range and the static-analysis scorer awards full marks.
0.1.1 — 2026-09-03 #
Shorten pubspec.yaml description so it fits the pub.flutter-io.cn 180-char
guidance and passes the static-analysis check.
0.1.0 — 2026-09-03 #
First public release. Chainable Eloquent-style ORM for Flutter and Dart
on top of Drift: Model, QueryBuilder, relationships, eager loading,
observers, pagination, casts, soft deletes, dirty tracking, schema +
migrations, transactions, raw SQL, reactive streams.
Added #
$casts— per-column cast registry onModel.int,double,string,bool,date,datetime,json,array(seeCastType). Read viaModel.getAttribute(key), write viaModel.setAttribute(key, value). Applies automatically throughModelQuery.create(map).isDirty/isClean/wasChanged/getOriginal/$original/$dirty/$changes— Laravel-style dirty tracking.save(),update(), andrefresh()snapshot the row; pending writes viasetAttributemark columns dirty.- SoftDeletes mixin —
with SoftDeletes<...>addsdelete()(setsdeleted_at),forceDelete(),restore(), and atrashedgetter. QueryBuilder defaults to excluding trashed rows on tables with adeleted_atcolumn;withTrashed()/onlyTrashed()/withoutTrashed()flip the filter. saveQuietly/deleteQuietly/Model.withoutEvents(...)— skip observer dispatch on individual calls or in a callback scope.pluck(column)/pluck(column, key)— single-column lists orMap<key, model>projections.value(column)— first row's column value, or null.sole()— assert exactly one row matches; throwsModelNotFoundExceptionorMultipleRecordsFoundException.firstOrCreate/firstOrNew/updateOrCreate/upsert— idempotent creation helpers.whereHas/orWhereHas/has/orHas/doesntHave/whereDoesntHave/orDoesntHave/orWhereDoesntHave— relationship existence predicates, fold into the parent WHERE.withCount/withSum/withAvg/withMin/withMax— correlated aggregate subquery columns attached to each parent row.onlyTrashedexception —ModelNotSoftDeletableExceptionthrown byonlyTrashed()when the table has nodeleted_atcolumn.- Benchmark suite —
benchmark/eloquent_vs_drift.dartruns INSERT / FIND / WHERE / ALL / COUNT / SAVE head-to-head against raw drift, prints µs/op and overhead %.
Fixed #
ModelQuery.createnow honors$casts— values land in the database as the cast's storage type, not as whatever the user typed.wasChangedaftersave— columns written throughsetAttributestay in the change-set until the nextsave()orrefresh(), matching Laravel semantics.- SoftDeletes on
ModelQuery— the implicitdeleted_at IS NULLfilter is now applied byall(),find(),first(),count(),exists(), andmin/max/avg/sum. min/max/sum/avgrespect the soft-delete filter — previously they queried every row, including trashed ones, because they emitted rawcustomSelectSQL that skipped_trashedExpression().restore()+refresh()—restore()was no-op on the in-memory instance untilrefresh()re-read the row; now the underlying UPDATE applies and the snapshot is re-taken.withCountaggregate type —comments_countis now anint(wasObject?in some code paths).sumnull result — when no rows match,sum()returns0instead of throwing on a null column read.WithTimestampsaftersave+refresh— the mixin'spersistTimestampsno longer throws on the abstractrefresh(); timestamps are now correctly visible in the post-refresh row data.
Changed #
Eloquent.dispose()no longer closes the database — the database is owned by the caller.dispose()now only drops the package's reference to the database. The caller is responsible forclose().- Lazy snapshot in
Modelconstructor —_originalis left empty afterModel(data); populated on first call togetOriginal, aftersave(), or afterrefresh(). Cuts the per-row construction cost on read paths by ~75%. Model.wrap(data)simplified — now just flips$exists = true. The previous implementation did a redundant$wrapplus a second snapshot. Called on every fetched row.QueryBuilder.count()fast path — when no user predicates are present, emits a staticSELECT COUNT(*) ... WHERE "deleted_at" IS NULLthat hits SQLite's prepared-statement cache. Dropped COUNT overhead from ~600% to ~13%.
Performance #
benchmark/eloquent_vs_drift.dart is the reference. Numbers below are
overhead vs the equivalent raw drift statement on a 1k-row table on
SQLite in-memory.
| Operation | Overhead |
|---|---|
count() (no predicates) |
~13% |
all() (1k rows) |
~9% |
where(...).orderBy().limit().get() |
~50% |
find(id) |
~63% |
create(map) (fair vs insert+read-back) |
~175% |
new Model(...).save() (vs bare insert) |
~262% (lower bound) |
The wrapper pays for two things it cannot avoid: (a) one extra SELECT round-trip to populate auto-incremented / defaulted columns after INSERT, and (b) per-instance state (casts, dirty tracking, snapshot). The 0.1.0 release keeps per-row wrapper overhead at ~50ns — about 5% of drift's own round-trip — by removing redundant work in the hot path.