> 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

# Runtime Schemas

> Define entities from data, such as a CMS content type an admin creates, and apply them to the database while the process is running.

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

Some schemas only exist at runtime: a CMS content type an admin creates through a UI, or a tenant whose columns are rows in another table.

UQL reads no TypeScript types at runtime, so this needs no separate API: the [imperative](https://uql-orm.dev/entities/imperative.md) one registers a class you mint yourself, column by column.

```typescript
import {
  defineEntity,
  removeEntity,
  type ColumnType,
  type Scalar,
} from 'uql-orm';
import { Migrator } from 'uql-orm/migrate';

/** What the admin UI stored: one column per field. */
type ContentType = {
  name: string;
  fields: { name: string; type: ColumnType }[];
};

async function registerContentType(contentType: ContentType) {
  // Every value is a scalar column; `id` is declared to type an insert's id. Named after the
  // content type, so its errors read like any other entity's.
  const entity = {
    [contentType.name]: class {
      id!: string;
      [column: string]: Scalar;
    },
  }[contentType.name];

  defineEntity(entity, {
    name: contentType.name,
    fields: {
      id: { type: 'uuid', isId: true },
      ...Object.fromEntries(
        contentType.fields.map((field) => [field.name, { type: field.type }]),
      ),
    },
  });

  // One entity, one table: a new one costs an existence check and a `CREATE TABLE IF NOT EXISTS`.
  await new Migrator(pool).sync({ entity });
  return entity;
}
```

Columns every content type carries (`createdBy`, a tenant key) belong on a base named with [`extends`](https://uql-orm.dev/entities/inheritance.md#naming-a-base-you-cannot-extend), which a minted class has no way to extend.

If your UI keeps its own vocabulary (`text`, `longtext`, `money`), translate it to a [column type](https://uql-orm.dev/entities/basic.md#field-options) as you build the fields. A SQL type states what the column is; `String` would leave its width to a default.

The class is only an identity for the registry: nothing constructs it, and rows come back as plain objects. Pass it to a querier like any hand-written entity.

## Applying it to the database

`sync({ entity })` is the runtime path: one entity, one table.

- **The table does not exist**: one existence check, then `CREATE TABLE IF NOT EXISTS`, with no catalogue read. Instances racing the same save settle instead of colliding.
- **The table exists**: UQL reads its columns and applies the same additive changes a full [`sync`](https://uql-orm.dev/migrations.md#syncing-without-a-migration-file) would. A new column is added; a retyped or dropped one is refused and stays a migration.

A `Migrator` built without an `entities` option reads the registry live, so an entity registered later is included with no restart. One pinned to an explicit list still syncs an entity outside it.

## Editing and deleting a content type

Registering the same content type again mints a second class, and two entities then map one table. Keep the entity `registerContentType` returned and forget the previous one first:

```typescript
const previous = registered.get(contentType.name);
if (previous) removeEntity(previous);
registered.set(contentType.name, await registerContentType(contentType));
```

A deleted content type needs `removeEntity(entity)` too; otherwise the registry keeps it, and its table in every diff, for the life of the process.

## What survives at compile time

The row type is whatever the minted class says; the one above is a bag of scalar columns with a typed key. Queries are checked against it: `$where`, `$sort`, `$populate` and projections take any column by name, with every operator available. Column *names* cannot be checked, because none was known at compile time.

Declaring `id!: string` beside the index signature types the key: an insert reports `string | undefined` instead of every scalar, so `findOneById` takes what the insert returned. Where the key is not called `id`, name it with the `idKey` brand:

```typescript
import { idKey, type Scalar } from 'uql-orm';

class Content {
  declare [idKey]?: 'pk';
  pk!: string;
  [column: string]: Scalar;
}
```

To get column names back, generate an interface per content type (`npx uql-migrate types` writes one for every registered entity) and declare the class with those columns instead of an index signature:

```typescript
class Post {
  id!: string;
  title?: string;
  views?: number;
}
```

## What this deliberately does not do

- **Change a column.** Retyping, renaming or dropping one is refused by a sync and stays a migration on every engine.
- **Lock across instances.** `CREATE TABLE IF NOT EXISTS` settles two instances answering the same admin save; anything stronger is yours to coordinate.
- **Validate rows against the content type.** The database enforces what the DDL says; the rest belongs to your application.
