> 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

# Query Filters

> Named $where fragments applied to every query of an entity, for soft-delete, multi-tenancy and row-level security.

Source: https://uql-orm.dev/querying/filters

A filter is a named `$where` fragment attached to an entity and applied to every query on it unless bypassed. [Soft-delete](https://uql-orm.dev/entities/soft-delete.md) is a built-in one; define your own for visibility flags, tenants and the like.

## Defining a filter

Use the `@Filter` decorator, the `@Entity({ filters })` option, or `defineFilter` from the [imperative API](https://uql-orm.dev/entities/imperative.md):

```ts
import { Entity, Id, Field, Filter } from 'uql-orm';

@Filter('active', { where: { status: 'active' }, default: false })
@Entity()
export class Task {
  @Id({ type: Number }) id?: number;
  @Field({ type: String }) status?: string | null;
}
```

```ts
defineFilter(Task, 'active', { where: { status: 'active' }, default: false });
```

`where` is a `$where` fragment, or a function of the [context](#parameterized-filters--context) that returns one. `default` (`true` unless set) says whether the filter applies without being asked for; set it `false` for an opt-in filter like `active`. When a `where` function returns `undefined`, `onMissing` decides: `'skip'` (the default) leaves the filter out, and `'throw'` fails the query.

A filter that is not a [security filter](#security-filters-row-level-security) yields to your query: if your `$where` names the same field, your condition wins.

## Bypassing filters

Every read, update and delete takes `filters` in its options:

```ts
pool.findMany(Task, {}, { filters: false }); // disable all filters
pool.findMany(Task, {}, { filters: { softDelete: false } }); // disable one
pool.findMany(Task, {}, { filters: { active: true } }); // enable an opt-in filter
```

## Parameterized filters & context

A `where` function receives the current context, such as the tenant. Set it for a block of code with `withContext`, and read it anywhere with `getContext()`:

```ts
@Filter('tenant', {
  // undefined makes the query throw UqlSecurityError instead of running unscoped
  where: (ctx) =>
    ctx?.tenantId != null ? { companyId: ctx.tenantId } : undefined,
  security: true,
})
@Entity()
export class Invoice {
  @Id({ type: Number }) id?: number;
  @Field({ type: Number }) companyId?: number | null;
}

await withContext({ tenantId: 42 }, () => pool.findMany(Invoice, {}));
// ... WHERE (companyId = 42)
```

The context follows `await`, `Promise.all` and transactions, but not event callbacks such as emitters, timers and queues; see [event-driven pipelines](https://uql-orm.dev/multi-tenancy.md#event-driven-pipelines-emitters-timers-queues). To type its keys, declare `interface UqlContext { tenantId: number }` inside `declare module 'uql-orm'`.

## Security filters (row-level security)

A filter with `security: true` enforces isolation:

- It always applies, ignoring `filters: false` and a bypass by name.
- It is AND-merged, so a `$where` on the same field cannot widen it.
- It fails closed: when its `where` returns `undefined`, as with no context, the query throws `UqlSecurityError`.

Over HTTP the server reads only query keys, so a client cannot send `filters` or `context`.

[Multi-tenancy](https://uql-orm.dev/multi-tenancy.md) covers setting the context per request, the `{}` escape hatch for trusted cross-tenant jobs, and how security filters guard writes.
