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.