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

Validator-integration fixtures: zod, valibot, zod/mini, arktype. Runtime-behaviour tests (actually importing the transpiled module and calling it) stay in generate.test.ts — the fixtures here just pin the emitted source shape. We also cover prettyErrors: false for each, which changes the emitted file significantly (inline issue-throwing instead of the shared runtime helper), and the plain-TS default when no validator is configured.

Most tests on this page override sqlfu.config.ts to set the generate.validator they care about; the default below is just the plain-TS baseline used by the last test.

default config
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
};

validator: zod emits zod schemas with namespace-merged exports

Section titled “validator: zod emits zod schemas with namespace-merged exports”
input
definitions.sql
create table posts (
id integer primary key,
slug text not null,
title text,
status text not null check (status in ('draft', 'published'))
);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'zod'},
};
sql/find-post-by-slug.sql
select id, slug, title, status from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import type {Client} from 'sqlfu';
import {z} from 'zod';
const Params = z.object({
slug: z.string(),
});
const Result = z.object({
id: z.number(),
slug: z.string(),
title: z.string().nullable(),
status: z.enum(["draft", "published"]),
});
const sql = `select id, slug, title, status from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const parsedParams = Params.safeParse(params);
if (!parsedParams.success) throw new Error(z.prettifyError(parsedParams.error));
const rows = await client.all(query(parsedParams.data));
if (rows.length === 0) return null;
const parsed = Result.safeParse(rows[0]);
if (!parsed.success) throw new Error(z.prettifyError(parsed.error));
return parsed.data;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = z.infer<typeof findPostBySlug.Params>;
export type Result = z.infer<typeof findPostBySlug.Result>;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
title: string | null;
status: string;
};
input
definitions.sql
create table posts (
id integer primary key,
slug text not null,
title text,
status text not null check (status in ('draft', 'published'))
);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'valibot'},
};
sql/find-post-by-slug.sql
select id, slug, title, status from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import {type Client, prettifyStandardSchemaError} from 'sqlfu';
import * as v from 'valibot';
const Params = v.object({
slug: v.string(),
});
const Result = v.object({
id: v.number(),
slug: v.string(),
title: v.nullable(v.string()),
status: v.picklist(["draft", "published"]),
});
const sql = `select id, slug, title, status from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const parsedParamsResult = Params['~standard'].validate(params);
if ('then' in parsedParamsResult) throw new Error('Unexpected async validation from Params.');
if ('issues' in parsedParamsResult) throw new Error(prettifyStandardSchemaError(parsedParamsResult) || 'Validation failed');
const rows = await client.all(query(parsedParamsResult.value));
if (rows.length === 0) return null;
const parsed = Result['~standard'].validate(rows[0]);
if ('then' in parsed) throw new Error('Unexpected async validation from Result.');
if ('issues' in parsed) throw new Error(prettifyStandardSchemaError(parsed) || 'Validation failed');
return parsed.value;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = v.InferOutput<typeof findPostBySlug.Params>;
export type Result = v.InferOutput<typeof findPostBySlug.Result>;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
title: string | null;
status: string;
};

validator: zod-mini emits zod/mini schemas

Section titled “validator: zod-mini emits zod/mini schemas”
input
definitions.sql
create table posts (id integer primary key, slug text not null, title text);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'zod-mini'},
};
sql/find-post-by-slug.sql
select id, slug, title from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import {type Client, prettifyStandardSchemaError} from 'sqlfu';
import * as z from 'zod/mini';
const Params = z.object({
slug: z.string(),
});
const Result = z.object({
id: z.number(),
slug: z.string(),
title: z.nullable(z.string()),
});
const sql = `select id, slug, title from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const parsedParamsResult = Params['~standard'].validate(params);
if ('then' in parsedParamsResult) throw new Error('Unexpected async validation from Params.');
if ('issues' in parsedParamsResult) throw new Error(prettifyStandardSchemaError(parsedParamsResult) || 'Validation failed');
const rows = await client.all(query(parsedParamsResult.value));
if (rows.length === 0) return null;
const parsed = Result['~standard'].validate(rows[0]);
if ('then' in parsed) throw new Error('Unexpected async validation from Result.');
if ('issues' in parsed) throw new Error(prettifyStandardSchemaError(parsed) || 'Validation failed');
return parsed.value;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = z.infer<typeof findPostBySlug.Params>;
export type Result = z.infer<typeof findPostBySlug.Result>;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
title: string | null;
};

validator: zod + prettyErrors: false drops the prettify helper and safeParse wrapper

Section titled “validator: zod + prettyErrors: false drops the prettify helper and safeParse wrapper”
input
definitions.sql
create table posts (id integer primary key, slug text not null);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'zod', prettyErrors: false},
};
sql/find-post-by-slug.sql
select id, slug from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import type {Client} from 'sqlfu';
import {z} from 'zod';
const Params = z.object({
slug: z.string(),
});
const Result = z.object({
id: z.number(),
slug: z.string(),
});
const sql = `select id, slug from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const rows = await client.all(query(Params.parse(params)));
return rows.length > 0 ? Result.parse(rows[0]) : null;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = z.infer<typeof findPostBySlug.Params>;
export type Result = z.infer<typeof findPostBySlug.Result>;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
};

