Skip to content

Soft deletes — Examples

Instead of physically deleting a row, stamp a deleted_at and hide it from reads. A Soft mixin carries the column and a policy that filters deleted rows out of every SELECT:

abstract type Soft {
deleted_at: datetime;
policy not_deleted for select using ( .deleted_at is null );
}
type Article extends Base, Soft {
required title: str;
}

“Delete” is an update that sets the timestamp:

update Article filter .id = $id<uuid> set { deleted_at := now() };

Reads through the app role only ever see live rows — the policy appends WHERE deleted_at IS NULL for you:

multi select Article { id, title };

Counting explicitly (e.g. from a privileged role that bypasses RLS) still works with an is null filter:

select count(Article filter .deleted_at is null);

Save the three queries as soft_delete_article.aql, list_articles.aql, and count_live_articles.aql:

await runner.query.softDeleteArticle({ id }); // stamps deleted_at
const live = await runner.query.listArticles(); // ListArticlesRow[] — deleted rows hidden by the policy
const n = await runner.query.countLiveArticles(); // number