# Atomic updates (-= and +=)

> `x.Stock -= n` and `x.Stock += n` on a stored number are written as ONE statement relative to the row as the database holds it, refused when it would pass the field's [Min] or [Max] — so concurrent changes each take their share, nothing is lost, and the last unit cannot be sold twice.

<!-- id: entity-atomic-updates · area: entity · stability: stable · html: https://osysharp.com/reference/entity/atomic-updates/ -->

## Summary        {#summary}
Writing `x.Stock -= n` (or `+= n`) on a stored number does not write the number you computed. It writes **the change**:
one statement, `stock = stock - n`, relative to the row as the database holds it at that instant, and refused when the
result would pass the field's `[Min]` or `[Max]`. Two orders for a shirt there are two of both land; two orders for the
last one cannot both have it — the second is refused with the field and its bound named. No read, no retry, no lock.

Every other write is **last writer wins**, exactly as before: `x.Stock = 5` writes 5.

## Signature      {#signature}
```osy syntax
row.Number -= amount;            // also: row.Number = row.Number - amount;  row.Number--;
row.Number += amount;            // also: row.Number = row.Number + amount;  row.Number++;
```

## Description    {#description}

### What it writes
For a field declared `[Min(0)] int OnHand;`, the line `lot.OnHand -= n;` commits as

```sql
UPDATE lot SET on_hand = on_hand - @n WHERE id = @id AND on_hand - @n >= 0 RETURNING on_hand
```

Deciding and writing are one statement, so there is no window between them: whatever another request committed a
moment earlier is what the subtraction starts from. The value the code read is never written back, so a stale read
cannot undo somebody else's change.

### The bound is the field's own `[Min]` and `[Max]`   {#bounds}
| declared | `-= n` is refused when | `+= n` is refused when |
|---|---|---|
| no `[Min]`, no `[Max]` | never — it may go as far negative as it likes | never |
| `[Min(0)]` | the result would be below 0 | never |
| `[Min(-10)]` | the result would be below −10 — an overdraft down to ten | never |
| `[Max(100)]` | never | the result would be above 100 |
| `[Range(0, 100)]` | below 0 | above 100 |

A `[Min]` or `[Max]` with a `when` condition is checked as an ordinary rule at commit, not by the statement.

### What counts as an adjustment
The left side and the read on the right are **the same field of the same row**, and the field is a stored,
non-nullable `int`, `long`, `decimal` or `double`:

| written | is |
|---|---|
| `lot.OnHand -= n;` · `lot.OnHand = lot.OnHand - n;` · `lot.OnHand--;` | an adjustment |
| `lot.OnHand += n;` · `lot.OnHand = lot.OnHand + n;` · `lot.OnHand++;` | an adjustment |
| `lot.OnHand = counted;` | a plain write — last writer wins |
| `lot.OnHand = other.OnHand - n;` | a plain write — it reads another row |
| `lot.OnHand = lot.OnHand * 2;` | a plain write — only `+` and `-` are relative |

### In the unit of work
Reading the field after an adjustment answers the value you would expect — what was read, plus the change — and two
adjustments of one field in one unit of work commit as their sum. A plain write after an adjustment replaces it: the
row then commits the number you wrote. `UnitOfWork.Discard()` and a savepoint's undo take an adjustment back with the
rest. After the commit the row holds **what the database answered**, which includes everybody else's changes — so
`lot.OnHand` read after `UnitOfWork.Commit()` is the shelf as it really is.

A page action that writes `row.OnHand -= 1` sends the change, not the number, so it reaches the server as the same
statement. Security applies to it exactly as to any update of that field.

