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
npm install aathenaNode 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
npx aathena initinit is interactive and does the whole setup in one pass:
- Resolves the AWS region from
--region, theAWS_REGION/AWS_DEFAULT_REGIONenvironment variables, or a prompt. - Lists your Glue databases and Athena workgroups, and inherits the workgroup's default output location when it has one.
- Writes
aathena.config.jsonand addsnode_modules/to.gitignore. - Lets you multi-select which tables to scaffold starter SQL for.
- Probes each table, following Presto/Trino views to the tables underneath, for injected-projection partitions that need a
WHEREpredicate. - Writes
tables/{database}/{table}/default.sqlwith the right-- @paramandWHERElines. - Runs
generateto produce the typed query functions. - Writes a runnable
src/main.tsthat calls every scaffolded query. - 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:
npx aathena init --region eu-west-1 --database sampledb --tables events,usersRun it
npx tsx src/main.tsIf 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:
-- 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:
// 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:
// 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
- Writing queries - placeholders,
@paramannotations - Running queries - the client, typed rows, debugging