# Storage types

You never name a column type. Each column is the store engine's own type
for the field's type:

| Field type | PostgreSQL |
|---|---|
| `Uuid` and types based on it | `uuid` |
| `Int` | `smallint`, `integer` or `bigint`, from the declared range |
| `Decimal { scale: 2 }` | `numeric(p, 2)` |
| `Bool`, `Text`, `Bytes` | `boolean`, `text`, `bytea` |
| `Instant`, `CalendarDate` | `timestamptz(3)`, `date` |
| `Duration` | `bigint`, in milliseconds |
| `Enum` without data | `text` holding the variant name; a standard enum holds its code, `GBP`, `Europe/London` |
| `Money` ([Money and payments](money-and-payments.md)) | two columns, always: `<field>_amount bigint` of minor units and `<field>_currency char(3)`, with a check constraint holding the currency to the one a type fixes |
| `ZonedTime` ([Configuration and time](configuration-and-time.md)) | two columns: `<field>_instant timestamptz(3)` and `<field>_zone text` holding its zone's code |
| `Enum` with data, `Object`, `List`, `Set`, `Map` | `jsonb` |

Structured fields, recursive ones included, are stored whole as `jsonb`,
in the API's JSON, which caches and Redis entities hold too
([Structured values](structured-values.md)).

The database enforces types, required fields, keys and references. The
application checks each field's own rules. Renaming an enum variant
changes stored values, so it needs a migration.

## Standard types in columns

A standard type whose data entry lists named columns is stored in those,
as `Money` is, rather than as `jsonb`
([Every boundary through the contract](every-boundary-through-the-contract.md)).
Every standard object and choice type is stored in named columns laid out
from its parts, `<field>_<part>`:

- a part that may be `none` is a column that may be `NULL`;
- an amount of money takes two columns, and another standard object its
  own;
- a choice's variant's name goes in `<field>_kind`, then each part its
  variants hold;
- a `ZonedTime` goes in `<field>_instant` and `<field>_zone`.

A range is one column of PostgreSQL's range type of its ends, a
`LocalTime` is `time`, and a `LocalDateTime` is `timestamp` without a zone,
each named for the field. Every standard enum holds its code
(`Europe/London`, `GET`), as `Currency` does. A quantity's column carries
a comment naming its kind and SI unit (`Length in metres (m)`). A rate
type's fixed `from:` and `to:` get check constraints, as a `Money` type's
`currency:` does.

Only the standard collections (`RichText`, `RichTextBlock`,
`LocalisedText`) stay structured `jsonb`, holding their contract's JSON,
which the route codec writes and reads back.

A geometry (`Point` and the other five, or `Geometry`) is one PostGIS
`geography` column. It is untyped, and a check constraint holds it to its
geometry type and to WGS84. The first migration with one creates the
extension ([Storage](geospatial-storage.md) under Geospatial).

## Changing what a field allows

Each migration's snapshot records what every field allows beyond its
column type: an enum's variants, and the field's bounds, lengths and
patterns. So changing them is drift that `jadpo check` reports like any
other.

- **A change that allows more**, such as a variant added or a longer
  `max_length`, is a migration with nothing to run.
- **A change that allows less**, such as a variant renamed or removed, a
  shorter `max_length` or a smaller range, needs a decision. `backfill:`
  gives the records already stored, whose value the field no longer
  allows, one it does. The migration's SQL replaces exactly those.

Here `large` leaves `BoxSize`, and parcels stored as large become medium:

```jadpo
type BoxSize = Enum {
    small
    medium
}
```

database/migrations/20261020T1401_drop_large/changes.jadpo:

```jadpo
// changes.jadpo
/// Stops sending large boxes.
migration {
    Parcel.size: {
        backfill: BoxSize.medium  // parcels stored as large become medium
    }
}
```

The migration's SQL is `UPDATE "parcel" SET "size" = 'medium' WHERE
"size" NOT IN ('medium', 'small')`.

An older snapshot that doesn't record what fields allow has it recorded by
the next migration, with nothing to decide.

## A stored value is checked when it is read

Every stored value is checked when it is read, by its field's validator:
the same check a write and a backfill use. That is the field's own rules,
then its type's along its chain, a built-in type's own check (`Email`,
`Url`), and a standard object's rules across its parts. Nothing checks a
value twice, and a value a write would keep is never refused on read.

Every read that makes a stored value checks it: a query's records, `get`,
each kind of include (a parent, a child, children), a nearest query's
records, a map layer's properties and geometry, a `geo.` condition's
recheck, and the record a `create` or an `update` gives back, with every
stored field it holds. So does each field a backfill worked out in Jadpo
reads.

An aggregate makes a stored value only for a group by a field and for
`min` and `max`, which are checked. A count, a sum, an average and a time
bucket are worked out, not stored, so a sum past a field's `max:` is no
defect. A field kept as JSON is read by its codec with its type's and the
field's rules ([Structured values](structured-values.md)), and not checked
again. A field a migration retires has no declaration left, so a backfill
reads it as its built-in type alone.

**A row that breaks a rule.** A row written around Jadpo that breaks a
rule is a defect naming the field and the rule as written, never the
value: `` a stored `Tool.stars` that breaks `max: 5` ``. So the fault
behind a 500 or a stopped backfill names what to fix and leaks nothing.
`none` breaks no rule. A backfill stops as it does on any defect, naming
the record's key (`it read a stored …`).

`scale:` can't break on read. A decimal with `scale:` is kept in a
`numeric(p, s)` column, which rounds anything written into it to its
places, so ranges, lengths and patterns are the rules such a row can
break.

Not built yet: a new `pattern:` on a stored field, since PostgreSQL's
regular expressions aren't Jadpo's (`jadpo migrate new` says so);
comparing or ordering by a standard type stored in named columns, or by a
standard collection, in a query; and `CHECK` constraints holding fields'
rules in the database.
