Skip to content

feat: add Commanded.Projections.Ecto - #27

Merged
yordis merged 1 commit into
mainfrom
yordis/feat-add-ecto-projector
Nov 18, 2025
Merged

yordis merged 1 commit into
mainfrom
yordis/feat-add-ecto-projector

Conversation

@yordis

@yordis yordis commented Nov 18, 2025 •

Copy link
Copy Markdown
Member

Note

Integrates Commanded.Projections.Ecto into core with optional Ecto deps, migrations, tests, and docs for building read models via projections.

  • Projections:
    • Add Commanded.Projections.Ecto providing project/2-3, after_update/3, and schema_prefix/1-2 callbacks with idempotent tracking via projection_versions.
    • Include migrations for projection_versions (with and without schema prefixes).
    • Add comprehensive tests and support modules (Repo, test projectors, events, assertions).
  • Config/Build:
    • Add optional deps :ecto and :ecto_sql; update Dialyzer apps and docs module groups.
    • Add ecto tasks to setup/reset aliases.
    • Configure test ecto_repos and Commanded.Projections.Repo in config/test.exs.
  • Docs:
    • New guides: ecto-projections-getting-started and building-read-models-with-ecto; update read model projections and fork differences to reflect built-in Ecto projections.

Written by Cursor Bugbot for commit 12b7941. This will update automatically on new commits. Configure here.

@coderabbitai

coderabbitai Bot commented Nov 18, 2025 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@yordis has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 1 minutes and 5 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e52ff09 and 12b7941.

⛔ Files ignored due to path filters (1)
  • mix.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .github/workflows/ci.yml (1 hunks)
  • config/test.exs (1 hunks)
  • guides/explanations/fork-differences.md (1 hunks)
  • guides/explanations/read-model-projections.md (1 hunks)
  • guides/howtos/building-read-models-with-ecto.md (1 hunks)
  • guides/howtos/ecto-projections-getting-started.md (1 hunks)
  • lib/commanded/projections/ecto.ex (1 hunks)
  • mix.exs (4 hunks)
  • priv/repo/migrations/20170609113553_create_projection_versions.exs (1 hunks)
  • priv/repo/migrations/20170929080308_create_projection_version_with_prefix.exs (1 hunks)
  • test/projections/after_update_callback_test.exs (1 hunks)
  • test/projections/deprecated_projection_test.exs (1 hunks)
  • test/projections/ecto_projection_test.exs (1 hunks)
  • test/projections/error_callback_test.exs (1 hunks)
  • test/projections/init_callback_test.exs (1 hunks)
  • test/projections/projection_version_schema_prefix_test.exs (1 hunks)
  • test/projections/runtime_config_projector_test.exs (1 hunks)
  • test/support/error_projector.ex (1 hunks)
  • test/support/events.ex (1 hunks)
  • test/support/projection.ex (1 hunks)
  • test/support/projection_assertions.ex (1 hunks)
  • test/support/repo.ex (1 hunks)
  • test/support/runtime_config_projector.ex (1 hunks)
  • test/support/test_application.ex (1 hunks)
  • test/test_helper.exs (1 hunks)

Walkthrough

Adds built-in Ecto-backed projections: a new Commanded.Projections.Ecto module with macros and idempotent transactional projection logic, a projections repo, migrations, documentation, test support modules, and an extensive test suite covering callbacks, schema_prefix strategies, and error handling.

Changes

