Skip to content

feat: implement bulk insert repository methods for transactions and operations - bulk recorder #1928

Merged
Ygohr merged 6 commits intodevelopfrom
feature/mdz-1903
Mar 20, 2026
Merged

feat: implement bulk insert repository methods for transactions and operations - bulk recorder #1928
Ygohr merged 6 commits intodevelopfrom
feature/mdz-1903

Conversation

@Ygohr
Copy link
Contributor

@Ygohr Ygohr commented Mar 19, 2026

Summary

Implements bulk insert repository methods (CreateBulk and CreateBulkTx) for both Transaction and Operation entities as part of the Bulk Recorder feature. This is Phase 1 of the transaction-layer bulk insert implementation, focusing on the repository layer foundation.

Motivation

The current transaction layer processes RabbitMQ messages one at a time, with each transaction's operations inserted individually in a loop. For 50 transactions with 5 operations each, this results in 300 database round-trips and 50 individual RabbitMQ ACKs.

With bulk insert at the repository layer:

  • 1 bulk transaction INSERT (50 rows)
  • 1 bulk operation INSERT (250 rows)
  • 2-4 total database round-trips

Expected gain: 10-100x performance improvement depending on bulk size.

Semantic Decision

Feature - Adds new CreateBulk and CreateBulkTx methods to Transaction and Operation repositories for multi-row INSERT operations with idempotency support via ON CONFLICT DO NOTHING.

Changes

New Files

File Purpose
pkg/repository/bulk.go Shared types: DBExecutor interface and BulkInsertResult struct for tracking attempted/inserted/ignored counts
components/transaction/internal/adapters/postgres/operation/operation_createbulk_test.go Comprehensive tests for Operation CreateBulk and CreateBulkTx methods (580+ lines)
components/transaction/internal/adapters/postgres/transaction/transaction_createbulk_test.go Comprehensive tests for Transaction CreateBulk and CreateBulkTx methods (530+ lines)

Modified Files

File Change
pkg/utils/metrics.go Added 9 bulk recorder metrics (attempted/inserted/ignored for transactions and operations, bulk size, duration, fallback count)
operation.postgresql.go Added CreateBulk() and CreateBulkTx() methods with chunking, sorting, and idempotency
operation.postgresql_mock.go Added mock implementations for new interface methods
transaction.postgresql.go Added CreateBulk() and CreateBulkTx() methods with chunking, sorting, and idempotency
transaction.postgresql_mock.go Added mock implementations for new interface methods

OpenTelemetry Metrics

Metric Type Description
bulk_recorder_transactions_attempted_total Counter Transactions sent to bulk INSERT
bulk_recorder_transactions_inserted_total Counter Transactions actually inserted
bulk_recorder_transactions_ignored_total Counter Transactions skipped (duplicates)
bulk_recorder_operations_attempted_total Counter Operations sent to bulk INSERT
bulk_recorder_operations_inserted_total Counter Operations actually inserted
bulk_recorder_operations_ignored_total Counter Operations skipped (duplicates)
bulk_recorder_bulk_size Histogram Messages per bulk batch
bulk_recorder_bulk_duration_seconds Histogram Time for bulk processing
bulk_recorder_fallback_total Counter Fallback activations when bulk fails

Key Implementation Details

Deadlock Prevention:

  • Operations and transactions are sorted by ID before bulk insert
  • Deterministic ordering prevents concurrent bulk operations from deadlocking

Idempotency:

  • Uses ON CONFLICT (id) DO NOTHING for all bulk inserts
  • Duplicates are silently ignored (not errors)
  • RowsAffected() correctly reports only newly inserted rows
  • Consistent with existing individual INSERT behavior

Chunking:

  • Large bulks are automatically chunked to ~1,000 rows per INSERT
  • Stays within PostgreSQL's 65,535 parameter limit
  • Transaction: 15 columns = 15,000 params per chunk
  • Operation: 30 columns = 30,000 params per chunk

Transaction Support:

  • CreateBulkTx() accepts a DBExecutor interface
  • Allows caller to control transaction boundaries
  • Enables atomic multi-table operations (transactions + operations in same DB tx)

