entity-derive 0.22.8

Derive macro for generating DTOs, repositories, and SQL from a single entity definition
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
<a id="top"></a>

<p align="center">
  <img src="logo.png" alt="entity-derive logo" width="200"/>
  <h1 align="center">entity-derive</h1>
  <p align="center">
    <strong>One macro to rule them all</strong>
  </p>
  <p align="center">
    Generate DTOs, repositories, mappers, and SQL from a single entity definition
  </p>
</p>

<p align="center">
  <a href="https://crates.io/crates/entity-derive">
    <img src="https://img.shields.io/crates/v/entity-derive.svg?style=for-the-badge" alt="Crates.io"/>
  </a>
  <a href="https://docs.rs/entity-derive">
    <img src="https://img.shields.io/docsrs/entity-derive?style=for-the-badge" alt="Documentation"/>
  </a>
  <a href="https://github.com/RAprogramm/entity-derive/actions">
    <img src="https://img.shields.io/github/actions/workflow/status/RAprogramm/entity-derive/ci.yml?style=for-the-badge" alt="CI Status"/>
  </a>
</p>

<p align="center">
  <a href="https://codecov.io/gh/RAprogramm/entity-derive">
    <img src="https://img.shields.io/codecov/c/github/RAprogramm/entity-derive?style=for-the-badge&token=HGuwZf0REV" alt="Coverage"/>
  </a>
  <a href="https://github.com/RAprogramm/entity-derive/blob/main/LICENSES/MIT.txt">
    <img src="https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge" alt="License: MIT"/>
  </a>
  <a href="https://api.reuse.software/info/github.com/RAprogramm/entity-derive">
    <img src="https://img.shields.io/reuse/compliance/github.com%2FRAprogramm%2Fentity-derive?style=for-the-badge" alt="REUSE Compliant"/>
  </a>
  <a href="https://github.com/RAprogramm/entity-derive/wiki">
    <img src="https://img.shields.io/badge/Wiki-Documentation-green?style=for-the-badge&logo=github" alt="Wiki"/>
  </a>
  <a href="https://raprogramm.github.io/entity-derive/">
    <img src="https://img.shields.io/badge/Docs-Website-blue?style=for-the-badge&logo=mdbook" alt="Documentation Site"/>
  </a>
</p>

---

## The Problem

Building a typical CRUD application requires writing the same boilerplate over and over: entity struct, create DTO, update DTO, response DTO, row struct, repository trait, SQL implementation, and 6+ From implementations.

**That's 200+ lines of boilerplate for a single entity.**

## The Solution

```rust,ignore
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
    #[id]
    pub id: Uuid,

    #[field(create, update, response)]
    pub name: String,

    #[field(create, update, response)]
    pub email: String,

    #[field(skip)]
    pub password_hash: String,

    #[field(response)]
    #[auto]
    pub created_at: DateTime<Utc>,
}
```

**Done.** The macro generates everything else.

---

## Installation

```toml
[dependencies]
entity-derive = { version = "0.22", features = ["postgres", "api"] }
```

Generated code reaches its own runtime through the `entity-derive`
facade, so nothing else has to be added on its behalf. The crates you
still declare are the ones your entity itself uses — the pool and column
types, and `serde` when the DTOs are serialized:

```toml
sqlx = { version = "0.9", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
uuid = { version = "1", features = ["v4", "v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
```

### Feature flags

