# QueryStatement and Operator

> **Prerequisite**: this reference covers the query side of the declarative layer — `QueryStatement`
> shape, the `Operator` enum, `include` for relations. For validation and formatting decorators
> (`@ApplySchema`, `@Format`, `@Sanitize`) and the `FieldSchema` type, use
> [`field-schema-and-decorators.md`](field-schema-and-decorators.md).

## When it applies

You are composing a `where`, `include`, `order`, `limit`, or `offset` clause inside a handler or
service; or you are reaching for an operator (`iLike`, `in`, `between`, …) to express a filter
predicate; or you are loading a relation via `include` and need to understand what passes through
the field-schema pipeline and what doesn't.

## `QueryStatement` shape

Exported from `@aurorajs.dev/core-common`. The main fields used in catalyst handlers:

```typescript
class QueryStatement {
  where?: any; // filter predicate
  include?: any; // eager-loaded relations
  order?: any; // sort order
  limit?: number;
  offset?: number;
  attributes?: any; // projection (columns to select)
  group?: any; // GROUP BY
  lock?: any; // SELECT … FOR UPDATE / SHARE
  // … plus a few less-used fields
}
```

Catalyst handlers pass `QueryStatement` through two parameters, each with a distinct role:

- **`queryStatement`** — the client-facing filter/sort/pagination payload. Comes from the resolver
  or controller as user input.
- **`constraint`** — the server-side scope (tenant isolation, permission filter). Injected by
  middleware or decorators upstream and forwarded unchanged through the handler.

Real signatures from
`backend/src/@api/library/book/controllers/library-paginate-books.controller.ts`:

```typescript
async main(
  @Body('query') queryStatement?: QueryStatement,
  @Body('constraint') constraint?: QueryStatement,
  @Timezone() timezone?: string,
  @Auditing() auditing?: AuditingMeta,
) {
  return await this.handler.main(queryStatement, constraint, {
    timezone,
    repositoryOptions: { auditing },
  });
}
```

The handler treats both as read-only inputs — it forwards them to services unchanged. Merging them
is the service layer's job.

## `Operator` enum

Exported from `@aurorajs.dev/core-common`. Each value is a string tag of the form `'[name]'`.
**Always use the typed enum — never the string literal.** The string literal pattern is a typo/drift
trap: a typo in `'[iLike]'` (e.g., `'[ilike]'` with lowercase `l`) fails silently at runtime because
the operator is not recognized and the filter is treated as a literal value comparison.

### Catalog grouped by purpose

**Comparison**

| Operator     | SQL equivalent   |
| ------------ | ---------------- |
| `eq`         | `= value`        |
| `ne`         | `!= value`       |
| `gt` / `gte` | `> value` / `>=` |
| `lt` / `lte` | `< value` / `<=` |

**Inclusion**

| Operator | SQL equivalent |
| -------- | -------------- |
| `in`     | `IN (…)`       |
| `notIn`  | `NOT IN (…)`   |

**Text patterns**

| Operator     | SQL equivalent                          |
| ------------ | --------------------------------------- |
| `like`       | `LIKE '%hat'`                           |
| `notLike`    | `NOT LIKE '%hat'`                       |
| `iLike`      | `ILIKE '%hat'` (case-insensitive, PG)   |
| `notILike`   | `NOT ILIKE '%hat'` (PG)                 |
| `startsWith` | `LIKE 'hat%'`                           |
| `endsWith`   | `LIKE '%hat'`                           |
| `substring`  | `LIKE '%hat%'`                          |
| `regexp`     | `REGEXP/~ '^[h\|a\|t]'` (MySQL/PG)      |
| `notRegexp`  | `NOT REGEXP/!~ '^[h\|a\|t]'`            |
| `iRegexp`    | `~* '^[h\|a\|t]'` (case-insensitive PG) |
| `notIRegexp` | `!~* '^[h\|a\|t]'` (PG)                 |

**Ranges**

| Operator     | SQL equivalent          |
| ------------ | ----------------------- |
| `between`    | `BETWEEN 6 AND 10`      |
| `notBetween` | `NOT BETWEEN 11 AND 15` |

**Logical / meta**

| Operator     | Purpose                       |
| ------------ | ----------------------------- |
| `and` / `or` | Compose predicates.           |
| `not`        | Negate (e.g., `IS NOT TRUE`). |
| `is`         | `IS NULL` / `IS TRUE` / …     |
| `col`        | Reference a column by name.   |
| `join`       | Implicit join shorthand.      |

**PG-specific range / array ops**

| Operator        | SQL equivalent                                     |
| --------------- | -------------------------------------------------- |
| `overlap`       | `&&` (have points in common)                       |
| `contains`      | `@>` (PG range contains element, or range ⊇ range) |
| `contained`     | `<@` (contained by)                                |
| `adjacent`      | `-\|-` (ranges touch)                              |
| `strictLeft`    | `<<` (strictly left of)                            |
| `strictRight`   | `>>` (strictly right of)                           |
| `noExtendRight` | `&<` (does not extend to the right of)             |
| `noExtendLeft`  | `&>` (does not extend to the left of)              |

**Array / quantifier**

| Operator      | Purpose                        |
| ------------- | ------------------------------ |
| `any`         | `= ANY (array)`                |
| `all`         | `= ALL (array)`                |
| `values`      | Value list in `VALUES` clause. |
| `placeholder` | Parameter placeholder marker.  |

### Synthesized usage example

The catalyst backend has no real usage of `Operator` today — CRUD handlers rely on the generator's
default filters. Expect this pattern as custom business rules land. The example below is SYNTHESIZED
and labeled as such; update this reference when the first real usage lands.

