Skip to content
Open
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
31 changes: 11 additions & 20 deletions cala-ledger/src/entry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ impl Entries {
.await
}

/// List a transaction's entries in `(created_at, id)` order.
///
/// Follow the returned continuation to read further pages. Callers that
/// need transaction sequence order can sort the collected entries by
/// `entry.values().sequence`.
#[es_entity::errlanes::instrument(
level = "debug",
name = "cala_ledger.entries.list_for_transaction_id",
Expand All @@ -129,26 +134,12 @@ impl Entries {
pub async fn list_for_transaction_id(
&self,
transaction_id: TransactionId,
) -> Result<Vec<Entry>, CalaFault> {
let page = self
.repo
.list_for_transaction_id_by_created_at(
transaction_id,
Default::default(),
Default::default(),
)
.await?;
let mut entries = match page.into_page() {
es_entity::Page::Last { entities } | es_entity::Page::HasNext { entities, next: _ } => {
entities
}
};
entries.sort_by(|a, b| {
let a_sequence = a.values().sequence;
let b_sequence = b.values().sequence;
a_sequence.cmp(&b_sequence)
});
Ok(entries)
query: es_entity::PaginatedQueryArgs<EntryByCreatedAtCursor>,
direction: es_entity::ListDirection,
) -> Result<es_entity::PaginatedQueryRet<Entry, EntryByCreatedAtCursor>, CalaFault> {
self.repo
.list_for_transaction_id_by_created_at(transaction_id, query, direction)
.await
}
}

Expand Down
13 changes: 11 additions & 2 deletions cala-ledger/tests/transaction_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ async fn batch_of_postings_on_overlapping_accounts_chains_balances() -> anyhow::

// Every posting's entries are readable and attached to their transaction.
for tx in posted.iter() {
let entries = cala.entries().list_for_transaction_id(tx.id()).await?;
let mut entries = helpers::expect_single_page(
cala.entries()
.list_for_transaction_id(tx.id(), Default::default(), Default::default())
.await?,
);
entries.sort_by_key(|entry| entry.values().sequence);
assert_eq!(entries.len(), 6);
assert_eq!(entries[0].values().sequence, 1);
}
Expand Down Expand Up @@ -789,7 +794,11 @@ async fn outbox_events_are_grouped_per_posting_not_by_type() -> anyhow::Result<(
for tx in posted.iter() {
let tx_id = tx.id().to_string();
expected.push(("transaction_created".to_string(), tx_id.clone()));
let entries = cala.entries().list_for_transaction_id(tx.id()).await?;
let entries = helpers::expect_single_page(
cala.entries()
.list_for_transaction_id(tx.id(), Default::default(), Default::default())
.await?,
);
for _ in 0..entries.len() {
expected.push(("entry_created".to_string(), tx_id.clone()));
}
Expand Down
108 changes: 108 additions & 0 deletions cala-ledger/tests/transaction_entry_pagination.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
mod helpers;

use cala_ledger::{tx_template::*, *};

#[tokio::test]
async fn list_for_transaction_id_exposes_complete_bidirectional_pagination() -> anyhow::Result<()> {
let pool = helpers::init_isolated_pool().await?;
let mut jobs = helpers::init_jobs(pool.clone()).await?;
let config = CalaLedgerConfig::builder()
.pool(pool)
.exec_migrations(false)
.build()?;
let cala = CalaLedger::init(config, &mut jobs).await?;
let journal = cala.journals().create(helpers::test_journal()).await?;
let (sender, recipient) = helpers::test_accounts();
let sender = cala.accounts().create(sender).await?;
let recipient = cala.accounts().create(recipient).await?;

for count in [2, 100, 102, 250] {
let mut entries = Vec::new();
for _ in 0..count / 2 {
for (account, direction) in [(sender.id(), "'DEBIT'"), (recipient.id(), "'CREDIT'")] {
entries.push(
NewTxTemplateEntry::builder()
.entry_type("'PAGINATION_TEST'")
.account_id(format!("uuid('{account}')"))
.layer("'SETTLED'")
.direction(direction)
.units("decimal('1')")
.currency("'USD'")
.build()?,
);
}
}
let code = format!("pagination_{count}");
let template = NewTxTemplate::builder()
.id(TxTemplateId::new())
.code(&code)
.transaction(
NewTxTemplateTransaction::builder()
.journal_id(format!("uuid('{}')", journal.id()))
.effective("date()")
.build()?,
)
.entries(entries)
.build();
cala.tx_templates().create(template).await?;
let tx = cala
.post_transaction(TransactionId::new(), &code, Params::new())
.await?;
let stored = cala.entries().find_all(&tx.values().entry_ids).await?;
assert_eq!(stored.len(), count, "all posted entries must exist");
for direction in [
es_entity::ListDirection::Ascending,
es_entity::ListDirection::Descending,
] {
let mut expected: Vec<_> = stored
.values()
.map(|entry| (entry.created_at(), entry.id()))
.collect();
expected.sort();
if matches!(direction, es_entity::ListDirection::Descending) {
expected.reverse();
}
for first in [17, 100] {
let mut query = es_entity::PaginatedQueryArgs { first, after: None };
let mut listed = Vec::new();
loop {
let page = cala
.entries()
.list_for_transaction_id(tx.id(), query, direction)
.await?;
assert_eq!(page.requested_size(), first);
assert!(page.entities().len() <= first);
match page.into_page() {
es_entity::Page::Last { entities } => {
listed.extend(entities);
break;
}
es_entity::Page::HasNext { entities, next } => {
assert_eq!(entities.len(), first);
assert_eq!(next.first, first);
assert_eq!(entities.last().unwrap().id(), next.after.id);
listed.extend(entities);
query = next.into();
}
}
}
let actual: Vec<_> = listed
.iter()
.map(|entry| (entry.created_at(), entry.id()))
.collect();
assert_eq!(
actual, expected,
"pages must preserve order without skips or duplicates"
);
}
}
}
let missing = cala
.entries()
.list_for_transaction_id(TransactionId::new(), Default::default(), Default::default())
.await?;
assert!(
matches!(missing.into_page(), es_entity::Page::Last { entities } if entities.is_empty())
);
Ok(())
}