# Architecture (contributor guide)

For **using** the package see the [README](../README.md) and the
[Queries](QUERIES.md) / [Mutations](MUTATIONS.md) / [API](API.md) guides. This
document is for people **working on** the package.

- [Design principles](#design-principles)
- [Folder structure](#folder-structure)
- [The pipeline](#the-pipeline)
- [The plan (AST)](#the-plan-ast)
- [Compilers & dialects](#compilers--dialects)
- [Adding an engine](#adding-an-engine)
- [Adding a validation rule](#adding-a-validation-rule)
- [Testing & debugging](#testing--debugging)

---

## Design principles

1. **Pure transform.** No I/O, no engine clients, no product coupling. Input in,
   query object out.
2. **Stable public contract.** `QueryInput` / `MutationInput` are engine-neutral.
   Engine differences never leak into the contract.
3. **Compiler = shared flow, dialect = engine syntax.** Adding an engine is a
   dialect, not a change to the flow or the contract.
4. **Fail with a typed error and a path.** Every rejection is a
   `QueryBuilderError` with a `code` and a `path` into the input.

---

## Folder structure

```
src/
  index.ts                      Public barrel (only client-facing exports)
  query-builder.ts              QueryBuilder + MutationBuilder (entry points)
  input/
    input-types.ts              Public input contract (types)
    normalize-input.ts          Defaults (limit, skip, cursor)
    validate-input.ts           Read input shape validation
    validate-mutation.ts        Mutation input + safety validation
  plan/
    query-plan.ts               Read AST node classes (QueryPlan)
    mutation-plan.ts            Mutation AST (MutationPlan, AssignmentNode)
    input-to-plan.ts            QueryInput  → QueryPlan
    input-to-mutation-plan.ts   MutationInput → MutationPlan
    validate-plan.ts            Semantic + engine validation, engineRules
  compilers/
    compiler.ts                 QueryCompiler contract + result types
    compiler-registry.ts        Engine → compiler map
    register-default-compilers.ts   Wires the built-in engines
    sql/
      sql-compiler.ts           Shared SQL flow + SqlDialect interface
      postgres.ts               PostgresDialect + PostgresCompiler
      clickhouse.ts             ClickHouseDialect + ClickHouseCompiler
    search-dsl/
      search-dsl-compiler.ts    Shared search flow + SearchDslDialect interface
      elasticsearch.ts          ElasticsearchDialect + compiler (reference)
      opensearch.ts             OpenSearchDialect (extends ES) + compiler
  metadata/
    output-metadata-builder.ts  Shared output-field metadata
    output-name-generator.ts    Deterministic aggregation/group names
  errors/
    query-builder-error.ts      QueryBuilderError + codes
__tests__/                      Tests, grouped by functionality
  builders/                     QueryBuilder / MutationBuilder + validation
  queries/                      Read-query features (operators, nested paths, ordering)
  mutations/                    insert / upsert / update / delete
  compilers/                    Shared flow + per-engine dialect output
  scripts/
    debug.ts                    Dev-only: transpile sample inputs on all engines
```

---

## The pipeline

**Reads** (`QueryBuilder`):

```
QueryInput
  → validateInputShape        (input/validate-input.ts)
  → normalizeInput            (input/normalize-input.ts)
  → inputToPlan  → QueryPlan  (plan/input-to-plan.ts)
  → validatePlan              (plan/validate-plan.ts — semantic + engineRules)
  → compiler.compile(plan)    (compilers/<family>/…)
  → TranspileResult
```

**Writes** (`MutationBuilder`):

```
MutationInput
  → validateMutationInput            (input/validate-mutation.ts)
  → inputToMutationPlan → MutationPlan
  → compiler.compileMutation(plan)
  → MutationTranspileResult
```

Compilers only ever consume a validated plan — never the raw input.

---

## The plan (AST)

The plan is the engine-agnostic logical model — query *intent*, not syntax.
Every node extends `PlanNode` and carries a `path` back to the input location it
came from, which is what makes precise error paths possible.

All field references funnel through `FieldRefNode` (`field`, `type`,
`nestedPath`), so nested-field rendering and output naming have a single source
of truth (`FieldRefNode.qualifiedName()`). Aggregation and date-group output
names are generated once (`OutputNameGenerator`) and stored on the nodes, so
every compiler and the metadata builder agree.

---

## Compilers & dialects

A **compiler** owns the flow for a family; a **dialect** supplies the
engine-specific fragments.

- SQL family — `SqlCompiler` (flow) + `SqlDialect` (syntax). A concrete engine is
  `new SqlCompiler(new YourDialect())`.
- Search family — `SearchDslCompiler` + `SearchDslDialect`.

`SqlDialect` methods:

| Method | Purpose |
| ------ | ------- |
| `escapeIdentifier` | Quote a column/table (`"col"` / `` `col` ``). |
| `renderFieldPath` | Read a nested JSON path. |
| `bindValue` | Bind a value → placeholder (`$n` / `{pN:Type}`). |
| `renderLiteral` | Inline a value for `plainQuery` (debug). |
| `renderDateGroup` | Date bucketing expression. |
| `renderInsert` | Assemble `INSERT` (+ optional `ON CONFLICT`). |
| `renderAssignment` | One `UPDATE` assignment (`set`/`increment`/`append`/`removeField`). |
| `renderDelete` / `renderUpdate` | Assemble `DELETE` / `UPDATE`. |

`SearchDslDialect` methods: `renderTerm`, `renderRange`, `renderWildcard`,
`renderTermsAggregation`, `renderDateHistogram`, `renderSearchAfter`.

---

## Adding an engine

1. **SQL family:** create `compilers/sql/<engine>.ts` with a `SqlDialect`
   implementation and `class <Engine>Compiler extends SqlCompiler`. **Search
   family:** create a `SearchDslDialect` (or extend `ElasticsearchDialect`) and a
   compiler.
2. Add the engine to the `Engine` union in `input/input-types.ts`.
3. Register it in `compilers/register-default-compilers.ts`.
4. Add golden-output tests under `__tests__/` in the matching group (e.g. `__tests__/compilers/<engine>.test.ts`).

That is the whole checklist — the flow, plan and input contract are untouched.
Example of nested-path divergence a new dialect must handle: PostgreSQL uses
`-> / ->>`, ClickHouse uses `JSONExtract*`; a future MySQL dialect would use
`JSON_EXTRACT`, etc.

---

## Adding a validation rule

For a "known unsupported combination" on a specific engine, push a
`PlanValidationRule` instead of scattering `if (engine === …)`:

```ts
import { engineRules, QueryBuilderError } from '@qrvey/query-builder'; // internally: from plan/validate-plan

engineRules.push({
  validate(plan, ctx) {
    if (ctx.engine === 'clickhouse' && /* unsupported thing */ false) {
      return [
        new QueryBuilderError({
          code: 'UNSUPPORTED_ENGINE_BEHAVIOR',
          path: '$',
          message: 'ClickHouse does not support …',
        }),
      ];
    }
    return [];
  },
});
```

Shared semantic rules (output-shape combinations, pagination, limits, grouped
ordering) live directly in `validatePlan`.

---

## Testing & debugging

```bash
yarn type-check   # tsc, no emit
yarn test         # jest — golden output per engine
yarn build        # tsup → CJS + ESM + d.ts
yarn debug        # run __tests__/scripts/debug.ts: transpile samples on all engines
```

Tests live under `__tests__/`, grouped by functionality, and assert the exact
compiled query per engine, which doubles as living documentation of each
dialect's output.

> Note: the `lib` target does not include `String.prototype.replaceAll`; use
> `split(x).join(y)` for string replacement.
