Open a database topic
Choose an entrypoint
Annotate ordinary DTO classes, generate their typed client, choose a database entrypoint, and use the generated table getters. Each query belongs to that database view. Migrations have a separate, fixed history for the chosen engine.
Import the generated client, the engine, and the capabilities your application uses:
import 'package:my_app/models.orm.dart';
import 'package:orm/sqlite.dart';
import 'package:orm/orm.dart';
import 'package:orm/sql.dart';
Future<void> main() async {
final engine = await sqlite(const SqliteOptions.file('app.sqlite'));
final db = Database.fromSql(engine);
try {
final List<String> titles = await db.task
.where((t) => t.done.eq(.value(false)))
.select((t) => t.title)
.get();
print(titles);
} finally {
await db.close();
}
}
The engine factory returns an owning SqlDatabase. Database.fromSql(engine) adds
a typed model view over the same runtime and connection resources; closing either
root view closes those resources. PostgreSQL's
postgres(options) creates a lazy pool synchronously; it first connects when
an operation acquires a connection. The other connection helpers return futures
that complete after opening and validating their connection.
Imports
SQLite uses sqlite.dart on native platforms and the web. SqliteOptions.memory()
is portable. persistent(name, nativePath: ...) uses the native path or the named
browser database. Flutter bundles its browser resources automatically; see
SQLite Web setup. Platform transports are internal.
| Module or use | Import | Purpose |
|---|---|---|
| Values | package:orm/values.dart |
Codecs, decimal/temporal/domain value contracts |
| Driver contracts | package:orm/driver.dart |
SQL commands, raw results, connections and capabilities |
| Engine adapters | package:orm/sqlite.dart, postgres.dart, mysql.dart, mariadb.dart |
Engine factory, driver, options and backend marker |
| SQL | package:orm/sql.dart |
SqlDatabase, typed expressions, queries, selections, SQL descriptions, sessions and observations |
| Physical schema | package:orm/schema_model.dart |
Columns, keys, constraints and indexes |
| Models | package:orm/orm.dart |
Database, model queries, typed writes and subscriptions |
| Declarations | package:orm/schema.dart |
@Model, @Projection, column, key and relation annotations |
| Migration or snapshot | package:orm/migrate.dart |
Frozen history, steps, checksums and execution against SqlDatabase |
| Project configuration | package:orm/config.dart |
defineConfig with direct model/output/history paths and a lazy connection factory |
| Package CLI | package:orm/cli.dart |
runOrmCli(arguments) loads runnable project configuration |
| Migration-only executable | package:orm/migrate_cli.dart |
Programmatic commands with a static history and raw connection factory |
| Generation scripts | package:orm/generate.dart |
Analyze declarations and write derived Dart files |
| build_runner configuration | package:orm/builder.dart |
Builder factories only |
Generated clients import and re-export the original annotated DTO classes and add typed table getters. They do not define replacement row classes. Unrelated application helpers remain in their source libraries. Types used inside rows, such as a custom ID or enum, remain owned by their defining library. Import that library when constructing those values. Use import prefixes for multiple generated clients.
Each shared type has one public owner. Import values.dart for codecs and domain
values, driver.dart for execution options and dialects, and schema_model.dart
for physical metadata. Generated files import their dependencies explicitly.
select is the SelectQuery extension from sql.dart; import it in every
library that selects query results, including ordinary generated model queries.
Importing only orm.dart and a generated client does not bring select into
scope. Neither library re-exports the SQL module.
The retired runtime.dart and drivers/ facades have no compatibility aliases.
The SQL layer does not depend on model queries or generation. Migrations use raw
sessions and physical metadata without current model declarations. Runtime
libraries do not import analyzer/build tooling or choose a concrete adapter.
The single package still resolves its tooling dependencies during pub get.
Import public paths; lib/src/ is implementation detail.
For raw SQL, use final engine = await sqlite(options) and import sql.dart
for engine.raw(...), engine.query(...) and streaming. Add the optional model
view with Database.fromSql(engine). db.sql exposes that same runtime to
migrations and catalogs. This does not open a second pool.
For offline compilation, import sql.dart and a generated client:
final command = SqlBuilder(SqlDialect.postgres)
.user.where((u) => u.email.eq(.value('seven@example.com')))
.select((u) => u.id)
.compile();
// command.sql and separately bound command.parameters; no connection opened.
Executing an unbound builder fails with QUERY.UNBOUND. Generated table getters target QueryContext, implemented by SqlBuilder and
the ORM. Raw Sql descriptions are independent of a context: compile with
statement.compile(capabilities), or execute with db.raw(statement) and
db.query(statement.returns(resultShape)). Types remain static; missing engine
variants fail before I/O. See raw SQL.
Three kinds of Dart files
models.dart contains the current DTO declarations. models.orm.dart and
models.snapshot.dart are reproducible outputs; regenerate them after editing the
declaration. migrations/m0001_*.dart is reviewed history. It contains its own frozen
physical schema and fixed fingerprint, independent of the latest models.
Generation validates the structure and types. A migration history selects SQLite, PostgreSQL, MySQL or MariaDB and validates that engine's capabilities. A SQLite-only declaration may use a virtual indexed column; PostgreSQL restrictions should not prevent its generation. The selected engine still rejects unsupported DDL before applying it. Changing a connection URL does not convert a migration history.
Shape, identity and selection
Each original annotated class is its complete row type. A User and an unrelated class with
identical fields remain different Dart types. Record projections remain
structural. Two models can declare identical field shapes while keeping distinct row types
and SQL table identities. table.alias() creates a new occurrence for joins
and self joins. Capturing a field from an unrelated query is a scope error.
select((u) => u.email) returns List<String> from get(). .row composes SQL
expressions into a positional Record; .map(...) shapes decoded values into a
named Record or DTO. A Dart mapper does not become a SQL expression. Runtime
fields({...}) selection deliberately returns Map<String, Object?>.
alias.optional(selection) checks that alias's presence. It does not prove another
left-joined alias exists. Give each optional alias its own guard, or use nullable
expressions. Relationships follow the same rule: one() can be absent, required()
checks presence, and many() loads a collection. Relationship execution
documents joins, batched queries and consistency boundaries.
Preparing and executing
where, select, orderBy, take, skip and map return new descriptions.
Repeated where adds predicates; orderBy, take and skip replace their own
settings. compile()/inspect() perform no I/O. A stream acquires a cursor when
listened to. Choose the read operation by the expected result:
| Operation | Empty result | One row | Multiple rows |
|---|---|---|---|
get() |
Empty list | List of one | List of all |
first() |
Cardinality error | The row | First row |
firstOrNull() |
null | The row | First row |
single() |
Cardinality error | The row | Cardinality error |
singleOrNull() |
null | The row | Cardinality error |
A selected SQL NULL still counts as a row. Strict reads return that null when their result type allows it. Use a Record projection if an optional scalar read must distinguish an absent row from a row containing null.
Generated create(...) accepts literal field values and immediately returns a
complete model. Omitted optional values use their declared defaults; explicit
null remains distinct from omission. patch(...) updates named literal fields
and returns the affected-row count. update(input), insert(input),
insertMany(inputs) and delete() also execute and return Future<int>.
Await these methods directly:
await db.user.byId(id).patch(nickname: null);
await db.user.byId(id).delete();
Named create and patch accept only model fields. For execution options use
typed inputs with update(input, options: ...), insert(input, options: ...),
or the advanced plan path. No model field name is reserved for those controls.
Typed insert and patch inputs are immutable data that can be passed between
functions or combined before executing:
final request = userPatch(nickname: null);
final policy = userPatch.values(score: .expression((u) => u.score.plus(1)));
final patch = userPatch.overlay([request, policy]);
if (!userPatch.isEmpty(patch)) {
await db.user.byId(id).update(patch);
}
Use .plan for inert write descriptions, RETURNING and explicit preparation.
Constructing a plan or an insert/update/delete description evaluates no input
expressions or client defaults and executes no SQL:
final insert = db.user.plan.insert(userInsert(email: 'new@example.com'));
final prepared = insert.prepare();
print(prepared.compile().sql);
await prepared.execute();
final nickname = await db.user.byId(id).plan
.update(userPatch(nickname: null))
.returning().select((u) => u.nickname)
.single(options: options);
overlay applies layers in order; a later supplied value wins and .keep()
leaves the earlier intent intact. Use the generated factory's .values(...)
for WriteValue.set, .keep, .databaseDefault, and .expression intents.
userInsert.overlay(base, patches) preserves insert-only fields such as a
generated identity. Input objects expose data fields without composition methods.
Each terminal on a planned write prepares fresh assignments and defaults. Explicit
prepare() validates and freezes a core Mutation or BatchInsert; repeated
execution of that prepared object reuses its values and retains conflict clauses,
SQL inspection and native RETURNING. Structural, scope and known capability
checks precede default factories. Known query shape, session ownership and
execution options are checked before input callbacks. An invalid expression AST
produced by a callback can only be rejected after it runs. Dart callback, codec
and factory effects cannot be rolled back. Cancellation and expired sessions
reject before preparation at execution terminals; explicit prepare() has no
execution options and freezes values when called.
Byte storage (Uint8List) is copied once when an encoded parameter is bound.
Prepared writes and their compiled parameters expose that read-only snapshot,
including through its buffer views; attempts to modify it throw
UnsupportedError. Generated literal inputs, expression callbacks and client
defaults bind during preparation. An expression built beforehand with value(...)
already owns its snapshot. Other mutable storage returned by a custom codec remains
caller-owned and must stay stable for every replay; preparation does not deep-copy
application objects. Codecs.bytes itself retains its normal encode/decode ownership.
insert.row() returns a complete model using native RETURNING or a transactional
primary-key readback. write.returning().select(selection).get() requires native
RETURNING. The whole write executes before cardinality checks; an empty update
is an error. plan.insertMany(inputs).prepare().returning(selection) exposes batch
RETURNING. An owned batch transaction decodes results before committing.
Query is a read description. Generated complete-model queries retain typed
patch(...), update(input), delete() and their advanced plan. Selections,
mapping, grouping, DISTINCT and compound
queries expose read operations. Handwritten assignment writes remain available
through db.table(userTable).where(...).update((u) => [...]). Unsupported ordered,
limited or joined writes still fail at preparation.
Named result declarations compose across reads and relationships. Put the
declaration in a model library importing schema.dart:
@Projection()
final class UserCard({required final int id, required final String email});
After generation, import the generated client and sql.dart in application code:
import 'package:orm/driver.dart';
import 'package:orm/orm.dart';
import 'package:orm/sql.dart';
import 'package:my_app/models.orm.dart';
Selection<UserCard> person(UserFields u) =>
userCard(id: u.id, email: u.email);
Projection<UserCard, UserCardFields> personSql(UserFields u) =>
userCard.sql(id: u.id, email: u.email);
Future<({List<UserCard> all, List<UserCard> matching})> readPeople(
Database<Backend> db,
String search,
) async {
final cards = await db.user.select(person).get();
final searchable = db.user.select(personSql).asCte('people');
final matching = await searchable
.where((p) => p.email.contains(search))
.get();
return (all: cards, matching: matching);
}
The generated callable shape accepts Selection values, including nested
relationships. Its .sql(...) form accepts only scalar Expr slots and exports
named fields through CTE/UNION. A mapped Dart result cannot claim SQL fields.
Use Selection<Result> for ordinary reusable helpers and
Projection<Result, ResultFields> when callers need named SQL output fields.
Annotating a flat helper as Selection<Result> erases those named fields; it
still decodes the same result. SQL composition continues to validate descriptor
identity, codecs, nullability, scope and session before execution.
Each slot has declaration identity; identical underlying expressions do not
merge named output slots. See defaults and execution.
Connection and transaction ownership
The root SqlDatabase owns its driver; Database uses that runtime. session borrows one connection; transaction
borrows one connection and commits or rolls back the callback's writes. Only use
the callback's tx to construct that transaction's queries. Subqueries, CTEs and
UNION operands must all use the same view. Use savepoint for a recoverable nested
unit; borrowed views expire at callback completion.
Pass tx into helper functions. Calling the captured root db separately inside
the callback requests separate work: a pool may run it outside the transaction,
and a single-connection SQLite driver may wait on the callback's own lease.
Composition checks reject mixed query descriptions; they do not rewrite unrelated
root calls into transaction calls.
await db.transaction((tx) async {
final user = await tx.user.create(email: 'seven@example.com');
await tx.post.create(authorId: user.id,
title: 'First post',
createdAt: DateTime.now());
});
Catching ordinary typed SELECT/RETURNING mapper, codec or cardinality errors inside an explicit transaction leaves it usable. Let an error escape when the transaction must roll back. Driver failures, interrupted batch statements and raw result-decoding failures retain their failure policy; a caught failure in those categories may prevent commit.
Await every operation before returning from the callback. Batch inserts use one transaction across chunks. A normal query with batched relationships reuses its connection but does not automatically create a snapshot transaction. Choose an appropriate explicit transaction when a consistent multi-query snapshot matters. Query observations expose actual SQL and transaction statements.
Raw SQL remains explicit: bind values in SqlCommand or typed sql expressions,
declare affected/read tables for subscriptions, and use reviewed migration steps
for schema changes. The ORM does not infer an object graph, external writes,
automatic transaction replay or destructive renames.
Custom codecs, manual Table definitions and Driver implementations are explicit extension contracts. Their authors supply the correct storage mapping and native behavior. Dart types cannot prove that a live external schema still matches those definitions; use catalog verification when that boundary matters.
Libraries
- mariadb Open a database
- Mariadb connections for SQL execution and optional typed model views.
- mysql Open a database
- Mysql connections for SQL execution and optional typed model views.
- orm Open a database
- Optional typed model views and query change subscriptions.
- postgres Open a database
- Postgres connections for SQL execution and optional typed model views.
- sqlite Open a database
- Sqlite connections for SQL execution and optional typed model views.
Classes
-
Database<
B extends Backend> Open a database - Typed queries and change notifications over an owned SQL runtime.