Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Data

Data

Storage types

On this page

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) 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) 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).

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 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 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:

example
type BoxSize = Enum {    small    medium}

database/migrations/20261020T1401_drop_large/changes.jadpo:

example
// 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), 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.

Markdown for agents: /docs/storage-types.md.Built from Jadpo main at a707efb.