sql
js/database/sql.ts
fino:database/sql — typed SQL file functions.
This module turns directive-style .sql files into callable JavaScript
functions. It layers OXC-backed TypeScript syntax validation onto the
-- function directive format and supports structural placeholders such as
{{ user.id }}.
A SQL module is plain SQL annotated with two directives. -- import type
... from '...' preserves TypeScript type imports for the generated module,
and -- function name(params): ReturnType starts a named function whose
body is every SQL line up to the next directive. Comment lines between the
directive and the first SQL line become the function's description; both
parameter types and the return type default to string when omitted.
Positional ? markers in the body are rewritten to named placeholders in
parameter order.
Placeholders come in two forms. {{ path }} resolves a dotted or bracketed
path (input.id, rows[0], opts['key']) against the call arguments and
escapes string values as SQL literals. {{! path }} splices the value raw
and is intended only for trusted SQL fragments such as ORDER BY clauses —
never user input.
The parser validates TypeScript syntax but does not type-check imported
declarations. The pipeline has two consumers: compileSqlModule() produces
live functions at runtime, and toSqlModuleSource() emits TypeScript source
— the module loader uses the latter so .sql files can be imported
directly, and fino:database/migrate builds its migration engine on the
same parser.
import { compileSqlModule, parseSqlModule } from 'fino:database/sql';
const queries = compileSqlModule(parseSqlModule(`
-- import type { User } from './types.ts'
-- function findUser(input: User): string
-- Find one user by id.
SELECT * FROM users WHERE id = '{{ input.id }}'
-- function listUsers(order: string)
SELECT * FROM users ORDER BY {{! order }}
`));
queries.findUser({ id: "O'Brien" }); // quotes escaped safely
queries.listUsers('created_at DESC'); // raw fragment, trusted input only
Classes
class MigrationParseError extends Error {
Error thrown when a SQL module cannot be parsed.
Raised for invalid TypeScript syntax in an import or function directive,
malformed parameter names, and placeholder problems such as unbalanced
braces or empty {{ }} slots. The message is prefixed with
source:lineNum so parse failures point at the offending line of the
original .sql text. Also re-exported from fino:database/migrate.
import { MigrationParseError, parseSqlModule } from 'fino:database/sql';
try {
parseSqlModule('-- function bad(input: )\nSELECT 1', { source: 'bad.sql' });
} catch (err) {
if (err instanceof MigrationParseError) {
console.error(err.source, err.lineNum, err.message); // 'bad.sql' 1 'bad.sql:1 ...'
}
}
Properties
source: string
Label of the SQL module that failed to parse — the source option given
to parseSqlModule(), or (unknown) when none was provided.
lineNum: number
One-based line number within the SQL text where parsing failed.
Constructors
constructor(message: string, source: string, lineNum: number)
Builds the error, prefixing message with the source:lineNum location.
Interfaces
interface SqlParamIR {
Parameter parsed from a -- function directive.
One entry per parameter in the signature. A parameter written without a
type annotation defaults to string.
import { parseSqlModule } from 'fino:database/sql';
const ir = parseSqlModule('-- function q(id: number, name)\nSELECT 1');
ir.functions[0].params;
// [{ name: 'id', type: 'number' }, { name: 'name', type: 'string' }]
Properties
name: string
Parameter name as written in the directive signature.
type: string
TypeScript type annotation text, or string when the directive omitted
one.
interface SqlFunctionIR {
One SQL function parsed from a module.
Produced by parseSqlModule() for each -- function directive and
consumed by compileSqlModule() and toSqlModuleSource().
import { parseSqlModule } from 'fino:database/sql';
const [fn] = parseSqlModule(`-- function findUser(input: { id: string })
-- Look up a single user.
SELECT * FROM users WHERE id = '{{ input.id }}'
`).functions;
fn.name; // 'findUser'
fn.returnType; // 'string'
fn.description; // ['Look up a single user.']
Properties
name: string
Function name from the directive; becomes the exported/compiled function name.
params: SqlParamIR[]
Parameters in declaration order; positional ? markers in the body are
bound to these by index.
returnType: string
TypeScript return type annotation, defaulting to string when the
directive has none.
sql: string
SQL body with comment lines removed, positional ? markers already
rewritten to named placeholders, and lines joined with \n.
description: string[]
Comment lines found between the directive and the first SQL line; emitted
as a doc comment by toSqlModuleSource().
interface SqlModuleIR {
Parsed SQL module with preserved type imports and named functions.
The intermediate representation shared by every consumer of the SQL module
pipeline: compileSqlModule() turns it into live functions,
toSqlModuleSource() renders it back out as a TypeScript module.
import { parseSqlModule, type SqlModuleIR } from 'fino:database/sql';
const ir: SqlModuleIR = parseSqlModule('-- function ping()\nSELECT 1', { source: 'queries.sql' });
for (const fn of ir.functions) console.log(fn.name);
Properties
imports: string[]
Normalized import type ...; statements collected from -- import type
directives, in file order.
functions: SqlFunctionIR[]
Parsed functions in the order their directives appear.
source: string
Source label the module was parsed under, carried along so downstream
consumers such as fino:database/migrate can report errors against the
original file.
interface ParseSqlModuleOptions {
Options for parsing a SQL module.
import { parseSqlModule } from 'fino:database/sql';
parseSqlModule('-- function up()\nCREATE TABLE t (id TEXT)', { source: 'migrations/001-init.sql' });
Properties
source?: string
Label used in MigrationParseError messages, typically the file path of
the SQL text. Defaults to (unknown).
interface CompileSqlModuleOptions {
Options for compiling SQL functions.
import { compileSqlModule, parseSqlModule, escapeSqlLiteral } from 'fino:database/sql';
const queries = compileSqlModule(parseSqlModule("-- function q(id)\nSELECT * FROM t WHERE id = '{{ id }}'"), {
escape: (value) => escapeSqlLiteral(value, 'postgres'),
});
Properties
escape?: (value: unknown) => unknown
Escape function applied to every {{ path }} placeholder value before
interpolation. Defaults to escapeSqlLiteral in its SQLite dialect.
Functions
function parseSqlModule(text: string, options: ParseSqlModuleOptions = {}): SqlModuleIR
Parse SQL directives into a typed SQL module IR.
The parser accepts -- import type ... and -- function name(...)
directives. TypeScript syntax in imports and signatures is validated with
OXC, while SQL bodies remain plain text except for placeholder validation.
A function's body runs from its directive to the next directive or end of
input. Comment lines before the first SQL line are captured as the
function's description; comment lines and blank lines never become part of
the SQL body. Positional ? markers are rewritten to {{ param }}
placeholders in declaration order, and any ? beyond the parameter count
is left untouched. Lines outside any directive are ignored, so a plain SQL
file with no directives parses to an empty module.
Throws MigrationParseError when a directive fails TypeScript validation,
a parameter name is invalid, or a SQL line has unbalanced or empty
placeholder braces.
import { parseSqlModule } from 'fino:database/sql';
const ir = parseSqlModule(`-- import type { User } from './types.ts'
-- function findUser(input: User): string
-- Find a user by id.
SELECT * FROM users WHERE id = '{{ input.id }}'
-- function byStatus(status)
SELECT * FROM users WHERE status = '?'
`, { source: 'queries.sql' });
ir.functions[1].sql; // "SELECT * FROM users WHERE status = '{{ status }}'"
function escapeSqlLiteral(value: unknown, dialect: 'sqlite' | 'postgres' = 'sqlite'): unknown
Escape a value for SQL literal interpolation.
Strings use backslash escaping by default: the SQLite dialect prefixes
single quotes and backslashes with \. PostgreSQL mode doubles single
quotes and leaves backslashes untouched. Non-string values (numbers,
booleans, null) pass through unchanged.
This is the default escape function for compileSqlModule(); pass a bound
dialect through CompileSqlModuleOptions.escape to switch databases.
import { escapeSqlLiteral } from 'fino:database/sql';
escapeSqlLiteral("O'Brien"); // "O\\'Brien"
escapeSqlLiteral("O'Brien", 'postgres'); // "O''Brien"
escapeSqlLiteral(42); // 42
function compileSqlModule(
ir: SqlModuleIR,
options: CompileSqlModuleOptions = {
}
): Record<string, (
...args: unknown[]
) => string>
Compile a parsed SQL module into callable JavaScript functions.
Each generated function takes positional arguments matching the directive's
parameter list and returns the rendered SQL string. {{ path }}
placeholders resolve dotted or bracketed paths against those arguments and
are escaped; {{! path }} placeholders are raw and must only receive
trusted SQL fragments. A raw placeholder whose path is a bare name of
another function in the same module (and not a parameter) calls that
function with no arguments and splices its output, which allows shared SQL
fragments to be composed.
Rendering throws if a placeholder path is malformed or if any segment of the path is missing from the arguments. Nullish raw values render as an empty string.
import { compileSqlModule, parseSqlModule } from 'fino:database/sql';
const queries = compileSqlModule(parseSqlModule(`-- function userColumns()
id, name, status
-- function findUser(input: { id: string })
SELECT {{! userColumns }} FROM users WHERE id = '{{ input.id }}'
`));
queries.findUser({ id: "O'Brien" });
// "SELECT id, name, status FROM users WHERE id = 'O\\'Brien'"
function toSqlModuleSource(ir: SqlModuleIR): string
Generate a TypeScript module from parsed SQL functions.
Type imports are preserved, named functions are exported with their
declared parameter and return types, and the default export contains every
generated function by name. Function descriptions become doc comments on
the exports. The generated source is self-contained: placeholder path
reading and SQLite-style literal escaping are inlined as private helpers,
so the output has no runtime dependency on this module. Unlike
compileSqlModule(), raw placeholders in generated code only read argument
values — they do not splice sibling functions.
This is how the module loader supports importing .sql files directly: the
file is parsed, converted with this function, and transpiled like any other
TypeScript module.
import { parseSqlModule, toSqlModuleSource } from 'fino:database/sql';
const source = toSqlModuleSource(parseSqlModule(`-- import type { User } from './types.ts'
-- function up(input: User)
CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL);
-- function down()
DROP TABLE users;
`));
source.includes('export function up(input: User): string'); // true
source.includes('export default { up, down };'); // true