Skip to content

Commit 598012f

Browse files
authored
refactor: statement: optimise for huge statements (#118)
1 parent 6e4b256 commit 598012f

File tree

8 files changed

+429
-37
lines changed

8 files changed

+429
-37
lines changed

server/internal/api/controller/statement_controller_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ func createAccount(testHelper *TestHelper, name string, balance float64) float64
4242
func waitForStatementDone(testHelper *TestHelper, statementId float64) map[string]any {
4343
var status string
4444
var data map[string]any
45-
for i := 0; i < 20; i++ {
45+
for i := 0; i < 8; i++ {
4646
resp, response := testHelper.MakeRequest(http.MethodGet, "/statement/"+strconv.FormatFloat(statementId, 'f', 0, 64), nil)
4747
Expect(resp.StatusCode).To(Equal(http.StatusOK))
4848
data = response["data"].(map[string]any)
4949
status = data["status"].(string)
50-
if status == "done" {
50+
if status != "processing" {
5151
break
5252
}
53-
time.Sleep(1 * time.Second)
53+
time.Sleep(5 * time.Second)
5454
}
5555
Expect(status).To(Equal("done"))
5656
return data
@@ -835,15 +835,15 @@ var _ = Describe("StatementController", func() {
835835
Expect(transactions).To(HaveLen(3))
836836
})
837837

838-
It("should parse statement with 10000 transactions", func() {
838+
It("should parse statement with 100K transactions", func() {
839839
testHelper := createUniqueUser(baseURL)
840840
accountId := createAccount(testHelper, "Account 1", 1000.0)
841841

842-
// Build XLSX data (header + 10000 rows)
842+
// Build XLSX data (header + 100K rows)
843843
data := [][]string{
844844
{"Txn Date", "Details", "Ref No.", "Debit", "Credit", "Balance"},
845845
}
846-
for i := 1; i <= 1000; i++ {
846+
for i := 1; i <= 100000; i++ {
847847
row := []string{
848848
"22 Aug 2022",
849849
fmt.Sprintf("Desc%d", i),
@@ -877,7 +877,7 @@ var _ = Describe("StatementController", func() {
877877
transactions := txData["transactions"].([]any)
878878
Expect(len(transactions)).To(Equal(10))
879879
Expect(txData).To(HaveKey("total"))
880-
Expect(txData["total"]).To(Equal(1000.0))
880+
Expect(txData["total"]).To(Equal(100000.0))
881881
})
882882

883883
It("should error when accountId doesn't exist", func() {

server/internal/mock/repository/statement_repository.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ func (m *MockStatementRepository) CreateStatementTxn(ctx context.Context, statem
6161
return nil
6262
}
6363

64+
// CreateStatementTxns adds multiple mappings between statement and transactions for testing
65+
func (m *MockStatementRepository) CreateStatementTxns(ctx context.Context, statementId int64, transactionIds []int64) error {
66+
m.mu.Lock()
67+
defer m.mu.Unlock()
68+
for _, txId := range transactionIds {
69+
m.statementTxnMappings = append(m.statementTxnMappings, statementTxnMapping{
70+
StatementId: statementId,
71+
TransactionId: txId,
72+
})
73+
}
74+
return nil
75+
}
76+
6477
func (m *MockStatementRepository) UpdateStatementStatus(ctx context.Context, statementId int64, input models.UpdateStatementStatusInput) (models.StatementResponse, error) {
6578
m.mu.Lock()
6679
defer m.mu.Unlock()

server/internal/mock/repository/transaction_repository.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,62 @@ func (m *MockTransactionRepository) CreateTransaction(ctx context.Context, input
7676
return tx, nil
7777
}
7878

79+
func (m *MockTransactionRepository) CreateTransactions(ctx context.Context, inputs []models.CreateBaseTransactionInput, categoryIds [][]int64) ([]models.TransactionResponse, error) {
80+
m.mu.Lock()
81+
defer m.mu.Unlock()
82+
83+
if len(inputs) != len(categoryIds) {
84+
return nil, customErrors.NewTransactionAlreadyExistsError(nil)
85+
}
86+
87+
results := make([]models.TransactionResponse, 0, len(inputs))
88+
89+
for i, input := range inputs {
90+
// Check for duplicate transaction
91+
for _, tx := range m.transactions {
92+
if tx.CreatedBy == input.CreatedBy &&
93+
tx.Date.Format("2006-01-02") == input.Date.Format("2006-01-02") &&
94+
tx.Name == input.Name &&
95+
tx.Amount == *input.Amount {
96+
existingDesc := ""
97+
if tx.Description != nil {
98+
existingDesc = *tx.Description
99+
}
100+
inputDesc := input.Description
101+
if existingDesc == inputDesc {
102+
return nil, customErrors.NewTransactionAlreadyExistsError(nil)
103+
}
104+
}
105+
}
106+
107+
// Create new transaction
108+
newId := m.nextId
109+
m.nextId++
110+
111+
baseTx := models.TransactionBaseResponse{
112+
Id: newId,
113+
Name: input.Name,
114+
Description: &input.Description,
115+
Amount: *input.Amount,
116+
Date: input.Date,
117+
CreatedBy: input.CreatedBy,
118+
AccountId: input.AccountId,
119+
}
120+
121+
tx := models.TransactionResponse{
122+
TransactionBaseResponse: baseTx,
123+
CategoryIds: categoryIds[i],
124+
}
125+
126+
m.transactions[newId] = tx
127+
m.categoryMap[newId] = categoryIds[i]
128+
129+
results = append(results, tx)
130+
}
131+
132+
return results, nil
133+
}
134+
79135
func (m *MockTransactionRepository) UpdateCategoryMapping(ctx context.Context, transactionId int64, userId int64, categoryIds []int64) error {
80136
m.mu.Lock()
81137
defer m.mu.Unlock()
@@ -99,6 +155,20 @@ func (m *MockTransactionRepository) GetTransactionById(ctx context.Context, tran
99155
return tx, nil
100156
}
101157

158+
func (m *MockTransactionRepository) GetTransactionsByIds(ctx context.Context, transactionIds []int64, userId int64) ([]models.TransactionResponse, error) {
159+
m.mu.RLock()
160+
defer m.mu.RUnlock()
161+
162+
var transactions []models.TransactionResponse
163+
for _, id := range transactionIds {
164+
tx, ok := m.transactions[id]
165+
if ok && tx.CreatedBy == userId {
166+
transactions = append(transactions, tx)
167+
}
168+
}
169+
return transactions, nil
170+
}
171+
102172
func (m *MockTransactionRepository) UpdateTransaction(ctx context.Context, transactionId int64, userId int64, input models.UpdateBaseTransactionInput) error {
103173
tx, ok := m.transactions[transactionId]
104174
if !ok || tx.CreatedBy != userId {

server/internal/repository/statement_repository.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
type StatementRepositoryInterface interface {
1919
CreateStatement(ctx context.Context, input models.CreateStatementInput) (models.StatementResponse, error)
2020
CreateStatementTxn(ctx context.Context, statementId int64, transactionId int64) error
21+
CreateStatementTxns(ctx context.Context, statementId int64, transactionIds []int64) error
2122
UpdateStatementStatus(ctx context.Context, statementId int64, input models.UpdateStatementStatusInput) (models.StatementResponse, error)
2223
GetStatementByID(ctx context.Context, statementId int64, userId int64) (models.StatementResponse, error)
2324
ListStatementByUserId(ctx context.Context, userId int64, limit, offset int) ([]models.StatementResponse, error)
@@ -65,6 +66,45 @@ func (r *StatementRepository) CreateStatementTxn(ctx context.Context, statementI
6566
return nil
6667
}
6768

69+
func (r *StatementRepository) CreateStatementTxns(ctx context.Context, statementId int64, transactionIds []int64) error {
70+
if len(transactionIds) == 0 {
71+
return nil
72+
}
73+
74+
const batchSize = 1000
75+
76+
// Process in batches of 1000
77+
for batchStart := 0; batchStart < len(transactionIds); batchStart += batchSize {
78+
batchEnd := batchStart + batchSize
79+
if batchEnd > len(transactionIds) {
80+
batchEnd = len(transactionIds)
81+
}
82+
83+
batchTxIds := transactionIds[batchStart:batchEnd]
84+
85+
// Build bulk insert using VALUES for this batch
86+
placeholders := make([]string, 0, len(batchTxIds))
87+
args := make([]interface{}, 0, len(batchTxIds)*2)
88+
argIndex := 1
89+
90+
for _, txID := range batchTxIds {
91+
placeholders = append(placeholders, fmt.Sprintf("($%d, $%d)", argIndex, argIndex+1))
92+
args = append(args, statementId, txID)
93+
argIndex += 2
94+
}
95+
96+
query := fmt.Sprintf(`INSERT INTO %s.%s (statement_id, transaction_id) VALUES %s`,
97+
r.schema, r.mappingTableName, strings.Join(placeholders, ", "))
98+
99+
_, err := r.db.ExecuteQuery(ctx, query, args...)
100+
if err != nil {
101+
return statementErrors.NewStatementCreateError(err)
102+
}
103+
}
104+
105+
return nil
106+
}
107+
68108
func (r *StatementRepository) UpdateStatementStatus(ctx context.Context, statementId int64, input models.UpdateStatementStatusInput) (models.StatementResponse, error) {
69109
logger.Debugf("Updating statement %d with status %s", statementId, input.Status)
70110
fieldsClause, argValues, argIndex, err := helper.CreateUpdateParams(&input)

0 commit comments

Comments
 (0)