# Running queries

Every generated query is a function that takes a client and its typed
parameters.

```typescript
import { createClient } from 'aathena';
import { byStatus } from './generated';

const athena = createClient();
const result = await byStatus(athena, { status: 'active', rowLimit: 99 });
```

`createClient()` with no argument finds `aathena.config.json` by walking up
from the current working directory. Pass a config to override it, which is
what you want in tests or when the project root is not on disk:

```typescript
import { createClient } from 'aathena';

// createClient() with no argument reads aathena.config.json from the project
// root. Pass a config to override it - useful in tests, or when the project
// root is not on disk (bundled Lambda deploys).
const athena = createClient({
  region: 'us-east-1',
  database: 'analytics',
  workgroup: 'primary',
  outputLocation: 's3://my-athena-results/output/',
});
```

## Rows are typed from the catalog

Column names are emitted exactly as Glue reports them - no camelCasing, no
renaming - so a row object matches the table it came from. Scalars land as
native TypeScript, and Parquet/ORC arrays, maps and structs are parsed back
recursively, even nested:

```typescript
// Glue: order_id bigint, placed_at timestamp, tags array<varchar>,
// metadata map<string,int>, address struct<city:string>,
// items array<struct<qty:int>>, partitioned by dt string

import { createClient } from 'aathena';
import { detail } from './generated';

const athena = createClient();
const result = await detail(athena, { dt: '2026-08-26', rowLimit: 33 });
const row = result.rows[0];

row.order_id;        // bigint | null
row.placed_at;       // Date | null
row.tags;            // string[] | null
row.metadata;        // Record<string, number> | null
row.address?.city;   // string | undefined - struct field access
row.items?.[0].qty;  // number | undefined - nested array of struct
row.dt;              // string - partition keys are the only NOT NULL columns
```

Note the nullability. Athena guarantees `NOT NULL` only for partition keys, so
`dt` is the one column above typed without `| null`. That is a rule worth
internalising early: it is what stops `row.address.city` from compiling and
sends you to `row.address?.city` instead.

See [Type mapping](../reference/type-mapping.md) for the full table.

## Retries

`client.query()` retries `StartQueryExecution` with exponential backoff and
full jitter when Athena answers `TooManyRequestsException` or
`CONCURRENT_QUERY_LIMIT_EXCEEDED`, up to 6 attempts. This applies to every
call - generated or inline - including the tasks dispatched by `parallel()`.

## Debugging: export the rendered SQL

Pass `{ exportTo: <path> }` as the optional third argument to write the
rendered SQL, with parameter values substituted, to disk:

```typescript
import { createClient } from 'aathena';
import { byStatus } from './generated';

const athena = createClient();

// The rendered SQL, with parameter values substituted, is written to disk.
// The query still runs; missing parent directories are created and the file
// is overwritten on each call.
await byStatus(
  athena,
  { status: 'active', rowLimit: 99 },
  { exportTo: './debug/by-status.sql' },
);
```

The query still executes. Missing parent directories are created, and the file
is overwritten on each call.

## Running queries concurrently

`parallel()` runs several queries at once under a bounded cap that respects
Athena's per-account active-DML quota. Tasks are thunks rather than promises,
so the helper controls when each query actually starts:

```typescript
import { createClient, parallel } from 'aathena';
import { active, byStatus } from './generated';

const athena = createClient();

// Tasks are thunks, not promises, so parallel() decides when each query
// actually starts rather than racing them all at import time.
const [users, events] = await parallel(
  [
    () => active(athena, { minAge: 18, rowLimit: 99 }),
    () => byStatus(athena, { status: 'active', rowLimit: 99 }),
  ],
  { concurrency: 'auto', client: athena },
);
```

With `concurrency: 'auto'`, the cap is resolved in this order:

1. `AathenaConfig.maxConcurrency`, if set.
2. A live AWS Service Quotas lookup (`L-D405C694` for DML, `L-FCDFE414` for
   DDL). This uses `@aws-sdk/client-service-quotas`, an optional dependency
   loaded by dynamic import, and needs the `servicequotas:GetServiceQuota` IAM
   permission. Without it the lookup fails quietly and the next step applies.
3. A region-aware conservative fallback: half the AWS-documented default,
   clamped to `[5, 25]`.

| Option | Default | Meaning |
| --- | --- | --- |
| `concurrency` | `5` | a number, or `'auto'` |
| `client` | - | required when `concurrency: 'auto'` and `maxConcurrency` is unset |
| `kind` | `'dml'` | `'dml'` or `'ddl'`, selects which quota to probe |
| `reserveHeadroom` | `1` | subtracted from the resolved quota |
| `mode` | `'all'` | `'all'` rejects on the first failure; `'allSettled'` returns per-task settlements |

## Errors

Everything aathena throws extends `AathenaError`, so you can catch the
specific cases you handle and let the rest fall through:

```typescript
import {
  AathenaError,
  QueryFailedError,
  QueryTimeoutError,
  createClient,
} from 'aathena';
import { byStatus } from './generated';

const athena = createClient();

try {
  const result = await byStatus(athena, { status: 'active', rowLimit: 99 });
  console.log(result.rows.length);
} catch (err) {
  if (err instanceof QueryTimeoutError) {
    console.log(`Timed out after ${err.timeoutMs}ms: ${err.queryExecutionId}`);
  } else if (err instanceof QueryFailedError) {
    console.log(`Athena error: ${err.athenaErrorMessage}`);
  } else if (err instanceof AathenaError) {
    // QueryCancelledError, ColumnParseError, or anything else aathena threw
    console.log(`aathena error (${err.name}): ${err.message}`);
  } else {
    throw err;
  }
}
```

| Class | Thrown when |
| --- | --- |
| `QueryTimeoutError` | the query exceeded the configured timeout; carries `timeoutMs` and `queryExecutionId` |
| `QueryFailedError` | Athena reported a failure; carries `athenaErrorMessage` |
| `QueryCancelledError` | the execution was cancelled |
| `ColumnParseError` | a returned value did not parse as its declared column type |
| `AathenaError` | the base class for all of the above |

## Next

- [Configuration](../reference/configuration.md) - every field of `aathena.config.json`
- [Type mapping](../reference/type-mapping.md) - Athena types to TypeScript
