Daravelv1.0.0
ENID GitHub

Database

SQLite and PostgreSQL are supported at runtime (DB_CONNECTION=sqlite or pgsql); the MySQL grammar exists but its driver is planned for Tier 2. Every query is built by a grammar with bound values; identifiers are quoted.

Migrations#

final class CreatePostsTable extends Migration {
  const CreatePostsTable();

  @override
  String get name => '2026_01_01_000000_create_posts_table';

  @override
  Future<void> up(SchemaBuilder schema) => schema.create('posts', (table) {
    table.id();
    table.string('title');
    table.text('body').nullable();
    table.boolean('published').defaultTo(false);
    table.timestamps();
  });

  @override
  Future<void> down(SchemaBuilder schema) => schema.dropIfExists('posts');
}

Migrations are listed in bootstrap/migrations.dart and recorded in Laravel's migrations table (id, migration, batch), so a Laravel app and a Daravel app can share one ledger. Commands: migrate, migrate:status, migrate:rollback, migrate:reset, migrate:fresh, db:seed. On SQLite, column changes rebuild the table safely.

Models#

Models are "hybrid active records": an attribute bag plus a static ModelDefinition that replaces Laravel's magic properties.

final class Post extends Model {
  static final ModelDefinition<Post> definition = ModelDefinition<Post>(
    table: 'posts',
    factory: Post.new,
    fillable: ['title', 'body', 'published'],
    casts: {'published': Cast.boolean},
    scopes: {'published': (query, args) => query.where('published', true)},
  );

  @override
  ModelDefinition<Post> get modelDefinition => Post.definition;

  static ModelQuery<Post> query() => definition.query();

  int get id => getAttr('id');
  String get title => getAttr('title');
  bool get published => getAttr('published');
}

dartisan make:model writes this for you and dartisan model:sync regenerates the typed accessors from the table. Relations (HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough) are declared in relations: and eager loaded with with_([...]) (with is a reserved word in Dart). Soft deletes, observers, model events, global scopes, withCount and whereHas work as in Eloquent.

ModelDefinition.morphClass is the Laravel class name of the model (App\Models\Post by default). It is stored in polymorphic columns such as personal_access_tokens.tokenable_type and used in No query results for model [App\Models\Post] 1.

Querying#

await Post.query().create({'title': 'Hello', 'published': true});
await DB.table('posts').insert({'title': 'Draft'});

final published = await Post.query()
    .where('published', true)
    .orderByDesc('id')
    .get();
final titles = await DB.table('posts').orderBy('id').pluck('title');
final count = await Post.query().scope('published').count();

await DB.transaction(() async {
  final post = await Post.query().findOrFail(1);
  await post.update({'title': 'Hello, Daravel'});
});

Transactions follow the zone: nested calls become savepoints, and after-commit callbacks (App.make<DatabaseManager>().afterCommit(...), used by queued jobs and Event.dispatchAfterCommit) run only when the outer transaction commits.

Listening to queries#

DB.listen((QueryExecuted query) { ... }) is called after every query of the builder, the ORM, raw SQL and migrations, with its SQL, bindings, duration and connection, for example to log slow queries:

DB.listen((query) {
  if (query.time > const Duration(milliseconds: 100)) {
    Log.warning('Slow query', {'sql': query.sql, 'ms': query.time.inMilliseconds});
  }
});

Without listeners, queries are not wrapped and cost nothing extra.

Pagination#

paginate(), simplePaginate() and cursorPaginate() read the page (or cursor) and the absolute path from the current request, like Laravel, and serialize to Laravel's exact JSON: the same keys in the same order, links with Laravel's sliding window and page numbers, current_page_url for simple pagination, and cursors encoded like Laravel's Cursor::encode() so a cursor issued by either framework works in the other. Use withQueryString() or appends({...}) to keep extra query parameters.