Skip to main content

Soft Deletes

Soft deletes allow you to "delete" records by setting a timestamp rather than removing them from the database. This is useful for maintaining audit trails or implementing trash/restore functionality.

Enabling Soft Deletes

Add the SoftDeletes marker mixin to your model:

(table: 'posts')
class SoftDeletePost extends Model<SoftDeletePost> with SoftDeletes {
const SoftDeletePost({required this.id, required this.title});

(isPrimaryKey: true, autoIncrement: true)
final int id;

final String title;
}

The code generator detects this mixin and:

  • Adds a virtual deletedAt field if not explicitly defined
  • Applies soft-delete implementation to the generated tracked class
  • Enables soft-delete query scopes automatically

Timezone-Aware Soft Deletes

For deletion timestamps stored in UTC, use SoftDeletesTZ:

(table: 'articles')
class SoftDeleteArticleTz extends Model<SoftDeleteArticleTz>
with SoftDeletesTZ {
const SoftDeleteArticleTz({required this.id, required this.title});

(isPrimaryKey: true, autoIncrement: true)
final int id;

final String title;
}

Migration Setup

Add the soft delete column in your migration:

class AddSoftDeletesToPosts extends Migration {
const AddSoftDeletesToPosts();


void up(SchemaBuilder schema) {
schema.table('posts', (table) {
// Non-timezone aware
table.softDeletes();

// OR timezone aware (UTC storage)
// table.softDeletesTz();
});
}


void down(SchemaBuilder schema) {
schema.table('posts', (table) {
table.dropColumn('deleted_at');
});
}
}

Querying Soft Deleted Records

Default Behavior

By default, soft-deleted records are excluded from queries:

Future<void> softDeleteDefault(DataSource dataSource) async {
// Only returns non-deleted posts
final posts = await dataSource.query<$Post>().get();
}

Include Soft Deleted

Use withTrashed() to include soft-deleted records:

Future<void> softDeleteWithTrashed(DataSource dataSource) async {
// Returns all posts including deleted
final allPosts = await dataSource.query<$Post>().withTrashed().get();
}

Only Soft Deleted

Use onlyTrashed() to get only soft-deleted records:

Future<void> softDeleteOnlyTrashed(DataSource dataSource) async {
// Returns only deleted posts
final trashedPosts = await dataSource.query<$Post>().onlyTrashed().get();
}

Soft Delete Operations

Deleting Records

Standard delete operations soft-delete when the model has SoftDeletes:

Future<void> softDeleteOperations(
DataSource dataSource,
Post post,
int postId,
) async {
final repo = dataSource.repo<$Post>();

// Soft delete (sets deleted_at)
await repo.delete(post);
await repo.delete({'id': postId});
}

Restoring Records

Restore soft-deleted records:

Future<void> softDeleteRestore(
DataSource dataSource,
Post post,
int postId,
int userId,
) async {
final repo = dataSource.repo<$Post>();

// Restore a single record
await repo.restore(post);
await repo.restore({'id': postId});

// Restore using query callback
await repo.restore((Query<$Post> q) => q.whereEquals('author_id', userId));
}

Force Delete

Permanently remove a record (bypass soft delete):

Future<void> softDeleteForce(
DataSource dataSource,
Post post,
int postId,
) async {
final repo = dataSource.repo<$Post>();

// Permanently delete
await repo.forceDelete(post);
await repo.forceDelete({'id': postId});
}

Checking Soft Delete Status

The trashed getter returns true if the model has been soft deleted.

if (user.trashed) {
print('User is in the trash');
}

Carbon Integration

The deletedAt getter returns a CarbonInterface?, allowing for fluent date manipulation:

print(user.deletedAt?.format('Y-m-d H:i:s'));
print(user.deletedAt?.diffForHumans()); // "deleted 5 minutes ago"
deletedAt is Immutable

Like other timestamp fields, deletedAt returns an immutable Carbon instance. You can safely chain methods without affecting the model's state:

final deletedYesterday = user.deletedAt?.subDay(); // Safe - returns new instance

Flexible Setters

The deletedAt setter accepts both DateTime and CarbonInterface:

user.deletedAt = DateTime.now();
// or
user.deletedAt = Carbon.now();

Combining with Timestamps

You can use both Timestamps and SoftDeletes:

(table: 'posts')
class CombinedPost extends Model<CombinedPost> with Timestamps, SoftDeletes {
const CombinedPost({required this.id, required this.title});

(isPrimaryKey: true, autoIncrement: true)
final int id;
final String title;
}

// Or timezone-aware versions
(table: 'tz_posts')
class CombinedPostTz extends Model<CombinedPostTz>
with TimestampsTZ, SoftDeletesTZ {
const CombinedPostTz({required this.id, required this.title});

(isPrimaryKey: true, autoIncrement: true)
final int id;
final String title;
}

Migration:

void softDeleteMigrationCombined(SchemaBuilder schema) {
schema.create('posts', (table) {
table.id();
table.string('title');
table.timestampsTz(); // created_at, updated_at
table.softDeletesTz(); // deleted_at
});
}