# constraints

> The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the storage-shaping Immutable, Precision and MaxBytes. They are checked when the row is written, so a bad row cannot reach the database from any code path.

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

## Summary        {#summary}
Constraints are per-member rules enforced **when the row is written**. They are not form validation you can forget to
call: a row that breaks one cannot reach the database, whatever code path tried to write it — a function, an import,
a test, an API call.

## Signature      {#signature}
```osy title="the per-member rules, and where each one goes" syntax
[Unique(<Member>, <Member>)]                      // …or over a COMBINATION, declared on the entity
entity <Name> {
  [Required]                 <Type>   <Member>;   // must be present
  [Unique]                   string   <Member>;   // no two rows may share a value
  [MaxLength(<n>)]           string   <Member>;   // bounded text
  [Min(<n>), Max(<n>)]       int      <Member>;   // an inclusive numeric range
  [Pattern("<regex>")]       string   <Member>;   // must match
}
```

Every constraint takes an optional **message** as its last argument — what the user is told when the rule refuses
them:

```osy title="giving a constraint the message the user is told" syntax
  [Pattern("<regex>", "<message>")]  string  <Member>;
  [MaxLength(<n>, "<message>")]      string  <Member>;
  [Required("<message>")]            <Type>  <Member>;
  [Unique("<message>")]              string  <Member>;   // shown when the value collides with another row
```

…including the **entity-level composite** form, where the message goes last, after the members:

```osy title="a message on a combination — the members first, the sentence last" syntax
[Unique(<Member>, <Member>, "<message>")]
entity <Name> { … }
```

`[Unique]` is the one whose default wording helps least — a collision otherwise surfaces as the raw index name — so
its message is worth writing: `[Unique("That slug is already taken.")]`.

## Description    {#description}

### Which constraint attributes are there?   {#the-constraints}

| Attribute | Rule | On a null value |
|---|---|---|
| `[Required]` | the member must be present when the row is written | **this is the one that rejects null** |
| `[Unique]` | no two rows may hold the same value | passes — nulls do not collide |
| `[MaxLength(n)]` · `[MinLength(n)]` | text is at most / at least `n` characters | passes |
| `[Min(n)]` · `[Max(n)]` | an **inclusive** numeric range | passes |
| `[Pattern("…")]` | text matches the regular expression | passes |
| `[Immutable]` | may be set on create, but **never updated** | passes (it governs updates, not presence) |
| `[Precision(p, s)]` | a decimal stored with `p` total digits and `s` after the point | passes |
| `[MaxBytes(n)]` | the value's stored size is at most `n` bytes | passes |

Read that last column carefully: **every constraint except `[Required]` lets null through.** That is the correct
behaviour — "if there is a value, it must look like this" is a different rule from "there must be a value" — but it
surprises people. If a code must be present *and* well-formed, say both:

```osy title="present AND well-formed needs both" test app=entity-constraints
entity Product {
  [Required, Pattern("^[A-Z]{3}$")] string Code;   // must exist, and must be three capitals
  [Pattern("^[A-Z]{3}$")] string AltCode;          // may be absent; if present, must match
}
```

**Give a `[Pattern]` a message.** Every other constraint refuses in words a person can act on — *"Email is required"*,
*"Name is too long"*. A pattern refuses with the regular expression, which explains nothing to the person reading it:

```osy title="say what the shape means" test app=entity-constraints-message
entity Product {
  [Required, Pattern("^[A-Z]{3}$", "must be three capital letters")] string Code;
}
```

### Can I stack several on one member?   {#combining}
Attributes stack, in one bracket or several — whichever reads better:

```osy title="the full constraint set" test app=entity-constraints
entity Coupon {
  [Required, Unique, MaxLength(20)] string Code;   // present, one of a kind, bounded
  [Min(1), Max(100)] int PercentOff;               // inclusive: 1 and 100 both pass
  [MaxLength(500)] string Notes;                   // optional, but bounded when given
}
```

