dust_dart 0.2.0
dust_dart: ^0.2.0 copied to clipboard
Annotations and runtime support for generating Dart code with dust.
Changelog #
All notable changes to dust_dart are documented in this file.
The format is based on Keep a Changelog.
[Unreleased] #
0.2.0 - 2026-09-13 #
Added #
-
Option<T>operations from Rust:Option.fromNullable,toNullable,toIterable,contains,isSomeAnd,isNoneOr,unwrap,expect,mapOr,mapOrElse,inspect,filter,and,or,orElse,xor,zip,zipWith,unzip,okOr,okOrElse,flattenandtranspose. A presentnullis preserved everywhere excepttoNullable, lazy forms do not call callbacks whose branch does not apply, andunwrapandexpectthrowStateErroronNone. -
UnsafeSql—fetch,fetchAs<T>(sql, parameters, mapper), andexecute— for the administrative SQL build-time validation cannot reach: migrations,EXPLAIN, one-off operations.It hangs off
DatabaseClientasunsafe, so a generated facade exposes it and an executor does not. A request handler is handed an executor, and no cast takes an executor to aDatabaseClient, which is the difference fromraw:db as Executoralways succeeded, because every pool, connection and transaction implementsExecutor.The decoder is passed explicitly rather than resolved from the row type. Generated terminals exist only for validated queries, and that asymmetry is deliberate — the checked path is the ergonomic one.
Changed #
Important
Breaking. Every inline query terminal now returns Result<T, SqlxError>
instead of throwing. This is why the version is 0.2.0 rather than 0.1.5: a
pubspec asking for dust_dart: ^0.1.4 resolves up to 0.1.x but stops before
0.2.0, so an app upgrades when it says so rather than on the next pub get.
Upgrading is dust_dart: ^0.2.0 and a dust build to regenerate.
Added #
-
Tests for the
SqlxErrorfactories and theDatabaseClienthelpers, taking the package to 100% line coverage. -
DatabaseClient.migrate(), applying the migrations a database was generated with. SQLite applies them while opening, so it returnsOk; PostgreSQL is reached over a network and applies them here.
Changed #
-
Database: the pool vocabulary follows SQLx.
DatabaseExecutorisExecutor— the type a query runs against, which is what SQLx'sExecutormeans —DatabaseConnectionisConnection, andDatabaseTransactionisTransaction.Poolis unchanged.SqliteConnectOptionsalready matchedsqlx-sqliteexactly, so this finishes a precedent rather than setting one.The old
ConnectionandTransactionmarker types are gone: they existed only as aliases of the types that now carry those names.
Removed #
-
The
SqlxDrivertypedef. -
queryRaw,QueryRaw,RawSql,RawSqlx, and theExecutorinterface.ExecutorwasExecutorplus arawchannel, and every pool, connection and transaction implemented it — sodb as Executoralways succeeded and the fence stopped nobody. Withrawgone the type has nothing left to add, so it goes too.Pool,ConnectionandTransactionnow extendConnection/Transactiondirectly, andtransaction()hands its callback aTransaction.Unchecked SQL is
UnsafeSqlon the database facade. A DAO or handler holding an executor cannot reach it.
Changed #
-
Database: the query terminals return
Result.QueryAs.fetchOneWith,fetchOptionalWithandfetchAllWith,QueryScalar.fetchOneandfetchOptional,QueryRaw.fetch,QueryExecute.execute, and the generatedextension $TypeQueryterminals all hand backResult<T, SqlxError>.Generated
@SqlxDaomethods have always returnedResult; the inline path wrapped the same executor call in an unwrap that threwStateError('SQL operation failed: ...'), destroying the typedSqlxErroron the way out. Two paths through the same executor answered a failed query in two different ways, and only one of them could be handled.
Migrating #
A call that used the value directly now matches on the result:
// Before
final user = await queryAs<UserRow>(sql, [id]).fetchOne(db);
print(user.email);
// After
final user = await queryAs<UserRow>(sql, [id]).fetchOne(db);
switch (user) {
case Ok(:final value):
print(value.email);
case Err(:final error):
print('lookup failed: $error');
}
To keep the throwing behavior at one call site, unwrapOrElse supplies it:
final user = (await queryAs<UserRow>(sql, [id]).fetchOne(db))
.unwrapOrElse((error) => throw StateError('$error'));
@SqlxDao methods are unaffected — they already returned Result.
0.1.4 - 2026-09-03 #
Added #
RowDeserializer<T>, the row-side mirror ofDeserializer<DartT, JsonT>. Reading a row constructs a value, so — exactly as withDeserialize— there is no instance to declare the method on, and a generated witness object carries the capability instead.RowMapperDeserializer<T>, which wraps a plainRowMapper<T>function as aRowDeserializer<T>, andRowDeserializerMapper.asMapper, the reverse view. The latter mirrorsDeserializerJsonMirroron the JSON side.QueryAs.fetchOneWith,fetchOptionalWith, andfetchAllWith, which take the row mapping as an argument. For a row type Dust does not generate.
Removed #
RowMapperRegistryandregisterRowMapperare gone, along with theregisterRowMapperinitializer generated row files used to emit. A process-wideMap<Type, RowMapper>filled by top-level initializers made a missing row mapping a runtimeSqlxError.decode, and whether it hit depended on whether the part file had been imported anywhere in the isolate.QueryAs.fetchOne,fetchOptional, andfetchAllare no longer instance methods. They are generated per row type instead, asextension $TypeQuery on QueryAs<Type>. Call sites are unchanged —queryAs<Order>(sql, args).fetchOne(db)still works — but Dart now resolves the terminal from the static type at compile time, so a row type with no@Derive([FromRow()])has no terminal and the call does not compile. An instance member would always beat an extension member, which is why they had to move.
Changed #
- Generated row output follows the naming the other derives use. The public
extension TypeFromRow on Typewith itsstatic fromRowis replaced by a private_$TypeFromRow(Row row)function, mirroring serde's_$TypeSerialize, plus the public$TypeRowDeserializerwitness, mirroring$TypeSerializer.
Migrating
Nothing changes for @SqlxDao, and nothing changes for a queryAs<T> call
whose T derives FromRow:
// unchanged
queryAs<Order>(sql, args).fetchOne(db);
Two things move:
// before — a public generated extension
final order = OrderFromRow.fromRow(row);
// after — the public generated witness
final order = const $OrderRowDeserializer().deserialize(row);
// before — a mapper argument on the query, resolved through the registry
queryAs<Legacy>(sql, args).fetchOne(db);
// after — the mapping goes to the terminal
queryAs<Legacy>(sql, args).fetchOneWith(db, Legacy.fromRow);
A row library that uses a show clause on package:dust_dart/db.dart needs
QueryAs and Executor added to it.