Skip to content

Getting started

aathena reads your AWS Glue catalog and generates a typed TypeScript function for every SQL file you write. You keep writing SQL; the types come from the catalog rather than from hand-written interfaces that drift.

Install

bash
npm install aathena

Node 20 or later. AWS credentials are resolved by the standard SDK chain, so whatever already works for the AWS CLI works here.

Scaffold a project

bash
npx aathena init

init is interactive and does the whole setup in one pass:

  1. Resolves the AWS region from --region, the AWS_REGION / AWS_DEFAULT_REGION environment variables, or a prompt.
  2. Lists your Glue databases and Athena workgroups, and inherits the workgroup's default output location when it has one.
  3. Writes aathena.config.json and adds node_modules/ to .gitignore.
  4. Lets you multi-select which tables to scaffold starter SQL for.
  5. Probes each table, following Presto/Trino views to the tables underneath, for injected-projection partitions that need a WHERE predicate.
  6. Writes tables/{database}/{table}/default.sql with the right -- @param and WHERE lines.
  7. Runs generate to produce the typed query functions.
  8. Writes a runnable src/main.ts that calls every scaffolded query.
  9. Offers to write the aathena agent skill into your agent's skills directory, so a coding agent picks it up on its next session.

Every prompt has a flag, so init also runs unattended. Without a terminal to answer it, the skill step is skipped unless --skill asks for it:

bash
npx aathena init --region eu-west-1 --database sampledb --tables events,users

Run it

bash
npx tsx src/main.ts

If a scaffolded query needs partition values, main.ts passes REPLACE_ME placeholders with a note at the top of the file. Replace them with real values first.

What you get

A query is one .sql file, and codegen turns it into one exported function with a typed parameter object and a typed result:

sql
-- Complex Glue types: arrays, maps and structs come back parsed, recursively.
--
-- Note which columns are nullable. Athena guarantees NOT NULL only for
-- partition keys, so `dt` is the one column typed without `| null`.
--
-- '{{dt}}'       → string  (quoted)
-- {{rowLimit}}   → number  (LIMIT keyword)

SELECT order_id, placed_at, tags, metadata, address, items, dt
FROM orders
WHERE dt = '{{dt}}'
ORDER BY placed_at DESC
LIMIT {{rowLimit}}

becomes:

ts
// Auto-generated by aathena

// Source: tables/sampledb/orders/detail.sql

import { createQuery, schema } from 'aathena/runtime';
import type { Orders } from '../../../types/sampledb/orders';

export interface DetailParams {
  dt: string;
  rowLimit: number;
}

const schemaDef = {
  dt: schema.string,
  rowLimit: schema.positiveInt,
};

export const detail = createQuery<Orders, DetailParams>(
  'tables/sampledb/orders/detail.sql',
  schemaDef,
);

bound to a row type read straight from Glue:

ts
// Auto-generated by aathena

// Source: sampledb.orders

export interface Orders {
  order_id: bigint | null; // JS BigInt
  placed_at: Date | null;
  tags: string[] | null; // array<varchar>
  metadata: Record<string, number> | null; // map<string,int>
  address: { city: string } | null; // struct<city:string>
  items: { qty: number }[] | null; // array<struct<qty:int>>
  dt: string;
}

Both files are real output from the example project in this repository, and both are typechecked in CI.

Next

Released under the MIT License.