```typescript
// SYNTHESIZED — no real anchor in catalyst backend today.
import { Operator, QueryStatement } from '@aurorajs.dev/core-common';

async main(
  query: IamFindActiveTagsByPrefixInput,
  handlerMeta?: HandlerMeta,
): Promise<IamTag[]> {
  const queryStatement: QueryStatement = {
    where: {
      [Operator.and]: [
        { isActive: true },
        { name: { [Operator.iLike]: `${query.prefix}%` } },
      ],
    },
    order: [['createdAt', 'DESC']],
    limit: query.limit ?? 50,
  };

  return await this.getService.main(queryStatement, undefined, handlerMeta);
}
```

Notice `Operator.and` as the key (not the string `'[and]'`), `Operator.iLike` instead of
`'[iLike]'`. TypeScript's type checker cannot catch a typo in a string literal; it will in
`Operator.iLikE`.

## `include` — loading relations

Catalyst queries support eager-loading relations declared in the YAML's `relationship` properties.
The relation's `field` in the YAML (e.g., `field: tenant`) becomes the association key used in
`include`:

```typescript
const queryStatement: QueryStatement = {
  where: { id: accountId },
  include: [{ association: 'user' }, { association: 'tenant' }],
};
```

You can nest includes for graph loading:

```typescript
{
  include: [
    {
      association: 'user',
      include: [{ association: 'preferences' }],
    },
  ],
}
```

### Relation formatting — recursive pipeline (requires core-back@>=1.1.0)

**Since `@aurorajs.dev/core-back@1.1.0`** (capability `schema-pipeline`), `formatRecord` and
`sanitizeRecord` recurse through relation entries automatically. The `FieldSchema` contract became
a discriminated union `scalar | relation`, and the CLI-generated schemas emit `relation` entries
for every declared `relationship` block in the YAML. The pipeline resolves each relation target
through a `target: () => <Target>FieldSchema` thunk at request-time, applying every handler
(including `PasswordTypeHandler.format()` → `undefined`) to nested sub-records.

**Concrete behaviour after the fix:**

```typescript
const queryStatement: QueryStatement = {
  where: { id: accountId },
  include: [{ association: 'user' }],
};
const account = await findByIdService.main(accountId, queryStatement, meta);

// @Format(IamAccountFieldSchema) ran on `account`.
// IamAccountFieldSchema contains `{ name: 'user', type: 'relation',
//   cardinality: 'one', target: () => IamUserFieldSchema }`.
// The pipeline recurses into account.user with IamUserFieldSchema.
// account.user.password === undefined   ← masked correctly
```

Same behaviour on input: a POST with `{ ..., user: { password: 'raw' } }` (nested payload) runs
`sanitizeRecord` recursively and hashes `user.password` via `PasswordTypeHandler.sanitize()`.

**Requirements:**

- Consumer runtime: `@aurorajs.dev/core-back@>=1.1.0`.
- Generated code: must be regenerated with a CLI that emits `relation` entries (the current one).
  Older regenerated output has only scalar entries; the runtime falls back to flat formatting
  silently.

If a project is pinned to core-back pre-1.1.0 or has schemas generated by an older CLI, the old
limitation still applies: relations come back unformatted. Upgrade both ends and regenerate
(`catalyst generate`) to activate the fix.

#### Historical note

Before `core-back@1.1.0`, `formatRecord` iterated only top-level schema fields, so sensitive
fields in included relations (e.g., `account.user.password`) leaked raw bcrypt hashes to the
client. The canonical workaround was a direct `formatRecord()` call on the related record inside
the handler. That workaround is no longer needed — if you find one in the codebase, delete it and
verify the generated `*.field-schema.ts` now declares the corresponding `relation` entry. See
`openspec/changes/archive/<fecha>-emit-relation-field-schema-entries/` in `aurora-catalyst-cli`
for the closure of SPEC-07.

## Archetype-specific anti-patterns

- **String-literal operators** (`'[iLike]'`, `'[eq]'`, `'[and]'`, …). → Import `Operator` from
  `@aurorajs.dev/core-common` and use `Operator.iLike`, `Operator.eq`, `Operator.and`. The literal
  form bypasses TypeScript's type system; a typo becomes a silent runtime bug.
- **Adding a manual `formatRecord()` call on an included relation.** With `core-back@>=1.1.0`, the
  pipeline recurses automatically via the `relation` entries emitted by the CLI. A manual
  `formatRecord(account.user, IamUserFieldSchema, ...)` was the pre-fix workaround; it now
  double-formats (harmless for most handlers, but it hides real bugs in your generated schema —
  the fact that it is "working" may be masking a missing `relation` entry). Delete the manual call
  and verify the generated `IamAccountFieldSchema` declares the `user` relation entry.
- **Leaking `constraint` back to the client.** `constraint` encodes server-side security scope
  (tenant id, permission filter). Do not echo it in responses or error messages — it can reveal the
  multi-tenant structure or permission boundaries.
- **Mutating `queryStatement` or `constraint` inside the handler.** Treat both as read-only inputs.
  If the handler needs to build a merged criteria object, build a NEW object; don't reassign keys on
  the incoming ones. The service layer expects unchanged inputs for its own merging logic.
- **Using `attributes` for field masking.** `attributes` is a projection (which columns to SELECT),
  not a sensitive-field filter. Relying on it to hide a password column is fragile because any
  consumer who forgets to set `attributes` will leak the column. Use the field schema's `format`
  (via `@Format` / `@ApplySchema`) instead, which is enforced on every read regardless of how the
  query is constructed.
