Advanced Features
Transactions, raw SQL, JSONB operations, window functions, incremental patching, and other advanced capabilities.
Transactions
Zanith provides a unified transaction context. Inside a transaction, you get the same API as the main client — model queries, raw SQL, everything in one atomic operation.
await db.transaction(async (tx) => { // Create user const user = await tx.user.create({ data: { email: '[email protected]', name: 'Alice' }, }); // Create related records await tx.post.create({ data: { title: 'First Post', authorId: user.id }, }); // Raw SQL inside same transaction await tx.raw`UPDATE counters SET n = n + 1 WHERE name = 'users'`; // If any operation throws, everything rolls back});Raw SQL
When you need full SQL control, use tagged template literals. Values are automatically parameterized — never interpolated into the SQL string.
// Values become $1, $2, etc.const users = await db.raw<{ id: string; email: string }>` SELECT id, email FROM users WHERE created_at > ${startDate} AND role = ${role} LIMIT ${limit}`; // users: Array<{ id: string, email: string }> // Complex queries that don't fit the builderconst report = await db.raw` WITH monthly AS ( SELECT DATE_TRUNC('month', created_at) as month, COUNT(*) as count FROM posts WHERE published = true GROUP BY 1 ) SELECT * FROM monthly ORDER BY month DESC`;JSONB operations
PostgreSQL's JSONB is first-class in Zanith. Access nested values, check containment, and test key existence — all through typed field references.
// Text extraction: metadata->>'type' = 'article'.where(({ post }) => post.metadata.jsonText('type').eq('article')) // JSON path access: metadata->'config' (returns JSON, not text).where(({ post }) => post.metadata.jsonPath('config')) // Containment: metadata @> '{"featured": true}'::jsonb.where(({ post }) => post.metadata.jsonContains({ featured: true })) // Key existence: metadata ? 'tags'.where(({ post }) => post.metadata.jsonHasKey('tags'))Incremental schema patching
Instead of rebuilding the entire schema graph when a model changes, Zanith patches only the affected parts. This takes 0.7ms for a single model change.
import { applyPatch } from 'zanith'; // Original graphconst graph = compileSchema([User, Post], [Role]); // Patch: add a field to User without rebuilding Postconst patched = applyPatch(graph, { upsertModels: [updatedUserModel],}); // patched is a NEW graph — original is not mutated// Post model is untouchedAvailable patch operations:
| Operation | Effect |
|---|---|
upsertModels | Add or replace models |
removeModels | Remove models (and their relations) |
upsertEnums | Add or replace enums |
removeEnums | Remove enums |
addRelations | Add new relation edges |
removeRelations | Remove specific relations |
Error handling
Zanith normalizes PostgreSQL errors into structured, actionable types.
import { UniqueConstraintError, ForeignKeyError, NotNullError } from 'zanith'; try { await db.user.create({ data: { email: '[email protected]' } });} catch (err) { if (err instanceof UniqueConstraintError) { // err.constraint — constraint name // err.fields — ['email'] console.log(`Duplicate: ${err.fields.join(', ')}`); } if (err instanceof ForeignKeyError) { // err.constraint, err.detail } if (err instanceof NotNullError) { // err.column }}Query logging
Observe every query Zanith executes for debugging and monitoring.
// The logger interfaceinterface QueryEvent { sql: string; params: unknown[]; durationMs: number; rowCount: number; error?: Error; timestamp: Date;} // Parameter masking for safe loggingimport { maskParams, formatQueryLog } from 'zanith'; const event = { sql: 'SELECT ...', params: ['secret'], durationMs: 5, rowCount: 10 };console.log(formatQueryLog(event));// [Zanith] OK 5ms | SELECT ... | params: [sec***ret] | rows: 10Database adapters
Zanith supports two PostgreSQL drivers. Both are optional peer dependencies.
node-postgres (pg)
import { PgAdapter } from 'zanith/adapters/pg'; const adapter = new PgAdapter({ connectionString: 'postgresql://user:pass@localhost:5432/mydb', // OR individual fields: host: 'localhost', port: 5432, database: 'mydb', user: 'myuser', password: 'mypass', pool: { max: 20 },});postgres.js
import { PostgresJsAdapter } from 'zanith/adapters/postgres'; const adapter = new PostgresJsAdapter({ connectionString: 'postgresql://user:pass@localhost:5432/mydb',});