# Counters and other one-step updates

When a new value depends only on the stored one, the database works it out
in one step and nothing needs checking. A view counter is the usual case:

```jadpo
action view_video(video_id: Video.id) fails VideoNotFound -> Video {
    return attempt Video.update(video_id) {
        set: {
            view_count: ViewCount(Video.view_count + 1)
            last_viewed_at: clock.now
        }
    }
}
```

On the right of `set:`, `Video.view_count` means the value stored at the
moment of the write, while `video.view_count` would be the copy loaded
earlier. Such an update takes the record's `id` instead of a loaded
record. Its values may use only parameters, constants
([Constants](constants.md)), `clock.now` and the record's stored
fields, so no increment is ever lost.

The statement is `UPDATE "video" SET "view_count" = ("view_count" + $1), ...`,
and no row is `VideoNotFound`.

The database works a stored field out only with `+` and `-`, which give
the same result in SQL as in Jadpo (`+` joining text is written `||`).
They must sit inside a construction the declared ranges prove:
`ViewCount(...)` here, since a count of at least 0 plus 1 is at least 0.
Anything else needs the record loaded first.

A decimal added to a stored field may not have more decimal places than
the field's `scale`, since PostgreSQL would round the sum its own way
(half away from zero) rather than Jadpo's. A literal counts by its own
places and any other value by its type's `scale`. A value whose type sets
no `scale` is refused (`data.stored_decimal_scale`).

## Which isolation level an action runs at

Which isolation level an action runs at follows from what it does. You
write nothing for it: the compiler decides it from the graph.

An action that loads no record, neither itself nor through any action it
calls, runs at PostgreSQL's read committed level. Of two requests counting
the same row at once, the second waits for the first, adds to its count
and commits, and neither reruns.

An action that loads a record runs at repeatable read
([Isolation levels](all-or-nothing-actions-and-concurrent-changes.md#isolation-levels)). So a counter racing another
request on a row it loaded reruns the action, as any concurrent change
does.

An action loads a record when it:

- reads one: `get`, or a query, with what it includes;
- is handed one: a parameter whose type holds a record, such as
  `video: Video`, or a list or object holding one;
- writes one back: an `update`, `delete` or transition of a record, rather
  than of its `id`;
- runs a transition whose `when:` can refuse a visible record, since
  telling that refusal from a missing record takes a second statement,
  which must see the first one's snapshot.

A create, and an update, delete or other transition by `id`, counters
among them, loads nothing: the store checks a reference a create sets in
the same statement. Calling a function never loads a record.

`jadpo check` decides each action and fixture from these and the actions
and fixtures it calls. It writes the level into the manifest as
`isolation` (`read_committed` or `repeatable_read`), and the runtime
begins the transaction at it (`BEGIN ISOLATION LEVEL READ COMMITTED` or
`REPEATABLE READ`). An action another action calls joins that action's
transaction at its level, which already counts what the callee does. Each
action a test calls begins its own.

So `view_video` above runs at read committed. Two requests counting one
video at once both commit at their first attempt, the second waiting for
the first's lock and adding to its count. A counter in an action that
read the video first still reruns the second. Both are checked live
against PostgreSQL in `crates/jadpo-cli/tests/writes.rs`.

What runs outside an action, such as a read a test makes itself,
migrations and purge, runs at repeatable read.
