Skip to content

feat: Add Entity Fetcher Service for Event Sourcing#226

Merged
NickSeagull merged 17 commits into
mainfrom
feat/entity-reduction
Nov 18, 2025
Merged

feat: Add Entity Fetcher Service for Event Sourcing#226
NickSeagull merged 17 commits into
mainfrom
feat/entity-reduction

Conversation

@NickSeagull

@NickSeagull NickSeagull commented Nov 18, 2025

Copy link
Copy Markdown
Member

This PR introduces a new EntityFetcher service that makes it easy to reconstruct the current state of an entity by reading and applying all its events from the event store. Think of it as a "time machine" that replays your entity's history to show you what it looks like right now.

✨ Why This Matters

If you're building applications with event sourcing (where you store changes as a series of events instead of just the final state), you need a way to reconstruct what an entity looks like at any point in time. This PR gives you that capability out of the box.

Real-world example: Imagine you have a bank account that stores events like "deposit $100" and "withdraw $50". The EntityFetcher reads all these events and tells you the current balance is $50.

🚀 Quick Start

Here's how you use it:

-- 1. Define your entity state
data BankAccount = BankAccount 
  { balance :: Int
  , isOpen :: Bool
  }

-- 2. Define how events change the state
applyEvent :: BankAccount -> BankAccountEvent -> BankAccount
applyEvent account (MoneyDeposited amount) = 
  account { balance = account.balance + amount }

-- 3. Create the fetcher
fetcher <- EntityFetcher.new eventStore initialState applyEvent

-- 4. Fetch current state
currentAccount <- fetcher.fetch entityName streamId
-- Now you have the current balance!

📦 What's Included

New Modules

  • Service.EntityFetcher.Core - The main EntityFetcher implementation
  • Service.EntityFetcher - Re-exports everything you need

Key Features

  • ✅ Memory-efficient streaming (processes events one-by-one, not all at once)
  • ✅ Works with existing EventStore (no new infrastructure needed)
  • ✅ Type-safe event application
  • ✅ Comprehensive test coverage (100+ test cases)

Test Coverage

Added complete test suites covering:

  • ✅ Single and multiple event scenarios
  • ✅ Empty stream handling
  • ✅ Concurrent fetch operations
  • ✅ Performance with large event streams (tested up to 1M events)
  • ✅ Error scenarios

🔧 Technical Details

How It Works

  1. Reads events from the EventStore using stream consumption
  2. Applies your reduction function to each event sequentially
  3. Returns the final computed state

Memory Efficiency

The implementation uses Stream.consume to process events one-at-a-time, so even if you have thousands of events, it won't load them all into memory at once.

Bug Fix Included

This PR also fixes a bug in the Postgres EventStore where reading stream events with FromAndAfter could return duplicate events. The fix ensures subsequent batches use position-based pagination correctly.

🧪 Testing

All existing tests pass, plus we added:

  • 30+ new test cases for EntityFetcher
  • Performance tests with 10, 100, 1,000, and 1,000,000 events
  • Concurrency tests to ensure thread-safety
  • Integration tests with both InMemory and Postgres stores

Run tests with:

nix-shell --run "cabal test core"

📚 Example Domain

The tests include a complete example domain (bank accounts) showing:

  • Event definitions (AccountOpened, MoneyDeposited, etc.)
  • State structure (BankAccountState)
  • Event application logic
  • Full entity lifecycle

This serves as documentation by example!

🤔 Design Decisions

Why streaming? We could have loaded all events into an array first, but that would use too much memory for entities with long histories. Streaming keeps memory usage constant.

Why a separate service? This keeps the EventStore focused on storage/retrieval, while EntityFetcher handles the business logic of state reconstruction. Follows single responsibility principle.

Why pass the reduce function? This keeps the fetcher generic - it doesn't need to know about your specific domain events. You bring your own logic.

🎓 Learning Resources

If you're new to event sourcing, here's what you need to know:

  • Event: Something that happened (e.g., "money was deposited")
  • Entity: A thing in your system (e.g., a bank account)
  • State: What the entity looks like right now
  • Reduction: Applying an event to update the state

The EntityFetcher handles the reduction part for you!

✅ Checklist

  • Code compiles without warnings
  • All existing tests pass
  • New functionality has test coverage
  • Documentation by example included (test domain)
  • Memory-efficient implementation
  • Works with both InMemory and Postgres stores
  • Bug fix for duplicate event pagination included

💬 Questions?

If anything is unclear or you get stuck trying to use this:

  1. Check the test files for usage examples
  2. Ask in Discord - we're here to help!
  3. Remember: If you're stuck for >15 minutes, that's a bug in our system - let us know!

Time to review: ~10-15 minutes to understand the concepts, ~5 minutes to review the code structure

Summary by CodeRabbit

  • Bug Fixes

    • Fixed batch processing for event stream reads to correctly handle relative position tracking across multiple batches, preventing duplicate events in subsequent reads.
  • Tests

    • Expanded test coverage for entity fetching functionality with comprehensive scenarios including state management, error handling, and performance boundaries.

@NickSeagull NickSeagull self-assigned this Nov 18, 2025
@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Code Review for Entity Reduction PR

I've completed my review of this entity reduction implementation. Here's my comprehensive feedback:

Todo List:

  • Set up development environment and build project
  • Analyze new Service.Entity modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/Entity/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/Entity/Core.hs:70,77,79
    • Descriptive type parameters (state event vs a b): core/service/Service/Entity/Core.hs:47-48
    • Pipe operator usage: core/service/Service/Entity/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.Entity as a simple re-export module
    • Service.Entity.Core containing the core implementation
    • Comprehensive test modules under Test.Service.Entity.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/Entity/Core.hs:40-45

⚠️ Areas for Improvement:

