# The data model (entities and their rules)

> An entity is a table — and the rules that are true about it. Not just its columns: what must hold for a row to exist, and who may see or change it, live ON the entity, next to the data, and are enforced for every path that ever touches it. This is where an app starts, and getting it right is most of getting the app right.

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

## Summary        {#summary}
An **entity** is a table of rows your app stores. But the declaration is not a list of columns — it is everything that
is **true** about those rows:

```osy title="a table, and what is true about it" test app=entity-index
entity Order {
  [Required, Unique, MaxLength(20)] string Code;   // the shape …
  [Required] decimal Total;
  bool Cancelled;

  invariant Total >= 0;                            // … what must hold for a row to exist …

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }         // … and who may touch it
}
```

Three different questions, answered in one place: **what shape is a row** ([entity members](https://osysharp.com/reference/entity/properties/)), **what makes a row
valid** ([constraints](https://osysharp.com/reference/entity/constraints/) · [invariant](https://osysharp.com/reference/entity/invariants/)), and **who may see or change it** ([The security model](https://osysharp.com/reference/security/index/)).

They are enforced for **every** path that ever touches the table — this function, the next one, the UI, a workflow, an
import you write in six months. There is nowhere to write a row that goes around them, so there is nowhere to forget.

## Description    {#description}

### What the platform gives every row   {#free-columns}
You never declare an identifier, and you never declare an audit trail:

| Member | What it is |
|---|---|
| `Id` | the row's identity, a `Guid`, assigned for you |
| `CreatedAt` · `ModifiedAt` | when the row was written, and last changed |
| `CreatedBy` · `ModifiedBy` | **who** — the signed-in principal, not a value your code passes |

`CreatedBy` is worth pausing on, because it is not just an audit column: it is stamped from the **security principal**,
so a rule may authorise on it. That is how ownership works with **no owner field and nothing in your code that assigns
one**:

```osy title="ownership, declared once, with no owner field" test app=entity-index-owner
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read where Id == user.Id; }
}

entity Note {
  [Required, MaxLength(200)] string Title;

  security {
    allow create when IsAuthenticated;
    allow read, update, delete where CreatedBy == user.Id;   // ← ownership, in one line
  }
}
```

Every reader of `Note` now sees only their own — including `Note.Count()`, which honestly means *"how many are mine"*.

### What an unset member reads back   {#optional}
**The declaration decides**, and it reads exactly as C# does:

```osy title="what the type spelling decides about being unset" syntax
decimal Amount;            // has an honest zero → unset reads 0.
decimal? Amount;           // `?` → may be unset. Unset reads null.
string Title;              // NO honest zero → REQUIRED. You must supply it.
```

Whether a member is required is carried by its **type spelling** — full model in
[Optional and required members](https://osysharp.com/reference/types/optional-and-required/); the shapes are:

| Type | Bare (no `?`, no default) |
|---|---|
| `int` `long` `double` `decimal` `bool` `TimeSpan` — an **honest zero** | reads its zero (`0`, `false`, `0m`); never null |
| `string` `DateTime` `DateOnly` `TimeOnly` `Guid` `Json` `RichText` `Markdown` `Vector` `byte[]`, any **enum** — **no** honest zero | **REQUIRED** — you must supply it |
| an entity reference (`Customer Owner;`) | **optional** — reads `null`; `[Required]` demands it |
| any `T?` (`int?` `string?` `DateTime?` …) | optional — reads `null` |

The reason those types have no zero is worth a sentence: the year `0001` is not a date anybody meant (it sorts first
and matches `Due < today`), all-zero bits is not a `Guid` anyone assigned, and `default(string)` is `null`, not `""` —
an empty string is a value someone *typed*, not the absence of one. Rather than invent a value it would then have to
answer questions about, the platform **requires** you to supply one. Say `?` (`DateTime? Due;`) when "maybe unset" is
genuinely what you mean, or give a default (`string Tag = "";`) when the empty value really is the right starting point.

**A required member is checked when the value becomes real** — for an entity, at **commit**: `new Ticket {}` compiles
(an empty draft is a normal state — a create-form binds inputs to fill it), and the row is refused at save if a
required member is still unset, naming it. `[Required]` on a *reference* opts that one exception into the same rule.

It is also the **only** constraint that rejects an unset value. `[MaxLength]`, `[Unique]`, `[Pattern]` and the rest all
let one through, because "no value" is not a violation of "at most 20 characters" ([constraints](https://osysharp.com/reference/entity/constraints/)).

**The consequence to know about.** An [invariant](https://osysharp.com/reference/entity/invariants/) reads a member, and a member that *can* be unset
reads back `null` — so an invariant over an optional member is asking a question about a value that may not be there:

```osy title="an invariant over an optional member quietly makes it mandatory" syntax
decimal? Total;           // may be unset …
invariant Total >= 0;     // … and the invariant reads it — so a row with no Total is REFUSED
```

That has quietly made `Total` mandatory. If a member takes part in an invariant, either give it a type that always has
a value (`decimal Total;` — the invariant then holds trivially on `0`), mark it `[Required]` (say what you mean), or
guard the invariant with `when` so it only applies to the rows that have the value.

### Where do I write a rule — on the member, or on the row?   {#rules}
Two kinds, and the distinction is not academic — it decides where you write the rule:

- A **constraint** speaks about **one member**: `[Required]`, `[Unique]`, `[MaxLength(n)]`, `[Min]`/`[Max]`,
  `[Pattern]`. ([constraints](https://osysharp.com/reference/entity/constraints/))
- An **invariant** speaks about **the whole row** — a relationship *between* members, optionally guarded so it applies
  only to the rows it should. ([invariant](https://osysharp.com/reference/entity/invariants/))

```osy title="one member, versus the whole row" test app=entity-index
entity Product {
  [Required, MaxLength(80)] string Name;      // a constraint: about Name alone
  bool IsPerishable;
  int ShelfDays;

  invariant ShelfDays <= 30 when IsPerishable  // an invariant: about the ROW — two members, conditionally
    message "Perishable items can have at most 30 shelf days.";

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}
```

Both are enforced **when the row is written**, by the database — not by the function that happens to be writing it. So
a violation arrives as an ordinary `ValidationException` that a caller can catch ([try / catch / finally](https://osysharp.com/reference/function/try-catch/)), and the
faulted write leaves nothing behind:

```osy title="the rule holds, and the bad row does not survive" run app=entity-index
[Test]
void A_row_that_breaks_its_invariant_is_never_written() {
  Assert.Throws<ValidationException>(() => new Product { Name = "Milk", IsPerishable = true, ShelfDays = 90 });

  Assert.Empty(Product.ToList());
}

[Test]
void A_member_left_unset_reads_back_its_zero() {
  var product = new Product { Name = "Salt" };      // IsPerishable and ShelfDays never set

  var stored = Product.Single(p => p.Name == "Salt");

  Assert.Equal(0, stored.ShelfDays);        // int → 0, exactly as a C# field
  Assert.False(stored.IsPerishable);        // bool → false
}
```

The `?` is how you get the other behaviour — and it is the difference between "the shelf life is zero days" and
"nobody has recorded a shelf life". A type with no meaningful zero is optional whether you write the `?` or not:

```osy title="`?` opts out of the zero; a date has no zero to opt out of" run app=entity-index
entity Delivery {
  [Required] string Reference;
  int Attempts;              // has a zero → unset is 0
  int? WeightGrams;          // opted out    → unset is null
  DateTime? ArrivedAt;       // no zero      → optional (`?`), so unset reads null (a bare DateTime would be required)

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

[Test]
void The_question_mark_is_what_distinguishes_no_value_from_zero() {
  var delivery = new Delivery { Reference = "D-1" };   // nothing else set

  var stored = Delivery.Single(d => d.Reference == "D-1");

  Assert.Equal(0, stored.Attempts);      // zero attempts — a real, countable fact
  Assert.Null(stored.WeightGrams);       // nobody weighed it — a different fact
  Assert.Null(stored.ArrivedAt);         // and a date never has a zero to fall back to
}
```

### Relations: the child points up, the parent reads down   {#relations}
A child holds a reference to its parent — that member **is** the foreign key. The parent reads its children back
through a **collection** member:

```osy title="both directions, declared once" test app=entity-index-rel
entity Invoice {
  [Required, Unique, MaxLength(20)] string Number;

  [ForeignKey(Invoice)] Line[] Lines;          // the parent reads DOWN
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity Line {
  [Required] Invoice Invoice;                  // the child points UP — this is the FK
  [Required, MaxLength(80)] string Description;
  decimal Amount;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}
```

**Read children through the collection — never with a query filtered by the foreign key.** `invoice.Lines` is the
navigation; `Line.Where(l => l.Invoice == invoice)` is you re-implementing it by hand, and worse:

```osy title="navigate the graph; do not re-query it" run app=entity-index-rel
[Test]
void A_parent_reads_its_children_through_its_collection() {
  var invoice = new Invoice { Number = "INV-1" };
  var a = new Line { Invoice = invoice, Description = "Design", Amount = 100m };
  var b = new Line { Invoice = invoice, Description = "Build", Amount = 250m };

  var found = Invoice.Single(i => i.Number == "INV-1");

  Assert.Equal(2, found.Lines.Count);
  Assert.Equal(350m, found.Lines[0].Amount + found.Lines[1].Amount);
}
```

**A collection has no order.** It is a set of rows, exactly as a table is — `invoice.Lines[0]` is *a* line, not the
first one you added, and it may differ between runs. When the order matters, ask for it: query the children with an
[`OrderBy`](https://osysharp.com/reference/query/ordering/), and you get the order you asked for.

`[Required]` on the reference is how you say a child **cannot exist without its parent** — an orphan then becomes a
violation rather than a stray row. See [relations](https://osysharp.com/reference/entity/relations/), and [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) for pre-loading a graph you are
about to walk.

### An entity is stored; a `class` is not   {#class}
Declare a **`class`** when the value only ever lives in memory — a computed result, a payload you assemble, a shape you
return. It has no table, no `Id`, no audit columns, and no `security { }`, because there is nothing stored to secure.

The test is simply: *does a row of this outlive the request?* If yes, it is an entity. See [class methods](https://osysharp.com/reference/class/methods/).

### Do I write migrations when the model changes?   {#evolution}
You do not write migrations. `osy compile` reconciles the database with your source, and the two directions are
deliberately not symmetric:

- **Adding is applied.** A new entity, a new member, a new index — the schema moves, and it moves quietly.
- **Removing is *proposed*, not executed.** Delete a member from your source and the column is **not** dropped. The
  compile reports it as a proposed drop and **keeps the data**; dropping it takes an explicit acknowledgement, and in
  **production** an unacknowledged drop is a hard error rather than something that happens while you are not looking.

That asymmetry is the platform refusing to destroy data on the strength of a deletion you may not have meant. It is
not a gap in the tooling — it is the tooling declining to be clever with the one thing it cannot undo.

### Why so much hangs off the entity design   {#the-work}
Most of designing an Osy# app is designing its entities, because so much hangs off them:

- your **queries** are over them, and the security rules are compiled *into* those queries ([Querying data](https://osysharp.com/reference/query/index/));
- your **functions** contain no authorization code, because the entity's rules already do that job
  ([Functions (the unit of work)](https://osysharp.com/reference/function/index/));
- your **UI** names an entity and gets its data.

Time spent getting the model and its rules right is not preparation for the work. It is most of the work.

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — declaring one, and what you get for free
- [entity members](https://osysharp.com/reference/entity/properties/) — the members, the types, and optional-by-default
- [relations](https://osysharp.com/reference/entity/relations/) — references, collections, and the FK query you should not write
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Required]` · `[Unique]` · `[MaxLength]` · `[Min]`/`[Max]` · `[Pattern]`
- [invariant](https://osysharp.com/reference/entity/invariants/) — a rule about the whole row
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — `entity Sub : Base`, and what a subtype does and does not carry
- [sealed](https://osysharp.com/reference/entity/sealed/) — how a type says no one may derive from it
- [enum](https://osysharp.com/reference/enum/declaration/) — a closed set of values
- [Counter](https://osysharp.com/reference/counter/declaration/) — a sequence the platform hands out
- [The security model](https://osysharp.com/reference/security/index/) — the rules that live on the entity, and why they are in the query
- [Querying data](https://osysharp.com/reference/query/index/) — reading the model back
