# Delete

> Ends a query chain with a set-based DELETE: every row the chain selects is deleted in the database, at the call, in one statement — and the call answers how many went. It lands with your unit of work's commit, or not at all. Zero is an answer, not an error. A row the caller cannot read, or that the entity's delete rules refuse, is simply not in the set.

<!-- id: query-delete · area: query · stability: stable · html: https://osysharp.com/reference/query/delete/ -->

## Summary        {#summary}
`.Delete()` ends a query chain the way `.Count()` does — but instead of counting the matching rows it **deletes**
them, set-based, in the database, at the call site. The answer is how many rows were deleted. It is the same verb
as C#'s `ExecuteDelete`, under the natural name.

## Signature      {#signature}
```osy syntax
<Entity>.Where(o => …).Delete()                    →   int   // every matching row, one statement
<Entity>.Where(o => …).OrderBy(k).Take(n).Delete() →   int   // at most n rows — the chunked purge
<Entity>.Delete()                                  →   int   // the WHOLE set — deliberate, like C#'s ExecuteDelete
<a Query<T> binding or parameter>.Delete()         →   int   // a deferred chain composes into the terminal
```

## Description    {#description}

### Which rows does it delete?   {#the-target-set}
Exactly the rows the chain would have **returned to you**, narrowed further by the entity's `allow delete` rules.
A row your read security hides is not in the set; a row the delete rules refuse stays untouched. Neither is an
error — the statement deletes fewer rows, and the count says how many it was.

```osy title="delete all of a company's stale orders" test app=query-delete
entity Order {
  [Required] string Status;
  decimal Total;
}

int PurgeStale() {
  return Order.Where(o => o.Status == "Stale").Delete();
}
```

Zero is a normal answer: a predicate that matches nothing deletes nothing and answers `0`. A bare
`Order.Delete()` is the whole set, on purpose — the receiver names the set as plainly as `Order.Count()` does, and
the declared security still bounds it. And a [`Query<T>`](https://osysharp.com/reference/query/deferred/) built elsewhere — a binding, or a
parameter crossing a function boundary — ends in the terminal exactly like an inline chain.

### When does it run?   {#immediacy}
At the call — the statement runs there, and the count it answers is real — but it **lands with your unit of work's
commit**, together with everything else the unit of work writes, or not at all. Your reads after it already see the
rows gone (they run inside the same transaction); nobody else does until the commit. A later step that throws, a
commit that is refused, or a `UnitOfWork.Discard()` rolls the delete back with the rest. A statement that fails and
is caught undoes only itself, and one inside a `try` body that faults goes with that body.

It is a statement against the STORED rows, so it does not see rows you have created or edited in the current unit
of work. If you hold uncommitted changes of the same entity type, the call refuses and tells you to `Commit()` or
`Discard()` them first — silently deleting around your pending edits would be worse.

From the statement to the commit the unit of work holds one database connection, and the rows the statement
selected stay locked — so a long wait between the two (a call to a slow service) holds them that long. On a page
whose unit of work stays open between requests, the statement lands when the server call that ran it ends.

### What happens to related rows?   {#cascade}
The same thing a per-row delete does: a required child (`[Required] Order Order;` on the child) is deleted with
its parent, an optional reference is set to null, and a relation declared to restrict blocks the delete. The
answered count is the **target** rows — cascaded children are not counted.

```osy title="children go with their parents; the count is the parents" test app=query-delete
entity Invoice {
  [Required] string State;
  [ForeignKey(Invoice)] InvoiceLine[] Lines;
}
entity InvoiceLine {
  [Required] Invoice Invoice;   // required → deleted with its invoice
  int Qty;
}

int DropDrafts() {
  return Invoice.Where(i => i.State == "Draft").Delete();   // lines cascade; count = invoices
}
```

### How do I delete a lot without one huge statement?   {#chunked-purge}
`OrderBy` and `Take` compose like on any chain, which gives the chunked-purge idiom — delete a bounded slice per
call and stop when the answer is zero:

```osy title="purge in bounded chunks" test app=query-delete
int PurgeOldest() {
  return Order.Where(o => o.Status == "Stale").OrderBy(o => o.Total).Take(100).Delete();
}
```

### One row I already hold?   {#per-row}
The receiver decides which verb you get. A **loaded entity** deletes that row through the unit of work, at commit,
like any other staged write; a **query chain** deletes set-based, at the call (landing with the same commit):

```osy title="the two receivers, side by side" test app=query-delete
void DropOne(Order o) {
  o.Delete();                                       // this row — staged, lands at commit
}
int DropMatching(string status) {
  return Order.Where(x => x.Status == status).Delete();   // the set — at the call, counted
}
```

### A list in memory?   {#local-lists}
`.Delete()` deletes **database rows**. A local list already has its verb — `list.RemoveAll(x => …)` — and the
compiler says so if you reach for the wrong one.

## Examples       {#examples}
```osy title="a maintenance function" test app=query-delete
entity Session2 {
  [Required] string Token;
  DateTime ExpiresAt;
}

int ReapExpired() {
  return Session2.Where(s => s.ExpiresAt < DateTime.UtcNow).Delete();
}
```

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — saying which rows, before the terminal
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Take`, for the chunked purge
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `allow delete` rules that narrow the set
