Skip to content

Raw SQL & RLS Gate

When the builder can’t express what you need, selectExpr and whereExpr let you drop to raw SQL. These hatches are guarded against injection.

Add a raw expression to the SELECT list:

const rows = await db
.from(users)
.selectExpr("COUNT(*)", [], "cnt")
.selectExpr("MAX(age)", [], "max_age")
.select();

The third argument is the output alias.

Add a raw fragment to the WHERE clause. Bind values as params — never interpolate them:

const rows = await db
.from(users)
.whereExpr("EXISTS (SELECT 1 FROM posts WHERE posts.user_id = users.id)")
.select();

selectExpr / whereExpr reject any expression containing ;, --, or /* at build time. Those are the patterns that enable SQL injection (statement termination, line comment, block comment). A rejected expression throws MountError("VALIDATION", ...).

When a Driver is in enforceRls mode with a registry protecting a table, the builder throws MountError("FORBIDDEN") if you query that table without applying a policy.

import { applyPolicy, allowOwner } from "@mountsqli/auth";
const scoped = applyPolicy(db.from(files), allowOwner("ownerId"), { userId });
await scoped.select(); // safe — policy injected as WHERE

.unsafe() tells the gate you accept the risk:

await db.from(files).unsafe().select(); // bypasses RLS enforcement

withFilters(FilterNode[]) injects compiled policy filters without going through applyPolicy:

const scoped = db.from(files).withFilters(applyPolicy(policy, ctx));

A guarded report query with a raw aggregate, under RLS:

export async function userReport(userId: string) {
return applyPolicy(db.from(files), allowOwner("ownerId"), { userId })
.selectExpr("COUNT(*)", [], "fileCount")
.select();
}
  • Prefer builder methods; use raw SQL only when necessary.
  • Bind every dynamic value as a param in whereExpr.
  • Apply RLS policies at the edge, not per handler.
  • Putting ; in a raw expression — the guard rejects it.
  • Querying a protected table in enforce mode without applyPolicy or .unsafe().