The page before a cursor is bounded in SQL, beside the LIMIT

A client pages a pool's history as "the N trades before this time". The bound
was applied by FilterResults after the database had already taken the newest N,
so every page after the first threw its rows away and came back empty. It sits
in the WHERE now, on the same index the pool does.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-16 12:40:39 -07:00
co-authored by Hanzo Dev
parent 7c5736ac42
commit dfb9c57fc3
2 changed files with 55 additions and 2 deletions
+28
View File
@@ -4,6 +4,7 @@ package storage
import (
"context"
"fmt"
"strings"
"testing"
)
@@ -59,3 +60,30 @@ func TestPoolHistoryReadsTheIndex(t *testing.T) {
t.Fatalf("pool history still scans or sorts: %s", joined)
}
}
// The page before a cursor is the older trades, not the newest ones thrown away.
func TestPageBeforeCursorReachesOlderTrades(t *testing.T) {
s, err := New(t.TempDir())
if err != nil {
t.Fatalf("open store: %v", err)
}
defer s.Close()
if err := s.Init(context.Background()); err != nil {
t.Fatalf("init: %v", err)
}
for i := 1; i <= 6; i++ {
s.SeedSwap(fmt.Sprintf("s%d", i), &SeedSwapData{Timestamp: int64(i * 100), Pool: "0xpool", AmountUSD: "1"})
}
got, err := s.GetSwaps(context.Background(), 2, "timestamp", "desc", map[string]interface{}{"pool": "0xPOOL", "timestamp_lt": float64(400)})
if err != nil {
t.Fatalf("GetSwaps: %v", err)
}
rows := got.([]interface{})
if len(rows) != 2 {
t.Fatalf("want the 2 trades before t=400, got %d", len(rows))
}
ids := []string{rows[0].(map[string]interface{})["id"].(string), rows[1].(map[string]interface{})["id"].(string)}
if ids[0] != "s3" || ids[1] != "s2" {
t.Fatalf("want s3, s2 (newest first, before the cursor); got %v", ids)
}
}
+27 -2
View File
@@ -747,11 +747,22 @@ func (s *Store) GetSwaps(_ context.Context, limit int, orderBy, orderDirection s
// 1000 newest chain-wide, kept the handful that matched, and usually
// answered with none at all. The pool column is indexed for exactly this.
args := []interface{}{}
query := "SELECT id, data FROM swaps"
conds := []string{}
if pool, ok := whereString(where, "pool"); ok {
query += " WHERE pool = ? COLLATE NOCASE"
conds = append(conds, "pool = ? COLLATE NOCASE")
args = append(args, pool)
}
// A page of history is asked for as "the N before this time", and that bound
// has to sit beside the LIMIT for the same reason the pool does: applied
// after it, the query returns the newest N and then throws them all away.
if before, ok := whereNumber(where, "timestamp_lt"); ok {
conds = append(conds, "timestamp < ?")
args = append(args, before)
}
query := "SELECT id, data FROM swaps"
if len(conds) > 0 {
query += " WHERE " + strings.Join(conds, " AND ")
}
if orderBy == "timestamp" {
if strings.EqualFold(orderDirection, "desc") {
query += " ORDER BY timestamp DESC"
@@ -813,6 +824,20 @@ func whereString(where map[string]interface{}, key string) (string, bool) {
return s, true
}
// whereNumber reads a numeric bound out of a `where` map, however the client
// wrote it — GraphQL variables arrive as float64, literals may arrive as strings.
func whereNumber(where map[string]interface{}, key string) (float64, bool) {
v, ok := where[key]
if !ok || v == nil {
return 0, false
}
f, err := strconv.ParseFloat(fmt.Sprint(v), 64)
if err != nil {
return 0, false
}
return f, true
}
func (s *Store) swapToMap(id string, sw *SeedSwapData) map[string]interface{} {
var pool interface{} = sw.Pool
if p, _ := s.loadPool(sw.Pool); p != nil {