Soft deletes
Soft deletion preserves a row and writes a deletion date. Enable both soft_deletes = True and a declared date column, normally deleted_at. Use the complete Document from scopes, which includes that setup.
Column, identity and SQL
Section titled “Column, identity and SQL”DELETED_AT = "removed_at" changes the expected name; declare and migrate that column. Inline columns are normalized to nullable by the metaclass; a shared table_definition with a nonnullable stamp raises OrmConfigurationException. The flag alone without a column does not enable soft deletion: deletion would be physical.
async def archive_document(document_id: int): document = await Document.findOrFail(document_id) if not await document.delete(): raise ValueError("Deletion was rejected") return document.trashed()delete() writes UTC and maintains the update timestamp when declared; the instance keeps its identity and persisted state. Model queries add deleted_at IS NULL by default. DB.table("documents") does not add that filter.
Instance API
Section titled “Instance API”trashed
Section titled “trashed”def trashed(self) -> boolChecks the in-memory deletion stamp and returns a bool; models without an active deletion column return False. Does not query SQL or prove row existence.
is_deleted = document.trashed()restore
Section titled “restore”async def restore(self) -> boolInstance: returns False without a deletion column or persistence. Runs restoring, sets None, performs the dirty UPDATE and emits restored; a veto returns False. Does not fire saving/updating/saved. May return True when already restored with no changes.
async def restore_document(document_id: int) -> bool: document = await Document.withTrashed().findOrFail(document_id) return await document.restore()forceDelete
Section titled “forceDelete”async def forceDelete(self) -> boolInstance: runs deleting, physically removes by original PK and emits deleted, including for models without soft deletion. Returns bool; unsaved instances/veto return False. There is no separate forceDeleted event.
async def permanently_remove(document_id: int) -> bool: document = await Document.withTrashed().findOrFail(document_id) return await document.forceDelete()Visibility and mass operations
Section titled “Visibility and mass operations”withTrashed
Section titled “withTrashed”def withTrashed(self) -> SelfIncludes live and deleted rows in this builder; returns the same builder. Does not disable other scopes. Configure before execution.
async def every_document(): return await Document.withTrashed().get()onlyTrashed
Section titled “onlyTrashed”def onlyTrashed(self) -> SelfSelects only rows with nonnull deletion stamps on execution. Without an active soft deletion column it adds no such filter.
async def deleted_documents(): return await Document.onlyTrashed().get()withoutTrashed
Section titled “withoutTrashed”def withoutTrashed(self) -> SelfRestores default exclusion before execution; does not remove filters already materialized in a used builder.
async def live_documents(): return await Document.withTrashed().withoutTrashed().get()restore
Section titled “restore”async def restore(self) -> intBuilder: returns 0 without a deletion column; otherwise restricts to deleted rows and clears the stamp. Returns a count without per-row restoring/restored. Use a new builder for this operation.
async def restore_tenant(tenant_id: int) -> int: return await Document.where("tenant_id", tenant_id).restore()forceDelete
Section titled “forceDelete”async def forceDelete(self) -> intBuilder: includes deleted rows and sends permanent DELETE with filters/scopes; returns a count. No events or ORM child/link deletion are performed.
async def purge_tenant(tenant_id: int) -> int: return await Document.where("tenant_id", tenant_id).forceDelete()delete
Section titled “delete”async def delete(self) -> intBuilder: with soft deletion updates the stamp and update timestamp; otherwise sends DELETE. Returns a count; does not hydrate or dispatch individual listeners.
async def archive_tenant(tenant_id: int) -> int: return await Document.where("tenant_id", tenant_id).delete()Relationships, events and edge cases
Section titled “Relationships, events and edge cases”Relationships query related models with their ordinary visibility. Include deleted rows through the related builder (team.players().withTrashed() if Player enables soft deletion), or define a direct variant for eager loading. Soft deleting a parent neither deletes children nor triggers SQL deletion cascades because it is UPDATE rather than DELETE.
Instance events are deleting → deleted or restoring → restored. There are no separate “soft deleted” events or saving/updating emissions for these operations. Vetoes stop writes; exceptions propagate. Mass methods do not execute these listeners.
SQL UNIQUE constraints still see deleted rows: archiving does not free a unique email/name. Restoration can conflict with schema constraints. trashed() only reflects memory, not another writer’s restore/purge. A physical deletion marks that instance unsaved; saving it afterwards may attempt a new INSERT. Filter permanent cleanup deliberately and use transactions when several related changes are involved.