Cohort / File(s) Change Summary
Core Projection Module
lib/commanded/projections/ecto.ex
New Ecto-backed projections implementation: __using__/1 macro, ProjectionVersion schema, update_projection/3, transaction helpers, schema_prefix resolution, optional callbacks (after_update/3, schema_prefix/1/2), and project macro forms (deprecated block and new lambda forms) generating handle/2.
Configuration & Dependencies
config/test.exs, mix.exs
Added Commanded.Projections.Repo to :ecto_repos and test DB config (Sandbox); added optional deps :ecto and :ecto_sql; updated project module groups/exports and Dialyzer PLT; adjusted mix aliases for ecto tasks.
Documentation Guides
guides/explanations/read-model-projections.md, guides/howtos/ecto-projections-getting-started.md, guides/howtos/building-read-models-with-ecto.md
Updated wording to reflect built-in Ecto support; added getting-started and detailed how-to covering schemas, projectors, runtime vs compile-time config, error handling, after_update, and schema_prefix patterns with migration guidance.
Database Migrations
priv/repo/migrations/20170609113553_create_projection_versions.exs, priv/repo/migrations/20170929080308_create_projection_version_with_prefix.exs
New migrations to create projection_versions table (primary key projection_name, last_seen_event_number, timestamps) and a schema-prefixed example migration with up/0 and down/0.
Test Support Modules
test/support/events.ex, test/support/projection.ex, test/support/repo.ex, test/support/projection_assertions.ex, test/support/error_projector.ex, test/support/runtime_config_projector.ex, test/support/test_application.ex
Added test event structs, Projection Ecto schema, Commanded.Projections.Repo (Ecto.Repo), projection assertion helpers, error-projector and runtime-config projector support modules, and a TestApplication using the in-memory event store.
Projection Tests
test/projections/*.exs (ecto_projection_test.exs, deprecated_projection_test.exs, error_callback_test.exs, after_update_callback_test.exs, init_callback_test.exs, projection_version_schema_prefix_test.exs, runtime_config_projector_test.exs)
New comprehensive tests covering event projection flows, deprecation warnings, error callback behavior, after_update invocation, init callback, schema_prefix variants (static, config, function, callbacks), concurrency, idempotency, and runtime config scenarios.
Test Setup
test/test_helper.exs
Starts Commanded.Projections.Repo, creates projections table via inline migration and migrator up, starts ExUnit, and configures Ecto SQL Sandbox in manual mode.
CI Workflow
.github/workflows/ci.yml
Reordered quality-assurance steps to run formatting/compilation/analysis after tests (step order changed; commands unchanged).

Sequence Diagram(s)

sequenceDiagram
    participant ES as Event Store / Handler
    participant Projector as Generated Projector (handle/2)
    participant EctoProj as Commanded.Projections.Ecto
    participant Multi as Ecto.Multi
    participant Repo as Commanded.Projections.Repo
    participant UserFn as user multi_fn
    participant After as after_update/3

    ES->>Projector: handle(event, metadata)
    Projector->>EctoProj: update_projection(event, metadata, user_multi_fn)
    EctoProj->>Multi: build base Multi (incl. ProjectionVersion upsert)
    EctoProj->>UserFn: invoke to extend Multi
    EctoProj->>Repo: Repo.transaction(Multi)
    Repo-->>EctoProj: {:ok, changes} / {:error, reason} / Ecto.StaleEntryError
    alt success
        EctoProj->>After: after_update(event, metadata, changes) (if implemented)
        After-->>EctoProj: :ok | {:error, reason}
    else already-seen / stale
        EctoProj-->>Projector: treat as already processed (no-op)
    end
    EctoProj-->>ES: return handling result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Focus areas:
    • update_projection/3 transaction flow, idempotency, upsert semantics, and Ecto.StaleEntryError handling.
    • schema_prefix resolution logic and defoverridable callback wiring.
    • Macro expansion for project/ forms, deprecation warnings, and generated handle/2.
    • Tests that use the SQL Sandbox and start multiple projector instances for isolation and concurrency.
    • Migration correctness and inline migration in test_helper.exs.

Poem

🐰 I stitched projections with Ecto thread,
Multi hops bounce where events are led.
Prefixes twirl, versions keep the beat,
Callbacks whisper after each small feat.
Hooray — read models snug and neat! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: introducing Commanded.Projections.Ecto as a new feature for Ecto-backed projections.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the new Ecto projection system, documentation, tests, and build configuration.

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.

@yordis
yordis force-pushed the yordis/feat-add-ecto-projector branch from 6512394 to 2cbee05 Compare November 18, 2025 01:28
@yordis
yordis marked this pull request as ready for review November 18, 2025 01:30
@yordis
yordis force-pushed the yordis/feat-add-ecto-projector branch 8 times, most recently from a895ff2 to e52ff09 Compare November 18, 2025 03:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (13)
priv/repo/migrations/20170929080308_create_projection_version_with_prefix.exs (1)

1-19: Migration for prefixed projection_versions is consistent with the base schema

The test.projection_versions table mirrors the default projection_versions schema and the up/down pair (create schema, create table; drop table, drop schema CASCADE) is sound for the intended Postgres test setup. If you ever expect the test schema to be pre-created, you could switch to CREATE SCHEMA IF NOT EXISTS test, but for isolated test DBs this is not required.

If you want to be extra safe about CREATE SCHEMA semantics across Postgres versions, please confirm via local migration runs against the Postgres 17 image used in CI.

lib/commanded/projections/ecto.ex (1)

178-205: Tiny style nit: avoid binding an unused exception in rescue

In the rescue block you bind exception in Ecto.StaleEntryError -> but never use exception. This is harmless, but you could simplify to rescue Ecto.StaleEntryError -> to avoid an unused variable warning and make intent clearer, while keeping the generic exception -> reraise exception, __STACKTRACE__ clause.

Please confirm your Elixir version’s compiler doesn’t already optimize away the unused binding or emit a warning here; if it does, the suggested tweak will silence it.

test/test_helper.exs (1)

5-15: Consider extracting the inline migration to a separate file.

While the inline migration works, it's unconventional. Most Elixir projects keep all migrations in priv/repo/migrations/ for consistency and visibility.

If you prefer to keep it inline, verify that the timestamp 20_170_609_120_000 doesn't conflict with any existing migrations:

#!/bin/bash
# Description: Check for migration timestamp conflicts

# List all migration files and their timestamps
fd -e exs . priv/repo/migrations/ --exec basename | sort

# Check if the timestamp 20170609120000 is used elsewhere
rg -n "20_?170_?609_?120_?000" priv/repo/migrations/
test/support/runtime_config_projector.ex (1)

7-13: Consider simplifying the pattern matching.

The event is pattern-matched twice: once in the function head (line 7) and again in the function body (line 8). You can extract the fields directly in the function head.

Apply this diff to simplify:

- project(%AnEvent{} = event, fn multi ->
-   %AnEvent{name: name, pid: pid} = event
+ project(%AnEvent{name: name, pid: pid}, fn multi ->
guides/howtos/building-read-models-with-ecto.md (1)

7-15: Docs are strong; consider a few minor clarity and wording tweaks

Overall this guide is very clear and matches the new Commanded.Projections.Ecto API well. A few minor polish points you might want to address:

  • In the first example schema (ExampleProjection), later snippets insert a struct including metadata (e.g. %ExampleProjection{name: name, metadata: metadata}). To avoid confusing copy‑paste users, either add field :metadata, :map to the schema example or omit metadata from the insert example so they’re consistent.
  • Wording/grammar nits:
    • “error tagged tuple” → “error‑tagged tuple” (hyphenates the compound adjective).
    • “tenant specific projections” → “tenant‑specific projections” in both occurrences.
    • The sentence “in a similar manner since the errors are caught …” could be shortened to “similarly, since the errors are caught …” to read a bit more smoothly.

These are all editorial; the technical content looks accurate.

Also applies to: 90-99, 160-193, 286-313

test/projections/ecto_projection_test.exs (1)

75-90: Double‑check Ecto sandbox ownership for the concurrent Task.async tests

The two concurrency tests ("should prevent first event being projected more than once" and "should prevent an event being projected more than once"), lines 75–90 and 92–131, run Projector.handle/2 inside Task.async/3 processes. Because handle/2 executes Ecto operations via Repo, these Task processes need appropriate sandbox access.

If your global test setup uses Ecto.Adapters.SQL.Sandbox in :manual mode (and only Sandbox.checkout(Repo) here), those Task processes won’t own a connection and may raise sandbox ownership errors. A common pattern is to make the test connection shared before spawning tasks, for example:

setup do
  start_supervised!(TestApplication)
  :ok = Sandbox.checkout(Repo)
  :ok = Sandbox.mode(Repo, {:shared, self()})
end

or to explicitly Sandbox.allow/3 each spawned Task PID.

If your test_helper already switches this repo to :shared for these tests, then you’re fine; otherwise, I’d adjust the setup to avoid intermittent failures when running the suite concurrently.

Please confirm your Ecto.Adapters.SQL.Sandbox mode for Commanded.Projections.Repo in test/test_helper.exs and, if it’s :manual, consider adding Sandbox.mode(Repo, {:shared, self()}) or equivalent here so the concurrency tests remain stable across environments.

Also applies to: 92-131

test/support/projection_assertions.ex (1)

7-24: Projection assertion helpers match usage; consider slightly richer error reporting in last_seen_event/1

The helpers line up nicely with how the tests use them:

  • assert_projections/2 expects a name field and keeps the assertions terse.
  • assert_seen_event/2 plus last_seen_event/1 map directly onto the projection_versions schema introduced in the migration.

One optional robustness improvement would be to handle unexpected results from SQL.query/3 explicitly, rather than falling through the case and raising a generic CaseClauseError. For example, you could add something like:

case SQL.query(Repo, sql, [projection_name]) do
  {:ok, %{num_rows: 0}} ->
    nil

  {:ok, %{rows: [[last_seen]], num_rows: 1}} ->
    last_seen

  {:error, reason} ->
    flunk("failed to query projection_versions: #{inspect(reason)}")
end

This keeps failures self‑describing if the table is missing or the query changes, which can be helpful when debugging CI.

If you adopt a change like this, please re‑run the projection tests to confirm no callers depend on CaseClauseError being raised today (which seems unlikely, but worth double‑checking).

test/projections/projection_version_schema_prefix_test.exs (3)

19-173: Schema prefix tests provide thorough coverage; only minor naming nit

Nice spread of cases (default, static, config-driven, anonymous functions, and callback-based schema_prefix/1 and /2), which should give strong confidence in the prefix resolution logic. As a minor future-proofing tweak, you might give each projector a unique :name to avoid any chance of cross-test interference if these ever start persisting data.


199-213: Consider loosening the assertion on the exact error message text

This test clearly guards against invalid :schema_prefix configuration, but asserting on the full error string can become brittle if the message wording changes. Matching on a stable substring or using a regex (e.g. containing "expected :schema_prefix option") would keep the intent while being less fragile.


216-225: apply/3 in helper can be replaced with direct module call

Since projector is a module, projector.schema_prefix(event, metadata) would work and let you drop the Credo suppression around apply/3. Not a big deal, but it keeps the helper in line with common Elixir style.

test/support/events.ex (1)

1-33: Event struct definitions are consistent; @enforce_keys would be a nice-to-have

The event structs are straightforward and align with how they’re used in the projections and tests. If you ever want tests to fail fast on missing required fields (like :pid or :schema), adding @enforce_keys to those event modules would make that intent explicit, but it’s not necessary for the current test usage.

test/support/error_projector.ex (2)

1-18: Module wiring looks correct for a test-only projector

The projector is correctly wired with application, name, and repo, and the aliases keep the rest of the module readable. If you run docs generation, consider adding @moduledoc false here to keep this test helper out of generated docs.


47-81: Error callbacks are explicit but could be slightly DRYed up

The four error/3 clauses all follow the same pattern of extracting pid, forwarding the error with send/2, and returning :skip. That explicitness is fine, but if this grows you might consider a small private helper to forward the error and return :skip to reduce duplication.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d03039 and e52ff09.

⛔ Files ignored due to path filters (1)
  • mix.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • .github/workflows/ci.yml (1 hunks)
  • config/test.exs (1 hunks)
  • guides/explanations/read-model-projections.md (1 hunks)
  • guides/howtos/building-read-models-with-ecto.md (1 hunks)
  • guides/howtos/ecto-projections-getting-started.md (1 hunks)
  • lib/commanded/projections/ecto.ex (1 hunks)
  • mix.exs (4 hunks)
  • priv/repo/migrations/20170609113553_create_projection_versions.exs (1 hunks)
  • priv/repo/migrations/20170929080308_create_projection_version_with_prefix.exs (1 hunks)
  • test/projections/after_update_callback_test.exs (1 hunks)
  • test/projections/deprecated_projection_test.exs (1 hunks)
  • test/projections/ecto_projection_test.exs (1 hunks)
  • test/projections/error_callback_test.exs (1 hunks)
  • test/projections/init_callback_test.exs (1 hunks)
  • test/projections/projection_version_schema_prefix_test.exs (1 hunks)
  • test/projections/runtime_config_projector_test.exs (1 hunks)
  • test/support/error_projector.ex (1 hunks)
  • test/support/events.ex (1 hunks)
  • test/support/projection.ex (1 hunks)
  • test/support/projection_assertions.ex (1 hunks)
  • test/support/repo.ex (1 hunks)
  • test/support/runtime_config_projector.ex (1 hunks)
  • test/support/test_application.ex (1 hunks)
  • test/test_helper.exs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
test/projections/ecto_projection_test.exs (2)
lib/commanded/projections/ecto.ex (4)
  • handle (232-236)
  • handle (256-258)
  • handle (269-273)
  • handle (294-296)
test/support/projection_assertions.ex (1)
  • assert_projections (7-11)
test/projections/error_callback_test.exs (1)
test/support/projection_assertions.ex (1)
  • assert_projections (7-11)
test/projections/runtime_config_projector_test.exs (2)
test/support/projection_assertions.ex (1)
  • assert_projections (7-11)
test/projections/error_callback_test.exs (1)
  • stub_event_store (150-163)
test/projections/projection_version_schema_prefix_test.exs (1)
lib/commanded/projections/ecto.ex (4)
  • handle (232-236)
  • handle (256-258)
  • handle (269-273)
  • handle (294-296)
test/projections/after_update_callback_test.exs (1)
lib/commanded/projections/ecto.ex (4)
  • handle (232-236)
  • handle (256-258)
  • handle (269-273)
  • handle (294-296)
test/projections/deprecated_projection_test.exs (2)
lib/commanded/projections/ecto.ex (4)
  • handle (232-236)
  • handle (256-258)
  • handle (269-273)
  • handle (294-296)
test/support/projection_assertions.ex (1)
  • assert_projections (7-11)
lib/commanded/projections/ecto.ex (2)
test/projections/projection_version_schema_prefix_test.exs (1)
  • schema_prefix (222-225)
test/projections/after_update_callback_test.exs (1)
  • after_update (19-25)
🪛 LanguageTool
guides/howtos/building-read-models-with-ecto.md

[grammar] ~162-~162: Use a hyphen to join words.
Context: ... example projector module where an error tagged tuple is explicitly returned from...

(QB_NEW_EN_HYPHEN)


[style] ~162-~162: Consider replacing this phrase with the adverb “similarly” to avoid wordiness.
Context: ...faulty Ecto.Multi database operations in a similar manner since the errors are caught and returne...

(IN_A_X_MANNER)


[grammar] ~286-~286: Use a hyphen to join words.
Context: ... An example usage could be for tenant specific projections where each tenant's...

(QB_NEW_EN_HYPHEN)


[grammar] ~308-~308: Use a hyphen to join words.
Context: ... An example usage could be for tenant specific projections where each tenant's...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
guides/howtos/ecto-projections-getting-started.md

19-19: Dollar signs used before commands without showing output

(MD014, commands-show-output)


42-42: Dollar signs used before commands without showing output

(MD014, commands-show-output)

⏰ 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). (1)
  • GitHub Check: Quality Assurance (1.18.x, 27)
🔇 Additional comments (22)
.github/workflows/ci.yml (1)

80-87: Order of checks after tests looks fine

Running tests first and then format/compile/credo is a reasonable trade-off; the new steps are correctly wired and won’t interfere with the Dialyzer cache/use that follows.

lib/commanded/projections/ecto.ex (2)

1-117: Ecto-backed projection wiring and idempotency logic look solid

This module is a faithful in-tree adaptation of commanded_ecto_projections with updated config keys (:commanded, :ecto_projection_repo / :ecto_projection_schema_prefix), a Code.ensure_loaded?(Ecto) guard, and the same ProjectionVersion-based idempotency pattern (conditional upsert with Ecto.StaleEntryError handling). update_projection/3 correctly:

  • tracks last_seen_event_number per projection_name,
  • uses the optional schema_prefix/2 and projection_versions schema,
  • wraps user multi_fn in a transaction,
  • and distinguishes already-seen events from real projection failures.

From a behaviour standpoint, this should match existing users’ expectations while making the repo configurable from either use options or compile_env.

To double-check behaviour parity with the original library, you can diff this module against lib/projections/ecto.ex in commanded_ecto_projections and run the new projection tests (including replay/idempotency cases) against both versions.


123-223: No issues found; original assessment is correct

Verification confirms the review comment's key claim: the after_update/3 invocation at line 102 uses apply() and returns the result directly without pattern-matching. The return value flows through unchanged, making the widened spec (:ok | {:ok, any} | {:error, any}) safe. The callback contract and schema_prefix handling are well-designed as stated.

mix.exs (1)

75-89: Ecto integration in deps, docs, aliases, and Dialyzer is correctly configured

All changes are properly aligned:

  • Optional Ecto/Ecto.SQL deps prevent forced dependency on consumers
  • Docs grouping and module nesting support the projections story
  • Aliases reference ecto tasks safely within test environment scope
  • Dialyzer plt_add_apps explicitly includes optional apps, which is the correct pattern for handling optional dependencies

The configuration follows Dialyxir best practices and poses no risk to the codebase.

test/support/projection.ex (1)

1-7: LGTM!

The test support schema is simple and appropriate for validating projection functionality.

config/test.exs (1)

42-50: LGTM!

The test configuration correctly sets up a dedicated Ecto repository with Sandbox mode for isolated, concurrent test execution.

test/test_helper.exs (2)

1-3: LGTM!

The repository startup follows standard Ecto test patterns.


17-19: LGTM!

The ExUnit configuration with manual Sandbox mode is correct for concurrent test execution with explicit checkouts.

test/support/test_application.ex (1)

1-8: LGTM!

The test application configuration is appropriate, using an in-memory event store for fast test execution.

test/projections/init_callback_test.exs (2)

7-21: LGTM!

The init/1 callback implementation is clean and correctly implements the test coordination pattern.


28-37: LGTM!

The test correctly verifies that the init/1 callback is invoked on start with the expected configuration.

test/support/repo.ex (1)

1-5: LGTM!

The repository definition is standard and correctly configured for the Postgres adapter.

guides/explanations/read-model-projections.md (1)

7-7: No issues found. The referenced guide files exist and links are correctly formatted.

Both guides/howtos/ecto-projections-getting-started.md and guides/howtos/building-read-models-with-ecto.md are present in the repository. The documentation build configuration (mix.exs) includes all guides via Path.wildcard("guides/**/*.{cheatmd,md}"), and ExDoc converts these markdown files to HTML with the corresponding filenames. The link format is consistent with other guide references in the codebase.

