> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Decorators

> Define entities with the @Entity, @Id and @Field decorators, and choose column types.

Source: https://uql-orm.dev/entities/basic

An entity is a plain TypeScript class that maps to a table. `@Entity` marks the class, `@Id` the primary key and `@Field` each column:

```ts
import { Entity, Id, Field } from 'uql-orm';
import { v7 as uuidv7 } from 'uuid';

@Entity()
export class User {
  @Id({ type: 'uuid', onInsert: uuidv7 })
  id!: string;

  @Field({ type: String, nullable: false })
  name!: string;

  @Field({ type: String, unique: true })
  email?: string | null;
}
```

These are standard TC39 decorators: leave `experimentalDecorators` off. To define entities without decorators, use the [imperative API](https://uql-orm.dev/entities/imperative.md).

## Column types

Every field states a `type`, and it must match the property: `@Field({ type: String })` on a `number` does not compile. Each dialect maps it to its own SQL type: `'uuid'` is `UUID` on Postgres and `CHAR(36)` on MySQL.

It takes `String`, `Number`, `Boolean`, `BigInt`, `Date`, or a SQL type name such as `'uuid'`, `'text'`, `'decimal'`, `'jsonb'` or `'vector'`.

- `Date` is stored in UTC to the millisecond on every engine.
- `Number` is a `BIGINT`. Past 2^53 it reads back as exact text, never a rounded number, except on D1, whose API returns a JS number. Use `BigInt` for an exact typed integer.
- A `decimal` reads and writes as exact text (`price?: string | null`). `type: Number` with `precision` and `scale` gives a JS number instead, which rounds past 15 digits. SQLite stores a float either way.
- A JSON property typed `Json<T>` counts as a field, so `$where`, `$select` and `$sort` take its [dot-notation paths](https://uql-orm.dev/querying/comparison-operators.md#jsonb-dot-notation-operators).

```ts
import type { Json } from 'uql-orm';

@Field({ type: 'decimal', precision: 12, scale: 2 })
price?: string | null;

@Field({ type: 'jsonb' })
metadata?: Json<{ theme?: string; priority?: number }> | null;
```

`columnType` sets the exact SQL type. For a type uql does not map (`tsvector`, `ltree`, `inet`, `citext`, geometry), pass a `sql` constant (never a bare string). It is emitted verbatim, so `length`, `precision`, `scale` and `dimensions` are rejected beside it:

```ts
import { sql } from 'uql-orm';

@Field({ type: String, columnType: 'varchar', length: 500 })
longBio?: string | null;

@Field({
  type: String,
  columnType: sql`tsvector`,
  computed: (post) => sql`to_tsvector('simple', ${post.body})`,
  stored: true,
})
readonly searchVector?: string | null;
```

### Values the database computes

These constants are SQL the database evaluates. Use them in a `defaultValue`, `onUpdate`, a stamp or `$where`, and in [migrations](https://uql-orm.dev/migrations/builder.md#default-expressions):

| Value | What it is | Where |
| - | - | - |
| `currentTimestamp` | the clock, UTC to the millisecond | everywhere |
| `currentDate` | today | everywhere |
| `currentTime` | the time of day | everywhere |
| `uuid` | a UUID: version 4, or version 1 on MySQL and MariaDB | not SQLite |
| `uuidv7` | a time-ordered version 7 UUID, which indexes better | Postgres 18+, MariaDB 11.7+ |

```ts
import { currentTimestamp } from 'uql-orm';

@Field({ type: Date, defaultValue: currentTimestamp })
createdAt?: Date | null;
```

Prefer `currentTimestamp` to a raw `CURRENT_TIMESTAMP`, which is not UTC to the millisecond on MySQL, SQL Server or SQLite. An engine lacking one of these throws; write [`sql`](https://uql-orm.dev/querying/raw-sql.md) there.

## Nullable columns

A column is nullable unless it declares `nullable: false`, and a nullable column’s property must include `null`. A mismatch does not compile:

```ts
@Field({ type: String, nullable: false }) title!: string;   // NOT NULL
@Field({ type: Date }) publishedAt?: Date | null;          // nullable
```

The [codemod](https://uql-orm.dev/codemod.md) adds `| null` to existing entities.

## Required fields

Declare a `nullable: false` property with `!`, like `name` above. Inserts must include it, and a read that selects it always has it:

```ts
await pool.insertOne(User, { name: 'Ada' }); // `{}` does not compile: `name` is missing
const [user] = await pool.findMany(User, { $select: { id: true, name: true } });
user.name.trim(); // a `string`, never `undefined`
```

An insert may still omit a `!` single-column key and a [version](https://uql-orm.dev/entities/optimistic-locking.md) column, which uql fills, and the fields the `filledKey` brand lists: each `nullable: false` field a default fills, such as a `defaultValue` or an `onInsert`. A read still has them:

```ts
import { currentTimestamp, filledKey } from 'uql-orm';

@Entity()
export class Product {
  [filledKey]?: 'stock' | 'createdAt';

  @Id({ type: Number }) id!: number;
  @Field({ type: String, nullable: false }) name!: string;
  @Field({ type: Number, nullable: false, defaultValue: 0 }) stock!: number;
  @Field({ type: Date, nullable: false, defaultValue: currentTimestamp })
  createdAt!: Date;
}

await pool.insertOne(Product, { name: 'Lamp' }); // `stock` and `createdAt` are filled
const [product] = await pool.findMany(Product, {});
product.stock.toFixed(); // a `number`, never `undefined`
```

Declare everything else an insert may omit with `?`: nullable columns, `eager: false` and `computed` fields, and relations. A composite key must be given in full.

## Primary keys

`@Id` takes the same options as `@Field`, and a key is never `null`. `@Id({ type: Number })` lets the database generate an integer, which keeps indexes small and joins fast. `@Id({ type: 'uuid', onInsert: uuidv7 })` generates a UUID in your code before the row exists, and cannot be guessed from a public URL.

### Naming the key

A key named `id`, `_id` or `uuid` is recognized. Any other name needs the `idKey` brand, or `findOneById` would accept any column’s values:

```ts
import { idKey } from 'uql-orm';

@Entity()
export class TaxCategory {
  [idKey]?: 'pk';

  @Id({ type: String, onInsert: uuidv7 })
  pk!: string;
}
```

## Composite primary keys

Several `@Id`s make one composite key, in declaration order. The brand names every key column, and every by-id call takes all of them:

```ts
@Entity()
export class Enrolment {
  [idKey]?: 'studentId' | 'courseId';

  @Id({ type: Number })
  studentId?: number;

  @Id({ type: String })
  courseId?: string;

  @Field({ type: String })
  grade?: string | null;
}

await pool.updateOneById(
  Enrolment,
  { studentId: 1, courseId: 'maths' },
  { grade: 'A' },
);
```

The type cannot require every key, so an id missing one throws at run time instead of matching more rows.

### Pointing at a composite key

A foreign key to a composite key needs one column per key column, paired in the relation, and the pairs form one `FOREIGN KEY`. `@Field({ references: () => Enrolment })` is refused. A junction pointing at a composite side needs this `@ManyToOne` too.

```ts
@ManyToOne({
  entity: () => Enrolment,
  references: (note, enrolment) => [
    { local: note.enrolmentStudentId, foreign: enrolment.studentId },
    { local: note.enrolmentCourseId, foreign: enrolment.courseId },
  ],
})
enrolment?: Enrolment;
```

A [`cascade`](https://uql-orm.dev/entities/relations.md#writing-related-rows-cascade) to-many or many-to-many over a composite side writes those columns too: each child, or junction row, carries the whole key of its parent and of its target.

### What is not supported

Reads, writes, cascading deletes and saving a relation all handle composite keys. Two paths throw on one: MongoDB, where a document has a single `_id` (use a unique compound index instead), and the HTTP `/:id` route, where a composite row is reachable through `$where`.

## Table name

The table takes the class name. `@Entity({ name })` overrides it, and `@Entity({ schema })` sets its [schema](https://uql-orm.dev/multiple-schemas.md).

### If your build minifies, name the table

A minifier renames classes (`User` becomes `b`), and the table name would follow. Set `@Entity({ name: 'user' })` in any build that minifies, Next’s server build included; `drift:check` catches a mismatch.

## Field options

`@Field` and `@Id` take these, besides `type`:

| Option | What it does |
| - | - |
| `columnType` | Exact SQL type, or a `sql` constant. |
| `length` | String length. Unset: `TEXT` (Postgres, SQLite) or `VARCHAR(255)` (MySQL, MariaDB). |
| `precision`, `scale` | A `decimal`’s digits and digits after the point. `precision` on a `Date` sets fractional-second digits. |
| `dimensions` | A vector’s dimensions: `{ type: 'vector', dimensions: 1536 }`. |
| `name`, `comment` | Column name (defaults to the property name) and its database comment. |
| `nullable` | Defaults to `true`. |
| `unique` | Unique index, named by `index` or `<table>__<column>_idx`. |
| `index` | `true` adds an [index](https://uql-orm.dev/entities/indexes.md); a string names it. A foreign key is indexed unless this is `false`. |
| `enum` | Allowed values as a [`CHECK`](https://uql-orm.dev/entities/enum-checks.md#enum-fields). Needs `as const`. |
| `defaultValue` | A value, or per-row SQL such as `currentTimestamp`. JSON columns take a document, `[]` or `{}`. |
| `references` | `() => Entity` makes a foreign key typed by the target’s key, so it takes no `type`. A [relation](https://uql-orm.dev/entities/relations.md) over it loads the target. |
| `onDelete` | The foreign key’s [`ON DELETE` action](https://uql-orm.dev/entities/relations.md#database-actions-ondelete-and-onupdate). A relation’s own wins. |
| `onInsert`, `onUpdate` | Value for a new row (`uuidv7`), or on every update (`() => new Date()`). |
| `updatable` | `false` leaves the field out of updates, e.g. `createdAt`. |
| `eager` | `false` leaves the field out of reads unless selected, e.g. `password`. |
| `autoIncrement` | `@Id` only. Defaults to `true` for numeric keys. |
| `computed` | A [database expression or relation aggregate](https://uql-orm.dev/entities/computed-fields.md). |
| `stored` | `true` makes a `computed` field a generated column; a list of events makes it a [stamp](https://uql-orm.dev/entities/computed-fields.md#stamps-stored-on-an-event). |
| `softDelete` | Marks the [soft-delete](https://uql-orm.dev/entities/soft-delete.md) field: `true` stamps `new Date()`, or give the value to stamp. |
| `version` | Makes the column an [optimistic lock](https://uql-orm.dev/entities/optimistic-locking.md). |
| `distance` | Default vector metric: `'cosine'`, `'l2'`, `'inner'` or `'l1'`. Falls back to the vector index’s, then `'cosine'`. |

An option that would have no effect does not compile, and `defineEntity` throws on it. `length` needs a string column, `precision` a numeric or date one, `scale` and `autoIncrement` a numeric one, `dimensions` and `distance` a vector. A `computed` field takes no `defaultValue`, `onInsert` or `onUpdate`, and unless `stored`, no `index`, `unique` or `columnType`. A relation aggregate takes neither `type` nor `stored`. `onUpdate` conflicts with `updatable: false`, and a key cannot be `nullable: true`.

Class-level decorators have their own pages: [`@Index`](https://uql-orm.dev/entities/indexes.md), [`@Filter`](https://uql-orm.dev/querying/filters.md), [`@Trigger`](https://uql-orm.dev/entities/triggers.md), and the [relation decorators](https://uql-orm.dev/entities/relations.md).
