# API reference

Everything exported from `@qrvey/query-builder`. Engine compilers, dialects, the
plan/AST node classes and the validation framework are internal and not exported.

- [Classes](#classes)
  - [QueryBuilder](#querybuilder)
  - [MutationBuilder](#mutationbuilder)
- [Builder types](#builder-types)
- [Input types](#input-types)
- [Output types](#output-types)
- [Plan types](#plan-types)
- [Errors](#errors)

---

## Classes

### `QueryBuilder`

Builds a read query.

```ts
class QueryBuilder {
  constructor(input: QueryInput, options?: QueryBuilderOptions);

  /** Validate input + plan for the resolved engine. Never throws. */
  validate(options?: TranspileOptions): ValidationResult;

  /** Return the validated QueryPlan. Throws the first QueryBuilderError. */
  plan(options?: TranspileOptions): QueryPlan;

  /** Compile the query for the resolved engine. Throws on invalid input. */
  transpile(options?: TranspileOptions): TranspileResult;
}
```

The engine is resolved from `options.engine` (per call) or the constructor
`options.engine`; if neither is set, a `MISSING_ENGINE` error is thrown.

### `MutationBuilder`

Builds a write mutation.

```ts
class MutationBuilder {
  constructor(input: MutationInput, options?: QueryBuilderOptions);

  /** Validate the mutation input. Never throws. */
  validate(): ValidationResult;

  /** Return the validated MutationPlan. Throws the first QueryBuilderError. */
  plan(): MutationPlan;

  /** Compile the mutation for the resolved engine. Throws on invalid input. */
  transpile(options?: TranspileOptions): MutationTranspileResult;
}
```

---

## Builder types

```ts
type QueryBuilderOptions = {
  engine?: Engine;
};

type ValidationResult = {
  valid: boolean;
  errors: QueryBuilderError[];
};

type TranspileOptions = {
  engine?: Engine;
};
```

---

## Input types

### `Engine`

```ts
type Engine = 'elasticsearch' | 'opensearch' | 'postgresql' | 'clickhouse';
```

### `FieldType`

```ts
type FieldType = 'string' | 'number' | 'boolean' | 'date';
```

### `QuerySource`

```ts
type QuerySource = {
  name: string;     // table (SQL) or index (ES/OS)
  schema?: string;  // SQL namespace; ignored by ES/OS
};
```

### `FieldPath`

```ts
type FieldPath = string[]; // nested access within a JSON/object field
```

### `FieldSelection`

```ts
type FieldSelection = {
  field: string;
  type: FieldType;
  path?: FieldPath;
};
```

### `FilterOperator`

```ts
type FilterOperator =
  | 'eq' | 'neq'
  | 'gt' | 'gte' | 'lt' | 'lte'
  | 'in' | 'notIn'
  | 'contains' | 'notContains'
  | 'startsWith' | 'endsWith'
  | 'between'
  | 'exists' | 'isNull' | 'isNotNull';
```

### `FilterCondition` & `FilterExpression`

```ts
type FilterCondition = {
  field: string;
  type: FieldType;
  path?: FieldPath;
  operator: FilterOperator;
  value?: unknown;         // array for `in`/`notIn`; [low, high] for `between`
};

type FilterExpression =
  | FilterCondition
  | { and: FilterExpression[] }
  | { or: FilterExpression[] }
  | { not: FilterExpression };
```

### `AggregationFunction` & `AggregationDefinition`

```ts
type AggregationFunction = 'count' | 'sum' | 'avg' | 'min' | 'max';

type AggregationDefinition = {
  function: AggregationFunction;
  field?: string;   // optional only for `count` (COUNT(*))
  type?: FieldType;
  path?: FieldPath;
};
```

### `DateGranularity`

```ts
type DateGranularity = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
```

### `GroupByField`

```ts
type GroupByField = {
  field: string;
  type: FieldType;
  path?: FieldPath;
  granularity?: DateGranularity; // date fields
  offset?: string;
  limit?: number;                // per-group-level top-N
};
```

### `HavingCondition` & `HavingExpression`

```ts
type HavingCondition = {
  aggregation: AggregationDefinition;
  operator: FilterOperator;
  value?: unknown;
};

type HavingExpression =
  | HavingCondition
  | { and: HavingExpression[] }
  | { or: HavingExpression[] }
  | { not: HavingExpression };
```

### `SortDirection` & `OrderByDefinition`

```ts
type SortDirection = 'asc' | 'desc';

type OrderByDefinition =
  | { field: string; type: FieldType; path?: FieldPath; direction: SortDirection }
  | { aggregation: AggregationDefinition; direction: SortDirection };
```

### `QueryInput`

```ts
type QueryInput = {
  source: QuerySource;
  select?: FieldSelection[];
  filter?: FilterExpression;
  groupBy?: GroupByField[];
  aggregations?: AggregationDefinition[];
  having?: HavingExpression;
  orderBy?: OrderByDefinition[];
  limit?: number;          // default 25, max 10_000
  skip?: number;           // offset pagination, max 10_000
  cursor?: unknown[];      // cursor pagination (search_after)
};
```

### `SetClause` & `UpdateAction`

```ts
type SetClause = {
  field: string;
  type: FieldType;
  value: unknown;
};

type UpdateAction =
  | { op: 'set'; field: string; type: FieldType; value: unknown }
  | { op: 'increment'; field: string; by: number }
  | { op: 'append'; field: string; type: FieldType; value: unknown }
  | { op: 'removeField'; field: string };
```

### `MutationInput`

```ts
type MutationInput =
  | {
      operation: 'insert';
      source: QuerySource;
      rows: Array<Record<string, unknown>>;
      onConflict?: string[];   // upsert key columns
    }
  | {
      operation: 'delete';
      source: QuerySource;
      filter?: FilterExpression;
      allowUnfiltered?: boolean;
    }
  | {
      operation: 'update';
      source: QuerySource;
      set?: SetClause[];
      actions?: UpdateAction[];
      filter?: FilterExpression;
      allowUnfiltered?: boolean;
    };
```

---

## Output types

### `QueryOutputField`

```ts
type QueryOutputField =
  | { name: string; kind: 'field'; sourceField: string; type: FieldType }
  | { name: string; kind: 'group'; sourceField: string; type: FieldType; granularity?: DateGranularity }
  | { name: string; kind: 'aggregation'; sourceField: string; function: AggregationFunction; type: FieldType };
```

### `QueryOutputMetadata`

```ts
type QueryOutputMetadata = {
  outputFields: QueryOutputField[];
  pagination?: {
    mode: 'offset' | 'search_after';
    requiresStableSort?: boolean;
  };
};
```

### `SqlTranspileResult` & `SearchDslTranspileResult`

```ts
type SqlTranspileResult = {
  engine: 'postgresql' | 'clickhouse';
  kind: 'sql';
  query: string;         // parameterized — run this
  plainQuery: string;    // literals inlined — logging/debug only
  values: unknown[];     // positional params
  namedValues: Record<string, unknown>; // named params (ClickHouse)
  metadata: QueryOutputMetadata;
};

type SearchDslTranspileResult = {
  engine: 'elasticsearch' | 'opensearch';
  kind: 'search-dsl';
  index: string;
  body: Record<string, unknown>;
  metadata: QueryOutputMetadata;
};

type TranspileResult = SqlTranspileResult | SearchDslTranspileResult;
```

### `SqlMutationResult` & `SearchDslMutationResult`

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

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

type MutationTranspileResult = SqlMutationResult | SearchDslMutationResult;
```

---

## Plan types

`QueryPlan` and `MutationPlan` are the validated logical models returned by
`.plan()`. They are exported as **types** (for annotating that return value);
their internal structure is an implementation detail and may change. Most
consumers use `.transpile()` and never touch the plan.

---

## Errors

Every failure is a `QueryBuilderError`.

```ts
class QueryBuilderError extends Error {
  readonly code: QueryBuilderErrorCode;
  readonly path: string;   // JSON-path-like pointer, e.g. `$.groupBy[0].limit`
  readonly details?: Record<string, unknown>;
  readonly payload: QueryBuilderErrorPayload;
}

type QueryBuilderErrorPayload = {
  code: QueryBuilderErrorCode;
  path: string;
  message: string;
  details?: Record<string, unknown>;
};
```

### `QueryBuilderErrorCode`

| Code | Raised when |
| ---- | ----------- |
| `INVALID_INPUT` | Input shape is invalid (missing/typed wrong). |
| `MISSING_ENGINE` | No engine on constructor or call. |
| `UNSUPPORTED_ENGINE` | Engine not registered. |
| `UNSUPPORTED_ENGINE_BEHAVIOR` | Engine cannot express the requested behavior. |
| `INVALID_IDENTIFIER` | Identifier contains a forbidden character (e.g. `;`). |
| `INVALID_FIELD_TYPE` | `type` is not a valid `FieldType`. |
| `INVALID_OPERATOR_FOR_TYPE` | Operator not valid for the field type. |
| `INVALID_IN_VALUE` | `in`/`notIn` value is not a valid array. |
| `INVALID_GROUP_SELECTION` | `select` combined with `groupBy`. |
| `INVALID_AGGREGATION_SELECTION` | `select` combined with `aggregations` without valid grouping. |
| `INVALID_HAVING_CONDITION` | `having` references an invalid aggregation. |
| `INVALID_ORDER_BY` | Grouped query orders by a non-grouped, non-aggregation field. |
| `INVALID_MUTATION` | Mutation shape is invalid. |
| `UNFILTERED_MUTATION` | `update`/`delete` with no filter and no `allowUnfiltered`. |
| `UNSUPPORTED_AGGREGATION_FOR_ENGINE` | Aggregation unsupported by the engine. |
| `UNSUPPORTED_DATE_GRANULARITY_FOR_ENGINE` | Granularity unsupported by the engine. |
| `UNSUPPORTED_PAGINATION_FOR_ENGINE` | Pagination mode unsupported by the engine. |
| `UNSUPPORTED_GROUP_LIMIT_FOR_ENGINE` | Per-group limit unsupported by the engine. |
| `LIMIT_EXCEEDED` | `limit` above the maximum. |
| `SKIP_EXCEEDED` | `skip` above the maximum. |
| `MAX_SELECTED_FIELDS_EXCEEDED` | More than 50 selected fields. |
| `SEARCH_AFTER_REQUIRES_ORDER_BY` | Cursor pagination without `orderBy`. |
| `AMBIGUOUS_PAGINATION` | Both `skip` and `cursor` provided. |

```ts
import { QueryBuilderError } from '@qrvey/query-builder';

try {
  new QueryBuilder(input, { engine: 'postgresql' }).transpile();
} catch (err) {
  if (err instanceof QueryBuilderError) {
    console.error(err.code, err.path, err.message);
  }
}
```