priv/repo/migrations/20170609113553_create_projection_versions.exs (1)

4-10: Schema for projection_versions matches test helpers and idempotency use‑case

The table structure (text PK projection_name, nullable last_seen_event_number as bigint, naive_datetime_usec timestamps) lines up with how last_seen_event/1 queries and how idempotent projections are tracked in tests. I don’t see missing constraints or indices for the intended usage.

If you haven’t already, please run the full migration set against a fresh Postgres instance and ensure the tests that rely on projection_versions (e.g. projection_version_schema_prefix_test.exs) execute cleanly.

test/projections/error_callback_test.exs (1)

24-68: Error‑path coverage and projector lifecycle handling look solid

The tests do a good job of exercising all error paths (tagged {:error, reason}, raised exceptions, invalid Ecto.Multi, and continuation after multiple failures) while keeping the projector process alive and asserting correct messages/log output. The Mox stubs and Sandbox.allow/3 usage for the projector process are also wired correctly for this scenario.

One small thing to keep in mind long‑term: the log assertion that matches an exact file/line (error_projector.ex:37) is quite brittle and will need updating if that module changes; if it becomes noisy, consider relaxing it to just assert the module/function.

Please confirm that these tests pass under your CI with MIX_ENV=test and your current Ecto.Adapters.SQL.Sandbox mode, as the behavior of the projector plus sandbox can differ slightly across Ecto versions.

