Are you working with @forge/sql and struggling with pagination + total count?

Here's a more efficient and safer way to do it using window functions.


💡 The problem

In typical pagination, we often do two queries:

SELECT * FROM users LIMIT 10 OFFSET 20;
SELECT COUNT(*) FROM users;

But if your SQL engine supports window functions — like TiDB in Atlassian Forge — you can get everything in a single query:

SELECT 
  id,
  name,
  COUNT(*) OVER() AS total_count
FROM users
ORDER BY name
LIMIT 10 OFFSET 20;

You’ll get a total_count in each row — and no second query needed.


✅ Using it with forge-sql-orm

import { sql } from "drizzle-orm";
import ForgeSQL from "forge-sql-orm";
import { users } from "./schema";

const forgeSQL = new ForgeSQL();

const result = await forgeSQL
  .select({
    id: users.id,
    name: users.name,
    totalCount: sql<number>`COUNT(*) OVER()`,
  })
  .from(users)
  .orderBy(users.name)
  .offset(formatLimitOffset(offset))
  .limit(formatLimitOffset(limit));

And here’s a safe helper for LIMIT/OFFSET:

export function formatLimitOffset(limitOrOffset: number): number {
  if (typeof limitOrOffset !== "number" || isNaN(limitOrOffset)) {
    throw new Error("limitOrOffset must be a valid number");
  }
  return sql.raw(`${limitOrOffset}`) as unknown as number;
}

⚠️ Why this matters — Forge SQL doesn’t support bind params for LIMIT/OFFSET

Libraries like Drizzle normally compile queries like:

SELECT ... FROM users ORDER BY name LIMIT ? OFFSET ?

But Forge SQL (powered by TiDB) doesn’t support parameterized LIMIT or OFFSET.

So you have to inline the values — carefully.


🔐 And yes, you can still get SQL injection in .prepare(...)

Even this Forge-native code is vulnerable, if limit or offset come from user input:

const result = await sql
  .prepare<User>(`SELECT * FROM users LIMIT ${limit} OFFSET ${offset}`)
  .execute();

Just because limit: number in TypeScript doesn’t mean it’s actually a number at runtime.

✅ Use runtime checks:

function safeLimitOffset(value: unknown): number {
  if (typeof value !== "number" || isNaN(value)) {
    throw new Error("Invalid limit or offset");
  }
  return value;
}

✅ One query, safer code, better performance

This pattern works in Atlassian Forge SQL, MySQL, TiDB, PlanetScale, and more.

If you're using @forge/sql, forge-sql-orm, or raw Drizzle — this will help keep things clean and fast.

🧑‍💻 Project: forge-sql-orm

Questions and feedback welcome!