DORM - Dart Object-Relational Mapping

A powerful ORM for Dart inspired by Hibernate (Java) and Entity Framework (C#) with LINQ-style query syntax and code generation.

Features

✨ Multi-Database Support

  • PostgreSQL (fully implemented)
  • SQLite (fully implemented)
  • MySQL (structure ready)

πŸ”₯ LINQ-Style Queries

  • Fluent query builder API
  • Type-safe operations
  • Method chaining

🎯 Entity Framework Patterns

  • Repository pattern with code generation
  • Singleton repositories
  • Transactions
  • Connection pooling

πŸš€ Advanced Features

  • Raw SQL queries
  • Stored procedures
  • Automatic code generation
  • Database migrations
  • Schema validation & change detection

Table of Contents


Installation

Add DORM to your pubspec.yaml:

dependencies:
  dormql: ^0.2.0
  postgres: ^3.1.0 # For PostgreSQL
  sqlite3: ^3.0.1 # For SQLite

dev_dependencies:
  build_runner: ^2.10.3

Quick Start

1. Define Your Entities

// lib/src/models/user_entity.dart
import 'package:dormql/dorm.dart';

part 'user_entity.orm.g.dart';

@Entity(tableName: 'users', dbType: DatabaseType.postgresql)
class UserEntity {
  @Id()
  int? id;

  String name;
  String email;

  UserEntity({this.id, required this.name, required this.email});
}

2. Define Your Database

// lib/src/db.dart
import 'package:dormql/dorm.dart';
import 'models/user_entity.dart';

part 'db.db.g.dart';

@Db(
  entities: [UserEntity],
  migrationVersion: 1,
  config: DbConfig.postgresql(
    host: 'localhost',
    port: 5432,
    database: 'mydb',
    username: 'user',
    password: 'password',
  ),
  name: 'mydb',
)
class Database {
  /// Connection is managed by generated code
  DatabaseConnection? _connection;
  DatabaseConnection? get connection => _connection;
}

The generator creates setup(), init(), close(), and schema methods automatically!

3. Generate Code

dart run build_runner build

4. Use Your Database

void main() async {
  final db = Database();
  await db.init();

  // Initialize with migrations and schema validation
  await db.initializeDatabase(
    migrations: [Migration001()],
    validateSchema: true,
  );

  // Access repositories (singleton, auto-initialized)
  final users = await db.userEntityRepository.getAll();

  // LINQ-style queries
  final activeUsers = await db.userEntityRepository
      .query()
      .where('email LIKE @pattern', {'pattern': '%@example.com'})
      .orderByDescending('created_at')
      .take(10)
      .toList();

  await db.close();
}

Public API Reference


Annotations

@Entity

Marks a class as a database entity.

@Entity(
  tableName: 'users',           // Table name (optional, defaults to snake_case of class name)
  dbType: DatabaseType.postgresql,  // Database type
)
class UserEntity { ... }

@Db

Marks a class as a database definition.

@Db(
  entities: [UserEntity, PostEntity],  // List of entity types
  migrationVersion: 1,                  // Current migration version
  config: DbConfig.postgresql(          // Database configuration (optional)
    host: 'localhost',
    port: 5432,
    database: 'mydb',
    username: 'user',
    password: 'password',
  ),
  name: 'mydb',                         // Database name (optional)
  generateSql: true,                    // Generate SQL file (optional)
  sqlDialect: DatabaseType.postgresql,  // SQL dialect (optional, defaults to config.dbType)
)
class Database {
  DatabaseConnection? _connection;
  DatabaseConnection? get connection => _connection;
}

Parameters:

  • entities - List of entity types to include
  • migrationVersion - Current migration version for schema tracking
  • config - Database connection configuration (optional)
  • name - Database name (optional)
  • generateSql - When true, generates SQL file at .dart_tool/dorm/<db_name>.sql
  • sqlDialect - Target SQL dialect for file generation (defaults to config.dbType)

DbConfig

Database configuration for the @Db annotation.

// PostgreSQL
DbConfig.postgresql(host: 'localhost', port: 5432, database: 'mydb', username: 'user', password: 'pass')

// MySQL
DbConfig.mysql(host: 'localhost', port: 3306, database: 'mydb', username: 'root', password: 'pass')

// SQLite
DbConfig.sqlite(database: '/path/to/db.sqlite')

@Id

Marks a field as the primary key.

@Id()
int? id;

@Column

Customizes column mapping.

@Column(name: 'user_email', type: ColumnType.varchar, length: 255)
String email;

@Ignore

Excludes a field from database mapping.

@Ignore()
String temporaryData;

@PrimaryKey

Defines a composite primary key (use on entity class).

@Entity(tableName: 'order_items')
@PrimaryKey(columns: ['order_id', 'product_id'])
class OrderItemEntity {
  int orderId;
  int productId;
  int quantity;
}

Validation: The generator will fail if any column in columns does not exist as a field in the entity.

@Unique

Defines unique constraints. Can be used on a field or on the entity class for composite unique constraints.

// Single column unique (on field)
@Unique()
String email;

// Composite unique (on entity class)
@Entity(tableName: 'user_roles')
@Unique(columns: ['user_id', 'role_id'], name: 'uq_user_role')
class UserRoleEntity {
  int userId;
  int roleId;
}

Validation: The generator will fail if any column in columns does not exist as a field in the entity.

@ForeignKeyConstraint

Defines a foreign key constraint on a field.

@ForeignKeyConstraint(
  referencedTable: 'users',
  referencedColumn: 'id',
  onDelete: ConstraintAction.cascade,
  onUpdate: ConstraintAction.noAction,
)
int? userId;

Parameters:

  • column - Column name (defaults to field name)
  • referencedTable - Referenced table name (required)
  • referencedColumn - Referenced column (default: 'id')
  • onDelete / onUpdate - Constraint actions (ConstraintAction.cascade, etc.)
  • name - Optional constraint name

@Check

Defines a check constraint on a field.

@Check(expression: 'age >= 0 AND age <= 150')
int age;

@Index

Creates a database index.

@Entity(tableName: 'users')
@Index(columns: ['email'], unique: true, name: 'idx_users_email')
class UserEntity { ... }

Validation: The generator will fail if any column in columns does not exist as a field in the entity.

Relationship Annotations

@OneToMany

Defines one-to-many and many-to-one relationships. Use isOwning: true on the side that owns the foreign key column.

// In UserEntity (the "one" side - inverse, no FK column)
@OneToMany(
  targetEntity: PostEntity,
  mappedBy: 'author',  // Field name in PostEntity
)
List<PostEntity>? posts;

// In PostEntity (the "many" side - owning, has FK column)
@OneToMany(
  targetEntity: UserEntity,
  foreignKey: 'user_id',     // FK column name in posts table
  referencedColumn: 'id',    // Referenced column in users table
  isOwning: true,
  onDelete: RelationAction.cascade,
)
UserEntity? author;

Parameters:

  • targetEntity - The related entity type (required)
  • mappedBy - Field name in target entity (for inverse side)
  • foreignKey - FK column name (for owning side)
  • referencedColumn - Referenced column (default: 'id')
  • isOwning - Whether this side owns the FK (default: false)
  • nullable - Whether relationship is nullable (default: true)
  • cascadeDelete - Cascade delete operations (default: false)
  • lazyLoad / eagerLoad - Loading strategy
  • onDelete / onUpdate - FK constraint actions (RelationAction.cascade, etc.)

@OneToOne

Defines a one-to-one relationship between entities. One side owns the FK column.

// In UserEntity (inverse side - no FK column)
@OneToOne(
  targetEntity: ProfileEntity,
  mappedBy: 'user',  // Field name in ProfileEntity
)
ProfileEntity? profile;

// In ProfileEntity (owning side - has FK column)
@OneToOne(
  targetEntity: UserEntity,
  foreignKey: 'user_id',
  isOwning: true,
  unique: true,  // Ensures 1:1 constraint
  onDelete: RelationAction.cascade,
)
UserEntity? user;

Parameters:

  • targetEntity - The related entity type (required)
  • mappedBy - Field name in target entity (for inverse side)
  • foreignKey - FK column name (for owning side)
  • referencedColumn - Referenced column (default: 'id')
  • isOwning - Whether this side owns the FK (default: false)
  • unique - Enforce uniqueness on FK column (default: true)
  • nullable - Whether relationship is nullable (default: true)
  • onDelete / onUpdate - FK constraint actions

Generated repository methods:

// Get the related entity
Future<ProfileEntity?> getProfile(int userId) async { ... }

// Set the related entity (owning side only)
Future<void> setProfile(int userId, int? profileId) async { ... }

@ManyToMany

Creates a junction table to link two entities. One side is the "owning" side (defines the junction table), the other is the "inverse" side (references the owning side).

// Owning side - defines the junction table
@ManyToMany(
  targetEntity: RoleEntity,
  joinTable: JoinTable(
    name: 'user_roles',
    joinColumn: JoinColumn(name: 'user_id', referencedColumn: 'id'),
    inverseJoinColumn: JoinColumn(name: 'role_id', referencedColumn: 'id'),
  ),
)
List<RoleEntity>? roles;

// Inverse side - references the owning field
@ManyToMany(
  targetEntity: UserEntity,
  mappedBy: 'roles',  // Field name in UserEntity
)
List<UserEntity>? users;

Auto-generated junction table: If joinTable is not specified, the generator creates one automatically:

// This will auto-generate junction table "users_role_entity"
@ManyToMany(targetEntity: RoleEntity)
List<RoleEntity>? roles;

Validation: The generator validates that:

  • Target entity exists in @Db entities list
  • mappedBy references a valid field in the target entity
  • Owning and inverse sides are properly configured

Complete ManyToMany Example:

// ============================================
// product_entity.dart - OWNING SIDE
// ============================================
import 'package:dorm/dorm.dart';

part 'product_entity.orm.g.dart';

@Entity(tableName: 'products', dbType: DatabaseType.postgresql)
class ProductEntity {
  @Id()
  int? id;

  String name;
  double price;

  /// Owning side - defines the junction table
  @ManyToMany(
    targetEntity: UserEntity,
    joinTable: JoinTable(
      name: 'products_users',
      joinColumn: JoinColumn(name: 'products_id', referencedColumn: 'id'),
      inverseJoinColumn: JoinColumn(name: 'users_id', referencedColumn: 'id'),
    ),
  )
  List<UserEntity>? users;

  ProductEntity({this.id, required this.name, required this.price});
}

// ============================================
// user_entity.dart - INVERSE SIDE
// ============================================
import 'package:dorm/dorm.dart';

part 'user_entity.orm.g.dart';

@Entity(tableName: 'users', dbType: DatabaseType.postgresql)
class UserEntity {
  @Id()
  int? id;

  String name;
  String email;

  /// Inverse side - references the owning side field name
  @ManyToMany(targetEntity: ProductEntity, mappedBy: 'users')
  List<ProductEntity>? products;

  UserEntity({this.id, required this.name, required this.email});
}

Generated Repository Methods:

// OWNING SIDE (ProductEntityRepository) - Full CRUD operations
await productRepo.getUsers(productId);           // Get all users for a product
await productRepo.addUser(productId, userId);    // Add user to product
await productRepo.removeUser(productId, userId); // Remove user from product
await productRepo.clearUsers(productId);         // Remove all users from product
await productRepo.setUsers(productId, [1, 2]);   // Replace all users

// INVERSE SIDE (UserEntityRepository) - Read-only
await userRepo.getProducts(userId);              // Get all products for a user

Usage Example:

void main() async {
  final db = Database();
  await db.init();

  // Create entities
  final product = ProductEntity(name: 'Laptop', price: 999.99);
  final savedProduct = await db.productEntityRepository.save(product);

  final user = UserEntity(name: 'John', email: 'john@example.com');
  final savedUser = await db.userEntityRepository.save(user);

  // Add relationship (from owning side)
  await db.productEntityRepository.addUser(savedProduct.id!, savedUser.id!);

  // Query from either side
  final usersForProduct = await db.productEntityRepository.getUsers(savedProduct.id!);
  final productsForUser = await db.userEntityRepository.getProducts(savedUser.id!);

  print('Users for product: ${usersForProduct.length}');
  print('Products for user: ${productsForUser.length}');

  await db.close();
}

Migrations

Creating a Migration

Migrations now have built-in helper methods that automatically skip operations if the object already exists:

class Migration001 extends DatabaseMigration {
  @override
  int get version => 1;

  @override
  String get description => 'Create users table';

  @override
  Future<void> up() async {
    // Option 1: Use schema object (recommended)
    await createTable(DatabaseSchema(
      tableName: 'users',
      columns: [
        ColumnSchema(name: 'id', type: 'INTEGER', primaryKey: true),
        ColumnSchema(name: 'name', type: 'VARCHAR(100)', nullable: false),
        ColumnSchema(name: 'email', type: 'VARCHAR(255)', nullable: false, unique: true),
        ColumnSchema(name: 'created_at', type: 'TIMESTAMP', defaultValue: 'CURRENT_TIMESTAMP'),
      ],
      indexes: [
        IndexSchema(name: 'idx_users_email', columns: ['email'], unique: true),
      ],
    ));

    // Option 2: Use raw SQL
    await connection.execute('''
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(255) NOT NULL UNIQUE
      )
    ''');

    // Helper methods (all skip if already exists)
    await createIndex(name: 'idx_users_name', table: 'users', columns: ['name']);
    await addColumn(table: 'users', column: 'bio', type: 'TEXT', nullable: true);
  }

  @override
  Future<void> down() async {
    await dropTable('users');
  }
}

Migration Helper Methods

All helper methods automatically check if the object exists and skip if it does:

Method Signature Description
createTable Future<void> createTable(DatabaseSchema schema) Creates table with IF NOT EXISTS
dropTable Future<void> dropTable(String tableName) Drops table with IF EXISTS
tableExists Future<bool> tableExists(String tableName) Returns true if table exists
createIndex Future<void> createIndex({name, table, columns, unique}) Creates index, skips if exists
dropIndex Future<void> dropIndex(String name) Drops index, skips if not exists
addColumn Future<void> addColumn({table, column, type, nullable, defaultValue}) Adds column, skips if exists
dropColumn Future<void> dropColumn({table, column}) Drops column, skips if not exists

Migration Types

DORM provides several migration types for different use cases:

1. Custom Migration (Extend DatabaseMigration)

class Migration001 extends DatabaseMigration {
  @override
  int get version => 1;

  @override
  String get description => 'Create users table';

  @override
  Future<void> up() async {
    await createTable(DatabaseSchema(...));
  }

  @override
  Future<void> down() async {
    await dropTable('users');
  }
}

2. RawSqlMigration

For simple SQL-based migrations:

final migration = RawSqlMigration(
  version: 2,
  description: 'Add status column to users',
  upSql: "ALTER TABLE users ADD COLUMN status VARCHAR(50) DEFAULT 'active';",
  downSql: 'ALTER TABLE users DROP COLUMN status;',
);

// Or with multiple statements
final migration = RawSqlMigration(
  version: 3,
  description: 'Add multiple columns',
  upSqlStatements: [
    'ALTER TABLE users ADD COLUMN age INTEGER;',
    'ALTER TABLE users ADD COLUMN phone VARCHAR(20);',
  ],
  downSqlStatements: [
    'ALTER TABLE users DROP COLUMN phone;',
    'ALTER TABLE users DROP COLUMN age;',
  ],
);

3. ManualMigration

For migrations with custom logic using callbacks:

final migration = ManualMigration(
  version: 4,
  description: 'Seed initial data',
  onUp: (connection, schemaManager) async {
    await connection.execute(
      "INSERT INTO users (name, email) VALUES ('Admin', 'admin@example.com')",
    );
  },
  onDown: (connection, schemaManager) async {
    await connection.execute(
      "DELETE FROM users WHERE email = 'admin@example.com'",
    );
  },
);

4. CompositeMigration

Combine multiple migrations into one:

final migration = CompositeMigration(
  version: 5,
  description: 'Add user profile fields',
  steps: [
    RawSqlMigration(
      version: 0, // ignored in composite
      description: 'Add bio column',
      upSql: 'ALTER TABLE users ADD COLUMN bio TEXT;',
      downSql: 'ALTER TABLE users DROP COLUMN bio;',
    ),
    RawSqlMigration(
      version: 0,
      description: 'Add avatar column',
      upSql: 'ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);',
      downSql: 'ALTER TABLE users DROP COLUMN avatar_url;',
    ),
  ],
);

MigrationRunner

Use MigrationRunner to execute migrations:

final runner = MigrationRunner(connection, [
  Migration001(),
  Migration002(),
  migration3,  // RawSqlMigration
  migration4,  // ManualMigration
]);

// Run all pending migrations
await runner.runMigrations();

// Rollback the last migration
await runner.rollbackLast();

// Run ad-hoc SQL (not tracked)
await runner.runSql('UPDATE users SET status = @status', parameters: {'status': 'active'});

// Run multiple SQL statements (not tracked)
await runner.runSqlStatements([
  'CREATE INDEX idx_users_status ON users(status);',
  'CREATE INDEX idx_users_created ON users(created_at);',
]);

// Run manual callback (not tracked)
await runner.runManual((connection, schemaManager) async {
  // Custom logic
});

Running Migrations

The setup() method handles the complete database initialization flow:

// Complete setup: connect β†’ create schema β†’ run migrations β†’ validate
await db.setup(
  config: DatabaseConfig.postgresql(...),  // Optional if defined in @Db
  migrations: [
    Migration001(),
    Migration002(),
    RawSqlMigration(version: 3, description: '...', upSql: '...'),
  ],
  autoCreateSchema: true,   // Creates tables from schema (default: true)
  validateSchema: true,     // Validates schema matches code (default: true)
);

Flow:

  1. Connect - Establishes database connection
  2. Create Schema - Creates all tables using IF NOT EXISTS (skips existing)
  3. Run Migrations - Executes pending migrations (skips already applied)
  4. Validate Schema - Checks schema matches code definitions

You can also call individual methods:

// Just connect
await db.init(config);

// Create schema tables (safe to call multiple times)
await db.createSchema();

// Run migrations only
await db.initializeDatabase(migrations: [...]);

// Get SQL without executing
final sql = db.getCreateSchemaSql();
print(sql.join('\n'));

Schema Validation

DORM automatically:

  • Generates a schema hash from your entities
  • Compares the hash with the stored hash in the database
  • Fails initialization if schema changed but migrationVersion was not bumped
// If you add a new column to UserEntity but don't bump migrationVersion:
// StateError: Schema has changed but migration version was not bumped!
// Differences found:
// Column "new_column" missing in table "users"
// Please increment migrationVersion in @Db annotation and create a migration.

Migration Best Practices

  1. Always test migrations on a copy of production data before deploying
  2. Never delete applied migrations in production
  3. Keep down() methods functional for rollback capability
  4. Use timestamp-based versions to avoid conflicts in team environments
  5. Bump migrationVersion in @Db annotation when schema changes

Schema to SQL

Accessing Entity Schema

Each entity has a generated schema extension in .schema.g.dart:

// Access schema via extension
final schema = UserEntitySchema.schema;
final tableName = UserEntitySchema.tableName;

// Or from an instance
final user = UserEntity();
// user.schema (if instance method needed)

Converting Schema to SQL

Use the extension methods to convert schema objects to SQL:

// Get schema from entity
final schema = UserEntitySchema.schema;

// Or create manually
final schema = DatabaseSchema(
  tableName: 'users',
  columns: [
    ColumnSchema(name: 'id', type: 'INTEGER', primaryKey: true),
    ColumnSchema(name: 'name', type: 'VARCHAR(100)', nullable: false),
    ColumnSchema(name: 'email', type: 'VARCHAR(255)', nullable: false, unique: true),
  ],
  foreignKeys: [
    ForeignKey(column: 'role_id', referencedTable: 'roles', referencedColumn: 'id'),
  ],
  indexes: [
    IndexSchema(name: 'idx_users_email', columns: ['email'], unique: true),
  ],
);

// Generate CREATE TABLE SQL with IF NOT EXISTS
final createSql = schema.toCreateTableSql(DatabaseType.postgresql);
// CREATE TABLE IF NOT EXISTS users (
//   id SERIAL PRIMARY KEY,
//   name VARCHAR(100) NOT NULL,
//   email VARCHAR(255) NOT NULL UNIQUE,
//   FOREIGN KEY (role_id) REFERENCES roles (id)
// );

// Generate CREATE INDEX SQL
final indexSql = schema.toCreateIndexSql(DatabaseType.postgresql);
// ['CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users (email);']

// Generate DROP TABLE SQL
final dropSql = schema.toDropTableSql(DatabaseType.postgresql);
// DROP TABLE IF EXISTS users;

SchemaManager

Use SchemaManager to execute schema operations:

final schemaManager = SchemaManager(connection);

// Create table (skips if exists)
await schemaManager.createTable(schema);

// Create multiple tables
await schemaManager.createTables([usersSchema, postsSchema, rolesSchema]);

// Check if table exists
final exists = await schemaManager.tableExists('users');

// Get all table names
final tables = await schemaManager.getTableNames();

// Drop table (skips if not exists)
await schemaManager.dropTable(schema);

Generated Code

SQL File Generation

When generateSql: true is set in @Db, the generator creates SQL files:

Location: .dart_tool/dorm/

Files generated:

  • <db_name>.sql - Current schema (overwritten on each build)
  • <db_name>_v<version>.sql - Versioned copy for each migration version

Example output:

-- ============================================================
-- Database: mydb
-- Generated: 2024-01-15T10:30:00.000Z
-- Migration Version: 1
-- SQL Dialect: postgresql
-- ============================================================

-- Entity Tables
-- ============================================================

-- Table: users
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL
);

