# Integration guide — consuming `@qrvey/query-builder` from `@qrvey/enginex`

`query-builder` is a **pure transform**: it returns a query object, it never
executes anything. This guide documents exactly how an executor (e.g.
`@qrvey/enginex`) should interpret the result of `.transpile()` and run it,
per engine.

Dependency direction is one-way: `enginex → query-builder`. Nothing here imports
enginex.

> Related: [Queries](QUERIES.md) · [Mutations](MUTATIONS.md) · [API reference](API.md)

---

## 1. Entry points

```ts
import { QueryBuilder, MutationBuilder } from '@qrvey/query-builder';

const read = new QueryBuilder(queryInput, { engine }).transpile();
const write = new MutationBuilder(mutationInput, { engine }).transpile();
```

- The `engine` can come from the constructor or `transpile({ engine })`.
- Both throw a `QueryBuilderError` (with `.code` and `.path`) on invalid input —
  wrap the call and surface `code`/`message` to the caller. Use `.validate()`
  first if you prefer errors as data instead of exceptions.

Discriminate the result with the `kind` field: `'sql'` or `'search-dsl'`.

---

## 2. Reads — `QueryBuilder.transpile()` → `TranspileResult`

### 2.1 SQL engines (`kind: 'sql'` — PostgreSQL, ClickHouse)

```ts
type SqlTranspileResult = {
  engine: 'postgresql' | 'clickhouse';
  kind: 'sql';
  query: string;        // parameterized — RUN THIS
  plainQuery: string;   // values inlined — logging/debug ONLY, never execute
  values: unknown[];    // positional params  ($1,$2 …)
  namedValues: Record<string, unknown>; // named params ({pN:Type})
  metadata: QueryOutputMetadata;
};
```

- **PostgreSQL** — positional parameters. Execute `query` with `values`:
  ```ts
  await pg.query(result.query, result.values);
  ```
- **ClickHouse** — named parameters (`{pN:Type}`). Execute `query` passing
  `namedValues` as `query_params`:
  ```ts
  await clickhouse.query({ query: result.query, query_params: result.namedValues });
  ```
- **Never execute `plainQuery`.** It exists only for logs/snapshots; it inlines
  literals and is not injection-safe by design.

### 2.2 Search engines (`kind: 'search-dsl'` — Elasticsearch, OpenSearch)

```ts
type SearchDslTranspileResult = {
  engine: 'elasticsearch' | 'opensearch';
  kind: 'search-dsl';
  index: string;                    // target index
  body: Record<string, unknown>;    // request body (_source/query/aggs/sort/size/from|search_after)
  metadata: QueryOutputMetadata;
};
```

```ts
const res = await es.search({ index: result.index, body: result.body });
```

### 2.3 `metadata` (both families)

```ts
type QueryOutputMetadata = {
  outputFields: Array<
    | { name; kind: 'field';       sourceField; type }
    | { name; kind: 'group';       sourceField; type; granularity? }
    | { name; kind: 'aggregation'; sourceField; function; type }
  >;
  pagination?: { mode: 'offset' | 'search_after'; requiresStableSort?: boolean };
};
```

- Use `outputFields` to map result rows/buckets to a stable shape. `name` is the
  column alias (SQL) / field name (ES) — for nested selections it is the dotted
  path (`profile.address.city`), consistent across engines.
- `pagination.mode`:
  - `offset` → next page = re-run with a larger `skip`.
  - `search_after` → feed the last row's sort values back as `cursor`;
    `requiresStableSort` means a total-order `orderBy` is required.

---

## 3. Writes — `MutationBuilder.transpile()` → `MutationTranspileResult`

Every mutation result carries `operation: 'insert' | 'update' | 'delete'`.

### 3.1 SQL engines (`kind: 'sql'`)

```ts
type SqlMutationResult = {
  engine: 'postgresql' | 'clickhouse';
  kind: 'sql';
  operation: 'insert' | 'update' | 'delete';
  query: string;        // RUN THIS
  plainQuery: string;   // debug ONLY
  values: unknown[];
  namedValues: Record<string, unknown>;
};
```

Execute exactly like a read (`query` + `values`/`namedValues`). The `operation`
is informational — the statement (`INSERT … ON CONFLICT`, `UPDATE`, `DELETE`,
ClickHouse `ALTER TABLE … UPDATE/DELETE`) is already fully formed in `query`.

### 3.2 Search engines (`kind: 'search-dsl'`)

```ts
type SearchDslMutationResult = {
  engine: 'elasticsearch' | 'opensearch';
  kind: 'search-dsl';
  operation: 'insert' | 'update' | 'delete';
  index: string;
  body: Record<string, unknown>;
  documents?: Array<Record<string, unknown>>; // present for insert
  onConflict?: string[];                       // present for upsert
};
```

Dispatch by `operation`:

| `operation` | What to call | Notes |
|---|---|---|
| `delete` | `POST {index}/_delete_by_query` with `body` | `body.query` is the criteria (or `{ match_all: {} }`). |
| `update` | `POST {index}/_update_by_query` with `body` | `body.query` + `body.script` (painless) do the mutation. |
| `insert` | Bulk index `documents` into `index` | Build the bulk NDJSON from `documents`. |

**Upsert on ES/OS:** when `onConflict` is present on an `insert`, derive each
document `_id` deterministically from those key fields (e.g. join their values)
so a re-index overwrites the existing document instead of creating a duplicate.

---

## 4. Error handling

All failures are `QueryBuilderError` with a stable `code` (e.g. `MISSING_ENGINE`,
`UNSUPPORTED_ENGINE`, `INVALID_INPUT`, `INVALID_MUTATION`, `UNFILTERED_MUTATION`,
`UNSUPPORTED_ENGINE_BEHAVIOR`) and a `path` pointing at the offending input
location. Map these to your own transport errors; don't execute anything when a
build throws.

---

## 5. Minimal executor sketch

```ts
function execute(result: TranspileResult | MutationTranspileResult, clients) {
  if (result.kind === 'sql') {
    return result.engine === 'clickhouse'
      ? clients.clickhouse.query({ query: result.query, query_params: result.namedValues })
      : clients.pg.query(result.query, result.values);
  }
  // search-dsl
  if ('operation' in result) {
    switch (result.operation) {
      case 'delete': return clients.es.deleteByQuery({ index: result.index, body: result.body });
      case 'update': return clients.es.updateByQuery({ index: result.index, body: result.body });
      case 'insert': return clients.es.bulkIndex(result.index, result.documents!, result.onConflict);
    }
  }
  return clients.es.search({ index: result.index, body: result.body });
}
```

> This sketch lives in the **executor** (EngineX), not in `query-builder`.
