Prepared Query
Use prequery() to create reusable, type-safe prepared statements that can be executed multiple times with different parameter values. Prepared queries compile the SQL once and reuse the compiled query for better performance.
Basic Usage
Creating a Prepared Query
import { prequery, eq } from "durcno";
import { db } from "./db/index.ts";
import { Users } from "./db/schema.ts";
// Create a prepared query with a username parameter
const findUserByUsername = prequery(
{ username: Users.username.arg() },
(args) => {
return db
.prepare()
.from(Users)
.select()
.where(eq(Users.username, args.username));
},
);
Executing a Prepared Query
Use .run() to execute the prepared query with specific values:
// Execute with type-safe parameters
const users = await findUserByUsername.run(db, { username: "john" });
// Type: { id: bigint; username: string; email: string | null; type: "admin" | "user"; createdAt: Date }[]
Prepared queries does not provide runtime validation of argument type. Make sure to validate the argument values before calling .run().
How It Works
- Define arguments: Create argument placeholders using
Column.arg()method - Build the query: Use
db.prepare().from(Table)to build the query structure - Wrap with prequery: Pass arguments and query builder to
prequery() - Execute: Call
.run(db, values)to execute with specific values
Multiple Arguments
You can define multiple arguments in a single prepared query:
import { prequery, and, eq } from "durcno";
import { db } from "./db/index.ts";
import { Users } from "./db/schema.ts";
const findUser = prequery(
{
username: Users.username.arg(),
email: Users.email.arg(),
type: Users.type.arg(),
},
(args) => {
return db
.prepare()
.from(Users)
.select()
.where(
and(
eq(Users.username, args.username),
eq(Users.email, args.email),
eq(Users.type, args.type),
),
);
},
);
// Execute with all required parameters
const result = await findUser.run(db, {
username: "john",
email: "john@example.com",
type: "admin",
});
Selective Column Selection
You can select specific columns in your prepared query:
const findUserInfo = prequery({ id: Users.id.arg() }, (args) => {
return db
.prepare()
.from(Users)
.select({ username: Users.username, email: Users.email })
.where(eq(Users.id, args.id));
});
const result = await findUserInfo.run(db, { id: 1n });
// Type: { username: string; email: string | null }[]
Complex Conditions
OR Conditions
import { prequery, or, eq } from "durcno";
const findByEitherUsername = prequery(
{
username1: Users.username.arg(),
username2: Users.username.arg(),
},
(args) => {
return db
.prepare()
.from(Users)
.select({ id: Users.id, username: Users.username })
.where(
or(
eq(Users.username, args.username1),
eq(Users.username, args.username2),
),
);
},
);
const result = await findByEitherUsername.run(db, {
username1: "john",
username2: "jane",
});
// Type: { id: bigint; username: string }[]
Combined AND/OR Conditions
import { prequery, and, or, eq } from "durcno";
const complexQuery = prequery(
{
username: Users.username.arg(),
type1: Users.type.arg(),
type2: Users.type.arg(),
},
(args) => {
return db
.prepare()
.from(Users)
.select({ username: Users.username, type: Users.type })
.where(
and(
eq(Users.username, args.username),
or(eq(Users.type, args.type1), eq(Users.type, args.type2)),
),
);
},
);
const result = await complexQuery.run(db, {
username: "john",
type1: "admin",
type2: "user",
});
// Type: { username: string; type: "admin" | "user" }[]
Working with Different Column Types
Numeric Arguments
import { prequery, eq } from "durcno";
import { Posts } from "./db/schema.ts";
const findPostsByUser = prequery({ userId: Posts.userId.arg() }, (args) => {
return db.prepare().from(Posts).select().where(eq(Posts.userId, args.userId));
});
const posts = await findPostsByUser.run(db, { userId: 1n });
Enum Arguments
const findByUserType = prequery({ userType: Users.type.arg() }, (args) => {
return db
.prepare()
.from(Users)
.select({ id: Users.id, type: Users.type })
.where(eq(Users.type, args.userType));
});
// TypeScript ensures only valid enum values can be passed
const admins = await findByUserType.run(db, { userType: "admin" });
// Type: { id: bigint; type: "admin" | "user" }[]
Date/Timestamp Arguments
const findUsersByDate = prequery(
{ createdAt: Users.createdAt.arg() },
(args) => {
return db
.prepare()
.from(Users)
.select({ id: Users.id, createdAt: Users.createdAt })
.where(eq(Users.createdAt, args.createdAt));
},
);
const result = await findUsersByDate.run(db, { createdAt: new Date() });
// Type: { id: bigint; createdAt: Date }[]
Type Safety
Prepared queries provide full type safety:
- Parameter types: Arguments are typed based on the column type
- Return types: Results are inferred from the selected columns
- Enum validation: Only valid enum values can be passed as arguments
const findUser = prequery({ id: Users.id.arg() }, (args) => {
return db
.prepare()
.from(Users)
.select({ username: Users.username })
.where(eq(Users.id, args.id));
});
// ✅ Correct: id is a bigint
await findUser.run(db, { id: 1n });
// ❌ TypeScript error: id should be bigint, not string
await findUser.run(db, { id: "1" });
Parameterized LIMIT and OFFSET
You can parameterize limit and offset values using Arg.number() and Arg.bigint(). This is useful when building paginated prepared queries where the page size or offset varies at runtime.
import { prequery, Arg } from "durcno";
import { db } from "./db/index.ts";
import { Users } from "./db/schema.ts";
const paginatedUsers = prequery(
{
lim: Arg.number(),
off: Arg.number(),
},
(args) => {
return db.prepare().from(Users).select().limit(args.lim).offset(args.off);
},
);
// Execute with different page sizes and offsets
const page1 = await paginatedUsers.run(db, { lim: 10, off: 0 });
const page2 = await paginatedUsers.run(db, { lim: 10, off: 10 });
Use Arg.bigint() when your limit or offset values may exceed the safe integer range:
const paginatedUsers = prequery(
{
lim: Arg.bigint(),
off: Arg.bigint(),
},
(args) => {
return db.prepare().from(Users).select().limit(args.lim).offset(args.off);
},
);
const page = await paginatedUsers.run(db, { lim: 10n, off: 20n });
Benefits of Prepared Queries
- Performance: SQL is parsed and planned once, then reused
- Type Safety: Full TypeScript inference for parameters and results
- Reusability: Define once, execute many times with different values
API Reference
prequery(args, statement)
Creates a prepared statement.
| Parameter | Type | Description |
|---|---|---|
args | Record<string, Arg> | Object mapping argument names to column arguments |
statement | (args) => QueryBuilder | Function that builds the query using the provided arguments |
Column.arg()
Creates an argument placeholder for a column, used in prepared queries.
const arg = Users.username.arg();
Arg.number()
Creates an argument placeholder that accepts a JS number. Use with .limit() and .offset() in prepared queries.
const arg = Arg.number();
Arg.bigint()
Creates an argument placeholder that accepts a JS bigint. Use with .limit() and .offset() in prepared queries when values may exceed the safe integer range.
const arg = Arg.bigint();
PreparedStatement.run(db, values)
Executes the prepared statement with the provided values.
| Parameter | Type | Description |
|---|---|---|
db | DB | Transaction | Database instance or transaction instance |
values | Record<string, T> | Object with values matching the argument types |
Returns a Promise that resolves to the query results.