validator: valibot + prettyErrors: false throws raw issues inline

Section titled “validator: valibot + prettyErrors: false throws raw issues inline”
input
definitions.sql
create table posts (id integer primary key, slug text not null);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'valibot', prettyErrors: false},
};
sql/find-post-by-slug.sql
select id, slug from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import type {Client} from 'sqlfu';
import * as v from 'valibot';
const Params = v.object({
slug: v.string(),
});
const Result = v.object({
id: v.number(),
slug: v.string(),
});
const sql = `select id, slug from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const parsedParamsResult = Params['~standard'].validate(params);
if ('then' in parsedParamsResult) throw new Error('Unexpected async validation from Params.');
if ('issues' in parsedParamsResult) throw Object.assign(new Error('Validation failed'), {issues: parsedParamsResult.issues});
const rows = await client.all(query(parsedParamsResult.value));
if (rows.length === 0) return null;
const parsed = Result['~standard'].validate(rows[0]);
if ('then' in parsed) throw new Error('Unexpected async validation from Result.');
if ('issues' in parsed) throw Object.assign(new Error('Validation failed'), {issues: parsed.issues});
return parsed.value;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = v.InferOutput<typeof findPostBySlug.Params>;
export type Result = v.InferOutput<typeof findPostBySlug.Result>;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
};

validator: zod-mini + prettyErrors: false throws raw issues inline

Section titled “validator: zod-mini + prettyErrors: false throws raw issues inline”
input
definitions.sql
create table posts (id integer primary key, slug text not null);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'zod-mini', prettyErrors: false},
};
sql/find-post-by-slug.sql
select id, slug from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import type {Client} from 'sqlfu';
import * as z from 'zod/mini';
const Params = z.object({
slug: z.string(),
});
const Result = z.object({
id: z.number(),
slug: z.string(),
});
const sql = `select id, slug from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const parsedParamsResult = Params['~standard'].validate(params);
if ('then' in parsedParamsResult) throw new Error('Unexpected async validation from Params.');
if ('issues' in parsedParamsResult) throw Object.assign(new Error('Validation failed'), {issues: parsedParamsResult.issues});
const rows = await client.all(query(parsedParamsResult.value));
if (rows.length === 0) return null;
const parsed = Result['~standard'].validate(rows[0]);
if ('then' in parsed) throw new Error('Unexpected async validation from Result.');
if ('issues' in parsed) throw Object.assign(new Error('Validation failed'), {issues: parsed.issues});
return parsed.value;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = z.infer<typeof findPostBySlug.Params>;
export type Result = z.infer<typeof findPostBySlug.Result>;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
};
input
definitions.sql
create table posts (
id integer primary key,
slug text not null,
title text,
status text not null check (status in ('draft', 'published'))
);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'arktype'},
};
sql/find-post-by-slug.sql
select id, slug, title, status from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import {type Client, prettifyStandardSchemaError} from 'sqlfu';
import {type} from 'arktype';
const Params = type({
slug: "string",
});
const Result = type({
id: "number",
slug: "string",
title: "string | null",
status: "\"draft\" | \"published\"",
});
const sql = `select id, slug, title, status from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const parsedParamsResult = Params['~standard'].validate(params);
if ('then' in parsedParamsResult) throw new Error('Unexpected async validation from Params.');
if ('issues' in parsedParamsResult) throw new Error(prettifyStandardSchemaError(parsedParamsResult) || 'Validation failed');
const rows = await client.all(query(parsedParamsResult.value));
if (rows.length === 0) return null;
const parsed = Result['~standard'].validate(rows[0]);
if ('then' in parsed) throw new Error('Unexpected async validation from Result.');
if ('issues' in parsed) throw new Error(prettifyStandardSchemaError(parsed) || 'Validation failed');
return parsed.value;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = typeof findPostBySlug.Params.infer;
export type Result = typeof findPostBySlug.Result.infer;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
title: string | null;
status: string;
};

validator: arktype + prettyErrors: false throws raw issues inline

Section titled “validator: arktype + prettyErrors: false throws raw issues inline”
input
definitions.sql
create table posts (id integer primary key, slug text not null);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'arktype', prettyErrors: false},
};
sql/find-post-by-slug.sql
select id, slug from posts where slug = :slug limit 1;
output
sql/.generated/find-post-by-slug.sql.ts
import type {Client} from 'sqlfu';
import {type} from 'arktype';
const Params = type({
slug: "string",
});
const Result = type({
id: "number",
slug: "string",
});
const sql = `select id, slug from posts where slug = ? limit 1;`;
const query = (params: findPostBySlug.Params) => ({ sql, args: [params.slug], name: "findPostBySlug" });
export const findPostBySlug = Object.assign(
async function findPostBySlug(client: Client, params: findPostBySlug.Params): Promise<findPostBySlug.Result | null> {
const parsedParamsResult = Params['~standard'].validate(params);
if ('then' in parsedParamsResult) throw new Error('Unexpected async validation from Params.');
if ('issues' in parsedParamsResult) throw Object.assign(new Error('Validation failed'), {issues: parsedParamsResult.issues});
const rows = await client.all(query(parsedParamsResult.value));
if (rows.length === 0) return null;
const parsed = Result['~standard'].validate(rows[0]);
if ('then' in parsed) throw new Error('Unexpected async validation from Result.');
if ('issues' in parsed) throw Object.assign(new Error('Validation failed'), {issues: parsed.issues});
return parsed.value;
},
{ Params, Result, sql, query },
);
export namespace findPostBySlug {
export type Params = typeof findPostBySlug.Params.infer;
export type Result = typeof findPostBySlug.Result.infer;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./find-post-by-slug.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
};

validator: valibot emits a Result schema with no Params when the query takes no args

Section titled “validator: valibot emits a Result schema with no Params when the query takes no args”
input
definitions.sql
create table posts (id integer primary key, slug text not null, title text);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'valibot'},
};
sql/latest-post.sql
select id, slug, title from posts limit 1;
output
sql/.generated/latest-post.sql.ts
import {type Client, prettifyStandardSchemaError} from 'sqlfu';
import * as v from 'valibot';
const Result = v.object({
id: v.number(),
slug: v.string(),
title: v.nullable(v.string()),
});
const sql = `select id, slug, title from posts limit 1;`;
const query = { sql, args: [], name: "latestPost" };
export const latestPost = Object.assign(
async function latestPost(client: Client): Promise<latestPost.Result | null> {
const rows = await client.all(query);
if (rows.length === 0) return null;
const parsed = Result['~standard'].validate(rows[0]);
if ('then' in parsed) throw new Error('Unexpected async validation from Result.');
if ('issues' in parsed) throw new Error(prettifyStandardSchemaError(parsed) || 'Validation failed');
return parsed.value;
},
{ Result, sql, query },
);
export namespace latestPost {
export type Result = v.InferOutput<typeof latestPost.Result>;
}

validator: zod + insert metadata skips result validation but keeps Params

Section titled “validator: zod + insert metadata skips result validation but keeps Params”
input
definitions.sql
create table posts (id integer primary key, slug text not null);
sqlfu.config.ts
export default {
db: './app.db',
migrations: './migrations',
definitions: './definitions.sql',
queries: './sql',
generate: {validator: 'zod'},
};
sql/insert-post.sql
insert into posts (slug) values (:slug);
output
sql/.generated/insert-post.sql.ts
import type {Client} from 'sqlfu';
import {z} from 'zod';
const Params = z.object({
slug: z.string(),
});
const sql = `insert into posts (slug) values (?);`;
const query = (params: insertPost.Params) => ({ sql, args: [params.slug], name: "insertPost" });
export const insertPost = Object.assign(
async function insertPost(client: Client, params: insertPost.Params) {
const parsedParams = Params.safeParse(params);
if (!parsedParams.success) throw new Error(z.prettifyError(parsedParams.error));
return client.run(query(parsedParams.data));
},
{ Params, sql, query },
);
export namespace insertPost {
export type Params = z.infer<typeof insertPost.Params>;
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./insert-post.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
};

no validator: plain-TS output matches the default wrapper shape

Section titled “no validator: plain-TS output matches the default wrapper shape”
input
definitions.sql
create table posts (id integer primary key, slug text not null);
sql/list-posts.sql
select id, slug from posts;
output
sql/.generated/list-posts.sql.ts
import type {Client} from 'sqlfu';
const sql = `select id, slug from posts;`;
const query = { sql, args: [], name: "listPosts" };
export const listPosts = Object.assign(
async function listPosts(client: Client): Promise<listPosts.Result[]> {
return client.all<listPosts.Result>(query);
},
{ sql, query },
);
export namespace listPosts {
export type Result = {
id: number;
slug: string;
};
}
sql/.generated/index.ts
export * from "./tables.js";
export * from "./list-posts.sql.js";
sql/.generated/tables.ts
// Generated by `sqlfu generate`. Do not edit.
// Row types for every table and view in your project's schema.
export type PostsRow = {
id: number;
slug: string;
};