Test Plan

  • Unit tests for CreateBulk with empty slice returns zero counts
  • Unit tests for CreateBulk with nil elements returns error
  • Unit tests for CreateBulk single item success
  • Unit tests for CreateBulk multiple items success
  • Unit tests for CreateBulk duplicate handling (ON CONFLICT DO NOTHING)
  • Unit tests for CreateBulk chunking behavior (>1000 items)
  • Unit tests for CreateBulk context cancellation between chunks
  • Unit tests for CreateBulk database error handling
  • Unit tests for CreateBulkTx with external transaction
  • Unit tests for ID sorting verification (deadlock prevention)
  • Unit tests for BulkInsertResult correct counting (attempted = inserted + ignored)
  • Mock implementations verified with mockgen

@Ygohr Ygohr self-assigned this Mar 19, 2026
@Ygohr Ygohr added the back-end Back-end Issues label Mar 19, 2026
@Ygohr Ygohr requested a review from a team as a code owner March 19, 2026 19:24
@lerian-studio
Copy link
Contributor

This PR is very large (8 files, 1732 lines changed). Consider breaking it into smaller PRs for easier review.

@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f33a164-f13a-4eb8-98d1-17ff6ac9035e

📥 Commits

Reviewing files that changed from the base of the PR and between 980e763 and f30e7f5.

📒 Files selected for processing (4)
  • components/transaction/internal/adapters/postgres/operation/operation.postgresql.go
  • components/transaction/internal/adapters/postgres/operation/operation_createbulk_test.go
  • components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go
  • components/transaction/internal/adapters/postgres/transaction/transaction_createbulk_test.go

Walkthrough

This pull request introduces bulk insertion capabilities for transactions and operations in the PostgreSQL adapter layer. New CreateBulk and CreateBulkTx methods are added to both OperationPostgreSQLRepository and TransactionPostgreSQLRepository, implementing chunked batch insertion (1000 rows per chunk) with in-place sorting, context cancellation checks, and partial result accumulation on errors. Supporting types—BulkInsertResult, DBExecutor interface, and ErrNilDBExecutor sentinel—are defined in pkg/repository. Mock implementations and comprehensive test suites validate edge cases, failure modes, and behavioral constraints. New Prometheus metrics track bulk operation attempts, insertions, ignores, sizes, and durations.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Repo as Repository<br/>(CreateBulk)
    participant Internal as createBulkInternal
    participant Chunk as insertChunk
    participant DB as DBExecutor<br/>(ExecContext)

    Caller->>Repo: CreateBulk(ctx, operations)
    
    rect rgba(100, 150, 200, 0.5)
    Note over Repo: Input Validation & Preparation
    Repo->>Internal: Short-circuit on empty input?
    alt Empty/Nil Input
        Repo-->>Caller: BulkInsertResult{0, 0, 0}, nil
    else Non-Empty
        Repo->>Internal: createBulkInternal(ctx, db, operations)
    end
    end

    rect rgba(150, 100, 200, 0.5)
    Note over Internal: Sort & Chunk Processing
    Internal->>Internal: Validate no nil elements
    Internal->>Internal: Sort by ID in-place
    Internal->>Internal: Set Attempted = len(slice)
    
    loop For each chunk (max 1000 rows)
        Internal->>Chunk: insertOperationChunk(ctx, db, chunk)
        Chunk->>DB: ExecContext(INSERT ... ON CONFLICT)
        DB-->>Chunk: RowsAffected
        Chunk-->>Internal: inserted count
        Internal->>Internal: Accumulate inserted
        
        alt Chunk Error
            Internal->>Internal: Break loop
        else Context Canceled
            Internal->>Internal: Check ctx.Done()
            alt Canceled
                Internal->>Internal: Break loop, save error
            end
        end
    end
    end

    rect rgba(200, 150, 100, 0.5)
    Note over Internal: Result Calculation
    Internal->>Internal: Ignored = Attempted - Inserted
    Internal-->>Repo: BulkInsertResult, error (if any)
    end

    Repo-->>Caller: BulkInsertResult{Attempted,<br/>Inserted, Ignored}, error
Loading
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely describes the primary feature: implementing bulk insert repository methods for transactions and operations as part of the bulk recorder.
Description check ✅ Passed Description is comprehensive with clear motivation, semantic decision, detailed changes, implementation details, and test coverage checklist. However, the PR description itself is not formatted according to the provided template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 19, 2026

🔒 Security Scan Results — transaction

Filesystem Scan

✅ No vulnerabilities or secrets found.