test/projections/after_update_callback_test.exs (1)

9-26: Focused validation of after_update/3 behavior looks correct

The embedded Projector plus the single test exercise the new after_update/3 callback nicely: you verify that handle/2 returns :ok, that the callback is invoked with the exact event/metadata, and that changes[:my_projection] contains the projected Projection struct. Running the projection in the test process (with Sandbox.checkout/1) keeps the DB ownership simple.

If you want stronger compile‑time guarantees in this test module, you could optionally add @impl Commanded.Projections.Ecto above after_update/3, but it isn’t required here.

Please run this test with the same Ecto version you intend to support to confirm that the shape of the changes map (keys and structs) matches your expectations across versions.

Also applies to: 33-45

test/projections/runtime_config_projector_test.exs (1)

21-54: Runtime configuration scenario and isolation between projector instances look good

This test cleanly demonstrates that two RuntimeConfigProjector instances, configured via runtime :application/:name options, share the same repo but maintain independent last_seen_event state. The pattern of sending {:events, events} directly, coupled with Mox stubs for the event store and Sandbox.allow/3 for both projector processes, is consistent with how the Commanded event handler pipeline works.

Just keep an eye on stub_event_store/1 if the Mock adapter’s callback surface evolves in the future; you’ll want the stubbed functions to match whatever the real adapter is expected to implement so tests keep exercising the realistic path.