Potential Bugs and Issues

  1. Memory Usage Concern (core/service/Service/Entity/Core.hs:53-77):

    streamMessages
      |> Stream.consume
        ( \state message -> do

    While the comment mentions "processes events one-by-one without loading everything into memory", this still processes ALL events sequentially. For entities with thousands of events, this could be slow and memory-intensive.

  2. Error Handling Pattern (core/service/Service/Entity/Core.hs:75):

    |> Task.mapError (EventStoreError <. EventStore.StorageFailure)

    The use of <. (composition operator) with error mapping is unclear and could hide the actual error source.

  3. Missing Pattern Completeness (core/service/Service/Entity/Core.hs:67-72):
    The pattern match only handles EventStore.StreamEvent and uses a catch-all _ for other message types. This could silently ignore important message types that should be handled explicitly.

Performance Considerations

  1. No Snapshotting: The current implementation always replays from event 0. For entities with many events, consider implementing snapshotting to avoid replaying thousands of events.

  2. Stream Processing: While using streams is good for memory, the current approach still processes events linearly. Consider implementing:

    • Parallel processing for independent events
    • Batch processing optimizations
    • Caching mechanisms

Security Concerns

  1. Input Validation: No validation on EntityName or StreamId parameters in the fetch function. Consider adding bounds checking or sanitization.

  2. Error Information Leakage: Error types expose internal EventStore.Error details that might leak implementation information to consumers.

Test Coverage

Comprehensive Test Suite (core/testlib/Test/Service/Entity/Fetch/Spec.hs):

The test coverage is excellent with scenarios covering:

  • Empty streams (line 26-43)
  • Single event processing (line 45-81)
  • Multiple event ordering (line 83-143)
  • Independent entity isolation (line 145-216)
  • Performance with 100 events (line 218-297)
  • Version consistency (line 312-384)
  • Account state transitions (line 386-445)

📝 Missing Test Cases:

  1. Error Scenarios: No tests for EventStore failures, malformed events, or network issues
  2. Edge Cases: Empty entity names, null stream IDs, duplicate events
  3. Performance Bounds: No tests verifying memory usage or performance limits
  4. Concurrent Access: No tests for multiple reducers accessing the same entity

Recommendations

  1. Add Error Handling Tests:

    it "handles EventStore errors gracefully" \context -> do
      -- Test error propagation and handling
  2. Consider Snapshotting Interface:

    data EntityReducer state event = EntityReducer
      { fetch :: EntityName -> StreamId -> Task Error state
      , fetchFromSnapshot :: EntityName -> StreamId -> Maybe Int -> Task Error state
      }
  3. Improve Error Types:

    data Error
      = EventStoreError EventStore.Error
      | ReductionError Text
      | InvalidInput Text  -- Add input validation errors
  4. Add Input Validation in the fetch function

Overall Assessment

This is a solid implementation that follows NeoHaskell conventions excellently. The architecture is clean, the tests are comprehensive for happy path scenarios, and the documentation is helpful. The main areas for improvement are around error handling, performance optimization for large event streams, and edge case testing.

The code is ready for review but would benefit from the suggested improvements before production use.


@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Todo List:

  • Set up development environment and build project
  • Analyze new Service.Entity modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback

Code Review for Entity Reduction PR

I've completed my review of this entity reduction implementation. Here's my comprehensive feedback:

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/Entity/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/Entity/Core.hs:74,79
    • Descriptive type parameters (state event vs a b): core/service/Service/Entity/Core.hs:47-48
    • Pipe operator usage: core/service/Service/EntityFetcher/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.EntityFetcher as a simple re-export module
    • Service.EntityFetcher.Core containing the core implementation
    • Comprehensive test modules under Test.Service.EntityFetcher.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/EntityFetcher/Core.hs:40-45

⚠️ Issues Found:

Critical Issue: Duplicate Implementation

MAJOR CONCERN: The PR contains two nearly identical implementations:

  • Service.Entity.Core (core/service/Service/Entity/Core.hs)
  • Service.EntityFetcher.Core (core/service/Service/EntityFetcher/Core.hs)

These modules are functionally identical but have different names. This creates:

  1. Code duplication - Same logic in two places
  2. Maintenance burden - Changes need to be applied twice
  3. Confusion - Unclear which module to use
  4. Missing module exports - Service.Entity.Core is not exposed in nhcore.cabal

Module Structure Issues

  1. Missing Export Module: There's no Service/Entity.hs re-export module, only Service/Entity/Core.hs
  2. Inconsistent Naming: Entity vs EntityFetcher - choose one naming convention
  3. Cabal Exposure: Only Service.EntityFetcher.* modules are exposed in nhcore.cabal:130-131, but Service.Entity.* modules are not

Code Quality Issues

  1. Error Handling Pattern (core/service/Service/EntityFetcher/Core.hs:75):

    |> Task.mapError (EventStoreError <. EventStore.StorageFailure)

    The use of <. (composition operator) with error mapping is unclear and could hide the actual error source.

  2. Missing Pattern Completeness (core/service/Service/EntityFetcher/Core.hs:65-72):

    case message of
      EventStore.StreamEvent event -> do
        -- handle event
      _ -> Task.yield state  -- catch-all

    The catch-all pattern could silently ignore important message types.

  3. Performance Concern (core/service/Service/EntityFetcher/Core.hs:60-75):
    While the comment mentions processing "one-by-one without loading everything into memory", this still processes ALL events sequentially. For entities with thousands of events, this could be slow.

Test Coverage

Comprehensive Test Suite:

The test coverage is excellent with scenarios covering:

  • Empty streams (core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs:26-43)
  • Single event processing (line 45-81)
  • Multiple event ordering (line 83-143)
  • Independent entity isolation (line 145-216)
  • Performance with 100 events (line 218-297)
  • Version consistency (line 312-384)
  • Account state transitions (line 386-445)

📝 Missing Test Cases:

  1. Error Scenarios: No tests for EventStore failures or malformed events
  2. Edge Cases: Empty entity names, concurrent access patterns
  3. Performance Bounds: No tests verifying memory usage limits

Security and Performance

  1. Input Validation: No validation on EntityName or StreamId parameters
  2. No Snapshotting: Always replays from event 0 - consider snapshotting for large event streams
  3. Error Information: Error types could leak internal implementation details

Recommendations

1. Resolve Duplication (Critical):

Choose either Service.Entity or Service.EntityFetcher and remove the duplicate:

-- Option A: Keep EntityFetcher, remove Entity
-- Option B: Keep Entity, remove EntityFetcher
-- Don't keep both with identical functionality

2. Fix Module Exports:

Add missing re-export module and update nhcore.cabal consistently

3. Improve Error Handling:

-- Instead of: |> Task.mapError (EventStoreError <. EventStore.StorageFailure)  
-- Use: |> Task.mapError EventStoreError

4. Add Input Validation:

fetch :: EntityName -> StreamId -> Task Error state
fetch entityName streamId = do
  -- Add validation here
  when (Text.isEmpty entityName.value) $
    Task.throw (InvalidInput "Entity name cannot be empty")
  -- Continue with fetch logic...

Overall Assessment

The implementation follows NeoHaskell conventions well and provides solid functionality, but the critical duplication issue must be resolved before merging. The test coverage is comprehensive for happy path scenarios. Choose one naming convention and remove the duplicate implementation.

Recommended Actions Before Merge:

  1. ✅ Remove duplicate implementation (keep one of Entity/EntityFetcher)
  2. ✅ Add missing re-export module
  3. ✅ Update cabal file consistently
  4. ✅ Fix error composition pattern
  5. ✅ Add input validation

@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude encountered an error —— View job


Todo List:

  • Set up development environment and build project
  • Analyze new Service.Entity modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback

I'll analyze this entity reduction implementation and provide a thorough code review.

@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude Code is working…

I'll analyze this and get back to you.

View job run

@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Code Review for Entity Reduction PR

Todo List:

  • Set up development environment and build project
  • Analyze new Service.Entity modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/EntityFetcher/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/EntityFetcher/Core.hs:70,77,79
    • Descriptive type parameters (state event vs a b): core/service/Service/EntityFetcher/Core.hs:47-48
    • Pipe operator usage: core/service/Service/EntityFetcher/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.EntityFetcher as a simple re-export module
    • Service.EntityFetcher.Core containing the core implementation
    • Comprehensive test modules under Test.Service.EntityFetcher.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/EntityFetcher/Core.hs:35-45

⚠️ Critical Issues:

1. Code Duplication (MUST FIX)

MAJOR CONCERN: Two nearly identical implementations exist:

  • Service.Entity.Core (core/service/Service/Entity/Core.hs)
  • Service.EntityFetcher.Core (core/service/Service/EntityFetcher/Core.hs)

The only differences are:

  • Module/type names (EntityReducer vs EntityFetcher)
  • Better documentation in EntityFetcher
  • Missing re-export module for Service.Entity

Issues Created:

  1. Code duplication - Same logic maintained in two places
  2. Maintenance burden - Bug fixes need to be applied twice
  3. Confusion - Unclear which module to use
  4. Incomplete exposure - Service.Entity.* modules not in nhcore.cabal:130-131

2. Code Quality Issues

Error Handling Pattern (core/service/Service/EntityFetcher/Core.hs:75):

|> Task.mapError (EventStoreError <. EventStore.StorageFailure)
  • The <. composition with error mapping is unclear and could hide error sources
  • Inconsistent with the direct EventStoreError mapping on line 57

Missing Pattern Completeness (core/service/Service/EntityFetcher/Core.hs:65-72):

case message of
  EventStore.StreamEvent event -> do
    -- handle event
  _ -> Task.yield state  -- catch-all

The catch-all pattern could silently ignore important message types that should be handled explicitly.

3. Performance Considerations

Memory Usage (core/service/Service/EntityFetcher/Core.hs:60-75):

While the comment claims to process "one-by-one without loading everything into memory", this still:

  • Processes ALL events sequentially for every fetch
  • No snapshotting mechanism for entities with thousands of events
  • Could become slow for long event histories

No Optimization Features:

  • No caching mechanism
  • No batch processing
  • No parallel event processing where possible

4. Security Concerns

  1. Input Validation: No validation on EntityName or StreamId parameters in the fetch function
  2. Error Information: Error types could expose internal EventStore.Error details to consumers

5. Test Coverage

Comprehensive Test Suite (core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs):

Excellent coverage with 15+ scenarios:

  • Empty streams, single/multiple events (lines 26-143)
  • Entity isolation (lines 145-216)
  • Performance with 100 events (lines 218-297)
  • Concurrent access patterns (lines 464-507)
  • Edge cases (empty names, long names, rapid fetches)
  • Version consistency and state transitions

📝 Missing Test Cases:

  1. Error Scenarios: No tests for EventStore failures, malformed events, or network issues
  2. Input Validation: No tests for invalid entity names or stream IDs
  3. Memory Bounds: No tests verifying memory usage with very large event streams

Recommendations

1. Resolve Duplication (Critical):

Choose either Service.Entity or Service.EntityFetcher - don't maintain both:

-- Recommended: Keep EntityFetcher (better documentation)
-- Remove: Service.Entity.* modules
-- Update: cabal file to be consistent

2. Fix Error Composition:

-- Instead of: |> Task.mapError (EventStoreError <. EventStore.StorageFailure)  
-- Use: |> Task.mapError EventStoreError

3. Add Input Validation:

fetch :: EntityName -> StreamId -> Task Error state
fetch entityName streamId = do
  when (Text.isEmpty entityName.value) $
    Task.throw (InvalidInput "Entity name cannot be empty")
  -- Continue with fetch logic...

4. Improve Error Types:

data Error
  = EventStoreError EventStore.Error
  | ReductionError Text
  | InvalidInput Text  -- Add input validation errors

5. Consider Future Enhancements:

  • Snapshotting interface for large event streams
  • Caching mechanisms for frequently accessed entities
  • Batch processing optimizations

Overall Assessment

This is a solid implementation that follows NeoHaskell conventions excellently and provides comprehensive test coverage for happy path scenarios. The architecture is clean and the documentation is helpful.

Critical blocker: The code duplication must be resolved before merging.

Recommendation: Keep Service.EntityFetcher (better documented), remove Service.Entity, and address the error handling patterns. The implementation is otherwise ready for production use.


@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Code Review for Entity Reduction PR

Todo List:

  • Set up development environment and build project
  • Analyze new Service.Entity modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/EntityFetcher/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/EntityFetcher/Core.hs:70,77,79
    • Descriptive type parameters (state event vs a b): core/service/Service/EntityFetcher/Core.hs:47-48
    • Pipe operator usage: core/service/Service/EntityFetcher/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.EntityFetcher as a simple re-export module
    • Service.EntityFetcher.Core containing the core implementation
    • Comprehensive test modules under Test.Service.EntityFetcher.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/EntityFetcher/Core.hs:35-45

⚠️ Critical Issues:

1. Code Duplication (MUST FIX)

MAJOR CONCERN: Two nearly identical implementations exist:

  • Service.Entity.Core (core/service/Service/Entity/Core.hs)
  • Service.EntityFetcher.Core (core/service/Service/EntityFetcher/Core.hs)

The only differences are:

  • Module/type names (EntityReducer vs EntityFetcher)
  • Better documentation in EntityFetcher
  • Missing re-export module for Service.Entity

Issues Created:

  1. Code duplication - Same logic maintained in two places
  2. Maintenance burden - Bug fixes need to be applied twice
  3. Confusion - Unclear which module to use
  4. Incomplete exposure - Service.Entity.* modules not in nhcore.cabal:130-131

2. Code Quality Issues

Error Handling Pattern (core/service/Service/EntityFetcher/Core.hs:75):

|> Task.mapError (EventStoreError <. EventStore.StorageFailure)
  • The <. composition with error mapping is unclear and could hide error sources
  • Inconsistent with the direct EventStoreError mapping on line 57

Missing Pattern Completeness (core/service/Service/EntityFetcher/Core.hs:65-72):

case message of
  EventStore.StreamEvent event -> do
    -- handle event
  _ -> Task.yield state  -- catch-all

The catch-all pattern could silently ignore important message types that should be handled explicitly.

3. Performance Considerations

Memory Usage (core/service/Service/EntityFetcher/Core.hs:60-75):

While the comment claims to process "one-by-one without loading everything into memory", this still:

  • Processes ALL events sequentially for every fetch
  • No snapshotting mechanism for entities with thousands of events
  • Could become slow for long event histories

No Optimization Features:

  • No caching mechanism
  • No batch processing
  • No parallel event processing where possible

4. Security Concerns

  1. Input Validation: No validation on EntityName or StreamId parameters in the fetch function
  2. Error Information: Error types could expose internal EventStore.Error details to consumers

5. Test Coverage

Comprehensive Test Suite (core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs):

Excellent coverage with 15+ scenarios:

  • Empty streams, single/multiple events (lines 26-143)
  • Entity isolation (lines 145-216)
  • Performance with 100 events (lines 218-297)
  • Concurrent access patterns (lines 464-507)
  • Edge cases (empty names, long names, rapid fetches)
  • Version consistency and state transitions

📝 Missing Test Cases:

  1. Error Scenarios: No tests for EventStore failures, malformed events, or network issues
  2. Input Validation: No tests for invalid entity names or stream IDs
  3. Memory Bounds: No tests verifying memory usage with very large event streams

Recommendations

1. Resolve Duplication (Critical):

Choose either Service.Entity or Service.EntityFetcher - don't maintain both:

-- Recommended: Keep EntityFetcher (better documentation)
-- Remove: Service.Entity.* modules
-- Update: cabal file to be consistent

2. Fix Error Composition:

-- Instead of: |> Task.mapError (EventStoreError <. EventStore.StorageFailure)  
-- Use: |> Task.mapError EventStoreError

3. Add Input Validation:

fetch :: EntityName -> StreamId -> Task Error state
fetch entityName streamId = do
  when (Text.isEmpty entityName.value) $
    Task.throw (InvalidInput "Entity name cannot be empty")
  -- Continue with fetch logic...

4. Improve Error Types:

data Error
  = EventStoreError EventStore.Error
  | ReductionError Text
  | InvalidInput Text  -- Add input validation errors

5. Consider Future Enhancements:

  • Snapshotting interface for large event streams
  • Caching mechanisms for frequently accessed entities
  • Batch processing optimizations

Overall Assessment

This is a solid implementation that follows NeoHaskell conventions excellently and provides comprehensive test coverage for happy path scenarios. The architecture is clean and the documentation is helpful.

Critical blocker: The code duplication must be resolved before merging.

Recommendation: Keep Service.EntityFetcher (better documented), remove Service.Entity, and address the error handling patterns. The implementation is otherwise ready for production use.


@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Code Review for Entity Reduction PR

Todo List:

  • Set up development environment and build project
  • Analyze new Service.EntityFetcher modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback on code quality, bugs, performance, and security

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/EntityFetcher/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/EntityFetcher/Core.hs:70,77,79
    • Descriptive type parameters (state event vs a b): core/service/Service/EntityFetcher/Core.hs:47-48
    • Pipe operator usage: core/service/Service/EntityFetcher/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.EntityFetcher as a simple re-export module
    • Service.EntityFetcher.Core containing the core implementation
    • Comprehensive test modules under Test.Service.EntityFetcher.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/EntityFetcher/Core.hs:35-45

Issues Found

⚠️ Code Quality Issues:

  1. Error Composition Pattern (core/service/Service/EntityFetcher/Core.hs:75):

    |> Task.mapError (EventStoreError <. EventStore.StorageFailure)
    • The <. composition with error mapping is unclear and inconsistent with direct EventStoreError mapping on line 57
    • Could hide actual error sources
    • Recommendation: Use consistent error handling: |> Task.mapError EventStoreError
  2. Incomplete Pattern Matching (core/service/Service/EntityFetcher/Core.hs:65-72):

    case message of
      EventStore.StreamEvent event -> do
        -- handle event
      _ -> Task.yield state  -- catch-all
    • The catch-all pattern could silently ignore important message types
    • Recommendation: Handle specific message types explicitly

Performance Considerations

  1. Sequential Event Processing (core/service/Service/EntityFetcher/Core.hs:60-75):

    • While the comment claims to process "one-by-one without loading everything into memory", this still processes ALL events sequentially for every fetch
    • For entities with thousands of events, this could become slow
    • Missing Optimization: No snapshotting mechanism for large event histories
  2. No Caching: Every fetch replays from the beginning with no optimization features:

    • No caching mechanism for frequently accessed entities
    • No batch processing optimizations
    • No parallel processing where applicable

Security Concerns

  1. Input Validation: No validation on EntityName or StreamId parameters in the fetch function

    • Could accept malformed or empty entity names
    • Recommendation: Add input validation
  2. Error Information: Error types expose internal EventStore.Error details that might leak implementation information

Test Coverage Assessment

Comprehensive Test Suite (core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs):

Excellent coverage with 15+ test scenarios:

  • Basic functionality: Empty streams, single/multiple events (lines 26-143)
  • Entity isolation: Independent entities don't affect each other (lines 145-216)
  • Performance testing: 100 events processing (lines 218-297)
  • Concurrency testing: Concurrent fetches and writes (lines 464-507)
  • Edge cases: Empty entity names, error scenarios
  • State transitions: Complex account state changes

Strong points:

  • Tests use realistic BankAccount domain model
  • Good use of property-based testing patterns
  • Comprehensive concurrent access testing
  • Performance validation with many events

📝 Missing Test Cases:

  1. Error Handling: No tests for EventStore failures, network issues, or malformed events
  2. Input Validation: No tests for invalid entity names or stream IDs
  3. Memory Bounds: No tests verifying memory usage with very large event streams
  4. Error Recovery: No tests for partial failures or retries

Best Practices Assessment

Follows NeoHaskell Conventions:

  • Descriptive type parameters (state event not a b)
  • Proper import structure with qualified imports
  • Use of Task.yield instead of pure/return
  • Pipe operator usage
  • No forbidden patterns (no let..in, where, point-free style)

Good Code Structure:

  • Clear separation of concerns
  • Excellent documentation with examples
  • Consistent error handling patterns (mostly)
  • Proper module organization

Recommendations

1. Fix Error Composition (Priority: High):

-- Instead of: |> Task.mapError (EventStoreError <. EventStore.StorageFailure)  
-- Use: |> Task.mapError EventStoreError

2. Add Input Validation (Priority: Medium):

fetch :: EntityName -> StreamId -> Task Error state
fetch entityName streamId = do
  when (Text.isEmpty entityName.value) $
    Task.throw (InvalidInput "Entity name cannot be empty")
  -- Continue with fetch logic...

3. Improve Error Types (Priority: Medium):

data Error
  = EventStoreError EventStore.Error
  | ReductionError Text
  | InvalidInput Text  -- Add input validation errors

4. Consider Future Enhancements (Priority: Low):

  • Snapshotting interface for large event streams
  • Caching mechanisms for frequently accessed entities
  • Batch processing optimizations

Overall Assessment

This is a solid implementation that follows NeoHaskell conventions excellently and provides comprehensive test coverage for happy path scenarios. The architecture is clean and the documentation is helpful.

Key Strengths:

  • Excellent adherence to NeoHaskell style guidelines
  • Comprehensive test coverage with realistic scenarios
  • Clean module organization and documentation
  • Good performance testing with 100+ events

Areas for Improvement:

  • Error handling consistency (composition pattern)
  • Input validation
  • Pattern matching completeness

Recommendation: The code is ready for production use after addressing the error composition pattern. The other improvements can be made incrementally. The implementation provides a solid foundation for entity reduction functionality.


@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Code Review for Entity Reduction PR

Todo List:

  • Set up development environment and build project
  • Analyze new Service.EntityFetcher modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback on code quality, bugs, performance, and security

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/EntityFetcher/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/EntityFetcher/Core.hs:70,77,79
    • Descriptive type parameters (state event vs a b): core/service/Service/EntityFetcher/Core.hs:47-48
    • Pipe operator usage: core/service/Service/EntityFetcher/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.EntityFetcher as a simple re-export module
    • Service.EntityFetcher.Core containing the core implementation
    • Comprehensive test modules under Test.Service.EntityFetcher.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/EntityFetcher/Core.hs:35-45

Issues Found

⚠️ Code Quality Issues:

  1. Error Composition Pattern (core/service/Service/EntityFetcher/Core.hs:75):

    |> Task.mapError (EventStoreError <. EventStore.StorageFailure)
    • The <. composition with error mapping is unclear and inconsistent with direct EventStoreError mapping on line 57
    • Could hide actual error sources
    • Recommendation: Use consistent error handling: |> Task.mapError EventStoreError
  2. Incomplete Pattern Matching (core/service/Service/EntityFetcher/Core.hs:65-72):

    case message of
      EventStore.StreamEvent event -> do
        -- handle event
      _ -> Task.yield state  -- catch-all
    • The catch-all pattern could silently ignore important message types
    • Recommendation: Handle specific message types explicitly

Performance Considerations

  1. Sequential Event Processing (core/service/Service/EntityFetcher/Core.hs:60-75):

    • While the comment claims to process "one-by-one without loading everything into memory", this still processes ALL events sequentially for every fetch
    • For entities with thousands of events, this could become slow
    • Missing Optimization: No snapshotting mechanism for large event histories
  2. No Caching: Every fetch replays from the beginning with no optimization features:

    • No caching mechanism for frequently accessed entities
    • No batch processing optimizations
    • No parallel processing where applicable

Security Concerns

  1. Input Validation: No validation on EntityName or StreamId parameters in the fetch function

    • Could accept malformed or empty entity names
    • Recommendation: Add input validation
  2. Error Information: Error types expose internal EventStore.Error details that might leak implementation information

Test Coverage Assessment

Comprehensive Test Suite (core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs):

Excellent coverage with 15+ test scenarios:

  • Basic functionality: Empty streams, single/multiple events (lines 26-143)
  • Entity isolation: Independent entities don't affect each other (lines 145-216)
  • Performance testing: 100 events processing (lines 218-297)
  • Concurrency testing: Concurrent fetches and writes (lines 464-507)
  • Edge cases: Empty entity names, error scenarios
  • State transitions: Complex account state changes

Strong points:

  • Tests use realistic BankAccount domain model
  • Good use of property-based testing patterns
  • Comprehensive concurrent access testing
  • Performance validation with many events

📝 Missing Test Cases:

  1. Error Handling: No tests for EventStore failures, network issues, or malformed events
  2. Input Validation: No tests for invalid entity names or stream IDs
  3. Memory Bounds: No tests verifying memory usage with very large event streams
  4. Error Recovery: No tests for partial failures or retries

Best Practices Assessment

Follows NeoHaskell Conventions:

  • Descriptive type parameters (state event not a b)
  • Proper import structure with qualified imports
  • Use of Task.yield instead of pure/return
  • Pipe operator usage
  • No forbidden patterns (no let..in, where, point-free style)

Good Code Structure:

  • Clear separation of concerns
  • Excellent documentation with examples
  • Consistent error handling patterns (mostly)
  • Proper module organization

Recommendations

1. Fix Error Composition (Priority: High):

-- Instead of: |> Task.mapError (EventStoreError <. EventStore.StorageFailure)  
-- Use: |> Task.mapError EventStoreError

2. Add Input Validation (Priority: Medium):

fetch :: EntityName -> StreamId -> Task Error state
fetch entityName streamId = do
  when (Text.isEmpty entityName.value) $
    Task.throw (InvalidInput "Entity name cannot be empty")
  -- Continue with fetch logic...

3. Improve Error Types (Priority: Medium):

data Error
  = EventStoreError EventStore.Error
  | ReductionError Text
  | InvalidInput Text  -- Add input validation errors

4. Consider Future Enhancements (Priority: Low):

  • Snapshotting interface for large event streams
  • Caching mechanisms for frequently accessed entities
  • Batch processing optimizations

Overall Assessment

This is a solid implementation that follows NeoHaskell conventions excellently and provides comprehensive test coverage for happy path scenarios. The architecture is clean and the documentation is helpful.

Key Strengths:

  • Excellent adherence to NeoHaskell style guidelines
  • Comprehensive test coverage with realistic scenarios
  • Clean module organization and documentation
  • Good performance testing with 100+ events

Areas for Improvement:

  • Error handling consistency (composition pattern)
  • Input validation
  • Pattern matching completeness

Recommendation: The code is ready for production use after addressing the error composition pattern. The other improvements can be made incrementally. The implementation provides a solid foundation for entity reduction functionality.

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces the EntityFetcher service for fetching entity state from event stores via event reduction. Test event type is standardized from MyEvent to BankAccountEvent across test suites. A batching fix corrects duplicate event handling in Postgres event stream reads.

Changes

Cohort / File(s) Summary
Core EntityFetcher Module
core/nhcore.cabal, core/service/Service/EntityFetcher.hs, core/service/Service/EntityFetcher/Core.hs
Introduces EntityFetcher service with re-export module structure. Core module defines Error type, EntityFetcher record with fetch function, and new constructor that reduces events into state via supplied reducer. Exposed in cabal.
EntityFetcher Test Infrastructure
core/testlib/Test/Service/EntityFetcher.hs, core/testlib/Test/Service/EntityFetcher/Core.hs, core/testlib/Test/Service/EntityFetcher/Fetch/Context.hs, core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs
Defines BankAccountState/BankAccountEvent test domain, fetcher factory, test context aggregator, and comprehensive spec suite covering single/multiple events, concurrent access, performance boundaries, and error scenarios.
Test Event Type Standardization
core/testlib/Test/Service/EventStore/Core.hs, core/testlib/Test/Service/EventStore.hs, core/testlib/Test/Service/EventStore/BatchValidation/..., core/testlib/Test/Service/EventStore/GlobalStreamOrdering/..., core/testlib/Test/Service/EventStore/IndividualStreamOrdering/..., core/testlib/Test/Service/EventStore/LocalPositionStamping/Spec.hs, core/testlib/Test/Service/EventStore/OptimisticConcurrency/..., core/testlib/Test/Service/EventStore/ReadAllBackwardsFromEnd/..., core/testlib/Test/Service/EventStore/ReadAllForwardsFromStart/..., core/testlib/Test/Service/EventStore/StreamTruncation/..., core/testlib/Test/Service/EventStore/Subscriptions/...
Replaces generic MyEvent type with concrete BankAccountEvent (with AccountOpened, MoneyDeposited, MoneyWithdrawn, AccountClosed constructors) across ~20 test context and spec modules. Updates type signatures, imports, and test data accordingly.
EventStore Integration
core/test/Service/EventStore/InMemorySpec.hs, core/test/Service/EventStore/Postgres/Internal/SubscriptionStoreSpec.hs, core/test/Service/EventStore/PostgresSpec.hs
Wires EntityFetcher into EventStore test suites using BankAccountEvent. Adds fetcher instantiation and EntityFetcher.spec invocation alongside existing EventStore tests.
Postgres Event Batching Fix
core/service/Service/EventStore/Postgres/Internal.hs
Introduces isFirstBatchRef flag in performReadStreamEvents loop to track first batch. Uses original relative position for first batch, switches to position-based FromAndAfter relative for subsequent batches to prevent duplicate event retrieval across batch boundaries.

Sequence Diagram

sequenceDiagram
    participant Test
    participant EF as EntityFetcher
    participant ES as EventStore
    participant Stream as Event Stream

    Test->>EF: new(eventStore, initialState, reducer)
    EF->>ES: initialize with store
    Note over EF: EntityFetcher ready

    Test->>EF: fetch(entityName, streamId)
    EF->>ES: readAllStreamEvents(streamId)
    ES->>Stream: query events
    Stream-->>ES: Event[...]
    ES-->>EF: StreamEvent[]

    loop for each event
        EF->>EF: reducer(event, state)
        Note over EF: accumulate state
    end

    EF-->>Test: final state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus areas requiring attention:
    • core/service/Service/EntityFetcher/Core.hs: Verify event reduction logic, error handling (EventStoreError vs ReductionError), and lazy Stream.consume behavior for large event sequences
    • core/service/Service/EventStore/Postgres/Internal.hs: Confirm batching fix correctly avoids duplicates while preserving order; test edge cases (single batch, large streams, boundary conditions)
    • core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs: Review comprehensive test coverage, particularly concurrency scenarios and performance boundary tests with environment-driven event counts
    • Broad type substitution across test files is repetitive but verify no edge cases missed in test data transformations (especially LocalPositionStamping and IndividualStreamOrdering specs which had logic changes beyond type replacement)

Poem

🏦 Entities arise from events distilled,
Through streams of truth, the state is filled,
Fetcher weaves the tale complete,
From many deposits, balance sweet. 🧮

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main feature introduced: an EntityFetcher service for event sourcing with state reconstruction capabilities.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/entity-reduction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Code Review for Entity Reduction PR

Todo List:

  • Set up development environment and build project
  • Analyze new Service.EntityFetcher modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback on code quality, bugs, performance, and security

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/EntityFetcher/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/EntityFetcher/Core.hs:70,77,79
    • Descriptive type parameters (state event vs a b): core/service/Service/EntityFetcher/Core.hs:47-48
    • Pipe operator usage: core/service/Service/EntityFetcher/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.EntityFetcher as a simple re-export module
    • Service.EntityFetcher.Core containing the core implementation
    • Comprehensive test modules under Test.Service.EntityFetcher.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/EntityFetcher/Core.hs:35-45

Issues Found

⚠️ Code Quality Issues:

  1. Error Composition Pattern (core/service/Service/EntityFetcher/Core.hs:75):

    |> Task.mapError (\errorText -> EventStoreError (EventStore.StorageFailure errorText))
    • This pattern is inconsistent with the direct EventStoreError mapping on line 57
    • The lambda wrapping adds unnecessary complexity
    • Recommendation: Use consistent error handling: |> Task.mapError EventStoreError
  2. Incomplete Pattern Matching (core/service/Service/EntityFetcher/Core.hs:65-72):

    case message of
      EventStore.StreamEvent event -> do
        -- handle event
      _ -> Task.yield state  -- catch-all
    • The catch-all pattern could silently ignore important message types that should be handled explicitly
    • Recommendation: Handle specific message types explicitly or add logging for unknown types

Performance Considerations

  1. Sequential Event Processing (core/service/Service/EntityFetcher/Core.hs:60-75):

    • While the comment claims to process "one-by-one without loading everything into memory", this still processes ALL events sequentially for every fetch
    • For entities with thousands of events, this could become slow
    • Missing Optimization: No snapshotting mechanism for large event histories
  2. No Caching: Every fetch replays from the beginning with no optimization features:

    • No caching mechanism for frequently accessed entities
    • No batch processing optimizations
    • No parallel processing where applicable

Security Concerns

  1. Input Validation: No validation on EntityName or StreamId parameters in the fetch function

    • Could accept malformed or empty entity names
    • Recommendation: Add input validation
  2. Error Information: Error types expose internal EventStore.Error details that might leak implementation information

Test Coverage Assessment

Comprehensive Test Suite (core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs):

Excellent coverage with 15+ test scenarios:

  • Basic functionality: Empty streams, single/multiple events (lines 26-143)
  • Entity isolation: Independent entities don't affect each other (lines 145-216)
  • Performance testing: 100 events processing (lines 218-297)
  • Concurrency testing: Concurrent fetches and writes (lines 464-507)
  • Edge cases: Empty entity names, error scenarios
  • State transitions: Complex account state changes

Strong points:

  • Tests use realistic BankAccount domain model
  • Good use of property-based testing patterns
  • Comprehensive concurrent access testing
  • Performance validation with many events

📝 Missing Test Cases:

  1. Error Handling: No tests for EventStore failures, network issues, or malformed events
  2. Input Validation: No tests for invalid entity names or stream IDs
  3. Memory Bounds: No tests verifying memory usage with very large event streams
  4. Error Recovery: No tests for partial failures or retries

Best Practices Assessment

Follows NeoHaskell Conventions:

  • Descriptive type parameters (state event not a b)
  • Proper import structure with qualified imports
  • Use of Task.yield instead of pure/return
  • Pipe operator usage
  • No forbidden patterns (no let..in, where, point-free style)

Good Code Structure:

  • Clear separation of concerns
  • Excellent documentation with examples
  • Consistent error handling patterns (mostly)
  • Proper module organization

Recommendations

1. Fix Error Composition (Priority: High):

-- Instead of: |> Task.mapError (\errorText -> EventStoreError (EventStore.StorageFailure errorText))
-- Use: |> Task.mapError EventStoreError

2. Add Input Validation (Priority: Medium):

fetch :: EntityName -> StreamId -> Task Error state
fetch entityName streamId = do
  when (Text.isEmpty entityName.value) $
    Task.throw (InvalidInput "Entity name cannot be empty")
  -- Continue with fetch logic...

3. Improve Error Types (Priority: Medium):

data Error
  = EventStoreError EventStore.Error
  | ReductionError Text
  | InvalidInput Text  -- Add input validation errors

4. Consider Future Enhancements (Priority: Low):

  • Snapshotting interface for large event streams
  • Caching mechanisms for frequently accessed entities
  • Batch processing optimizations

Overall Assessment

This is a solid implementation that follows NeoHaskell conventions excellently and provides comprehensive test coverage for happy path scenarios. The architecture is clean and the documentation is helpful.

Key Strengths:

  • Excellent adherence to NeoHaskell style guidelines
  • Comprehensive test coverage with realistic scenarios
  • Clean module organization and documentation
  • Good performance testing with 100+ events

Areas for Improvement:

  • Error handling consistency (composition pattern)
  • Input validation
  • Pattern matching completeness

Recommendation: The code is ready for production use after addressing the error composition pattern. The other improvements can be made incrementally. The implementation provides a solid foundation for entity reduction functionality.

@NickSeagull NickSeagull changed the title wip: entity reduction feat: Add Entity Fetcher Service for Event Sourcing Nov 18, 2025
@claude

claude Bot commented Nov 18, 2025

Copy link
Copy Markdown

Claude finished @NickSeagull's task —— View job


Code Review for Entity Reduction PR

Todo List:

  • Set up development environment and build project
  • Analyze new Service.EntityFetcher modules
  • Review test coverage and quality
  • Check adherence to NeoHaskell coding standards
  • Run tests to ensure stability
  • Provide comprehensive feedback on code quality, bugs, performance, and security

Code Quality and Best Practices

Excellent Areas:

  1. NeoHaskell Style Adherence: The code perfectly follows NeoHaskell conventions:

    • Proper qualified imports with explicit type imports: core/service/Service/EntityFetcher/Core.hs:7-12
    • Use of Task.yield instead of pure/return: core/service/Service/EntityFetcher/Core.hs:70,77,79
    • Descriptive type parameters (state event vs a b): core/service/Service/EntityFetcher/Core.hs:47-48
    • Pipe operator usage: core/service/Service/EntityFetcher/Core.hs:56-57
  2. Clean Architecture: Well-structured module hierarchy with clear separation:

    • Service.EntityFetcher as a simple re-export module
    • Service.EntityFetcher.Core containing the core implementation
    • Comprehensive test modules under Test.Service.EntityFetcher.*
  3. Documentation: Excellent docstrings with practical examples: core/service/Service/EntityFetcher/Core.hs:35-45

Issues Found

⚠️ Code Quality Issues:

  1. Error Composition Pattern (core/service/Service/EntityFetcher/Core.hs:75):

    |> Task.mapError (\errorText -> EventStoreError (EventStore.StorageFailure errorText))
    • This pattern is inconsistent with the direct EventStoreError mapping on line 57
    • The lambda wrapping adds unnecessary complexity
    • Recommendation: Use consistent error handling: |> Task.mapError EventStoreError
  2. Incomplete Pattern Matching (core/service/Service/EntityFetcher/Core.hs:65-72):

    case message of
      EventStore.StreamEvent event -> do
        -- handle event
      _ -> Task.yield state  -- catch-all
    • The catch-all pattern could silently ignore important message types that should be handled explicitly
    • Recommendation: Handle specific message types explicitly or add logging for unknown types

Performance Considerations

  1. Sequential Event Processing (core/service/Service/EntityFetcher/Core.hs:60-75):

    • While the comment claims to process "one-by-one without loading everything into memory", this still processes ALL events sequentially for every fetch
    • For entities with thousands of events, this could become slow
    • Missing Optimization: No snapshotting mechanism for large event histories
  2. No Caching: Every fetch replays from the beginning with no optimization features:

    • No caching mechanism for frequently accessed entities
    • No batch processing optimizations
    • No parallel processing where applicable

Security Concerns

  1. Input Validation: No validation on EntityName or StreamId parameters in the fetch function

    • Could accept malformed or empty entity names
    • Recommendation: Add input validation
  2. Error Information: Error types expose internal EventStore.Error details that might leak implementation information

Test Coverage Assessment

Comprehensive Test Suite (core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs):

Excellent coverage with 15+ test scenarios:

  • Basic functionality: Empty streams, single/multiple events (lines 26-143)
  • Entity isolation: Independent entities don't affect each other (lines 145-216)
  • Performance testing: 100 events processing (lines 218-297)
  • Concurrency testing: Concurrent fetches and writes (lines 464-507)
  • Edge cases: Empty entity names, error scenarios
  • State transitions: Complex account state changes

Strong points:

  • Tests use realistic BankAccount domain model
  • Good use of property-based testing patterns
  • Comprehensive concurrent access testing
  • Performance validation with many events

📝 Missing Test Cases:

  1. Error Handling: No tests for EventStore failures, network issues, or malformed events
  2. Input Validation: No tests for invalid entity names or stream IDs
  3. Memory Bounds: No tests verifying memory usage with very large event streams
  4. Error Recovery: No tests for partial failures or retries

Best Practices Assessment

Follows NeoHaskell Conventions:

  • Descriptive type parameters (state event not a b)
  • Proper import structure with qualified imports
  • Use of Task.yield instead of pure/return
  • Pipe operator usage
  • No forbidden patterns (no let..in, where, point-free style)

Good Code Structure:

  • Clear separation of concerns
  • Excellent documentation with examples
  • Consistent error handling patterns (mostly)
  • Proper module organization

Recommendations

1. Fix Error Composition (Priority: High):

-- Instead of: |> Task.mapError (\errorText -> EventStoreError (EventStore.StorageFailure errorText))
-- Use: |> Task.mapError EventStoreError

2. Add Input Validation (Priority: Medium):

fetch :: EntityName -> StreamId -> Task Error state
fetch entityName streamId = do
  when (Text.isEmpty entityName.value) $
    Task.throw (InvalidInput "Entity name cannot be empty")
  -- Continue with fetch logic...

3. Improve Error Types (Priority: Medium):

data Error
  = EventStoreError EventStore.Error
  | ReductionError Text
  | InvalidInput Text  -- Add input validation errors

4. Consider Future Enhancements (Priority: Low):

  • Snapshotting interface for large event streams
  • Caching mechanisms for frequently accessed entities
  • Batch processing optimizations

Overall Assessment

This is a solid implementation that follows NeoHaskell conventions excellently and provides comprehensive test coverage for happy path scenarios. The architecture is clean and the documentation is helpful.

Key Strengths:

  • Excellent adherence to NeoHaskell style guidelines
  • Comprehensive test coverage with realistic scenarios
  • Clean module organization and documentation
  • Good performance testing with 100+ events

Areas for Improvement:

  • Error handling consistency (composition pattern)
  • Input validation
  • Pattern matching completeness

Recommendation: The code is ready for production use after addressing the error composition pattern. The other improvements can be made incrementally. The implementation provides a solid foundation for entity reduction functionality.

@NickSeagull NickSeagull marked this pull request as ready for review November 18, 2025 15:44
@NickSeagull NickSeagull merged commit f8eaac1 into main Nov 18, 2025
2 of 4 checks passed
@NickSeagull NickSeagull deleted the feat/entity-reduction branch November 18, 2025 15:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3dbac64 and 44c49cc.

📒 Files selected for processing (31)
  • core/nhcore.cabal (2 hunks)
  • core/service/Service/EntityFetcher.hs (1 hunks)
  • core/service/Service/EntityFetcher/Core.hs (1 hunks)
  • core/service/Service/EventStore/Postgres/Internal.hs (2 hunks)
  • core/test/Service/EventStore/InMemorySpec.hs (1 hunks)
  • core/test/Service/EventStore/Postgres/Internal/SubscriptionStoreSpec.hs (2 hunks)
  • core/test/Service/EventStore/PostgresSpec.hs (2 hunks)
  • core/testlib/Test/Service/EntityFetcher.hs (1 hunks)
  • core/testlib/Test/Service/EntityFetcher/Core.hs (1 hunks)
  • core/testlib/Test/Service/EntityFetcher/Fetch/Context.hs (1 hunks)
  • core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs (1 hunks)
  • core/testlib/Test/Service/EventStore.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/BatchValidation/Context.hs (1 hunks)
  • core/testlib/Test/Service/EventStore/BatchValidation/Spec.hs (1 hunks)
  • core/testlib/Test/Service/EventStore/Core.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/GlobalStreamOrdering/Context.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/GlobalStreamOrdering/Spec.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/IndividualStreamOrdering/Context.hs (1 hunks)
  • core/testlib/Test/Service/EventStore/IndividualStreamOrdering/Spec.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/LocalPositionStamping/Spec.hs (7 hunks)
  • core/testlib/Test/Service/EventStore/OptimisticConcurrency/Context.hs (1 hunks)
  • core/testlib/Test/Service/EventStore/OptimisticConcurrency/Spec.hs (11 hunks)
  • core/testlib/Test/Service/EventStore/ReadAllBackwardsFromEnd/Context.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/ReadAllBackwardsFromEnd/Spec.hs (3 hunks)
  • core/testlib/Test/Service/EventStore/ReadAllForwardsFromStart/Context.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/ReadAllForwardsFromStart/Spec.hs (3 hunks)
  • core/testlib/Test/Service/EventStore/StreamTruncation/Context.hs (1 hunks)
  • core/testlib/Test/Service/EventStore/StreamTruncation/Spec.hs (1 hunks)
  • core/testlib/Test/Service/EventStore/Subscriptions/Context.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/Subscriptions/SimpleSpec.hs (2 hunks)
  • core/testlib/Test/Service/EventStore/Subscriptions/Spec.hs (12 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: tests-linux
  • GitHub Check: tests-macos
🔇 Additional comments (44)
core/test/Service/EventStore/Postgres/Internal/SubscriptionStoreSpec.hs (2)

16-16: BankAccountEvent import aligns this spec with the new test domain

Summoning your attention: this import correctly centralizes the subscription tests on BankAccountEvent, matching the rest of the EventStore test helpers and avoiding any leftover MyEvent usage.


315-321: createTestEvent now consistently uses BankAccountEvent

This helper’s signature and payload now agree (Event BankAccountEvent with MoneyDeposited {amount = 100}), keeping dispatch tests aligned with the new domain type without introducing extra complexity.

core/nhcore.cabal (2)

130-131: EntityFetcher modules are correctly surfaced in the public API

You have rightly exposed Service.EntityFetcher and Service.EntityFetcher.Core, making the new fetcher functionality available to downstream users in line with the rest of the service modules.


155-158: EntityFetcher test scaffolding integrated consistently with existing testlib exposure

The new Test.Service.EntityFetcher* modules follow the established pattern of exposing test helpers via testlib, which will keep higher-level specs and examples coherent around the BankAccount/EntityFetcher domain.

core/testlib/Test/Service/EventStore/BatchValidation/Context.hs (1)

11-24: Context now correctly targets EventStore BankAccountEvent

This Context/initialize pair cleanly migrates to EventStore BankAccountEvent, keeping batch-validation tests aligned with the shared BankAccount domain while preserving the simple, composable API.

core/testlib/Test/Service/EventStore/Core.hs (2)

1-27: BankAccountEvent test domain is well-shaped and cohesive

The new BankAccountEvent ADT, along with its JSON and Default instances, forms a clear, reusable test domain that matches how the rest of the suite now reasons about bank-account behavior. This is an appropriate consolidation over the previous MyEvent.


30-49: newInsertion correctly stamps local positions for BankAccountEvent insertions

newInsertion now yields Event.Insertion BankAccountEvent with a fixed MoneyDeposited {amount = 10} payload and derives localPosition from the provided index. This is simple, deterministic, and perfectly suited for ordering and pagination tests.

core/testlib/Test/Service/EventStore/LocalPositionStamping/Spec.hs (7)

18-22: Spec now cleanly targets EventStore BankAccountEvent

The import and spec type signature have been correctly migrated to EventStore BankAccountEvent, bringing this suite in line with the unified BankAccount test domain.


31-33: Primary stamping test uses meaningful BankAccountEvent scenarios

Switching to AccountOpened, MoneyDeposited, and MoneyWithdrawn gives this test a concrete, domain-relevant sequence while still exercising local-position stamping over three events.


88-93: Stronger assertion that all subscription events carry localPosition

The universal check over captured ensures no subscriber ever sees an event without a stamped localPosition, turning a previously implicit guarantee into an explicit, enforced contract.


95-111: Subscription test now validates presence of both positions 0 and 1

The hasPosition helper and subsequent checks guarantee that exactly one event with position 0 and exactly one with position 1 were delivered, catching both missing and duplicated local-position scenarios.


117-124: Batch insertion test correctly uses BankAccountEvent variants

firstBatch and secondBatch now exercise a realistic mixture of BankAccount events while still asserting local positions 0–4, preserving the essence of the local-position derivation test.


158-179: Explicit localPosition preservation test updated to BankAccountEvent

The two manual insertions using AccountOpened and MoneyDeposited with explicit positions 0 and 1 keep this test focused on “do not override caller-provided localPosition” while embracing the new domain type.


219-229: Auto-assignment test now uses BankAccountEvent while preserving intent

The two MoneyDeposited events without localPosition rely on the store to assign sequential positions 0 and 1; the test logic remains sound and now matches the unified BankAccount event family.

core/testlib/Test/Service/EventStore/ReadAllBackwardsFromEnd/Spec.hs (1)

14-14: Behold, the type refactoring is sound.

The systematic migration from MyEvent to BankAccountEvent across all type signatures and imports is executed with precision. The test logic remains unchanged while now operating upon the standardized event type.

Also applies to: 20-20, 35-35, 83-83

core/testlib/Test/Service/EventStore/StreamTruncation/Context.hs (1)

10-10: The type alignment is ordained.

The Context module now bears the BankAccountEvent type throughout its definition, from import to data field to function signature. This maintains perfect consistency with the broader refactoring.

Also applies to: 14-14, 19-19

core/test/Service/EventStore/InMemorySpec.hs (2)

7-9: The type application is decreed.

The EntityFetcher modules are properly imported, and the InMemory store is correctly specialized to BankAccountEvent through type application. This establishes the foundation for the combined test flow.

Also applies to: 15-16


18-23: The combined test flow is blessed.

The integration of EntityFetcher tests is accomplished with elegance. The store is paired with a fetcher constructed via EntityFetcherCore.newFetcher, and the combined entity is submitted to EntityFetcher.spec for validation.

core/testlib/Test/Service/EventStore/StreamTruncation/Spec.hs (1)

14-14: The truncation tests shall operate upon the ordained type.

The StreamTruncation specification is correctly updated to utilize BankAccountEvent, maintaining consistency with the broader refactoring decree.

Also applies to: 19-19

core/testlib/Test/Service/EventStore.hs (1)

7-7: The master test suite embraces the standardized type.

The primary EventStore test module is correctly updated to BankAccountEvent, ensuring all subordinate test specifications operate upon the unified event type.

Also applies to: 19-19

core/testlib/Test/Service/EventStore/ReadAllForwardsFromStart/Spec.hs (1)

14-14: The forward-reading tests are harmonized.

The systematic type migration to BankAccountEvent is executed flawlessly across all signatures and helper annotations. The test logic endures unchanged while operating upon the standardized event type.

Also applies to: 20-20, 35-35, 83-83

core/test/Service/EventStore/PostgresSpec.hs (2)

10-13: The Postgres implementation is properly specialized.

The EntityFetcher modules are imported, and all Postgres operations are correctly specialized to BankAccountEvent through type applications. This establishes type-safe event handling throughout the Postgres test suite.

Also applies to: 31-31, 39-39, 47-47, 54-54


59-64: The Postgres EntityFetcher integration mirrors its InMemory counterpart.

The combined test flow for Postgres follows the same blessed pattern as InMemory: store is paired with fetcher, and the unified entity undergoes EntityFetcher validation. This symmetry across implementations is most pleasing.

core/testlib/Test/Service/EventStore/BatchValidation/Spec.hs (1)

13-13: The validation tests complete the transformation.

The BatchValidation specification embraces BankAccountEvent, bringing the final module into alignment with the standardized event type. The refactoring across all test modules is now complete and consistent.

Also applies to: 17-17

core/testlib/Test/Service/EventStore/OptimisticConcurrency/Context.hs (1)

10-10: Behold, the type migration is sound.

The transition from MyEvent to BankAccountEvent is executed with precision. The import, field type, and function signature align harmoniously, maintaining type consistency across the test infrastructure.

Also applies to: 14-14, 19-23

core/testlib/Test/Service/EventStore/GlobalStreamOrdering/Spec.hs (1)

14-14: The ordering tests now speak the language of domains.

The type migration to BankAccountEvent is complete and correct. The explicit type annotation for matchPositions at line 94 demonstrates commendable type clarity in polymorphic contexts.

Also applies to: 19-20, 94-96

core/service/Service/EntityFetcher.hs (1)

1-5: A worthy facade for thy new service.

This module follows the blessed pattern of separating implementation (Core) from public interface. The re-export strategy provides a clean entry point for consuming code whilst maintaining internal flexibility.

core/testlib/Test/Service/EntityFetcher.hs (1)

1-15: The test scaffolding is architecturally sound.

This module serves as a proper test orchestrator, composing EventStore and EntityFetcher dependencies through Task-based initialization. The delegation to Fetch.spec maintains clean separation of concerns, and the type signatures correctly capture the test requirements with BankAccountEvent and BankAccountState.

core/testlib/Test/Service/EventStore/OptimisticConcurrency/Spec.hs (1)

17-17: The concurrency tests now wield domain-specific events with authority.

The migration from MyEvent to BankAccountEvent is executed comprehensively across all test scenarios. The use of semantically meaningful event types—AccountOpened, MoneyDeposited, MoneyWithdrawn—with appropriate domain fields (initialBalance, amount) enhances test readability whilst preserving the critical concurrency invariants being verified.

Also applies to: 22-22, 41-41, 68-68, 89-89, 160-160, 195-195, 228-228, 284-284, 338-338, 366-366, 387-387

core/testlib/Test/Service/EventStore/IndividualStreamOrdering/Spec.hs (1)

14-14: The stream ordering tests align with the new type hierarchy.

The type signatures are updated correctly throughout, maintaining consistency with the BankAccountEvent migration. The test logic remains pristine, unchanged by this mechanical refactor.

Also applies to: 18-19, 33-34

core/testlib/Test/Service/EventStore/Subscriptions/SimpleSpec.hs (1)

13-13: The subscription tests have embraced the domain event paradigm.

The explicit type annotation for receivedEvents at line 38 demonstrates prudent type specification in the presence of polymorphism, ensuring the ConcurrentVar is correctly typed from initialization. The migration to BankAccountEvent is complete and correct.

Also applies to: 17-18, 38-38

core/testlib/Test/Service/EventStore/ReadAllForwardsFromStart/Context.hs (1)

13-13: The context structure now properly reflects the domain event hierarchy.

The Context data type and its initialization function are updated with perfect consistency. Both the store and generatedEvents fields now correctly reference BankAccountEvent, ensuring type safety throughout the test infrastructure.

Also applies to: 22-25, 28-29

core/testlib/Test/Service/EventStore/ReadAllBackwardsFromEnd/Context.hs (1)

14-29: BankAccountEvent migration is consistent and type-safe

The switch to BankAccountEvent for store, generatedEvents, and initialize’s EventStore parameter is coherent with the new import and preserves the existing behavior of the context.

core/testlib/Test/Service/EventStore/IndividualStreamOrdering/Context.hs (1)

13-27: Type migration to BankAccountEvent is coherent with insertion logic

The update of store, allInsertions, and initialize to use EventStore BankAccountEvent and Event.Insertion BankAccountEvent aligns correctly with newInsertion and the rest of the context; no hidden behavioral changes are introduced.

core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs (1)

21-299: Extensive EntityFetcher coverage is well structured

The spec exercises a wide range of behaviors (empty streams, sequential events, multiple streams, concurrency, large event counts, edge cases) and wires the store/fetcher/context cleanly. This is a solid foundation for validating the new EntityFetcher service.

core/testlib/Test/Service/EventStore/Subscriptions/Context.hs (1)

13-38: Subscriptions context correctly aligned to BankAccountEvent

The migration of store, testEvents, initialize, and createTestEvents to BankAccountEvent is internally consistent and matches the updated import and insertion helper. The subscription tests will now run against the same bank-account domain as the rest of the suite.

core/testlib/Test/Service/EventStore/GlobalStreamOrdering/Context.hs (1)

12-12: The type parameter refactoring is executed with precision.

The transformation from MyEvent to BankAccountEvent has been applied consistently across imports, field types, and function signatures. This standardization strengthens the test infrastructure's semantic clarity.

Also applies to: 19-20, 24-24, 40-40

core/service/Service/EntityFetcher/Core.hs (2)

27-32: The EntityFetcher type design embodies elegant simplicity.

The single-field record with parametric polymorphism over state and event types provides the necessary flexibility while maintaining clarity. The type signature of fetch is precise and well-documented.


46-82: The error handling on line 75 stands verified as correct.

Stream.consume's type signature confirms it returns Task Text accumulator, and the mapError correctly converts Text errors to EventStoreError by wrapping them as EventStore.StorageFailure. The lambda properly lifts the pure reducer function into the Task monad context, satisfying Stream.consume's type requirements. The implementation demonstrates sound streaming architecture with proper error propagation.

core/testlib/Test/Service/EventStore/Subscriptions/Spec.hs (1)

18-18: The type system migration has been executed with unwavering consistency.

Every occurrence of MyEvent has been systematically transformed to BankAccountEvent across function signatures, type annotations, and helper functions. The test logic remains intact while the type semantics are elevated to the domain-specific BankAccountEvent.

Also applies to: 27-29, 36-36, 42-42, 103-103, 165-165, 261-263, 323-323, 371-371, 437-437, 502-502, 577-577, 623-623, 640-640

core/testlib/Test/Service/EntityFetcher/Core.hs (3)

18-39: The BankAccountState model exhibits sound domain design.

The state captures essential account properties: balance, open status, and version for event ordering. The initialState correctly represents an unopened account with zero balance, establishing a valid starting point for event reduction.


43-67: The event application logic faithfully implements state transitions.

Each BankAccountEvent is correctly mapped to its corresponding state mutation:

  • AccountOpened establishes initial balance and open status
  • MoneyDeposited/MoneyWithdrawn adjust balance arithmetically
  • AccountClosed terminates account operations while preserving balance
  • Version increment ensures event ordering integrity

The absence of validation (overdraft checks, state preconditions) is appropriate for a test fixture where events are assumed valid.


71-73: The newFetcher convenience function elegantly encapsulates domain wiring.

This helper correctly binds the EventStore to the bank account domain by providing initialState and applyEvent to EntityFetcher.new, simplifying test setup while maintaining type safety.

core/service/Service/EventStore/Postgres/Internal.hs (1)

765-793: The review comment rests upon an incorrect assumption about how the relative parameter affects query semantics.

The core claim—that using FromAndAfter for backward reads might alter comparison semantics from < to >=—is unfounded. The SQL comparison operator is determined solely by readDirection via toPostgresLocalPositionComparison:

-- Core.hs lines 68-72
toPostgresLocalPositionComparison :: Maybe ReadDirection -> Text
toPostgresLocalPositionComparison dir =
  case dir of
    Just Backwards -> "<"
    _ -> ">="

The relative parameter type (whether FromAndAfter or Before) is only checked for Start/End special cases in the SQL filter construction (Sessions.hs lines 346-349). Otherwise, both constructors fall through to use the same positionComparison derived from readDirection. Since relativePositionToInt64 treats both constructors identically, the actual position value is extracted the same way.

Therefore, using FromAndAfter unconditionally for subsequent batches does not risk breaking backward-read semantics. The backward read will continue to use < comparison as documented in the position-update logic (lines 825–828, 843–846), regardless of whether relative is FromAndAfter or Before.

Likely an incorrect or invalid review comment.

Comment on lines +16 to +19
data Error
= EventStoreError EventStore.Error
| ReductionError Text
deriving (Eq, Show)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The Error type's semantic boundaries require clarification.

The ReductionError constructor wraps Text, yet at line 75 it captures storage failures during stream consumption. This creates semantic ambiguity—is it a reduction error or a storage error? Consider either:

  1. Eliminating ReductionError if the reducer function cannot fail (pure state transitions)
  2. Renaming to StreamConsumptionError if it encompasses both reduction and streaming failures
  3. Using EventStoreError consistently for all EventStore-originated errors
🤖 Prompt for AI Agents
In core/service/Service/EntityFetcher/Core.hs around lines 16–19 the
ReductionError Text constructor is semantically ambiguous because it is used to
capture stream consumption/storage failures; rename ReductionError to
StreamConsumptionError (or alternatively collapse it into EventStoreError) and
update all usages (including the capture at line 75 and any pattern matches,
exports, and tests) to the new constructor so the type clearly denotes
stream/consumption failures rather than reducer logic errors; ensure Eq/Show
still derive and adjust any error construction sites to pass the same Text
payload under the new name.

Comment on lines +14 to +29
data Context = Context
{ store :: EventStore BankAccountEvent,
fetcher :: EntityFetcher BankAccountState BankAccountEvent,
streamId :: Event.StreamId,
entityName :: Event.EntityName
}


initialize ::
Task Text (EventStore BankAccountEvent, EntityFetcher BankAccountState BankAccountEvent) ->
Task Text Context
initialize newStoreAndFetcher = do
(store, fetcher) <- newStoreAndFetcher
streamId <- StreamId.new
let entityName = Event.EntityName "BankAccount"
pure Context {store, fetcher, streamId, entityName}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid hard‑coding the BankAccount entity name in multiple places

Hard‑coding "BankAccount" here is acceptable for now, but it risks drifting from other helpers if the name ever changes. Consider exposing a shared bankAccountEntityName constant from your test core module and using it here to keep entity naming consistent across tests.

🤖 Prompt for AI Agents
In core/testlib/Test/Service/EntityFetcher/Fetch/Context.hs around lines 14 to
29, the entity name "BankAccount" is hard-coded which can drift from other
helpers; import and use a shared constant (e.g. bankAccountEntityName) from the
test core module instead of the literal string, replace let entityName =
Event.EntityName "BankAccount" with using that shared constant, and add the
required import to the top of this module so all tests reference the same source
of truth for the BankAccount entity name.

Comment on lines +299 to +312
it "returns error when fetching from non-existent entity type" \context -> do
let wrongEntityName = Event.EntityName "NonExistentEntity"

result <-
context.fetcher.fetch wrongEntityName context.streamId
|> Task.asResult

-- This should succeed with initial state (empty stream)
-- OR return a specific error depending on implementation
-- For now, let's expect initial state for consistency
result
|> Result.isOk
|> shouldBe True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test description contradicts its actual expectation for non‑existent entity type

The test is named "returns error when fetching from non-existent entity type", but it asserts Result.isOk == True and treats success as the expected outcome. This contradiction will mislead future readers.

Align the description with the behavior (or vice versa). For example:

-      it "returns error when fetching from non-existent entity type" \context -> do
+      it "returns initial state for non-existent entity type" \context -> do
@@
-        -- This should succeed with initial state (empty stream)
-        -- OR return a specific error depending on implementation
-        -- For now, let's expect initial state for consistency
+        -- This should succeed with initial state (empty stream),
+        -- matching the current EntityFetcher behavior for unknown entity types.

Alternatively, if you intend an actual error, update the assertions to check for an Err with the appropriate error variant.

🤖 Prompt for AI Agents
In core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs around lines 299-312
the test name says it "returns error when fetching from non-existent entity
type" but the assertions expect a successful Result.isOk == True; make them
consistent by either renaming the test to reflect the current behavior (e.g.,
"returns initial state when fetching from non-existent entity type" and update
the inline comment accordingly) or change the assertions to expect an error
(pattern-match the Result.Err and assert the specific error variant), and ensure
the test comment matches the chosen behavior.

Comment on lines +572 to +588
describe "Edge Cases" do
before (Context.initialize newStoreAndFetcher) do
it "handles fetching with empty stream ID gracefully" \context -> do
-- This tests that the fetcher doesn't crash with unusual input
-- Note: StreamId is generated via UUID, so we use a valid one but unused stream
unusedStreamId <- StreamId.new
result <-
context.fetcher.fetch context.entityName unusedStreamId
|> Task.asResult

case result of
Ok state -> do
-- Should return initial state for non-existent stream
state.balance |> shouldBe 0
state.version |> shouldBe 0
Err _ -> fail "Expected Ok but got Err"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify test name: this uses an unused stream ID, not an “empty” one

The test "handles fetching with empty stream ID gracefully" generates a fresh StreamId via StreamId.new and never uses it for writes, so it is actually validating behavior for an unused/non-existent stream rather than an “empty stream ID”.

Consider renaming the test to better reflect what’s happening, e.g.:

-        it "handles fetching with empty stream ID gracefully" \context -> do
+        it "handles fetching from an unused stream ID gracefully" \context -> do

This will keep the intent obvious to anyone reading the suite.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe "Edge Cases" do
before (Context.initialize newStoreAndFetcher) do
it "handles fetching with empty stream ID gracefully" \context -> do
-- This tests that the fetcher doesn't crash with unusual input
-- Note: StreamId is generated via UUID, so we use a valid one but unused stream
unusedStreamId <- StreamId.new
result <-
context.fetcher.fetch context.entityName unusedStreamId
|> Task.asResult
case result of
Ok state -> do
-- Should return initial state for non-existent stream
state.balance |> shouldBe 0
state.version |> shouldBe 0
Err _ -> fail "Expected Ok but got Err"
describe "Edge Cases" do
before (Context.initialize newStoreAndFetcher) do
it "handles fetching from an unused stream ID gracefully" \context -> do
-- This tests that the fetcher doesn't crash with unusual input
-- Note: StreamId is generated via UUID, so we use a valid one but unused stream
unusedStreamId <- StreamId.new
result <-
context.fetcher.fetch context.entityName unusedStreamId
|> Task.asResult
case result of
Ok state -> do
-- Should return initial state for non-existent stream
state.balance |> shouldBe 0
state.version |> shouldBe 0
Err _ -> fail "Expected Ok but got Err"
🤖 Prompt for AI Agents
In core/testlib/Test/Service/EntityFetcher/Fetch/Spec.hs around lines 572 to
588, the test title "handles fetching with empty stream ID gracefully" is
misleading because the test generates a new StreamId and checks behavior for an
unused/non-existent stream; rename the test to something like "handles fetching
for unused/non-existent stream ID gracefully" and update the inline comment to
describe that a fresh StreamId is generated and not written to, ensuring the
test name and comments accurately reflect the intent.

Comment on lines +77 to 83
let events = Array.fromLinkedList [AccountOpened {initialBalance = 2000}, MoneyDeposited {amount = 15}]
payload <- Event.payloadFromEvents entityName streamId events
_result <- store.insert payload |> Task.mapError toText

-- Wait a bit for async notification
AsyncTask.sleep 100 |> Task.mapError (\_ -> "timeout")
-- Wait longer for async notifications to complete (increased from 100ms to 200ms to reduce flakiness)
AsyncTask.sleep 200

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Subscription test events updated; timeout increased to reduce flakiness

Using AccountOpened and MoneyDeposited keeps the subscription path aligned with the BankAccount domain. The 200ms sleep is a pragmatic improvement for async delivery, though a future refactor to wait on captured length rather than a fixed delay would yield even more deterministic tests.

🤖 Prompt for AI Agents
In core/testlib/Test/Service/EventStore/LocalPositionStamping/Spec.hs around
lines 77 to 83, replace the fixed AsyncTask.sleep 200 used to wait for async
notifications with a deterministic wait that polls the subscription capture
until it has the expected number of events (or until a short timeout) instead of
sleeping; implement a small retry loop or helper that checks the captured
notification list length equals 2 (or the expected length) with a max timeout
(e.g. 500ms) and fails the test if the condition isn’t met, then remove the
hardcoded sleep.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant