Skip to content

Commit 67c9756

Browse files
committed
fix(pagination): refactor sink status queries to improve pagination predictability
1 parent 60da22a commit 67c9756

File tree

2 files changed

+49
-24
lines changed

2 files changed

+49
-24
lines changed

sc_audit/views/sink_status.py

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,40 @@ def view_sinking_txs(
2727
limit: int | None = None,
2828
order: Literal['asc', 'desc'] = 'desc',
2929
) -> pd.DataFrame:
30-
stx_query = construct_stx_query(for_funder, for_recipient, from_date, before_date, finalized)
31-
30+
base_query = construct_stx_query(
31+
for_funder, for_recipient, from_date, before_date, finalized
32+
).subquery()
33+
subq_paging_token = base_query.c.paging_token
34+
# Determine order and cursor filter
3235
if order == 'asc':
33-
stx_query = stx_query.order_by(SinkingTx.paging_token.asc())
34-
if cursor:
35-
stx_query = stx_query.where(SinkingTx.paging_token > cursor)
36-
if order == 'desc':
37-
stx_query = stx_query.order_by(SinkingTx.paging_token.desc())
38-
if cursor:
39-
stx_query = stx_query.where(SinkingTx.paging_token < cursor)
40-
41-
if limit:
42-
stx_query = stx_query.limit(limit)
36+
subq_order = subq_paging_token.asc()
37+
stx_order = SinkingTx.paging_token.asc()
38+
cursor_filter = subq_paging_token > (cursor or 0)
39+
elif order == 'desc':
40+
subq_order = subq_paging_token.desc()
41+
stx_order = SinkingTx.paging_token.desc()
42+
cursor_filter = subq_paging_token < (cursor or 0)
4343

4444
with Session.begin() as session:
45+
# Step 1: Get unique paging_tokens for the page
46+
token_query = select(subq_paging_token).select_from(base_query)
47+
if cursor and cursor > 0:
48+
token_query = token_query.where(cursor_filter)
49+
50+
token_query = token_query.order_by(subq_order).distinct().limit(limit)
51+
paging_tokens = session.scalars(token_query).all()
52+
53+
if not paging_tokens:
54+
return pd.DataFrame()
55+
56+
# Step 2: Fetch SinkingTxs for those paging_tokens, eager load statuses
57+
stx_query = (
58+
select(SinkingTx)
59+
.outerjoin(SinkStatus)
60+
.options(contains_eager(SinkingTx.statuses))
61+
.where(SinkingTx.paging_token.in_(paging_tokens))
62+
.order_by(stx_order)
63+
)
4564
stx_records = session.scalars(stx_query).unique().all()
4665
txdf = pd.DataFrame.from_records(stx.as_dict() for stx in stx_records)
4766

@@ -55,27 +74,30 @@ def construct_stx_query(
5574
before_date: dt.date | None,
5675
finalized: bool | None,
5776
) -> Select[tuple[SinkingTx]]:
58-
q_txs = select(SinkingTx).outerjoin(SinkStatus).options(contains_eager(SinkingTx.statuses))
77+
# Only build the base query and filters
78+
q_txs = select(SinkingTx)
5979
if for_funder:
6080
q_txs = q_txs.where(SinkingTx.funder == for_funder)
61-
81+
6282
if for_recipient:
6383
q_txs = q_txs.where(SinkingTx.recipient == for_recipient)
64-
84+
6585
if from_date:
6686
from_dt = dt.datetime(from_date.year, from_date.month, from_date.day)
6787
q_txs = q_txs.where(SinkingTx.created_at >= from_dt)
68-
88+
6989
if before_date:
7090
before_dt = dt.datetime(before_date.year, before_date.month, before_date.day)
7191
q_txs = q_txs.where(SinkingTx.created_at < before_dt)
72-
92+
93+
if finalized is not None:
94+
q_txs = q_txs.outerjoin(SinkStatus)
7395
if finalized is False:
7496
# not_ and in_ do not work here because NULL and false are treated differently
7597
q_txs = q_txs.where(or_(SinkStatus.finalized == None, SinkStatus.finalized == False))
7698
elif finalized is True:
7799
q_txs = q_txs.where(SinkStatus.finalized == True)
78-
100+
79101
return q_txs
80102

81103

tests/test_views.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime as dt
22
from decimal import Decimal
3+
import re
34

45
import pandas as pd
56
import pytest
@@ -64,11 +65,8 @@ def test_construct_query_unfiltered(self):
6465
before_date=None,
6566
finalized=None
6667
)
67-
assert (
68-
"FROM sinking_txs LEFT OUTER JOIN sink_status ON sinking_txs.hash = sink_status.sinking_tx_hash"
69-
in str(stxq)
70-
)
7168
assert "WHERE" not in str(stxq)
69+
assert re.fullmatch(r'SELECT (sinking_txs\.[_a-z]+,?\s*)+FROM sinking_txs', str(stxq))
7270

7371
def test_construct_query_for_funder(self):
7472
stxq = sink_status_view.construct_stx_query(
@@ -170,13 +168,14 @@ def test_sink_status_before_date(self, mock_session_with_associations):
170168

171169
def test_sink_status_finalized_true(self, mock_session_with_associations):
172170
txdf = sink_status_view.view_sinking_txs(finalized=True)
173-
assert len(txdf) == 18
174-
assert txdf.carbon_amount.sum() == 23
171+
assert len(txdf) == 17
172+
assert txdf.statuses.map(add_amount_filled).sum() == txdf.carbon_amount.sum() == Decimal('21.015')
175173
assert all(txdf.statuses.astype(bool))
176174

177175
def test_sink_status_finalized_false(self, mock_session_with_associations):
178176
txdf = sink_status_view.view_sinking_txs(finalized=False)
179177
assert len(txdf) == 2
178+
assert txdf.statuses.map(add_amount_filled).sum() == Decimal('1.985')
180179
assert txdf.carbon_amount.sum() == Decimal('3.298')
181180
assert not any(txdf.statuses.astype(bool))
182181

@@ -393,3 +392,7 @@ def mock_session_with_associations(mock_mint_http, mock_sink_http, mock_session)
393392
retirement_from_block.load_retirement_from_block()
394393
sink_status_loader.load_sink_statuses()
395394
return mock_session
395+
396+
397+
def add_amount_filled(statuses: list) -> Decimal:
398+
return sum((s["amount_filled"] for s in statuses), start=Decimal())

0 commit comments

Comments
 (0)