Please confirm under your current Commanded/EventStore versions that no additional EventStore callbacks (e.g. streaming functions) are invoked in this scenario, so the Mox stubs here remain sufficient.

Also applies to: 57-74

test/projections/ecto_projection_test.exs (1)

11-28: Comprehensive coverage of projection semantics and configuration behavior

This suite does a nice job of exercising the main behaviors of Commanded.Projections.Ecto:

  • Event handling and idempotent projection for multiple event types.
  • Ignoring unprojected events and ensuring last_seen_event/1 only advances on projected events.
  • Returning error tuples from a projection and verifying no projection rows are written.
  • Validating configuration: environment‑based :repo requirement, override via repo: option, and enforcing :name presence on start.

The patterns you use (calling Projector.handle/2 directly with metadata containing handler_name/event_number, and querying via ProjectionAssertions) are consistent and easy to follow.

It would be good to re‑run this module in isolation to ensure the configuration‑validation tests don’t leak app env changes into other tests; the try/after blocks look correct, but env‑mutation bugs can be subtle.

Also applies to: 35-73, 133-177, 179-192

test/projections/deprecated_projection_test.exs (1)

17-71: Deprecation coverage for project do/end forms is well structured

Capturing stderr around the defmodule definitions is a good way to assert that the deprecated project/2 and project/3 do/end forms emit warnings while still verifying they behave correctly (first test) and that the new function‑style forms are encouraged in the messages (second and third tests).