### What happens when a change would pass the bound?   {#refusal}
A change that would pass the bound is refused at its commit with a `ValidationException` naming the field and the
bound — *"OnHand must be at least 0."*, or the message you declared (`[Min(0, "There are not that many left.")]`).
Nothing of that commit is written. Catch it like any other refused write, and discard what failed
([[function-throw#the-refused-row]]):

```osy syntax
try { lot.OnHand -= n; UnitOfWork.Commit(); }
catch (ValidationException e) { UnitOfWork.Discard(); return e.Message; }
```

In a workflow, a refusal the code's own read could not have seen coming — the stock was there when it looked, and gone
when it wrote — is a race, not a rule, so the step is run again: it reads afresh, finds the stock gone, and takes the
path you wrote for that.

### `[ConcurrencyCheck]` on a number is usually the wrong tool   {#not-concurrency-check}
[[entity-concurrency-check|`[ConcurrencyCheck]`]] refuses a write whose field changed since it was read. On a record
people edit that is exactly right; on a quantity it refuses the second of two orders that could both have been
served. An adjustment still honours a declared check — some numbers are lock tokens that exist to make two writers
meet — so the remedy for a quantity is to drop the check and declare the floor. `osy lint` says so
(`data-concurrency-check-on-a-moved-number`): a checked number the code only adds to or takes from, reads, never sets
outright, and never bounds by another column of its own row.

### Who took how much: a row per move   {#movements}
The running total answers *how many*; it cannot answer *who took two on Tuesday*. For that, write a row for each move
beside the total, in the same commit — one function that moves the number and records why, so no caller can do one
without the other. The shop kit does this: `MoveStockAt` adjusts the variant's and the location's numbers and writes a
`StockMove` (the change, what it held, the reason, the order or transfer, and who), so `Variant.Stock` is always the
running total of its moves.

## Examples       {#examples}

```osy title="the last one cannot be sold twice" test app=atomic-updates
entity Lot {
  [Required, MaxLength(40)] string Code;
  [Min(0, "There are not that many left.")] int OnHand = 0;
  /// An account that may run ten into debt, and no further.
  [Min(-10)] decimal Tab = 0;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

/// Takes `n` from the shelf — refused, with the declared sentence, when there are fewer than `n` left at commit.
[Server, AllowAnonymous]
string Take(Guid id, int n) {
  var lot = Lot.Single(x => x.Id == id);
  try {
    lot.OnHand -= n;
    UnitOfWork.Commit();
  } catch (ValidationException e) {
    UnitOfWork.Discard();
    return e.Message;
  }
  return $"{lot.OnHand} left";
}

[Server, AllowAnonymous]
void Charge(Guid id, decimal amount) {
  var lot = Lot.Single(x => x.Id == id);
  lot.Tab -= amount;                       // down to −10; past that the commit is refused
  UnitOfWork.Commit();
}
```

```osy title="a row per move beside the running total" test app=atomic-moves
enum MoveReason { Received, Sold, Counted }

entity Bin {
  [Required, MaxLength(40)] string Code;
  [Min(0)] int OnHand = 0;
  security { allow read, create, update when IsAuthenticated; }
}

entity BinMove {
  [Required] Bin Bin;
  int Change;
  MoveReason Reason;
  [CreatedAt] DateTime At;                 // when, and who — written by the platform
  [CreatedBy] Guid? By;
  security { allow read, create when IsAuthenticated; }
}

/// The ONE writer of a bin's number: it moves the total and records why, in the same commit.
[Server, Authorize(IsAuthenticated)]
void Move(Guid id, int change, MoveReason reason) {
  var bin = Bin.Single(x => x.Id == id);
  bin.OnHand += change;
  new BinMove { Bin = bin, Change = change, Reason = reason };
  UnitOfWork.Commit();
}
```

## See also       {#see-also}
- [ConcurrencyCheck](https://osysharp.com/reference/entity/concurrency-check/) — refusing a stale write to a record people edit
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Min]`, `[Max]` and `[Range]`, the bounds an adjustment is held to
- [throw](https://osysharp.com/reference/function/throw/) — `ValidationException`, and discarding a refused write
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — catching it
- [entity](https://osysharp.com/reference/entity/declaration/) — where the attributes go
