Implement a custom dORM engine¶
A custom engine connects the framework contracts to another storage system. The generated model source and repository calls remain the application-facing layer; the engine supplies the query, reference, and relationship implementations.
This guide assumes that the model and repository flow is already understood. It uses a package named dorm_example_database as a placeholder for the engine package you are creating.
Create the engine package¶
Create a Dart package and add the framework:
dart create -t package dorm_example_database
cd dorm_example_database
dart pub add dorm_framework
dart pub add dev:lints
The package name is not part of the framework contract. The current engine packages use the dorm_<backend>_database naming convention.
Create the implementation under lib/src/ and expose the public entry point from lib/dorm_example_database.dart.
Implement the supported engine boundary¶
The official extension boundary is the set of framework contracts:
The same query type Q is used by the engine's filters, reference, and
relationship implementations. P extends PageRequest declares which page
request types the engine accepts.
The engine entry point supplies the latter two implementations to generated Dorm code:
class Engine implements BaseEngine<Query, OffsetPageRequest> {
final BackendConnection connection;
const Engine(this.connection);
@override
BaseReference<Query, OffsetPageRequest> createReference() {
return Reference(connection);
}
@override
BaseRelationship<Query> createRelationship() {
return Relationship(connection);
}
}
DatabaseEntity calls createReference and createRelationship when it is constructed. Repository calls then delegate to those created objects.
Implement the backend query value¶
Implement every method required by BaseQuery<Q>:
class Query implements BaseQuery<Query> {
final BackendQuery value;
const Query(this.value);
@override
Query whereValue(String key, Object? value) {
return Query(this.value.whereEqual(key, value));
}
@override
Query whereText(String key, String prefix) {
return Query(this.value.whereStartsWith(key, prefix));
}
@override
Query whereDate(String key, DateTime date, DateFilterUnit unit) {
return Query(this.value.whereDate(key, date, unit));
}
@override
Query whereRange<R>(String key, FilterRange<R> range) {
return Query(this.value.whereRange(key, range));
}
@override
Query limit(int count) {
return Query(this.value.limit(count));
}
@override
Query offset(int count) {
return Query(this.value.offset(count));
}
@override
Query sorted(String key, {bool ascending = true}) {
return Query(this.value.sorted(key, ascending: ascending));
}
}
The BackendQuery calls above are placeholders for the operations supplied by the storage driver. BaseQuery does not define a portable serialized query language; Query is the backend-specific representation consumed by the engine.
Existing engine query types wrap different values:
- BLoC stores a table operation representation;
- Firebase wraps a Firebase Database query;
- MySQL stores SQL text and parameter maps.
The current implementations return a new query value when applying an operation. The interface requires a Q result for each method, but it does not independently enforce immutability.
Implement the reference¶
Implement BaseReference<Query, OffsetPageRequest> for the backend's direct
entity operations when the backend supports offset pages:
class Reference implements BaseReference<Query, OffsetPageRequest> {
final BackendConnection connection;
const Reference(this.connection);
@override
Future<Model?> peek<Data, Model extends Data, I extends Object>(
Entity<Data, Model, I, Creation<Data, I>> entity,
I id,
) async {
// Read one backend record and return entity.fromJson(id, data).
throw UnimplementedError();
}
// Implement the remaining operations. The put and putAll methods must be
// generic over C and receive Entity<Data, Model, I, C> together with C or
// List<C>, preserving the entity's creation type.
}
Each reference method receives an Entity. Use that entity for the model-specific operations instead of depending on generated model classes:
Entity operation |
Use at the backend boundary |
|---|---|
schema |
Table/storage name, field names, and foreign-key metadata |
primaryKeyCodec |
Encode and decode simple or composite identities |
fromData |
Construct a model from Dependency, generated identity, and Data |
identify |
Obtain the identity of a model being written |
toJson |
Convert a data/model value into backend field values |
fromJson |
Convert a backend map/row into a generated model |
convert |
Apply Data to an existing model while preserving its identity and other model fields |
The reference must preserve the framework operation semantics: peek returns a nullable single model, collection reads return lists, put returns a created model, and write/delete methods complete with Future<void> where specified.
pull and pullAll return streams. The framework exposes these methods as a common vocabulary, but it does not require every engine to provide continuous realtime events. The MySQL implementation currently emits an initial read only; a custom engine must document the stream behavior it actually provides.
Implement relationships¶
Implement BaseRelationship<Query> for the four association forms:
Each method receives readable sources and a callback that supplies a target ID or filter. Return the corresponding framework Association type and Join result shape.
The relationship implementation must preserve the framework cardinality semantics:
- required to-one paths omit parents without a target;
- nullable to-one paths preserve parents with
null; - to-many paths flatten related values;
- empty-preserving to-many paths retain parents with an empty list;
- many-to-many paths return the framework's nullable pair shape.
A source can expose a TableRelationPlan, CompositeRelationPlan, or another RelationPlan. Use a plan when the backend can execute a relationship structurally, such as a grouped SQL read. Keep readable operations as the fallback for sources that cannot be optimized.
The relation plan is an optimization hook, not a performance guarantee. The framework still expects the readable source operations and the documented result semantics.
Publish the engine entry point¶
Expose the engine-facing types from the package barrel:
library dorm_example_database;
export 'src/engine.dart' show Engine;
export 'src/filter.dart' show Filter;
export 'src/query.dart' show Query;
The current engine packages guarantee Engine, Filter, and Query through their barrel files. Concrete Reference and Relationship classes under lib/src are not the confirmed application-facing construction API. A custom package may keep those classes internal to its engine implementation.
Validate the engine¶
Add tests for the framework contract and run them from the custom engine package:
Test at least the operations implemented by the backend:
- simple and composite identity encoding where supported;
put,putAll,peek,peekAll,push,pushAll,patch, and deletion;- empty and filtered reads;
pullandpullAllstream behavior;- all relationship cardinalities used by the engine;
- serialization and deserialization of generated fields;
- backend errors and invalid identity handling.
Framework relationship-path tests and MySQL relationship tests demonstrate the kinds of contract cases that can be exercised without a full application. Live database tests require the external service and schema used by the engine.
Separate supported extension from incidental access¶
| Extension mechanism | Status |
|---|---|
Implement BaseEngine, BaseQuery, BaseReference, and BaseRelationship |
Officially supported |
Implement Entity, PrimaryKeyCodec, RelationSource, or relation plans through their public framework contracts |
Officially supported at the framework contract level |
| Use concrete engine constructors or types that are exported by a package barrel | Possible, subject to that package's public API |
| Import another package's lib/src classes or rely on generated normalization helper names | Incidental implementation use; not guaranteed |
| Depend on a backend's private transaction, cache, or driver behavior through an internal class | Incidental and backend-specific |
Transaction support is an optional framework capability. A custom engine may
implement TransactionalEngine<Q, P> when it can provide the callback,
commit, rollback, and temporary-context behavior required by that contract.
Offset pages are part of the common read surface; cursor requests remain
unsupported by the current engines.