Skip to content
pre-alpha — the TypeScript API may still shift. The SQL won't.

Type generation

sqlfu generate reads checked-in .sql files and emits TypeScript wrappers under sql/.generated/. The generated function name normally comes from the file path: sql/get-post.sql becomes getPost.

-- sql/get-post.sql
select id, slug, title
from posts
where id = :id;
import {getPost} from './sql/.generated/get-post.sql';
const post = await getPost(client, {id: 123});

Put @name in a block comment before each query when one .sql file contains more than one query.

/** @name listPosts */
select id, slug, title
from posts
order by id;
/** @name findPostBySlug */
select id, slug, title
from posts
where slug = :slug;

This emits one generated module, sql/.generated/posts.sql.ts, with both listPosts and findPostBySlug exports. If a file uses @name, every executable statement in that file must have its own @name.

Plain params use sqlfu’s normal :name placeholder syntax.

/** @name getPost */
select id, slug, title
from posts
where id = :id;
await getPost(client, {id: 123});

Use IN (:ids) or NOT IN (:ids) when one scalar param should expand into a comma-separated placeholder list. TypeSQL infers the array type from the IN operator, and sqlfu uses that inferred type to expand the runtime placeholders.

/** @name listPostsByIds */
select id, slug, title
from posts
where id in (:ids)
order by id;
await listPostsByIds(client, {ids: [1, 2, 3]});

At runtime sqlfu executes where id in (?, ?, ?) with [1, 2, 3]. Empty arrays throw before the query reaches SQLite.

Use dot paths when a query naturally accepts one object.

/** @name insertPost */
insert into posts (slug, title)
values (:post.slug, :post.title)
returning id, slug, title;
await insertPost(client, {
post: {
slug: 'hello-world',
title: 'Hello world',
},
});

The generated params type is {post: {slug: string; title: string}}. One object path segment is supported today; nested paths such as :post.author.id are intentionally rejected until the type shape is designed.

Use an object param directly after values when an INSERT column list already names the object fields. The generated param accepts either one object or a non-empty array.

/** @name insertPosts */
insert into posts (slug, title)
values :posts;
await insertPosts(client, {
posts: {slug: 'first', title: 'First'},
});
await insertPosts(client, {
posts: [
{slug: 'second', title: 'Second'},
{slug: 'third', title: 'Third'},
],
});

At runtime sqlfu executes values (?, ?) for one object or values (?, ?), (?, ?) for an array, and flattens values in the INSERT column-list order. Empty arrays throw. This inferred INSERT shorthand does not support RETURNING yet; use explicit dot-path values such as values (:post.slug, :post.title) for returning single-row inserts.

Row-value IN lists also infer object-array params from the left-hand column tuple.

/** @name listPostsByKeys */
select id, slug, title
from posts
where (slug, title) in (:keys)
order by id;
await listPostsByKeys(client, {
keys: [
{slug: 'first', title: 'First'},
{slug: 'third', title: 'Third'},
],
});
  • Runtime-expanded params, currently inferred scalar IN lists, row-value IN lists, and INSERT values :param objects, can appear only once in a query. Reusing the same expanded array in two places would require duplicating the driver arguments, so sqlfu rejects that shape for now.
  • Typed JSON params are not supported yet. A JSON column is still usable as a regular scalar param, but sqlfu does not infer or enforce the TypeScript object shape inside SQLite JSON text/blob values.
  • Parameter shape is inferred from SQL shape, not comment metadata. @name names queries; IN (:ids), (slug, title) in (:keys), and values :posts describe runtime placeholder expansion where the SQL shape changes.