Docker Image Scan

✅ No vulnerabilities found.

All security checks passed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@components/transaction/internal/adapters/postgres/operation/operation_createbulk_test.go`:
- Around line 62-63: Remove the redundant unused-var suppression by deleting the
blank identifier assignment "var _ = generateTestOperations" in the test file;
since generateTestOperations is already invoked in tests (calls around the
existing tests), simply remove that line to clean up the code and avoid an
unnecessary sentinel reference to the function.

In
`@components/transaction/internal/adapters/postgres/operation/operation.postgresql.go`:
- Around line 340-414: CreateBulk and CreateBulkTx duplicate most logic; extract
the shared validation, sorting, chunking, context-check and chunk-insert loop
into a helper like createBulkInternal/createBulkWithExecutor that accepts a
repository.DBExecutor (used by CreateBulk via r.getDB(ctx) and by CreateBulkTx
via the tx param) and a span name; move the common code from CreateBulk and
CreateBulkTx into that helper and have both methods call it (preserve existing
behavior: return partial results on error, set Attempted/Inserted/Ignored, and
keep span/tracing calls using the provided span name).

In
`@components/transaction/internal/adapters/postgres/transaction/transaction_createbulk_test.go`:
- Around line 53-54: Remove the unnecessary unused-variable suppression: delete
the line that assigns generateTestTransactions to the blank identifier (var _ =
generateTestTransactions). The helper function generateTestTransactions is
already referenced by tests (e.g., in the tests around lines where it's called),
so simply removing that suppression cleans up the file without affecting test
usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cdfaf0aa-7375-4617-bb25-eabb0a6c51a2

📥 Commits

Reviewing files that changed from the base of the PR and between 35e4c61 and 96a9b03.

📒 Files selected for processing (8)
  • components/transaction/internal/adapters/postgres/operation/operation.postgresql.go
  • components/transaction/internal/adapters/postgres/operation/operation.postgresql_mock.go
  • components/transaction/internal/adapters/postgres/operation/operation_createbulk_test.go
  • components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go
  • components/transaction/internal/adapters/postgres/transaction/transaction.postgresql_mock.go
  • components/transaction/internal/adapters/postgres/transaction/transaction_createbulk_test.go
  • pkg/repository/bulk.go
  • pkg/utils/metrics.go

@lerian-studio
Copy link
Contributor

lerian-studio commented Mar 19, 2026

📊 Unit Test Coverage Report: midaz-transaction

Metric Value
Overall Coverage 85.4% ✅ PASS
Threshold 85%

Coverage by Package

Package Coverage
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/grpc/in 100.0%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/http/in 78.5%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/mongodb 66.7%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/postgres/assetrate 100.0%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/postgres/balance 100.0%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/postgres/operation 90.0%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/postgres/operationroute 100.0%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/postgres/transaction 99.1%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/postgres/transactionroute 100.0%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/rabbitmq 93.1%
github.com/LerianStudio/midaz/v3/components/transaction/internal/adapters/redis/balance 100.0%
github.com/LerianStudio/midaz/v3/components/transaction/internal/services/command 90.4%
github.com/LerianStudio/midaz/v3/components/transaction/internal/services/query 95.2%
github.com/LerianStudio/midaz/v3/components/transaction/internal/services 100.0%

Generated by Go PR Analysis workflow

…ting shared logic into createBulkInternal function and improve input validation 🔨
@lerian-studio
Copy link
Contributor

This PR is very large (8 files, 1682 lines changed). Consider breaking it into smaller PRs for easier review.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go (1)

217-247: 🧹 Nitpick | 🔵 Trivial

Same redundant nil-element validation as in operation repository.

The nil-element validation at lines 228-236 duplicates the check in createBulkInternal (lines 287-295). Consider the same cleanup suggested for the operation repository to maintain consistency across both implementations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go`
around lines 217 - 247, The nil-element validation in
TransactionPostgreSQLRepository.CreateBulk duplicates the check already present
in createBulkInternal; remove the loop that checks for nil transactions from
CreateBulk (but keep the empty-slice early return) and rely on
createBulkInternal to validate individual entries, so only createBulkInternal
performs the per-item nil checks; update any error handling/logging in
createBulkInternal if needed to preserve span/log context when called from
CreateBulk.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@components/transaction/internal/adapters/postgres/operation/operation.postgresql.go`:
- Around line 267-296: Remove the redundant nil-element loop in CreateBulk and
rely on the existing validation in createBulkInternal; specifically, delete the
for i, op := range operations { if op == nil { ... } } block in CreateBulk so
CreateBulk only does the empty-slice early return and DB acquisition (getDB)
before delegating to createBulkInternal
(postgres.create_bulk_operations_internal), or alternatively factor the
nil-check into a shared helper and call it from both CreateBulk and
createBulkInternal if you want an explicit fast-fail before getDB.

---

Duplicate comments:
In
`@components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go`:
- Around line 217-247: The nil-element validation in
TransactionPostgreSQLRepository.CreateBulk duplicates the check already present
in createBulkInternal; remove the loop that checks for nil transactions from
CreateBulk (but keep the empty-slice early return) and rely on
createBulkInternal to validate individual entries, so only createBulkInternal
performs the per-item nil checks; update any error handling/logging in
createBulkInternal if needed to preserve span/log context when called from
CreateBulk.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f43c504b-9fc4-47b5-bec6-4a6256d17ffb

📥 Commits

Reviewing files that changed from the base of the PR and between 96a9b03 and 980e763.

📒 Files selected for processing (5)
  • components/transaction/internal/adapters/postgres/operation/operation.postgresql.go
  • components/transaction/internal/adapters/postgres/operation/operation_createbulk_test.go
  • components/transaction/internal/adapters/postgres/transaction/transaction.postgresql.go
  • components/transaction/internal/adapters/postgres/transaction/transaction_createbulk_test.go
  • pkg/repository/bulk.go

@lerian-studio
Copy link
Contributor

This PR is very large (8 files, 1694 lines changed). Consider breaking it into smaller PRs for easier review.

Copy link

@gandalf-at-lerian gandalf-at-lerian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid implementation. The design decisions are sound and the test coverage is thorough. A few observations worth discussing:

What's well done

  • DBExecutor interface is the right abstraction — clean separation between direct DB and tx-controlled paths
  • Deadlock prevention via ID sorting is correct and the comment explains why
  • Chunking logic correctly accounts for PostgreSQL's 65,535 parameter limit per entity (15 cols × 1000 = 15K, 30 cols × 1000 = 30K)
  • ON CONFLICT (id) DO NOTHING makes retries safe without extra coordination
  • The BulkInsertResult invariant (Attempted = Inserted + Ignored) is well-modeled and tested
  • Partial result on chunk failure is the right call — callers get what was committed + the error
  • Test for context cancellation before first chunk is a good edge case

One concern: partial commit semantics

The docstring acknowledges it explicitly, but worth highlighting: CreateBulk (non-Tx) commits chunks independently. If chunk 1 succeeds and chunk 2 fails, those first 1,000 rows are durable with no rollback path. This is safe only because of the idempotency guarantee — a retry will re-insert the same rows and DO NOTHING handles them correctly.

Make sure the caller layer (bulk recorder) is aware of this and implements retry logic. If it ACKs the RabbitMQ batch before calling CreateBulk, a partial failure leaves un-ACK'd messages that will be redelivered — which is actually the desired behavior here. Just confirm the ACK strategy is explicit.

Minor: sort.Slice vs slices.SortFunc

Go 1.21+ ships slices.SortFunc which avoids the closure allocation. Since midaz is already on a recent Go version, this is a low-priority cleanup but worth a note:

slices.SortFunc(operations, func(a, b *Operation) int {
    return strings.Compare(a.ID, b.ID)
})

The createBulkInternal duplication

Transaction and Operation implementations are structurally identical. The only differences are the entity type and insertXxxChunk call. This is expected in Go given the lack of generics-friendly repository patterns here — acceptable for Phase 1. If a Phase 2 refactor is planned, a generic bulkInsertChunked[T] helper could centralize the chunking/sorting/telemetry loop.

CI: all green. ✅

Approved. Phase 1 foundation looks solid — the Phase 2 (consumer integration) is where the real behavior gets validated.

@Ygohr Ygohr merged commit 4fa6ccb into develop Mar 20, 2026
25 checks passed
@Ygohr Ygohr deleted the feature/mdz-1903 branch March 20, 2026 12:36
@Ygohr Ygohr linked an issue Mar 20, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

back-end Back-end Issues size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bulk recorder

4 participants