### `[Required]` on a reference forbids an orphan   {#required-reference}
`[Required]` works on a [reference](https://osysharp.com/reference/entity/relations/) too, and it is how you say "this child cannot exist without
its parent":

```osy title="a child that cannot be orphaned" test app=entity-constraints
entity Order {
  [Required] string Code;
}

entity LineItem {
  [Required] Order Order;   // a line with no order is a violation, not a stray row
  decimal Amount;
}
```

### `[Unique]` is enforced by the database   {#unique}
`[Unique]` is a real unique index, not a check-then-insert in application code. That distinction matters under
concurrency: two requests racing to claim the same coupon code cannot both win, because the second one is rejected by
the database rather than by a check that already passed.

⛔ **AND A SWAP BETWEEN TWO EXISTING ROWS IS REFUSED — the whole commit rolls back, silently.** This is the one
interaction an ordered list actually performs, and it is the opposite of what most people assume, so it is worth
stating plainly:

```osy syntax title="this does not work, and nothing on screen says why"
[Unique] int Position;
// …then, in an action:
int mine = job.Position;
job.Position  = above.Position;   // ← both rows now hold the same value for an instant
above.Position = mine;
UnitOfWork.Commit();              // ← the index refuses; NOTHING is written
```

A unique INDEX is checked per statement, not at commit, so the intermediate state where two rows share a value is
rejected even though the state you were committing is legal. **Measured**: the two rows come back unchanged, and
before 2026-08-29 the refusal reached nobody — no error, no banner, no fault in the test. It now surfaces in a
failing test as `action 'MoveUp' failed: Constraint violation: 'Job.Position' must be unique`, but the write still
does not land.

**So for a position column, do one of these:**

| | |
|---|---|
| **leave it un-`[Unique]`** | the ordinary answer. A rank has no meaning in a gap or a repeat beyond "which comes first", and nothing else depends on it being distinct. |
| **write the midpoint instead of swapping** | `job.Position = (above.Position + below.Position) / 2m` over a `decimal` — one row changes, so no two rows ever collide. This also stops a move rewriting the whole tail. |

⚠ **`[Unique]` is not deferrable today, and that is why.** Foreign keys are (they are emitted as deferrable
constraints); `[Unique]` is emitted as a `CREATE UNIQUE INDEX`, and Postgres cannot defer an index. Making it
deferrable would make the swap above work as written — it is a real option and it is not built.

**Over a COMBINATION of members, write it on the entity** — `[Unique(A, B)]` above the declaration, naming two or
more members. It is the same real unique index, over the pair — **and it takes the same optional message, written
last**: `[Unique(A, B, "…")]`. That is one form, not two, and it is the one to reach for. Without the message the
person is shown the index's own words; with it, they are told what they did:

```osy title="one membership per person per channel, and what a second one is told" test app=entity-unique-composite
entity Channel {
  [Required, MaxLength(60)] string Name;
}

[Unique(Channel, Person, "They are already in this channel.")]
entity Membership {
  [Required] Channel Channel;
  [Required] User Person;
}

[Principal] entity User {
  [Required, MaxLength(200)] string Email;
}
```

This is the constraint a **join table** wants, and reaching for a `Membership.Any(…)` guard instead is the mistake the
paragraph above describes: two requests can both read "not a member" before either writes, and only the index is
enforced where that race is. Keep the guard as well if you want a quiet no-op on a double-click — but it is the
convenience, not the rule.

**Which field does the violation land on?** For a composite one, a field whose name is **the members joined with
`", "`, in declaration order** — `Channel, Person` for the `Membership` above. Not either member on its own: the rule
is about the combination, so what is refused is the combination, named as one thing. That is what a test aims at, and
it is the one string you need:

```osy syntax title="aiming a test at the pair, not at either member"
Assert.Violation("Channel, Person");                                // the pair collided
Assert.Violation("Channel, Person", "already in this channel");     // …and this is what it says
```

A single-member `[Unique]` is the ordinary case — the field is just the member. Either way the violation is raised
by the **server**, at the save, because "is this taken?" is a question about other rows; see
[[testing-ui#composite-unique]] for when a test may assert it.

⚠ A unique constraint is over a **table**, and one table holds a whole [inheritance](https://osysharp.com/reference/entity/inheritance/) hierarchy —
so one declared on a base spans every type derived from it. That is usually what you want, and the compiler says so
either way (it warns, naming every type covered).

### Write-once, decimal precision, and size bounds   {#storage-shaping}
Four constraints shape a value beyond "is it valid":

- **`[Immutable]`** is a rule about *updates*, not presence: the member may be set when the row is created and then
  **never changed**. It is how you say "an order's placed-date is written once" — an attempt to update it is refused,
  from any code path. Pair it with `[Required]` when the value must also be present from the start.
- **`[MinLength]`** is the floor to `[MaxLength]`'s ceiling: `[MinLength(n)]` requires text of at least `n` characters.
- **`[Precision]`** pins how a `decimal` is stored: `[Precision(p, s)]` gives `p` total significant digits, `s` of them
  after the decimal point. `[Precision(10, 2)]` is the shape of money — up to eight digits before the point, two after.
- **`[MaxBytes]`** bounds the stored size of a value in bytes: `[MaxBytes(n)]` caps a large field — a `Json` document or
  rich text — where the limit you care about is storage, not character count.

```osy title="immutable, precision, and a byte bound" test app=entity-constraints
entity Invoice {
  [Required, Immutable] DateTime IssuedAt;      // set once, at creation; never edited afterwards
  [Precision(10, 2)] decimal Amount;            // money: 8 digits before the point, 2 after
  [MinLength(3), MaxLength(20)] string Number;  // a bounded reference code
  [MaxBytes(1048576)] Json Payload;             // at most 1 MB of stored JSON
}
```

### The rule spans several members — what then?   {#beyond-one-member}
A constraint speaks about **one member**. When the rule spans several — "shelf days must be under 30, but only for
perishable items" — you want an [invariant](https://osysharp.com/reference/entity/invariants/).

## See also       {#see-also}
- [invariant](https://osysharp.com/reference/entity/invariants/) — rules that span several members of the row
- [entity members](https://osysharp.com/reference/entity/properties/) — the members these constrain
- [relations](https://osysharp.com/reference/entity/relations/) — `[Required]` on a reference