-- Table: posts
CREATE TABLE IF NOT EXISTS posts (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  user_id INTEGER NOT NULL
);

-- Junction Tables (ManyToMany)
-- ============================================================

-- Junction: users_roles
CREATE TABLE IF NOT EXISTS users_roles (
  users_id INTEGER NOT NULL,
  roles_id INTEGER NOT NULL,
  PRIMARY KEY (users_id, roles_id),
  FOREIGN KEY (users_id) REFERENCES users (id) ON DELETE CASCADE,
  FOREIGN KEY (roles_id) REFERENCES roles (id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_users_roles_users_id ON users_roles (users_id);
CREATE INDEX IF NOT EXISTS idx_users_roles_roles_id ON users_roles (roles_id);

Repository Extension

The @Db annotation generates repository access as an extension:

// Generated: db.db.g.dart
extension DatabaseRepositories on Database {
  UserEntityRepository get userEntityRepository { ... }
  PostEntityRepository get postEntityRepository { ... }
}

ManyToMany Repository Methods

For entities with @ManyToMany relationships, the generator creates dedicated methods:

// Generated: user_entity.orm.g.dart
class UserEntityRepository extends Repository<UserEntity> {
  // ... standard CRUD methods ...

  /// Get all roles for a user
  Future<List<RoleEntity>> getRoles(int userId) async { ... }

  /// Add a role to a user
  Future<void> addRole(int userId, int roleId) async { ... }

  /// Remove a role from a user
  Future<void> removeRole(int userId, int roleId) async { ... }

  /// Clear all roles from a user
  Future<void> clearRoles(int userId) async { ... }

  /// Set roles for a user (replaces all existing)
  Future<void> setRoles(int userId, List<int> roleIds) async { ... }

  /// Find user with related data
  Future<Map<String, dynamic>?> findByIdWithRelations(
    int id, {
    List<String> includes = const ['roles'],
  }) async { ... }

  /// Get all users with related data
  Future<List<Map<String, dynamic>>> getAllWithRelations({
    List<String> includes = const ['roles'],
  }) async { ... }
}

Usage:

// Get user with roles
final result = await userRepo.findByIdWithRelations(1, includes: ['roles']);
final user = result!['entity'] as UserEntity;
final roles = result['roles'] as List<RoleEntity>;

// Manage roles
await userRepo.addRole(userId, roleId);
await userRepo.removeRole(userId, roleId);
await userRepo.setRoles(userId, [1, 2, 3]);

Schema Definition

Schema is generated at the database level:

// Generated: db.db.g.dart
const userEntitySchema = DatabaseSchema(
  tableName: 'users',
  columns: [
    ColumnSchema(name: 'id', type: 'INTEGER', nullable: true, primaryKey: true),
    ColumnSchema(name: 'name', type: 'TEXT', nullable: false, primaryKey: false),
    ColumnSchema(name: 'email', type: 'TEXT', nullable: false, primaryKey: false),
  ],
);

const databaseSchemas = [userEntitySchema, postEntitySchema];

Lifecycle Extension

// Generated: db.db.g.dart
extension DatabaseLifecycle on Database {
  static const int currentMigrationVersion = 1;
  static const String schemaHash = 'abc123...';

  /// Get all schemas defined in code
  List<DatabaseSchema> get schemas => [...];

  /// One-liner setup: connect + migrate + validate
  Future<void> setup({
    DatabaseConfig? config,  // Uses annotation config if not provided
    List<DatabaseMigration> migrations = const [],
    bool validateSchema = true,
  }) async { ... }

  /// Initialize connection only
  Future<void> init(DatabaseConfig? config) async { ... }

  /// Run migrations and validate schema
  Future<void> initializeDatabase({
    List<DatabaseMigration> migrations = const [],
    bool validateSchema = true,
  }) async { ... }

  /// Retrieve schema from database
  Future<DatabaseSchema?> getSchemaFromDatabase(String tableName) async { ... }
  Future<List<DatabaseSchema>> getAllSchemasFromDatabase() async { ... }

  Future<int> getAppliedMigrationVersion() async { ... }
  Future<bool> hasPendingMigrations(List<DatabaseMigration> migrations) async { ... }

  /// Close connection
  Future<void> close() async { ... }
}

Repository API

The Repository<T> class provides CRUD operations for entities.

Constructor Parameters

Repository(
  String tableName, {
  String primaryKeyColumn = 'id',
  bool autoIncrementPrimaryKey = true,
})

Core Methods

Method Return Type Description
save(T entity) Future<T> Insert entity, returns saved entity with generated ID
findById(dynamic id) Future<T?> Find entity by primary key
getAll() Future<List<T>> Get all entities
delete(dynamic id) Future<void> Delete by primary key
deleteEntity(T entity) Future<void> Delete entity instance
query() QueryBuilder<T> Create LINQ-style query builder
setConnection(DatabaseConnection conn) void Set database connection

Abstract Methods (Implemented by Generated Code)

/// Convert database row to entity
T fromRow(Map<String, dynamic> row);

/// Convert entity to database row
Map<String, dynamic> toRow(T entity);

/// Load relationships for eager loading
Future<void> loadRelationships(T entity, List<String> includes);

Raw Query Methods

/// Execute raw SQL query
RawQuery executeRawQuery(String sql, [Map<String, dynamic>? parameters]);

/// Execute stored procedure
StoredProcedure executeProcedure(String name, {List<dynamic>? parameters});

QueryBuilder API

The QueryBuilder<T> class provides LINQ-style fluent query building.

Query Methods

Method Description
select(Function(SelectBuilder) builder) Select specific columns
where(String condition, [Map<String, dynamic>? params]) WHERE clause with parameters
whereSimple(String column, dynamic value) Simple equality WHERE
whereIn(String column, List<dynamic> values) WHERE IN clause
whereNotIn(String column, List<dynamic> values) WHERE NOT IN clause
whereBetween(String column, dynamic from, dynamic to) BETWEEN clause
whereLike(String column, String pattern) LIKE clause
whereILike(String column, String pattern) Case-insensitive LIKE (PostgreSQL)
whereNull(String column) IS NULL clause
whereNotNull(String column) IS NOT NULL clause

Join Methods

Method Description
innerJoin(String table, String condition) INNER JOIN
leftJoin(String table, String condition) LEFT JOIN
rightJoin(String table, String condition) RIGHT JOIN
include(String relationshipName) Eager load relationship

Ordering & Pagination

Method Description
orderBy(String column) ORDER BY ASC
orderByDescending(String column) ORDER BY DESC
skip(int count) OFFSET
take(int count) LIMIT
distinct() SELECT DISTINCT

Execution Methods

Method Return Type Description
toList() Future<List<T>> Execute and get all results
firstOrDefault() Future<T?> Get first result or null
first() Future<T> Get first result (throws if not found)
countSql() Future<int> Count matching records
any() Future<bool> Check if any records match
max(String column) Future<num?> Get maximum value
min(String column) Future<num?> Get minimum value
sum(String column) Future<num?> Get sum of column
avg(String column) Future<num?> Get average of column
toSql() String Get generated SQL (for debugging)

SelectBuilder

Used with the select() method to specify columns:

class SelectBuilder {
  void column(String name);
  void columns(List<String> names);
}

Usage Examples

// Select specific columns
final users = await userRepo
    .query()
    .select((s) => s.columns(['id', 'name', 'email']))
    .toList();

// Complex query with multiple conditions
final users = await userRepo
    .query()
    .where('status = @status', {'status': 'active'})
    .whereNotNull('email')
    .whereLike('name', '%John%')
    .orderByDescending('created_at')
    .skip(10)
    .take(20)
    .toList();

// Aggregations
final totalSales = await orderRepo.query().sum('amount');
final avgPrice = await productRepo.query().avg('price');
final maxAge = await userRepo.query().max('age');

// Check existence
final hasAdmins = await userRepo
    .query()
    .where('role = @role', {'role': 'admin'})
    .any();

DatabaseConnection API

The DatabaseConnection abstract class defines the interface for all database connections.

Interface Methods

abstract class DatabaseConnection {
  /// Execute query and return mapped results
  Future<List<Map<String, dynamic>>> query(
    String sql, {
    Map<String, dynamic>? parameters,
  });

  /// Execute non-query (INSERT, UPDATE, DELETE)
  Future<int> execute(String sql, {Map<String, dynamic>? parameters});

  /// Execute query and return raw results
  Future<List<List<dynamic>>> rawQuery(
    String sql, {
    Map<String, dynamic>? parameters,
  });

  /// Begin a transaction
  Future<DatabaseTransaction> beginTransaction();

  /// Close the connection
  Future<void> close();

  /// Check if connection is open
  bool get isOpen;

  /// Get database type
  DatabaseType get databaseType;
}

DatabaseTransaction Interface

abstract class DatabaseTransaction {
  Future<List<Map<String, dynamic>>> query(String sql, {Map<String, dynamic>? parameters});
  Future<int> execute(String sql, {Map<String, dynamic>? parameters});
  Future<void> commit();
  Future<void> rollback();
}

DatabaseConfig Factory Methods

// PostgreSQL
DatabaseConfig.postgresql({
  required String host,
  required int port,
  required String database,
  required String username,
  required String password,
  bool useSSL = false,
  int maxConnections = 10,
  Duration connectionTimeout = const Duration(seconds: 30),
})

// MySQL
DatabaseConfig.mysql({
  required String host,
  required int port,
  required String database,
  required String username,
  required String password,
  bool useSSL = false,
  int maxConnections = 10,
  Duration connectionTimeout = const Duration(seconds: 30),
})

// SQLite
DatabaseConfig.sqlite({required String filePath})

DatabaseType Enum

enum DatabaseType { postgresql, mysql, sqlite }

Schema API

DatabaseSchema

class DatabaseSchema {
  final String tableName;
  final List<dynamic> columns;
  final List<ForeignKey>? foreignKeys;
  final List<IndexSchema>? indexes;
  final List<String>? primaryKeyColumns;
  final List<UniqueConstraint>? uniqueConstraints;
  final List<CheckConstraint>? checkConstraints;

  const DatabaseSchema({
    required this.tableName,
    required this.columns,
    this.foreignKeys,
    this.indexes,
    this.primaryKeyColumns,
    this.uniqueConstraints,
    this.checkConstraints,
  });
}

ColumnSchema

class ColumnSchema {
  final String name;
  final String type;
  final bool nullable;
  final bool primaryKey;
  final bool unique;
  final String? defaultValue;
  final bool autoIncrement;

  const ColumnSchema({
    required this.name,
    required this.type,
    this.nullable = true,
    this.primaryKey = false,
    this.unique = false,
    this.defaultValue,
    this.autoIncrement = false,
  });
}

ForeignKey

class ForeignKey {
  final String column;
  final String referencedTable;
  final String referencedColumn;
  final ForeignKeyAction? onDelete;
  final ForeignKeyAction? onUpdate;
  final String? name;

  const ForeignKey({
    required this.column,
    required this.referencedTable,
    required this.referencedColumn,
    this.onDelete,
    this.onUpdate,
    this.name,
  });
}

enum ForeignKeyAction { cascade, restrict, setNull, setDefault, noAction }

Schema Extension Methods

extension DatabaseSchemaToSql on DatabaseSchema {
  /// Generate CREATE TABLE SQL
  String toCreateTableSql(DatabaseType dbType);

  /// Generate CREATE INDEX SQL statements
  List<String> toCreateIndexSql(DatabaseType dbType);

  /// Generate DROP TABLE SQL
  String toDropTableSql(DatabaseType dbType);

  /// Generate all SQL statements
  List<String> toAllSql(DatabaseType dbType);
}

SchemaManager

class SchemaManager {
  SchemaManager(DatabaseConnection connection);

  Future<bool> createTable(DatabaseSchema schema);
  Future<void> createTables(List<DatabaseSchema> schemas);
  Future<void> dropTable(DatabaseSchema schema);
  Future<bool> tableExists(String tableName);
  Future<bool> indexExists(String indexName, String tableName);
  Future<List<String>> getTableNames();
}

Migrations API

DatabaseMigration Abstract Class

abstract class DatabaseMigration {
  int get version;
  String get description;
  DatabaseType get dbType;

  void setConnection(DatabaseConnection conn);

  /// Migration up - create/modify schema
  Future<void> up();

  /// Migration down - rollback schema
  Future<void> down();

  // Helper methods
  Future<void> createTable(DatabaseSchema schema);
  Future<void> dropTable(String tableName);
  Future<bool> tableExists(String tableName);
  Future<void> createIndex({
    required String name,
    required String table,
    required List<String> columns,
    bool unique = false,
  });
  Future<void> dropIndex(String name);
  Future<void> addColumn({
    required String table,
    required String column,
    required String type,
    bool nullable = true,
    String? defaultValue,
  });
  Future<void> dropColumn({required String table, required String column});
}

MigrationRunner

class MigrationRunner {
  MigrationRunner(DatabaseConnection connection, List<DatabaseMigration> migrations);

  Future<void> runMigrations();
  Future<void> rollbackLast();
  Future<void> runSql(String sql, {Map<String, dynamic>? parameters});
  Future<void> runSqlStatements(List<String> statements);
  Future<void> runManual(MigrationCallback callback);
}

Migration Types

// Raw SQL Migration
RawSqlMigration({
  required int version,
  required String description,
  String upSql = '',
  String? downSql,
  List<String>? upSqlStatements,
  List<String>? downSqlStatements,
})

// Manual Migration with Callbacks
ManualMigration({
  required int version,
  required String description,
  required MigrationCallback onUp,
  MigrationCallback? onDown,
})

// Composite Migration
CompositeMigration({
  required int version,
  required String description,
  required List<DatabaseMigration> steps,
})

Raw Queries & Stored Procedures

RawQuery

class RawQuery {
  RawQuery(DatabaseConnection connection, String sql, [Map<String, dynamic> parameters = const {}]);

  Future<List<Map<String, dynamic>>> execute();
  Future<Map<String, dynamic>?> executeFirstOrDefault();
  Future<dynamic> executeScalar();
  Future<int> executeNonQuery();
  String toSql();  // Get SQL with parameters substituted (for debugging)
}

StoredProcedure

class StoredProcedure {
  StoredProcedure({
    required String name,
    String schema = 'public',
    List<dynamic> parameters = const [],
    DatabaseConnection? connection,
  });

  void setConnection(DatabaseConnection connection);
  Future<List<Map<String, dynamic>>> execute();
  Future<dynamic> executeScalar();
  Future<void> executeNonQuery();
}

StoredProcedureBuilder

class StoredProcedureBuilder {
  StoredProcedureBuilder(DatabaseConnection connection, String name);

  StoredProcedureBuilder withSchema(String schema);
  StoredProcedureBuilder addParameter(dynamic value);
  StoredProcedure build();
}

Usage Examples

// Raw query
final results = await repo.executeRawQuery(
  'SELECT * FROM users WHERE age > @age ORDER BY name',
  {'age': 18},
).execute();

// Stored procedure
final proc = repo.executeProcedure('calculate_total', parameters: [orderId]);
final total = await proc.executeScalar();

// Using builder
final proc = StoredProcedureBuilder(connection, 'get_user_stats')
    .withSchema('reporting')
    .addParameter(userId)
    .addParameter(startDate)
    .build();
final stats = await proc.execute();

Database Support Status

Database Status Package Required Notes
PostgreSQL βœ… Fully Implemented postgres: ^3.1.0 Production ready
SQLite3 βœ… Fully Implemented sqlite3: ^3.0.1 Production ready
MySQL πŸ”§ Structure Ready mysql1: ^0.20.0 Coming soon

Project Structure

lib/
β”œβ”€β”€ dorm.dart                 # Main library export
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ annotation.dart       # @Entity, @Db, @Column, etc.
β”‚   β”œβ”€β”€ repository.dart       # Base Repository class
β”‚   β”œβ”€β”€ query_builder.dart    # LINQ-style query builder
β”‚   β”œβ”€β”€ raw_query.dart        # Raw SQL query execution
β”‚   β”œβ”€β”€ stored_procedure.dart # Stored procedure execution
β”‚   β”œβ”€β”€ migration.dart        # DatabaseMigration & MigrationRunner
β”‚   β”œβ”€β”€ schema.dart           # DatabaseSchema, ColumnSchema
β”‚   β”œβ”€β”€ annotation/
β”‚   β”‚   β”œβ”€β”€ entity.dart       # @Entity, @Column, @Id, @Index, etc.
β”‚   β”‚   β”œβ”€β”€ relationship.dart # @OneToOne, @OneToMany, @ManyToMany
β”‚   β”‚   β”œβ”€β”€ database.dart     # @Db, DbConfig
β”‚   β”‚   β”œβ”€β”€ query.dart        # Query-related annotations
β”‚   β”‚   └── migration.dart    # Migration annotations
β”‚   └── database/
β”‚       β”œβ”€β”€ database_connection.dart
β”‚       β”œβ”€β”€ database_factory.dart
β”‚       β”œβ”€β”€ postgresql_connection.dart
β”‚       β”œβ”€β”€ mysql_connection.dart
β”‚       └── sqlite_connection.dart
└── tool/
    β”œβ”€β”€ entity_generator.dart # Generates .orm.g.dart
    β”œβ”€β”€ db_generator.dart     # Generates .db.g.dart
    └── db_schema_generator.dart # Generates schema SQL

Examples

See the /examples folder for complete working examples:

  • db_postgres_dorm_example/ - PostgreSQL example with entities and database
  • example/ - Basic usage examples

License

MIT