npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tetalang/teta

v0.5.3

Published

Type-safe SQL EDSL for building composable queries.

Readme

@tetalang/teta

Type-safe SQL EDSL frontend for TypeScript.

Published as @tetalang/teta on npm and @teta/teta on JSR.

@tetalang/teta builds typed query IR and keeps direct convenience helpers such as toSql(...). The reusable SQL backend lives in @tetalang/sql.

When another language needs to produce the same query representation, use the Portable IR v1 guide and render it through @tetalang/sql.

Install

Deno

deno add jsr:@teta/teta

JSR installations use @teta/teta and @teta/sql import specifiers. The examples below use the npm @tetalang scope.

Bun

bun add @tetalang/teta

Node.js

npm install @tetalang/teta

Quick start

import { and, asc, eq, filter, gte, map, sort, table, t, take, toSql, pipe } from "@tetalang/teta";

const users = table("users", {
  id: t.int(),
  email: t.string(),
  active: t.boolean(),
  age: t.int(),
});

const activeUsers = pipe(
  users,
  filter((user) => and(eq(user.active, true), gte(user.age, 18))),
  map((user) => ({ id: user.id, email: user.email })),
  sort((user) => asc(user.email)),
  take(10)
);

console.log(toSql(activeUsers, { dialect: "postgresql", format: "pretty" }));

The root entrypoint remains the easiest import path. For narrower module boundaries, the package also exposes subpath entrypoints:

import { table, t, filter } from "@tetalang/teta/query";
import { eq } from "@tetalang/teta/expr";
import { pipe } from "@tetalang/teta/pipe";
import { fn, windowFn } from "@tetalang/teta/advanced";
import { toAst } from "@tetalang/teta/inspect";

The default entrypoint intentionally excludes compiler-node constructors, database-specific function builders, and parser-specific AST inspection. Use @tetalang/teta/advanced for catalog-checked checkedFn(...), explicitly typed unsafeFn(...), custom fn(...), and windowFn(...) calls, @tetalang/teta/inspect when you explicitly need the backend parser AST, and @tetalang/sql directly when building or rendering public IR from another frontend.

Reusable functional pipelines can be saved with flow(...), while map(...) selects and computes output columns:

import { composeSteps, filterEq, flow, lower, map, pipe, take, whenStep } from "@tetalang/teta";

const activePublicUsers = flow(
  filterEq((user) => user.active, true),
  map((user) => ({ id: user.id, normalized_email: lower(user.email) })),
  whenStep(includeLimit, take(50))
);

flow(...) remains the general-purpose function composer. Use composeSteps(...) when compatible query transforms should become one frozen, metadata-bearing query step, including when the result will be passed to whenStep(...) or unlessStep(...):

const activePage = composeSteps(
  filterEq((user: typeof users.columns) => user.active, true),
  take(50)
);

const usersQuery = pipe(users, whenStep(includeActivePage, activePage));

Query values expose columns for typed expression reuse, but internal compiler details such as sources, stages, CTEs, and generated names are intentionally opaque. Use toIR(query) or explain(query, ...) when you need to inspect lowered query structure. Query steps are callable values with lightweight kind and stepName metadata for tooling/debugging.

Query construction is immutable and normalization is handled as a pure compiler pass. Newly allocated query and expression nodes are frozen once, while previously frozen plan structure is shared between derived queries.

Row shapes are constrained to SQL value types, and table schemas must be non-empty objects built from t.* column helpers. Aggregate projections are checked separately from row projections. In fold(...), use group(...) / groupShape(...) for grouping keys and aggregate helpers such as count(...), sum(...), or arrayAgg(...) for aggregate outputs.

Comparison filter helpers require at least one row callback; direct values are allowed only as the other operand. distinct() removes duplicate rows without changing the query's row type.

whenStep(...) and unlessStep(...) use host-language booleans to include or skip schema-preserving steps while building a query; use filter(...) and predicate expressions for conditions evaluated by SQL.

Use map(...) for explicit object-shaped projections. pick(...), drop(...), and rename(...) are pure record helpers that compose inside map(...):

import { drop, map, pick, rename, upper } from "@tetalang/teta";

