SQL views
Declare a read-only SQL VIEW with view(name, columns, select) — discovered + applied by the migrator, queried by-name like a table.
A view is a named, server-side SELECT you query by name exactly like a table, but which is never written to. Declare one with view(name, columns, select): columns describes the projected row shape (types the result + drives the read decoder), and select is the raw SELECT body emitted verbatim into CREATE VIEW.
import { view, id, text, boolean, timestamp } from '@voltro/database'
export const activeUsers = view(
'active_users',
{
id: id(),
email: text(),
active: boolean(),
createdAt: timestamp(),
},
`SELECT id, email, active, created_at AS "createdAt"
FROM users
WHERE deleted_at IS NULL`,
)The migrator discovers the view alongside your tables and emits it after the base tables it reads from — no ordering wiring needed. Pass it into the schema entity list the same way you pass tables.
Querying a view
A view is read-only. Query it with queryForView(...), which returns a query with the full .where(...) / .orderBy(...) / .take(...) / .with(...) surface but no mutation path (there's no INSERT/UPDATE/DELETE on a view).
import { queryForView, eq } from '@voltro/database'
const rows = await ctx.store.query(
queryForView(activeUsers).where(eq('email', someEmail)),
)The projected columns are decoded to their canonical JS shapes — a boolean() projection comes back a real boolean, a json<T>() projection a parsed object, a decimal() projection a string — the same read codec that runs for tables.
Idempotent per dialect
CREATE VIEW is emitted idempotently so re-running a migration is a no-op:
| Dialect | Emission |
|---|---|
| Postgres / MySQL / MariaDB | CREATE OR REPLACE VIEW |
| MSSQL | CREATE OR ALTER VIEW (SQL Server 2016 SP1+) |
| SQLite / Turso | DROP VIEW IF EXISTS + CREATE VIEW (no CREATE OR REPLACE; a view holds no data, so dropping is free) |
When to use a view
- Collapse a recurring filter/join into a named entity your handlers query directly (
active_users,open_orders). - Expose a stable read shape while the underlying tables evolve.
- Hand a reporting/read path a denormalized projection without duplicating the join logic in every query.
You own the SELECT body's cross-dialect portability — the framework emits it verbatim, the same contract as a raw() column or an expressionIndex(...) expression. Keep to standard SQL, or gate dialect-specific views behind your deployment's known backend.