Defining these projector modules inside the tests keeps the surface area small. Just be aware that if you ever re‑run these test modules in a code‑reloading setup, you may see module redefinition warnings; that’s usually harmless in CI environments.

When you bump Elixir or the compiler, please re‑run these tests to ensure the deprecation warnings still flow to stderr in the same way, as compiler messaging behavior can occasionally change between versions.

test/projections/projection_version_schema_prefix_test.exs (2)

8-17: Setup correctly isolates configuration and database state

Capturing the original :ecto_projection_schema_prefix, restoring it in on_exit/1, and checking out the sandboxed repo gives good test isolation; no changes needed here.


175-197: ProjectionVersion prefix test exercises the critical persistence path

Defining a projector with schema_prefix: "test", invoking handle/2, and asserting via Repo.get/3 with prefix: "test" is a clear way to ensure the projection version record is written under the correct schema and that last_seen_event_number is updated as expected.

test/support/error_projector.ex (1)

19-45: Projection clauses cleanly cover normal and failure scenarios

Each project/2 clause sets up a distinct scenario: a normal insert with a side-effect send/2, a manual {:error, :failed} return, a bad change, a raised exception, and an invalid query. This gives good coverage of success and different failure paths without any obvious correctness issues in the callbacks themselves.

@yordis
yordis force-pushed the yordis/feat-add-ecto-projector branch 3 times, most recently from 1498e46 to d7278b2 Compare November 18, 2025 03:27
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis
yordis force-pushed the yordis/feat-add-ecto-projector branch from d7278b2 to 12b7941 Compare November 18, 2025 03:29
@yordis
yordis merged commit d7bdcce into main Nov 18, 2025
6 of 7 checks passed
@yordis
yordis deleted the yordis/feat-add-ecto-projector branch November 18, 2025 03:43
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