const publicUsers = pipe(
  users,
  map((user) => ({
    id: user.id,
    email_upper: upper(user.email),
  }))
);

Use pick(...) to retain existing fields in a specific order. Pass names either as variadic arguments or as a key array:

const publicUsers = pipe(users, map(pick("id", "email")));
const sameUsers = pipe(users, map(pick(["id", "email"])));

Use drop(...) to remove fields while preserving the order of everything else:

const safeUsers = pipe(
  users,
  map(drop("password_hash", "recovery_token"))
);

Use rename(...) when every field name follows the same mapping rule:

const prefixedUsers = pipe(
  users,
  map(rename((key) => `user_${key}`))
);

Use join(...) with a join-kind specification. The optional second argument to the specification selects or merges the output columns:

import { eq, join, left } from "@tetalang/teta";

const orders = table("orders", {
  order_id: t.int(),
  user_id: t.int(),
  total: t.float(),
});

const usersWithOrders = pipe(
  users,
  join(orders, left(
    (user, order) => eq(user.id, order.user_id),
    (user, order) => ({
      user_id: user.id,
      order_total: order.total,
    })
  ))
);

The available specifications are inner(on, select?), left(on, select?), right(on, select?), and full(on, select?). Merge helpers such as dropOverlapRight() can be passed in the same position as a selector.

Predicates involving nullable expressions are typed as Expr<SqlBoolean | null> and are accepted by filter(...), matching SQL's three-valued boolean behavior.

For explicit frontend/backend use, lower the query to IR and render it through @tetalang/sql:

import { irToSql } from "@tetalang/sql";
import { toIR, toSql } from "@tetalang/teta";

const direct = toSql(activeUsers, { dialect: "postgresql" });
const explicit = irToSql(toIR(activeUsers), { dialect: "postgresql" });

For a standalone placeholder, provide a runtime SQL type descriptor and pass its value when rendering:

import { eq, filter, param, pipe, t, toSqlResult } from "@tetalang/teta";

const byId = pipe(
  users,
  filter((user) => eq(user.id, param("id", t.int())))
);

const result = toSqlResult(byId, {
  dialect: "postgresql",
  params: { id: 42 },
});

For positional driver placeholders, use numeric parameter names with array bindings:

const byPosition = pipe(
  users,
  filter((user) => eq(user.id, param("1", t.int())))
);

const positional = toSqlResult(byPosition, {
  dialect: "postgresql",
  parameterMode: "positional",
  parameterPrefix: "$",
  params: [42],
});

Prefer prepare(...) for reusable application queries. Its descriptor schema creates typed parameter expressions, requires exact binding keys, and validates values before rendering:

import { eq, filter, gte, pipe, prepare, t, toSqlResult } from "@tetalang/teta";

const byUserCriteria = prepare(
  { userId: t.int(), minimumAge: t.int() },
  (params) => pipe(
    users,
    filter((user) => eq(user.id, params.userId)),
    filter((user) => gte(user.age, params.minimumAge)),
  ),
);

const prepared = toSqlResult(byUserCriteria, {
  dialect: "postgresql",
  params: { userId: 42, minimumAge: 18 },
});

Schema descriptors also define decoded result types. Use decodeRow(...) or decodeRows(...) at the driver boundary when a schema needs to turn raw driver values into application values.

The callback form gives autocomplete and compile-time column checks while keeping query steps reusable:

import { and, asc, eq, filter, gte, map, sort, pipe } from "@tetalang/teta";

const activeUsers = pipe(
  users,
  filter((user) => and(eq(user.active, true), gte(user.age, 18))),
  map((user) => ({ id: user.id, email: user.email })),
  sort((user) => asc(user.email))
);

Additional predicate helpers include between(...), isNotIn(...)/notIn(...), and isDistinctFrom(...):

import { and, between, filter, isDistinctFrom, isNotIn } from "@tetalang/teta";

const adultUsers = pipe(
  users,
  filter((user) => and(
    between(user.age, 18, 64),
    isNotIn(user.email, ["[email protected]"]),
    isDistinctFrom(user.email, "[email protected]"),
  ))
);

More docs: