Compare commits

..

2 Commits

5 changed files with 71 additions and 16 deletions
+13
View File
@@ -2,6 +2,19 @@
Patterns extracted from Ecto's source code for building safe, composable data pipelines.
## Contents
1. [`cast/4` — The External/Internal Data Boundary](#1-cast4--the-externalinternal-data-boundary)
2. [`change/2` — Internal-Only Modifications](#2-change2--internal-only-modifications)
3. [Validation Pipeline — Composable Validators](#3-validation-pipeline--composable-validators)
4. [`validate_change/3` — Custom Validators](#4-validate_change3--custom-validators)
5. [`add_error/4` — Manual Error Injection](#5-add_error4--manual-error-injection)
6. [`put_change/3` vs `force_change/3` — Tracked vs Forced Changes](#6-put_change3-vs-force_change3--tracked-vs-forced-changes)
7. [Constraints vs Validations — DB-level Safety](#7-constraints-vs-validations--db-level-safety)
8. [`prepare_changes/2` — Last-Mile DB-Aware Transforms](#8-prepare_changes2--last-mile-db-aware-transforms)
9. [`apply_action/2` — Schemaless Validation](#9-apply_action2--schemaless-validation)
10. [`cast_assoc/3` vs `put_assoc/4` — External vs Internal Association Changes](#10-cast_assoc3-vs-put_assoc4--external-vs-internal-association-changes)
---
## 1. `cast/4` — The External/Internal Data Boundary
+26 -16
View File
@@ -2,6 +2,16 @@
Patterns extracted from Ecto's `Ecto.Multi` source code.
## Contents
1. [`Multi.new() |> Multi.insert/update/delete` — Named Operation Pipeline](#1-multinew--multiinsertupdatedelete--named-operation-pipeline)
2. [`Multi.run/3` — Arbitrary Code in a Transaction](#2-multirun3--arbitrary-code-in-a-transaction)
3. [Dependent Operations with Function Variants](#3-dependent-operations-with-function-variants)
4. [`Multi.merge/2` — Dynamic Transaction Composition](#4-multimerge2--dynamic-transaction-composition)
5. [`Multi.append/2` / `Multi.prepend/2` — Static Multi Composition](#5-multiappend2--multiprepend2--static-multi-composition)
6. [Tuple Keys — Dynamic Collections of Operations](#6-tuple-keys--dynamic-collections-of-operations)
7. [`Multi.to_list/1` — Testing Without a Database](#7-multito_list1--testing-without-a-database)
---
## 1. `Multi.new() |> Multi.insert/update/delete` — Named Operation Pipeline
@@ -17,18 +27,18 @@ def reset(account, params) do
end
# Execute:
case Repo.transact(PasswordManager.reset(account, params)) do
case Repo.transaction(PasswordManager.reset(account, params)) do
{:ok, %{account: account, log: log}} -> # success
{:error, :account, changeset, _} -> # account step failed
end
```
**Why:** Each operation is named. On success, `Repo.transact` returns `{:ok, results_map}` where each key is the name given to that operation. On failure, it returns `{:error, failed_name, failed_value, changes_so_far}`, making it immediately clear which step aborted the transaction and why. This is more precise than a bare transaction function where you'd have to inspect the return value to guess which step failed.
**Why:** Each operation is named. On success, `Repo.transaction` returns `{:ok, results_map}` where each key is the name given to that operation. On failure, it returns `{:error, failed_name, failed_value, changes_so_far}`, making it immediately clear which step aborted the transaction and why. This is more precise than a bare transaction function where you'd have to inspect the return value to guess which step failed.
**Anti-pattern:** Using an anonymous function with bare `case` statements inside a transaction, where failure attribution is implicit:
```elixir
# BAD — no way to know which operation failed from the return value alone
Repo.transact(fn ->
Repo.transaction(fn ->
case Repo.update(Account.password_reset_changeset(account, params)) do
{:ok, account} ->
case Repo.insert(Log.password_reset_changeset(account, params)) do
@@ -50,7 +60,7 @@ end)
**Example — before:**
```elixir
def create_user_with_profile(params) do
Repo.transact(fn ->
Repo.transaction(fn ->
case Repo.insert(User.changeset(params)) do
{:ok, user} ->
case Repo.insert(Profile.changeset(user, params)) do
@@ -71,7 +81,7 @@ def create_user_with_profile(params) do
|> Multi.insert(:profile, fn %{user: user} ->
Profile.changeset(user, params)
end)
|> Repo.transact()
|> Repo.transaction()
end
# Caller:
@@ -86,7 +96,7 @@ end
**Don't use this when:**
- You have a single database operation (just call `Repo.insert/update/delete` directly)
- Operations are simple and sequential with no branching (a plain `Repo.transact(fn -> ... end)` is more readable)
- Operations are simple and sequential with no branching (a plain `Repo.transaction(fn -> ... end)` is more readable)
- The overhead of building a Multi struct is not justified by the number of operations
**Over-application example:**
@@ -94,7 +104,7 @@ end
# Overkill for a single operation
Multi.new()
|> Multi.insert(:user, User.changeset(params))
|> Repo.transact()
|> Repo.transaction()
```
**Better alternative:**
@@ -102,7 +112,7 @@ Multi.new()
Repo.insert(User.changeset(params))
```
**Why:** `Ecto.Multi` introduces indirection. For simple cases, calling Repo functions directly or using `Repo.transact(fn -> ... end)` is clearer. The named-pipeline form pays off when 3+ operations are involved and failure attribution matters.
**Why:** `Ecto.Multi` introduces indirection. For simple cases, calling Repo functions directly or using `Repo.transaction(fn -> ... end)` is clearer. The named-pipeline form pays off when 3+ operations are involved and failure attribution matters.
---
@@ -144,7 +154,7 @@ end)
**Example — before:**
```elixir
def register_user(params) do
Repo.transact(fn ->
Repo.transaction(fn ->
{:ok, user} = Repo.insert(User.changeset(params))
# This runs outside Multi — if it fails, user was already inserted
case ExternalService.provision(user.id) do
@@ -163,7 +173,7 @@ def register_user(params) do
|> Multi.run(:provision, fn _repo, %{user: user} ->
ExternalService.provision(user.id)
end)
|> Repo.transact()
|> Repo.transaction()
end
```
@@ -496,7 +506,7 @@ Enum.reduce(accounts, Multi.new(), fn account, multi ->
end)
# Error pattern-matching:
case Repo.transact(multi) do
case Repo.transaction(multi) do
{:ok, results} -> Map.keys(results) # [{:account, 1}, {:account, 2}, ...]
{:error, {:account, id}, changeset, _} -> "account #{id} failed"
end
@@ -547,7 +557,7 @@ def reset_passwords(accounts, params) do
end
# Caller knows exactly which account failed:
case Repo.transact(reset_passwords(accounts, params)) do
case Repo.transaction(reset_passwords(accounts, params)) do
{:ok, _} -> :ok
{:error, {:account, id}, changeset, _} ->
Logger.error("Failed to reset account #{id}: #{inspect(changeset.errors)}")
@@ -609,7 +619,7 @@ test "password reset changeset is valid" do
account = %Account{password: "letmein"}
{:ok, %{account: updated}} =
PasswordManager.reset(account, valid_params)
|> Repo.transact()
|> Repo.transaction()
# The changeset validity was the whole point, not the DB state
assert updated.password != account.password
@@ -630,7 +640,7 @@ end
test "creates user with profile" do
{:ok, %{user: user, profile: profile}} =
UserRegistration.multi(valid_params())
|> Repo.transact()
|> Repo.transaction()
assert user.email == "test@example.com"
assert profile.user_id == user.id
@@ -685,7 +695,7 @@ test "comment changeset is valid given a post" do
end
```
**Why:** `Multi.to_list/1` reflects the state of the pipeline at build time. Deferred values (function variants, `Multi.run` callbacks, `Multi.merge` functions) are not evaluated until `Repo.transact` runs them. Test those deferred pieces independently rather than trying to inspect them through `to_list`.
**Why:** `Multi.to_list/1` reflects the state of the pipeline at build time. Deferred values (function variants, `Multi.run` callbacks, `Multi.merge` functions) are not evaluated until `Repo.transaction` runs them. Test those deferred pieces independently rather than trying to inspect them through `to_list`.
---
@@ -697,6 +707,6 @@ end
- If you have reusable Multi fragments to combine → `Multi.append/2` or `Multi.prepend/2`
- If you're updating a dynamic collection → tuple keys `{:operation, id}`
- If you want to validate changesets without hitting the DB → `Multi.to_list/1` in tests
- If operations are simple and static (no dynamic branching) → consider `Repo.transact(fn -> ... end)` instead
- If operations are simple and static (no dynamic branching) → consider `Repo.transaction(fn -> ... end)` instead
<!-- PATTERN_COMPLETE -->
+12
View File
@@ -2,6 +2,18 @@
Patterns extracted from Ecto's query layer source code.
## Contents
1. [Named Query Functions — Composable Query Building](#1-named-query-functions--composable-query-building)
2. [Query Piping — Schema to Query Pipeline](#2-query-piping--schema-to-query-pipeline)
3. [Named Bindings — Position-Independent Composition](#3-named-bindings--position-independent-composition)
4. [`dynamic/2` — Runtime-Constructed Predicates](#4-dynamic2--runtime-constructed-predicates)
5. [`subquery/1` — Correlated Subqueries](#5-subquery1--correlated-subqueries)
6. [`exclude/2` — Strip Clauses for Reuse](#6-exclude2--strip-clauses-for-reuse)
7. [Bindingless Queries — Data-Driven Clauses](#7-bindingless-queries--data-driven-clauses)
8. [`select_merge/3` — Augmenting Selects Dynamically](#8-select_merge3--augmenting-selects-dynamically)
9. [`fragment/1` and `type/2` — Escape Hatches for DB-Specific Expressions](#9-fragment1-and-type2--escape-hatches-for-db-specific-expressions)
---
## 1. Named Query Functions — Composable Query Building
+11
View File
@@ -2,6 +2,17 @@
Patterns extracted from `lib/ecto/schema.ex` in the Ecto source.
## Contents
1. [Base Schema Module — App-Wide Schema Defaults](#1-base-schema-module--app-wide-schema-defaults)
2. [`@primary_key false` — Composite or No Primary Key](#2-primary_key-false--composite-or-no-primary-key)
3. [Virtual Fields — In-Memory-Only Data](#3-virtual-fields--in-memory-only-data)
4. [`embedded_schema/1` — Schemaless Validation Structs](#4-embedded_schema1--schemaless-validation-structs)
5. [`@timestamps_opts` — Consistent Timestamp Types](#5-timestamps_opts--consistent-timestamp-types)
6. [Field `:source` Option — Column Name Mapping](#6-field-source-option--column-name-mapping)
7. [`redact: true` — Protecting Sensitive Fields](#7-redact-true--protecting-sensitive-fields)
8. [`__schema__/1` Reflection — Runtime Schema Introspection](#8-__schema__1-reflection--runtime-schema-introspection)
---
## 1. Base Schema Module — App-Wide Schema Defaults
+9
View File
@@ -2,6 +2,15 @@
Patterns extracted from Ecto's type system source code.
## Contents
1. [`use Ecto.Type` — The Four-Callback Custom Type](#1-use-ectotype--the-four-callback-custom-type)
2. [`embed_as/1` — Controlling Embedded Serialization](#2-embed_as1--controlling-embedded-serialization)
3. [`equal?/2` — Custom Equality for Change Detection](#3-equal2--custom-equality-for-change-detection)
4. [`Ecto.Enum` — Constrained Atom Fields](#4-ectoenum--constrained-atom-fields)
5. [`Ecto.ParameterizedType` — Types with Options](#5-ectoparameterizedtype--types-with-options)
6. [Schemaless Types — `{data, types}` Changesets](#6-schemaless-types--data-types-changesets)
---
## 1. `use Ecto.Type` — The Four-Callback Custom Type