| Feature | Default | What it does |
|---------|:-------:|--------------|
| `postgres` || Generate `sqlx::PgPool`-backed repository implementations |
| `events` || Generate `{Entity}Event` enum (`Created` / `Updated` / `Deleted` variants) |
| `commands` || CQRS command pattern: command structs + dispatcher (`#[entity(commands)]`, `#[command(...)]`) |
| `hooks` || `{Entity}Hooks` trait with before/after lifecycle methods |
| `transactions` || `{Entity}TransactionRepo` adapter + transaction builder helpers (`#[entity(transactions)]`) |
| `aggregate_root` || `New{Entity}` constructor type and transactional `save()` (`#[entity(aggregate_root)]`) |
| `migrations` || Compile-time `MIGRATION_UP` / `MIGRATION_DOWN` SQL constants (`#[entity(migrations)]`) |
| `projections` || Projection structs and `find_by_id_<projection>` lookups (`#[projection(...)]`) |
| `clickhouse` |   | **Not implemented** — selecting this dialect is a compile error ([#23]https://github.com/RAprogramm/entity-derive/issues/23) |
| `mongodb` |   | **Not implemented** — selecting this dialect is a compile error ([#24]https://github.com/RAprogramm/entity-derive/issues/24) |
| `streams` |   | `{Entity}Subscriber` using Postgres `LISTEN`/`NOTIFY` (pulls in `events`) |
| `outbox` |   | Transactional-outbox enqueue in generated writes + `OutboxDrainer` runtime (pulls in `events`) |
| `api` |   | Generate HTTP handlers (`axum`) and `utoipa` OpenAPI schemas |
| `validate` |   | Wire up `validator::Validate` on generated DTOs |
| `garde` |   | Maintained alternative: `garde::Validate` with translated `#[validate(...)]` rules (`validate` wins if both are on) |
| `tracing` |   | Wrap every generated async method in `#[tracing::instrument]` carrying `entity` + `op` span fields |

Default features cover the full entity-attribute surface so existing projects work without changes. For lean builds, opt out of what you don't need:

```toml
[dependencies]
# Just repositories — no events, hooks, commands, etc.
entity-derive = { version = "0.22", default-features = false, features = ["postgres"] }
```

If you use an entity attribute whose feature is disabled (e.g. `#[entity(commands)]` without `features = ["commands"]`), the macro emits a `compile_error!` at the attribute pointing to the missing feature.

Enable extras alongside the defaults:

```toml
[dependencies]
entity-derive = { version = "0.22", features = ["postgres", "api", "tracing", "streams"] }
tracing = "0.1"
tracing-subscriber = "0.3"
```

---

## Features

| Feature | Description |
|---------|-------------|
| **Zero Runtime Cost** | All code generation at compile time |
| **Type Safe** | Change a field once, everything updates |
| **Auto HTTP Handlers** | `api(handlers)` generates CRUD endpoints + router |
| **`OpenAPI` Docs** | Auto-generated Swagger/OpenAPI documentation |
| **Query Filtering** | Type-safe `#[filter]`, `#[filter(like)]`, `#[filter(range)]`, `#[filter(search)]` |
| **Sorting & Keyset** | `#[sort]` whitelisted ORDER BY + `list_after` cursor pagination |
| **Bulk Operations** | `find_by_ids`, atomic `create_many`, soft-delete-aware `delete_many` |
| **PATCH Semantics** | Dynamic UPDATE SET; double-`Option` distinguishes "leave" from "set NULL" |
| **Optimistic Locking** | `#[version]` — guarded, auto-incremented version column |
| **Typed Constraint Errors** | `typed_constraints` — violations resolved to `ConstraintError` with field info |
| **OpenAPI Overrides** | `#[schema(...)]` forwarded verbatim to the generated create/response DTOs and joined views |
| **Embedded Value Objects** | `#[embed(prefix, fields(...))]` — structs flattened to prefixed columns |
| **Relations** | `#[belongs_to]`, `#[has_many]` and many-to-many via `through = "junction"` |
| **Ownership Scoping** | `#[owner]` generates `find_by_id_scoped` / `list_by_owner` / `update_scoped` / `delete_scoped` |
| **Participant Scopes** | `#[scope(name: col_a \| col_b)]` generates `list_{name}` over an OR group, optionally narrowed by `within` |
| **Upsert** | `upsert(conflict = "…")` generates `INSERT ... ON CONFLICT DO UPDATE / DO NOTHING` |
| **Aggregate Roots** | `#[entity(aggregate_root)]` with `New{T}` DTOs and transactional `save` |
| **Transactions** | Multi-entity atomic operations |
| **Lifecycle Events** | `Created`, `Updated`, `Deleted` events |
| **Real-Time Streams** | Postgres LISTEN/NOTIFY integration |
| **Transactional Outbox** | `events(outbox)` — durable at-least-once event delivery with retry/backoff |
| **Lifecycle Hooks** | `{Entity}Hooks` trait plus `{Entity}Repo<H>`, a repository that runs the hooks around every mutation |
| **CQRS Commands** | Business-oriented command pattern; `sets(...)` turns one into a domain operation writing named columns |
| **Soft Delete** | `deleted_at` timestamp support |
| **Structured Logging** | Opt-in `tracing` feature wraps every generated async method in `#[tracing::instrument]` with `entity` + `op` fields |

---

## Documentation

| Topic | Languages |
|-------|:---------:|
| **Getting Started** ||
| Attributes | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Attributes-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Attributes-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Attributes-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Attributes-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Attributes-zh |
| Examples | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Examples-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Examples-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Examples-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Examples-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Examples-zh |
| **Features** ||
| Filtering | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Filtering-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Filtering-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Filtering-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Filtering-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Filtering-zh |
| Relations | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Relations-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Relations-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Relations-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Relations-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Relations-zh |
| Events | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Events-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Events-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Events-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Events-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Events-zh |
| Streams | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Streams-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Streams-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Streams-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Streams-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Streams-zh |
| Hooks | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Hooks-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Hooks-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Hooks-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Hooks-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Hooks-zh |
| Commands | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Commands-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Commands-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Commands-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Commands-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Commands-zh |
| **Advanced** ||
| Custom SQL | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Custom-SQL-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Custom-SQL-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Custom-SQL-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Custom-SQL-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Custom-SQL-zh |
| Web Frameworks | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Web-Frameworks-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Web-Frameworks-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Web-Frameworks-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Web-Frameworks-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Web-Frameworks-zh |
| Best Practices | [🇬🇧]https://github.com/RAprogramm/entity-derive/wiki/Best-Practices-en [🇷🇺]https://github.com/RAprogramm/entity-derive/wiki/Best-Practices-ru [🇰🇷]https://github.com/RAprogramm/entity-derive/wiki/Best-Practices-ko [🇪🇸]https://github.com/RAprogramm/entity-derive/wiki/Best-Practices-es [🇨🇳]https://github.com/RAprogramm/entity-derive/wiki/Best-Practices-zh |

---

## Quick Reference

### Entity Attributes

```rust,ignore
#[entity(
    table = "users",           // Required: table name
    schema = "public",         // Optional: schema (default: omitted)
    dialect = "postgres",      // Optional: database dialect
    aggregate_root,            // Optional: New{T} DTOs + transactional save
    soft_delete,               // Optional: use deleted_at instead of DELETE
    migrations(                // Optional: extra DDL alongside the table
        touch_updated_at,      // updated_at trigger (shared function)
        audit,                 // JSONB audit-log table + trigger
        extensions = "pg_trgm",// CREATE EXTENSION statements
    ),
    typed_constraints,         // Optional: constraint violations as typed errors
    upsert(                    // Optional: INSERT ... ON CONFLICT method
        conflict = "email",    // #[column(unique)] field(s) or unique_index columns
        action = "update",     // "update" (default) or "nothing"
    ),
    events,                    // Optional: generate lifecycle events
    events(outbox),            // Optional: + durable transactional outbox
    streams,                   // Optional: real-time Postgres NOTIFY
    hooks,                     // Optional: before/after lifecycle hooks
    commands,                  // Optional: CQRS command pattern
    transactions,              // Optional: multi-entity transaction support
    api(                       // Optional: generate HTTP handlers + OpenAPI
        tag = "Users",
        handlers,              // All CRUD, or handlers(get, list, create)
        security = "bearer",   // cookie, bearer, api_key, or none
        guard = "RequireAuth", // FromRequestParts extractor enforced in handlers
        guard(list = "none"),  // per-op override: create/get/update/delete/list/commands
        title = "My API",
        api_version = "1.0.0",
    ),
)]
```

### Field Attributes

```rust,ignore
#[id]                          // Primary key (auto-generated UUID)
#[auto]                        // Auto-generated: skipped by INSERT, gets a DB default in migrations
#[owner]                       // Ownership column: adds *_scoped methods
#[version]                     // Optimistic locking: guarded, auto-bumped
#[embed(prefix, fields(...))]  // Flatten a value object to prefixed columns
#[field(create)]               // Include in CreateRequest
#[field(update)]               // Include in UpdateRequest
#[field(response)]             // Include in Response
#[field(skip)]                 // Exclude from all DTOs
#[filter]                      // Exact match filter
#[filter(like)]                // ILIKE substring filter; pass the bare value, wildcards in it are escaped
#[filter(range)]               // Range filter (from/to)
#[filter(search)]              // Trigram substring search (pg_trgm)
#[sort]                        // Whitelisted dynamic ORDER BY
#[belongs_to(Entity)]          // Foreign key relation
#[has_many(Entity)]            // One-to-many relation
#[has_many(E, through = "t")]  // Many-to-many via junction table
#[projection(Name: fields)]    // Partial view
#[scope(name: col_a | col_b)]  // list_{name} over an OR group of columns
#[schema(value_type = T)]      // OpenAPI type override, forwarded to the generated structs
```

### Transactional Outbox

LISTEN/NOTIFY (`streams`) is fire-and-forget: events are lost if no
subscriber is listening. `events(outbox)` makes delivery durable — every
generated write inserts the serialized event into the `entity_outbox`
table in the same transaction as the DML, and a drainer delivers rows
with retry and exponential backoff:

```rust,ignore
#[derive(Entity, Serialize, Deserialize)]
#[entity(table = "orders", events(outbox), migrations)]
pub struct Order { /* ... */ }

sqlx::query(Order::MIGRATION_OUTBOX).execute(&pool).await?;

struct Notifier;

#[async_trait::async_trait]
impl entity_core::outbox::OutboxHandler for Notifier {
    type Error = anyhow::Error;
    async fn handle(&self, row: &OutboxRow) -> Result<(), Self::Error> {
        deliver(&row.entity, &row.payload).await
    }
}

entity_core::outbox::OutboxDrainer::new(pool, Notifier).run().await;
```

Rows are claimed with `FOR UPDATE SKIP LOCKED` (multiple drainers
cooperate), retried with exponential backoff and parked after
`max_attempts` for manual inspection. Delivery is at-least-once —
handlers must be idempotent. Composes with `streams`: NOTIFY wakes
subscribers instantly, the outbox guarantees nothing is lost. Requires
the `outbox` feature and `serde_json` in your crate.

### Lifecycle Hooks

`#[entity(hooks)]` emits the `{Entity}Hooks` trait and `{Entity}Repo<H>`,
a repository that owns a pool and a hooks implementation and runs the
hooks around every mutation:

```rust,ignore
#[derive(Entity)]
#[entity(table = "users", hooks)]
pub struct User { /* ... */ }

let repo = UserRepo::new(pool, Audit);

let user = repo.create(dto).await?;      // before_create → INSERT → after_create
let found = repo.find_by_id(id).await?;  // reads carry no hooks
```

A failing `before_*` aborts before anything is written; `after_delete`
and `after_restore` run only when a row was actually affected. The hook
error only has to convert into the repository error, so hooks may keep
their own error type. The bare pool keeps working exactly as before —
the wrapper is opt-in.

### Domain Operations

Columns that must change only through a named operation cannot be
`#[field(update)]` — that would put them in the public patch DTO and in
the upsert SET list. Declare the operation instead:

```rust,ignore
#[derive(Entity)]
#[entity(table = "citizens", commands)]
#[command(VerifyPassport, payload(passport_provider), sets(
    passport_verified = "true",
    passport_verified_at = "NOW()"
))]
pub struct Citizen { /* ... */ }

let verified = pool.verify_passport(VerifyPassportCitizen {
    id,
    passport_provider: Some("gov".into()),
}).await?;
```

One UPDATE writes the fixed expressions plus the payload columns and
nothing else. The expressions land in the statement verbatim, like
`#[column(default = "...")]`; the column names are checked against the
entity at compile time.

With `transactions` on, the same operation is available on the adapter,
so it can land together with other writes — there it returns
`Ok(None)` for a missing row, like the adapter's other methods:

```rust,ignore
let mut tx = pool.begin().await?;
let verified = CitizenTransactionRepo::new(&mut tx)
    .verify_passport(VerifyPassportCitizen { id, passport_provider: Some("gov".into()) })
    .await?;
tx.commit().await?;
```

### Participant Scopes

"Rows where this principal takes part in any role" is an OR over several
columns holding the same kind of value. Declare it once:

```rust,ignore
#[derive(Entity)]
#[entity(table = "disputes")]
#[scope(involving: requester_id | subject_id)]
#[scope(handled: requester_id | subject_id, within = parcel_id)]
pub struct Dispute { /* ... */ }

let mine = pool.list_involving(user_id, 20, 0).await?;
let here = pool.list_handled(parcel_id, user_id, 20, 0).await?;
```

The value is bound once and matched against every declared column;
`within` narrows the group to one parent first. Columns are checked at
compile time: an unknown name or a group whose columns disagree on their
type is an error at the declaration. Soft-delete aware, ordered by id
descending.

### Ownership Scoping

Mark the column carrying the owning principal's id with `#[owner]` and the
repository gains row-level scoped methods — "only this user's rows" without
hand-written predicates:

```rust,ignore
#[derive(Entity)]
#[entity(table = "orders")]
pub struct Order {
    #[id]
    pub id: Uuid,
    #[owner]
    pub user_id: Uuid,
    #[field(create, update, response)]
    pub note: String,
}

let mine: Vec<Order> = pool.list_by_owner(user_id, 20, 0).await?;
let order = pool.find_by_id_scoped(id, user_id).await?;          // None if not theirs
let updated = pool.update_scoped(id, user_id, patch).await?;     // None if not theirs
let removed = pool.delete_scoped(id, user_id).await?;            // false if not theirs
```

Scoped reads and writes never reveal whether a row exists for another
owner, and all of them respect `soft_delete`.

### Handler Guards

`security = "..."` only documents authentication in the OpenAPI spec. To
actually enforce it, pass a `guard` — any type implementing axum's
`FromRequestParts`. It is injected as a leading argument of every generated
handler, so a failed extraction rejects the request before the handler body
runs:

```rust,ignore
pub struct RequireAuth;

impl<S: Send + Sync> FromRequestParts<S> for RequireAuth {
    type Rejection = StatusCode;
    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
        parts.headers.contains_key("authorization")
            .then_some(Self)
            .ok_or(StatusCode::UNAUTHORIZED)
    }
}

#[derive(Entity)]
#[entity(table = "users", api(tag = "Users", handlers, guard = "RequireAuth", guard(list = "none")))]
pub struct User { /* ... */ }
```

Per-operation overrides accept `create`, `get`, `update`, `delete`, `list`
and `commands`; the literal `"none"` disables the guard for that operation.
Commands listed in `public = [...]` never receive a guard.

### Embedded Value Objects

Keep money-style value objects as real structs on the entity while
storing them as flat scalar columns — no JSONB detour:

```rust,ignore
pub struct Money { pub amount_cents: i64, pub currency: String }

#[derive(Entity)]
#[entity(table = "products", migrations)]
pub struct Product {
    #[id] pub id: Uuid,
    #[field(create, update, response)]
    #[embed(prefix = "price_", fields(amount_cents: i64, currency: String))]
    pub price: Money,
}
```

DDL, Row struct, CRUD SQL and dynamic updates operate on
`price_amount_cents` / `price_currency`; DTOs and the entity carry
`Money` itself. The declared shape is destructured against the real
struct at compile time — a renamed, retyped, missing or extra field
fails the build. `Option<Money>` parents are not supported yet.

### Typed Constraint Errors

The macro knows every constraint it creates. With `typed_constraints`,
write methods resolve violated constraint names and surface
`entity_core::ConstraintError { kind, constraint, field }` instead of a
raw driver error — no string-matching constraint names in handlers:

```rust,ignore
#[entity(table = "users", typed_constraints, error = "AppError")]
pub struct User {
    #[id] pub id: Uuid,
    #[field(create, response)] #[column(unique)] pub email: String,
}

match pool.create(dto).await {
    Err(AppError::Constraint(v)) if v.field == Some("email") => conflict_409(),
    other => other?,
}
```

Covers unique columns, `belongs_to` foreign keys, column checks and
`unique_index` names. Constraints the macro cannot infer — foreign keys
over natural keys, custom-named CHECKs, indexes from hand-written
migrations — are declared explicitly and take precedence over derived
entries with the same name:

```rust,ignore
#[entity(
    table = "orders",
    typed_constraints,
    constraint(name = "orders_currency_fkey", kind = "foreign_key", field = "currency"),
    constraint(name = "orders_window_check", kind = "check"),
)]
```

The custom `error` type must implement `From<ConstraintError>`; without
the flag behavior is unchanged.

### Trigram Search

`#[filter(search)]` on a text column gives the Query struct a fuzzy
substring filter (`col ILIKE '%' || $n || '%'`), while `migrations`
emit the matching `gin_trgm_ops` index and add `pg_trgm` to
`MIGRATION_EXTENSIONS` automatically. Unlike `#[filter(like)]`, the
value is bound as given: a `%` inside it acts as a wildcard rather than
matching literally.

```rust,ignore
#[field(create, update, response)]
#[filter(search)]
pub title: String,

let hits = pool.query(ArticleQuery { title: Some("rust".into()), ..Default::default() }).await?;
```

### Optimistic Locking

Mark an integer column with `#[version]` and concurrent updates stop
silently overwriting each other. The Update DTO gains a required
`expected_version`; the generated UPDATE bumps the column and only
applies when the stored version still matches — a stale write fails
with a conflict error instead of clobbering newer data:

```rust,ignore
#[derive(Entity)]
#[entity(table = "orders", migrations)]
pub struct Order {
    #[id] pub id: Uuid,
    #[field(create, update, response)] pub note: String,
    #[version] #[field(response)] #[auto] pub version: i32,
}

let patch = UpdateOrderRequest { note: Some("v2".into()), expected_version: order.version };
let updated = pool.update(order.id, patch).await?;   // version = version + 1
```

DDL gets `INTEGER NOT NULL DEFAULT 0` automatically. Works in plain,
scoped and transactional updates.

### Migration Triggers & Extensions

`migrations(...)` options emit the DDL production tables actually carry:

```rust,ignore
#[entity(table = "articles", migrations(touch_updated_at, audit, extensions = "pg_trgm"))]
pub struct Article { /* ... */ }

for ddl in Article::MIGRATION_EXTENSIONS { sqlx::query(ddl).execute(&pool).await?; }
sqlx::query(Article::MIGRATION_UP).execute(&pool).await?;
for ddl in Article::MIGRATION_TRIGGERS { sqlx::query(ddl).execute(&pool).await?; }
```

- `touch_updated_at` — shared `entity_touch_updated_at()` function + per-table
  BEFORE UPDATE trigger keeping `updated_at` fresh DB-side (requires an
  `updated_at` field, checked at compile time)
- `audit``entity_audit_log` table + trigger recording `to_jsonb(OLD/NEW)`
  diffs for INSERT/UPDATE/DELETE
- `extensions = "..."` — idempotent `CREATE EXTENSION` statements

### PATCH Semantics

Update DTOs are true partial patches. Fields absent from the payload are
left untouched — the generated UPDATE only includes columns actually
present. Nullable columns use double-`Option`, so "leave unchanged" and
"set NULL" are finally distinguishable:

```rust,ignore
// {}                          → nothing changes (fetch-and-return)
// {"nickname": null}          → nickname = NULL
// {"nickname": "neo"}         → nickname = 'neo'
let patch: UpdateProfileRequest = serde_json::from_str(body)?;
let profile = pool.update(id, patch).await?;
```

In Rust code: `None` = leave, `Some(None)` = SET NULL,
`Some(Some(v))` = SET v. Chainable setters say the same thing without
the nesting — `set_{field}` for every updatable column, `clear_{field}`
for the nullable ones, `expecting_version` where `#[version]` is
declared:

```rust,ignore
let patch = UpdateProfileRequest::default()
    .set_display_name("Neo".into())   // SET display_name = 'Neo'
    .clear_nickname();                // SET nickname = NULL

let profile = pool.update(id, patch).await?;
```

Struct-literal construction keeps working; the setters are additive.

### Bulk Operations

Every repository ships batch primitives — one round-trip reads and
atomic writes:

```rust,ignore
let posts: Vec<Post> = pool.find_by_ids(ids).await?;          // WHERE id = ANY($1)
let created: Vec<Post> = pool.create_many(dtos).await?;       // one transaction
let removed: u64 = pool.delete_many(stale_ids).await?;        // soft-delete aware
```

`create_many` rolls the whole batch back if any row fails; `delete_many`
returns the number of rows actually affected. With `events` delivery
(streams or outbox) per-row events are emitted inside the same
transaction.

### Sorting & Keyset Pagination

Mark sortable columns with `#[sort]` — the Query struct gains a
whitelisted sort selector (`{Entity}SortField`, one `Asc`/`Desc` variant
per column, JSON-friendly), so user input can never inject SQL. Every
repository also gets `list_after` keyset pagination that stays fast on
deep pages, unlike OFFSET:

```rust,ignore
let query = PostQuery {
    sort: Some(PostSortField::ViewsDesc),
    limit: Some(20),
    ..Default::default()
};
let top: Vec<Post> = pool.query(query).await?;

let page: Vec<Post> = pool.list_after(None, 20).await?;
let next: Vec<Post> = pool.list_after(page.last().map(|p| p.id), 20).await?;
```

With the default UUIDv7 ids, the id-ordered keyset walk is
chronologically stable.

### Many-to-Many Relations

Declare the junction table with `through` and the repository gains a
JOIN-backed lookup plus link management, while `migrations` emits the
junction DDL (composite primary key, cascading foreign keys):

```rust,ignore
#[derive(Entity)]
#[entity(table = "teams", migrations)]
#[has_many(User, through = "team_members")]
pub struct Team { /* ... */ }

for ddl in Team::MIGRATION_JUNCTIONS {
    sqlx::query(ddl).execute(&pool).await?;
}

pool.add_user(team_id, user_id).await?;          // idempotent link
let members: Vec<User> = pool.find_users(team_id).await?;
let linked = pool.has_user(team_id, user_id).await?;
let removed = pool.remove_user(team_id, user_id).await?;
```

### Postgres Enums

Derive `ValueObject` on a status-style enum and reference it from entities
with `#[column(pg_enum = "...")]`:

```rust,ignore
#[derive(ValueObject, Debug, Clone, Serialize, Deserialize)]
#[value_object(pg_type = "order_status", sqlx)]
pub enum OrderStatus { Pending, Shipped, Delivered }

#[derive(Entity)]
#[entity(table = "orders", migrations)]
pub struct Order {
    #[id]
    pub id: Uuid,
    #[field(create, update, response)]
    #[column(pg_enum = "order_status")]
    pub status: OrderStatus,
}

for ddl in Order::MIGRATION_TYPES {
    sqlx::query(ddl).execute(&pool).await?;
}
sqlx::query(Order::MIGRATION_UP).execute(&pool).await?;
```

- `ValueObject` generates `PG_TYPE` and idempotent `PG_CREATE_TYPE` constants;
  the opt-in `sqlx` flag additionally emits `sqlx::Type` / `Encode` / `Decode`
  impls so the enum binds and decodes without hand-written glue (omit it if
  you already derive `sqlx::Type` yourself).
- `#[column(pg_enum = "...")]` sets the DDL column type and registers the
  enum's DDL in `{Entity}::MIGRATION_TYPES` — run those before `MIGRATION_UP`.
- The declared name is checked against the enum's `pg_type` at compile time;
  a typo fails the build.

### Upsert

Declare a conflict target with a uniqueness guarantee (`#[id]`,
`#[column(unique)]` or `unique_index(...)`) and the repository gains an
`upsert` method backed by `INSERT ... ON CONFLICT`:

```rust,ignore
#[derive(Entity)]
#[entity(table = "users", upsert(conflict = "email"))]
pub struct User {
    #[id]
    pub id: Uuid,
    #[field(create, response)]
    #[column(unique)]
    pub email: String,
    #[field(create, update, response)]
    pub name: String,
}

let user = pool.upsert(CreateUserRequest { email, name }).await?;
```

`action = "update"` (default) overwrites all non-conflict columns with the
incoming values (`DO UPDATE SET col = EXCLUDED.col`) and returns the
persisted row. `action = "nothing"` keeps the existing row (`DO NOTHING`)
and returns `Option<Entity>` — `None` when a conflicting row already
existed. Requires `returning = "full"` (the default). With `streams`
enabled, upsert publishes a `Created` notification for every row it
returns. With `transactions`, the `{Entity}TransactionRepo` adapter
gains the same `upsert` so it can share atomicity with adjacent
statements (release-then-upsert login flows and the like).

### Transactions

Mark each participating entity with `#[entity(table = "…", transactions)]` and
drive a multi-entity transaction through `Transaction::run`. The closure
receives `&mut TransactionContext`; `run` commits on `Ok` and rolls back on
`Err` (or any panic) automatically:

```rust,ignore
use entity_core::transaction::Transaction;

Transaction::new(&pool)
    .run(async |ctx| {
        let user = ctx.users().create(create_user).await?;
        ctx.orders().create(order_for(user.id)).await?;
        Ok::<_, sqlx::Error>(user)
    })
    .await?;
```

Need conditional commit/rollback inside the closure? Use
[`run_with_commit`](https://docs.rs/entity-core/latest/entity_core/transaction/struct.Transaction.html#method.run_with_commit)
— it takes `TransactionContext` by value so the closure can call
`ctx.commit().await` (or `ctx.rollback().await`) itself.

### Validation Backends

`#[validate(...)]` constraints on entity fields translate to the chosen
backend. `validate` derives `validator::Validate` (unchanged). The new
`garde` feature derives `garde::Validate` instead, translating
`length`/`range`/`email`/`url`/`pattern` rules (Option-wrapped DTO
fields validate the inner value; unconstrained fields get
`garde(skip)`). `validator_derive` depends on the unmaintained
`proc-macro-error2` (RUSTSEC-2026-0173) — `garde` avoids it. When both
features are enabled, `validate` takes precedence.

### Tracing

Opt-in with the `tracing` feature. Every generated async method
(`create`, `find_by_id`, `update`, `delete`, `list`, `find_by_<field>`,
projections, transaction adapters, stream subscribers) is wrapped in
`#[tracing::instrument(skip_all, fields(entity, op), err(Debug))]`.

```toml
entity-derive = { version = "0.22", features = ["postgres", "tracing"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
```

With a subscriber initialized, a failed `User::create` surfaces as:

```text
ERROR entity.User.create: error=database error: duplicate key value violates unique constraint
  in entity.User.create with entity="User" op="create"
```

When the feature is off, generated code is byte-for-byte identical to a
build without the attribute — zero runtime cost.

---

## Code Coverage

<p align="center">
  <a href="https://codecov.io/gh/RAprogramm/entity-derive">
    <img src="https://codecov.io/gh/RAprogramm/entity-derive/graphs/sunburst.svg?token=HGuwZf0REV" alt="Coverage Sunburst"/>
  </a>
</p>


---

## Project

| Document | Purpose |
|---|---|
| [CONTRIBUTING.md]CONTRIBUTING.md | Branch, commit and PR conventions; local checks; the live Postgres suite |
| [SUPPORT.md]SUPPORT.md | Where to ask what, and what a good issue contains |
| [SECURITY.md]SECURITY.md | Private vulnerability reporting, supported versions, what is in scope |
| [CODE_OF_CONDUCT.md]CODE_OF_CONDUCT.md | Expected behaviour in project spaces |
| [RELEASE.md]RELEASE.md | How a release is cut and how versions are decided |
| [STABILITY.md]STABILITY.md | What the generated API guarantees across versions |