Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/DATABASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@ PostgreSQL via Prisma. Full source of truth: [`prisma/schema.prisma`](../prisma/

`blockchain_checkpoints` (one row per `(contractName, network)`) and `blockchain_events` (append-only raw event log, unique on `(contractName, network, rpcEventId)` — the Soroban RPC's own globally-unique event id) are the durability layer described in `ARCHITECTURE.md` §6. `blockchain_events` is intentionally kept even after a module has processed an event — it's the replay/audit source if a module's read-model logic needs to be rebuilt.

## Read-Path Indexes

Several ordered list / aggregation reads filter on one column and sort on
another (or group by one column). Each of those reads is backed by a
composite index matching its exact filter-and-sort (or group-by) shape, so
the planner resolves the predicate and the order from a single index instead
of sorting the matched rows in a second pass:

| Query shape (module) | Composite index |
|---|---|
| `notifications.listByUserId` — `WHERE user_id = ? AND status = ? … ORDER BY created_at DESC` (`notifications`) | `notifications(user_id, created_at desc)` |
| `auditLogs.list` — `ORDER BY created_at DESC` (unbounded append-only log) (`admin`) | `audit_logs(created_at desc)` |
| `DeliveryRepository.list` — `WHERE status = ? … ORDER BY created_at_chain DESC` (`deliveries`) | `deliveries(status, created_at_chain desc)` |
| `analytics.getGmvByToken` — `GROUP BY token` for `status = 'RELEASED'` (`analytics`) | `escrows(status, token)` |
| `analytics.getDriverTierCounts` — `GROUP BY tier` (`analytics`) | `driver_profiles(tier)` |

See `prisma/schema.prisma` for the authoritative definitions (each carries a
header comment naming the exact read it backs) and `API_REFERENCE.md` §
/notifications + §/analytics for the endpoint-level detail. When a new
composite's leading column fully covers an existing single-column index
(i.e. every predicate that could use the single-column index is served by
that composite as a prefix), the single-column index is dropped in the same
migration rather than kept — that's what removed the former `deliveries_status
_idx`, which was fully covered by `deliveries(status, created_at_chain desc)`
and would otherwise carry duplicate write/maintenance cost for no read benefit.
`deliveries`'s `sender_address`, `recipient_address`, and `driver_address`
indexes are kept because they are filter-only columns with no composite
covering them.

## Migrations

```bash
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- DropIndex
DROP INDEX "deliveries_status_idx";

-- CreateIndex
CREATE INDEX "deliveries_status_created_at_chain_idx" ON "deliveries"("status", "created_at_chain" DESC);
9 changes: 8 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,17 @@ model Delivery {
escrow Escrow?
dispute Dispute?

/// Backs `deliveryRepository.list`'s filter-and-sort shape
/// (`where: { …, status? }`, `orderBy: { createdAtChain: 'desc' }` in
/// `src/modules/deliveries/infrastructure/prisma-delivery-repository.ts`)
/// so the planner can satisfy the predicate and the sort from one index
/// instead of sorting matched rows. `status` alone is covered by this
/// composite's leading column, so the former single-column
/// `@@index([status])` was dropped rather than kept for both.
@@index([senderAddress])
@@index([recipientAddress])
@@index([driverAddress])
@@index([status])
@@index([status, createdAtChain(sort: Desc)])
@@map("deliveries")
}

Expand Down
Loading