PHORM Annotations 🚀

Pub Version Build Status GitHub Stars License: MIT Dart SDK

Annotation library for declarative SQL table and schema definitions in Dart.

phorm_annotations provides the building blocks for defining your database schema using standard Dart classes. It is designed to work with phorm_generator to automate SQL generation and runtime metadata.


Features

  • Declarative Schemas: Define tables and columns using @Schema, @Column, and @ID.
  • Relationships: Annotations for HasMany, BelongsTo, HasOne, and ManyToMany (with pivot tables).
  • Type Safety: Database-agnostic logical types mapped to SQLite.
  • Constraints: Support for UNIQUE, CHECK, and DEFAULT values.
  • Paranoid Mode: Built-in support for soft deletes via deletedAt.
  • Factories: Factory<T> interface for generating test/seed data.
  • Value Converters: ValueConverter<D, S> for custom Dart ↔ SQL type transformations.

Basic Usage

1. Annotate your model

import 'package:phorm_annotations/phorm_annotations.dart';

part 'user.sql.g.dart';

@Schema(
  tableName: 'users',
  paranoid: true,
  columnNaming: ColumnNamingStrategy.snakeCase,
  indexes: [
    Index(columns: ['email'], unique: true),
    Index(columns: ['first_name', 'last_name']),
  ],
)
class User extends Model with _$PhormUserMixin {
  @ID(autoIncrement: false, unique: true)
  @override
  final String id;

  @Column()
  final String firstName;

  @Column()
  final String lastName;

  @Column(unique: true)
  final String email;

  @Column(
    validators: [
      ContainsValidator(['M', 'F', 'Other'], constraint: 'gender_check'),
    ],
  )
  final String gender;

  User({
    required this.id,
    required this.firstName,
    required this.lastName,
    required this.email,
    required this.gender,
  });

  factory User.fromJson(Map<String, dynamic> json) => _$PhormUserFromJson(json);
}

2. Run the generator

dart run build_runner build

Annotations Reference

@Schema

Defines table-level configuration.

Property Type Description
tableName String? Explicit SQL table name.
indexes List<Index> List of table indexes.
paranoid bool Enables soft deletes (requires deletedAt column).
columnNaming ColumnNamingStrategy Strategy for mapping field names to SQL (snakeCase, camelCase, pascalCase).
dialect SqlDialectKind Target SQL dialect for DDL generation (sqlite (default), postgres, mysql).
relationships List<Relationship> Define HasMany, HasOne, or BelongsTo.

@Column

Standard column definition. PHORM automatically infers the SQLite type from the Dart field type.

@Column(unique: true, defaultValue: 'active')
final String status;
Property Type Description
columnName String? Override column name.
sqlType String? Explicit SQL type override as a raw string (e.g. 'VARCHAR(255)').
type SqlType? Explicit SQL type as a typed object (e.g. VARCHAR(255), DECIMAL(10, 2), JSONB()).
unique bool Enforce UNIQUE constraint.
nullable bool Mark column as NULL or NOT NULL.
defaultValue dynamic SQL DEFAULT value.
validators List<IValidator>? List of validators (triggers SQL CHECK and Dart validation).
converter ValueConverter? Custom Dart ↔ SQL value transformer.
collate String? Specify string collation (e.g. Collate.noCase).

SqlType objects are organised by dialect: common_types (VARCHAR, TEXT, INTEGER, BIGINT, BOOLEAN, REAL, DOUBLE, DECIMAL, DATE, TIME, TIMESTAMP, BLOB, JSON), sqlite_types (NUMERIC, Collate), postgres_types (JSONB), mysql_types (scaffolded). Resolution precedence: sqlType → type → converter → inferred Dart type.


Automatic Type Mapping

PHORM maps Dart types to SQLite types automatically:

Dart Type SQLite Type Notes
String TEXT Default for strings, UUIDs
int INTEGER Standard integer
bool INTEGER Stored as 1 (true) / 0 (false)
double REAL Floating point numbers
num NUMERIC Supports both int and double
DateTime TEXT Stored as ISO-8601 strings
Uint8List BLOB Binary data

Standard SQL Types

For manual overrides in @Column(sqlType: ...) or when adding columns in migrations, use the SqlTypes class to avoid hardcoding strings:

// In a migration
table.addColumn(name: 'age', type: SqlTypes.integer, version: 2);

// In a model
@Column(collate: Collate.noCase)
final String username;

Available types: SqlTypes.text, SqlTypes.integer, SqlTypes.real, SqlTypes.blob, SqlTypes.numeric.


Referential Actions

When defining relationships, use ReferentialAction for onDelete and onUpdate parameters:

HasMany(
  model: Order,
  foreignKey: 'user_id',
  onDelete: ReferentialAction.cascade,
);

Available actions: cascade, setNull, setDefault, restrict, noAction.


🔗 Relationships

Relationships define how models connect. They are used by phorm for automatic Eager Loading.

  • HasMany: One-to-Many (e.g., User has many Posts).
  • HasOne: One-to-One (e.g., User has one Profile).
  • BelongsTo: Many-to-One (e.g., Post belongs to User).
@Schema(
  tableName: 'users',
  relationships: [
    HasMany(model: Post, foreignKey: 'user_id'),
    ManyToMany(
      model: Role,
      pivotTable: 'user_roles',
      foreignKey: 'user_id',
      relatedKey: 'role_id',
    ),
  ],
)
class User extends Model with _$PhormUserMixin { ... }

Query Results

phorm provides two read methods with explicit return types:

Method Returns Use when
readAll(...) Result<T> You only need the list of records
readAllWithCount(...) ResultWithCount<T> You need the list + total count (pagination)
// Simple read — no cast needed
final result = await userService.readAll(limit: 20);
for (final user in result.data) { ... }

// With count — no cast needed
final paged = await userService.readAllWithCount(limit: 20, offset: 40);
print('Page 3: ${paged.data.length} of ${paged.count} total');

Factories

Use the Factory<T> interface to generate model instances for testing or seeding:

class UserFactory extends Factory<User> {
  int _i = 0;

  @override
  User create() {
    _i++;
    return User(id: 'user_$_i', name: 'User $_i', email: 'user$_i@example.com');
  }
}

final users = UserFactory().createMany(50); // List<User>

Status

  • Dart SDK: >=3.0.0 <4.0.0
  • License: MIT License

Libraries

phorm_annotations