Write to the database
There are two ways to write to the database in a Ponder app.
- Store API: The recommended way to write to the database. 100-1000x faster than raw SQL.
- Raw SQL: A useful escape hatch for logic that's too complex for the Store API.
Store API
The Store API is a SQL-like query builder optimized for common indexing workloads.
The examples below use this ponder.schema.ts
to demonstrate the core concepts.
import { onchainTable, primaryKey } from "ponder";
export const accounts = onchainTable("accounts", (t) => ({
address: t.hex().primaryKey(),
balance: t.bigint().notNull(),
nickname: t.text(),
}));
export const allowances = onchainTable(
"allowances",
(t) => ({
owner: t.hex().notNull(),
spender: t.hex().notNull(),
value: t.bigint().notNull(),
}),
(table) => ({
pk: primaryKey({ columns: [table.owner, table.spender] }),
})
);
Insert
Insert one or many rows into the database. Returns the inserted rows, including any default values that were generated.
import { accounts } from "ponder:schema";
// Insert a single row
const row = await db.insert(accounts).values({
address: "0x7Df1",
balance: 0n
});
// Insert multiple rows
const rows = await db.insert(accounts).values([
{ address: "0x7Df2", balance: -50n },
{ address: "0x7Df3", balance: 100n },
]);
Errors
If you insert a row that's missing a required column value (not null constraint violation), insert
will reject with an error.
import { accounts } from "ponder:schema";
const row = await db.insert(accounts).values({
address: "0x7Df1",
});
// Error: Column "balance" is required but not present in the values object.
If you insert a duplicate row (unique constraint violation), insert
will reject with an error.
import { accounts } from "ponder:schema";
const row = await db.insert(accounts).values({
address: "0x7Df1",
});
// Error: Column "balance" is required but not present in the values object.
Use conflict resolution to ignore unique constraint violations with onConflictDoNothing
or achieve upsert behavior with onConflictDoUpdate
.
Find
Find a single row by primary key. Returns the row, or null
if not found.
The second argument is an object that specifies the primary key value to search for.
import { accounts } from "ponder:schema";
const row = await db.find(accounts, { address: "0x7Df1" });
If the table has a composite primary key, the object must include a value for each column in the primary key.
import { allowances } from "ponder:schema";
const row = await db.find(allowances, {
owner: "0x7Df1",
spender: "0x7Df2"
});
Update
Update a row by primary key. Returns the updated row.
import { accounts } from "ponder:schema";
const row = await db
.update(accounts, { address: "0x7Df1" })
.set({ balance: 100n });
You can also pass a function to set
, which receives the existing row and returns the update object.
import { accounts } from "ponder:schema";
const row = await db
.update(accounts, { address: "0x7Df1" })
.set((row) => ({ balance: row.balance + 100n }));
Errors
If the target row is not found, update
will reject with an error.
import { tokens } from "ponder:schema";
const row = await db
.update(accounts, { address: "0xa4F0" })
.set({ balance: 200n });
// Error: No row found for address "0xa4F0".
If the new row violates a not null constraint, update
will reject with an error.
import { tokens } from "ponder:schema";
const row = await db
.update(accounts, { address: "0x7Df1" })
.set({ balance: null });
// Error: Column "balance" is required but not present in the object.
Delete
Delete a row by primary key. Returns true
if the row was deleted, or false
if not found.
import { accounts } from "ponder:schema";
const deleted = await db.delete(accounts, { address: "0x7Df1" });
Conflict resolution
The insert
method supports conflict resolution.
onConflictDoNothing
Use onConflictDoNothing
to skip the insert if the specified row already exists. This avoids unique constraint violation errors.
import { accounts } from "ponder:schema";
const row = await db
.insert(accounts)
.values({ address: "0x7Df1", balance: 0n })
.onConflictDoNothing();
onConflictDoUpdate
Use onConflictDoUpdate
to achieve "upsert" behavior.
If the row does not exist, it will be inserted using the specified values
. Otherwise, the existing row will be updated with the values passed to onConflictDoUpdate
.
import { accounts } from "ponder:schema";
const row = await db
.insert(accounts)
.values({ address: "0x7Df1", balance: 0n })
.onConflictDoUpdate({ value: 200n });
Just like with update
, you can pass a function to onConflictDoUpdate
that receives the existing row and returns the update object.
import { accounts } from "ponder:schema";
const row = await db
.insert(accounts)
.values({ address: "0x7Df1", balance: 0n })
.onConflictDoUpdate((row) => ({
balance: row.balance + 50n
}));
Raw SQL
Query builder
Use db.sql
to access the raw Drizzle PostgreSQL query builder. This is useful for complex queries that join multiple tables or use advanced SQL features.
import { accounts, tradeEvents } from "ponder:schema";
import { eq, and, gte, inArray, sql } from "drizzle-orm";
// Add 100 points to accounts with recent trades
await db.sql
.update(accounts)
.set({ points: sql`${accounts.points} + 100` })
.where(
inArray(
accounts.address,
db.sql
.select({ address: tradeEvents.from })
.from(tradeEvents)
.where(
gte(tradeEvents.timestamp, event.block.timestamp - 24 * 60 * 60)
)
)
);
Relational queries
Use db.sql.query
to access Drizzle's relational query builder. This provides a type-safe way to write complex SELECT
queries that join multiple tables.
import { accounts, tradeEvents } from "ponder:schema";
// Find recent large trades with account details
const trades = await db.sql.query.tradeEvents.findMany({
where: (table, { gt, gte, and }) =>
and(
gt(table.amount, 1_000n),
gte(table.timestamp, Date.now() - 1000 * 60 * 60)
),
limit: 10,
with: { account: true },
});
Visit the Drizzle documentation for more details on writing raw SQL queries.