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