From f11a2ba836aff4c58921456281b07e92d295da55 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 19 Mar 2026 13:37:28 +0100 Subject: [PATCH] modify sql --- .../src/api/handlers/transactions.rs | 104 +++++++++++++++--- ...240109000001_cursor_pagination_indexes.sql | 5 + frontend/src/api/transactions.ts | 9 +- frontend/src/pages/TransactionsPage.tsx | 45 ++++++-- 4 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 backend/migrations/20240109000001_cursor_pagination_indexes.sql diff --git a/backend/crates/atlas-server/src/api/handlers/transactions.rs b/backend/crates/atlas-server/src/api/handlers/transactions.rs index 6c1270f..8ad96b6 100644 --- a/backend/crates/atlas-server/src/api/handlers/transactions.rs +++ b/backend/crates/atlas-server/src/api/handlers/transactions.rs @@ -2,6 +2,7 @@ use axum::{ extract::{Path, Query, State}, Json, }; +use serde::Deserialize; use std::sync::Arc; use super::get_table_count; @@ -11,28 +12,101 @@ use atlas_common::{ AtlasError, Erc20Transfer, NftTransfer, PaginatedResponse, Pagination, Transaction, }; +/// Query parameters for the transaction list endpoint. +/// Supports cursor-based pagination via (block_number, block_index) for O(log N) +/// seeks on large partitioned tables, replacing O(N) OFFSET scans. +#[derive(Debug, Deserialize)] +pub struct TransactionListParams { + #[serde(default = "default_page")] + pub page: u32, + #[serde(default = "default_limit")] + pub limit: u32, + /// Fetch transactions older than this (block_number, block_index). + pub before_block: Option, + pub before_index: Option, + /// Fetch transactions newer than this (block_number, block_index). + pub after_block: Option, + pub after_index: Option, + /// When true, fetch the oldest page of transactions. + #[serde(default)] + pub last_page: bool, +} + +fn default_page() -> u32 { + 1 +} +fn default_limit() -> u32 { + 20 +} + pub async fn list_transactions( State(state): State>, - Query(pagination): Query, + Query(params): Query, ) -> ApiResult>> { - // Use optimized count (approximate for large tables, exact for small) let total = get_table_count(&state.pool, "transactions").await?; - - let transactions: Vec = sqlx::query_as( - "SELECT hash, block_number, block_index, from_address, to_address, value, gas_price, gas_used, input_data, status, contract_created, timestamp - FROM transactions - ORDER BY block_number DESC, block_index DESC - LIMIT $1 OFFSET $2" - ) - .bind(pagination.limit()) - .bind(pagination.offset()) - .fetch_all(&state.pool) - .await?; + let limit = params.limit.min(100) as i64; + + let transactions: Vec = + if let (Some(bb), Some(bi)) = (params.before_block, params.before_index) { + // Next page: transactions older than cursor + sqlx::query_as( + "SELECT hash, block_number, block_index, from_address, to_address, value, gas_price, gas_used, input_data, status, contract_created, timestamp + FROM transactions + WHERE (block_number, block_index) < ($1, $2) + ORDER BY block_number DESC, block_index DESC + LIMIT $3", + ) + .bind(bb) + .bind(bi) + .bind(limit) + .fetch_all(&state.pool) + .await? + } else if let (Some(ab), Some(ai)) = (params.after_block, params.after_index) { + // Prev page: transactions newer than cursor (fetch ASC, reverse) + let mut txs: Vec = sqlx::query_as( + "SELECT hash, block_number, block_index, from_address, to_address, value, gas_price, gas_used, input_data, status, contract_created, timestamp + FROM transactions + WHERE (block_number, block_index) > ($1, $2) + ORDER BY block_number ASC, block_index ASC + LIMIT $3", + ) + .bind(ab) + .bind(ai) + .bind(limit) + .fetch_all(&state.pool) + .await?; + txs.reverse(); + txs + } else if params.last_page { + // Last page: oldest transactions (fetch ASC, reverse) + let mut txs: Vec = sqlx::query_as( + "SELECT hash, block_number, block_index, from_address, to_address, value, gas_price, gas_used, input_data, status, contract_created, timestamp + FROM transactions + ORDER BY block_number ASC, block_index ASC + LIMIT $1", + ) + .bind(limit) + .fetch_all(&state.pool) + .await?; + txs.reverse(); + txs + } else { + // First page (default): newest transactions + sqlx::query_as( + "SELECT hash, block_number, block_index, from_address, to_address, value, gas_price, gas_used, input_data, status, contract_created, timestamp + FROM transactions + ORDER BY block_number DESC, block_index DESC + LIMIT $1", + ) + .bind(limit) + .fetch_all(&state.pool) + .await? + }; Ok(Json(PaginatedResponse::new( transactions, - pagination.page, - pagination.limit, + params.page, + params.limit, total, ))) } diff --git a/backend/migrations/20240109000001_cursor_pagination_indexes.sql b/backend/migrations/20240109000001_cursor_pagination_indexes.sql new file mode 100644 index 0000000..b1de4b0 --- /dev/null +++ b/backend/migrations/20240109000001_cursor_pagination_indexes.sql @@ -0,0 +1,5 @@ +-- Composite index for cursor-based (keyset) pagination on transactions. +-- Enables O(log N) index seeks instead of O(N) OFFSET scans for the +-- global transaction listing endpoint. +CREATE INDEX IF NOT EXISTS idx_transactions_block_idx +ON transactions(block_number DESC, block_index DESC); diff --git a/frontend/src/api/transactions.ts b/frontend/src/api/transactions.ts index 4034b8d..4767b24 100644 --- a/frontend/src/api/transactions.ts +++ b/frontend/src/api/transactions.ts @@ -6,12 +6,17 @@ export interface GetTransactionsParams { limit?: number; block_number?: number; address?: string; + before_block?: number; + before_index?: number; + after_block?: number; + after_index?: number; + last_page?: boolean; } export async function getTransactions(params: GetTransactionsParams = {}): Promise> { - const { page = 1, limit = 20, block_number, address } = params; + const { page = 1, limit = 20, block_number, address, before_block, before_index, after_block, after_index, last_page } = params; return client.get>('/transactions', { - params: { page, limit, block_number, address }, + params: { page, limit, block_number, address, before_block, before_index, after_block, after_index, last_page: last_page || undefined }, }); } diff --git a/frontend/src/pages/TransactionsPage.tsx b/frontend/src/pages/TransactionsPage.tsx index cad6beb..a14e885 100644 --- a/frontend/src/pages/TransactionsPage.tsx +++ b/frontend/src/pages/TransactionsPage.tsx @@ -7,6 +7,13 @@ import { useEthPrice } from '../hooks'; export default function TransactionsPage() { const [page, setPage] = useState(1); + const [cursor, setCursor] = useState<{ + beforeBlock?: number; + beforeIndex?: number; + afterBlock?: number; + afterIndex?: number; + lastPage?: boolean; + }>({}); const [autoRefresh, setAutoRefresh] = useState(() => { try { const v = localStorage.getItem('txs:autoRefresh'); @@ -16,7 +23,15 @@ export default function TransactionsPage() { } }); const [, setTick] = useState(0); - const { transactions, pagination, refetch, loading } = useTransactions({ page, limit: 20 }); + const { transactions, pagination, refetch, loading } = useTransactions({ + page, + limit: 20, + before_block: cursor.beforeBlock, + before_index: cursor.beforeIndex, + after_block: cursor.afterBlock, + after_index: cursor.afterIndex, + last_page: cursor.lastPage, + }); const [hasLoaded, setHasLoaded] = useState(false); useEffect(() => { if (!loading) setHasLoaded(true); @@ -130,6 +145,22 @@ export default function TransactionsPage() { } }; + // Cursor-based navigation: derive cursors from server-ordered data + const goFirst = () => { setCursor({}); setPage(1); }; + const goPrev = () => { + const f = transactions[0]; + if (f) setCursor({ afterBlock: f.block_number, afterIndex: f.block_index }); + setPage((p) => Math.max(1, p - 1)); + }; + const goNext = () => { + const l = transactions[transactions.length - 1]; + if (l) setCursor({ beforeBlock: l.block_number, beforeIndex: l.block_index }); + setPage((p) => p + 1); + }; + const goLast = () => { + if (pagination) { setCursor({ lastPage: true }); setPage(pagination.total_pages); } + }; + return (
@@ -321,7 +352,7 @@ export default function TransactionsPage() {