Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/iterate/sqlfu/llms.txt

Use this file to discover all available pages before exploring further.

Every .sql file you check in becomes a generated wrapper whose emitted SqlQuery carries a name field — the camelCase function name matching the symbol you import. A single instrument(client, ...hooks) call routes that name to OpenTelemetry spans, Sentry errors, PostHog events, and Datadog metrics. There are no peer dependencies: hooks are structurally typed so you bring your own SDK.

The name field

For sql/list-profiles.sql the generated file emits:
sql/.generated/list-profiles.sql.ts (generated — do not edit)
const query: SqlQuery = {sql: ListProfilesSql, args: [], name: 'listProfiles'};
Nested directories fold into the same camelCase: sql/users/list-profiles.sqlname: 'usersListProfiles', also the exported function name. Distinct file paths produce distinct names so collisions are impossible. Ad-hoc SQL via client.sql`...` has no name, but you can pass one explicitly:
client.run({sql: 'select 1', args: [], name: 'healthCheck'});

instrument helper

import {instrument} from 'sqlfu';

const client = instrument(
  baseClient,
  instrument.otel({tracer: myOtelTracer}),
  instrument.onError(({context, error}) => myErrorReportingService.report(error)),
);
instrument(client, ...hooks) wraps a client so every all / run call flows through the hooks in order. Sync clients take sync hooks; async clients take async hooks:
type SyncQueryExecutionHook = <TResult>(args: {
  context: QueryExecutionContext;   // {query, operation, system}
  execute: () => TResult;           // call the next hook / the underlying adapter
}) => TResult;

type AsyncQueryExecutionHook = <TResult>(args: {
  context: QueryExecutionContext;
  execute: () => Promise<TResult>;
}) => Promise<TResult>;
For a sync client, use normal try / catch. For an async client, make the hook async and await execute() inside the same shape. Helper hooks that work with either client kind return a paired QueryExecutionHook:
type QueryExecutionHook = {
  sync: SyncQueryExecutionHook;
  async: AsyncQueryExecutionHook;
};
instrument.otel and instrument.onError are reference implementations. QueryExecutionHook is the stable contract. Copy their bodies and edit them if your team has different conventions.

instrument.otel({tracer})

Emits one OTel span per query with:
  • db.query.summary: your query.name, when present
  • db.query.text: the parameterized SQL (values are in args, not interpolated into the text)
  • db.system.name: the adapter’s system (e.g. sqlite)
On throw, the hook records the exception as a span event and sets span status to ERROR. The tracer parameter is typed structurally (TracerLike), so there is no peer dependency on @opentelemetry/api. Pass any object with a startActiveSpan(name, fn) method. The real OTel Tracer satisfies this by construction.

instrument.onError(report)

Calls report({context, error}) whenever a query throws or its promise rejects, then always rethrows. Errors in the reporter itself are swallowed so they cannot mask the original error:
instrument.onError(({context, error}) => {
  console.error(`query ${context.query.name || 'sql'} failed:`, error);
});
Every driver error is a SqlfuError with a normalized .kind discriminator (unique_violation, missing_table, syntax, and so on). That makes .kind a natural bucketing dimension in your error reporter:
tags: {'db.error.kind': error.kind}

Recipes

Works with any OTLP backend: Honeycomb, Grafana Tempo, New Relic, Datadog APM, or self-hosted Jaeger. Swap the exporter URL for the target backend; the instrument.otel line stays identical.
import {trace} from '@opentelemetry/api';
import {instrument} from 'sqlfu';

const tracer = trace.getTracer('my-service');
const client = instrument(baseClient, instrument.otel({tracer}));
For Datadog APM, either point @opentelemetry/exporter-trace-otlp-http at Datadog’s OTLP intake, or pass dd-trace’s OTel-compatible tracer directly. Either way the hook call is the same.

Caveats

client.raw(sql) is not uniquely identified. raw interpolates values into the SQL text, so per-call distinctness depends on parameter values rather than a stable name. If you need named observability on dynamic SQL, assemble a SqlQuery directly:
client.run({sql, args, name: 'myQuery'});
iterate and transaction pass through unchanged. Queries issued inside a transaction still fire hooks because the transaction client is re-instrumented on entry. Transactions themselves do not get their own spans. If you want transaction-level spans, wrap client.transaction(...) calls yourself using your tracer. Composition order is outer-to-inner. instrument(client, a, b, c) means a wraps b wraps c wraps the underlying call. If you put instrument.otel first, the OTel span covers everything including any error-reporter work. You can nest instrument calls if you prefer a different composition order:
const myClient = instrument(instrument(baseClient, innerHook), outerHook);

Types

All exported from sqlfu:
  • instrument — callable, plus .otel and .onError
  • SyncQueryExecutionHook, SyncQueryExecutionHookArgs
  • AsyncQueryExecutionHook, AsyncQueryExecutionHookArgs
  • QueryExecutionHook, QueryExecutionHookArgs, QueryExecutionContext, QueryOperation
  • QueryErrorReport
  • TracerLike, SpanLike

Build docs developers (and LLMs) love