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
//! `ManyToMany<Target>` — explicit-through-model M2M relationships.
//!
//! # What
//!
//! A trait users impl per **direction** of a many-to-many relationship. For
//! the canonical `Person ↔ Group` pairing with an explicit `PersonGroup`
//! junction model, the "Person has many Groups" direction is
//! `impl ManyToMany<Group> for Person`, and the reverse
//! `impl ManyToMany<Person> for Group` is a separate impl. Each direction
//! reports its own [`this_fk`] / [`that_fk`] column pair, so the two sides
//! stay symmetrical without having to name the pair in a shared location.
//!
//! The [`Through`] associated type names the junction model — itself an
//! ordinary [`Model`] queryable via `Through::objects()`, just marked as a
//! through model in its [`ModelDescriptor`](crate::descriptor::ModelDescriptor)
//! (the `is_through = true` flag set by `#[model(..., through)]`). Keeping
//! `Through` queryable is deliberate: a junction row frequently carries
//! relation-specific data (a `role`, a `joined_at` timestamp, a JSONB
//! `policy` blob), and hiding those behind a generated association type
//! would force callers back into raw SQL to read them.
//!
//! # Why no default `related()` impl
//!
//! The shape of a typed M2M query — "select `Target` rows whose PK appears
//! in `Through.{that_fk}` for rows where `Through.{this_fk} == self.pk`" —
//! is trivially expressible in the framework's typed closure filter:
//!
//! ```ignore
//! let through_rows = PersonGroup::objects()
//! .filter(|f| f.person_id().eq(::djogi::relation::ForeignKey::new(self.id.clone())))
//! .fetch_all(&pool).await?;
//! ```
//!
//! but only with a compile-time-known column identifier on the `{Through}Fields`
//! handle. The trait cannot synthesise that identifier generically without
//! either (a) a stringly-typed filter escape hatch on `QuerySet<T>`
//! (rejected — the whole filter layer is typed by design), or (b) a second
//! trait indirection that promotes `this_fk` / `that_fk` from `&'static str`
//! to a field-handle type (workable but heavyweight for a single call site).
//!
//! So `related`, [`add_related`](ManyToMany::add_related), and
//! [`remove_related`](ManyToMany::remove_related) are **required** methods
//! with no default bodies. Users who hand-write an impl write a short typed
//! body that uses the existing filter API; the upcoming `many_to_many!`
//! macro (Phase 3 Task 7) generates these impls on behalf of the user from
//! a single invocation:
//!
//! ```ignore
//! many_to_many!(Person, Group, through = PersonGroup,
//! this_fk = person_id, that_fk = group_id,
//! relation = "groups");
//! ```
//!
//! That invocation stamps out both directions. Until the macro lands, the
//! hand-written form documented on each trait method is the supported path.
//!
//! # What the inherited seal does and does not cover
//!
//! `ManyToMany<Target>` sits atop [`Model`], which is itself sealed via
//! [`crate::model::__sealed::Sealed`]. The `Self: Model` supertrait bound
//! restricts **which types can be the implementor**: a downstream crate
//! cannot fabricate a type that satisfies the `Model` bound without going
//! through `#[derive(Model)]` first (the sole path that emits the `Sealed`
//! impl). A hand-rolled `impl Model for Hostile { ... }` fails to compile
//! and so does a hand-rolled `impl ManyToMany<…> for Hostile`.
//!
//! What the inherited seal does **not** cover: the **return values** of
//! [`this_fk`](ManyToMany::this_fk) / [`that_fk`](ManyToMany::that_fk) on a
//! legitimate `#[derive(Model)]`-derived implementor. Nothing stops a
//! downstream crate from writing
//! `impl ManyToMany<Group> for Person { fn this_fk() -> &'static str { "id; DROP TABLE users --" } … }`
//! with a real `Person` model. The seal only refuses fake `Model`s, not
//! hostile string returns from trait methods on legitimate models.
//!
//! **Mitigation:** any consumer that pushes `Self::this_fk()` /
//! `Self::that_fk()` into a SQL accumulator MUST first run the value
//! through [`crate::ident::assert_plain_ident`] (or the
//! `crate::ident::debug_assert_ident!` macro for debug-only checks). This
//! commit ships no SQL-emitting consumer of either method, so the contract
//! is documentary today; the upcoming `many_to_many!` macro (Phase 3 Task 7)
//! will validate at codegen time, and any future hand-written SQL emitter
//! must follow the same rule. Without that gate the trait would be a
//! string-based filter-bypass surface in disguise.
//!
//! # Where
//!
//! - [`ForeignKey<T>`](crate::relation::ForeignKey) — the FK wrapper types
//! junction-model columns use; both FK columns on [`Through`] are
//! `ForeignKey<Source>` / `ForeignKey<Target>` and decode the target PK
//! via `postgres_types::FromSql`.
//! - [`QuerySet::filter`](crate::query::QuerySet::filter) — the typed
//! closure API hand-written / macro-generated `related()` bodies call
//! into.
//! - `docs/guide/relations.md` (landing in Phase 3 Task 8) — user-facing
//! guide once the macro side lands.
use crateDjogiError;
use crateModel;
use Future;
/// One side of a many-to-many relationship, pivoting through an explicit
/// junction model.
///
/// See the module-level docs for the full rationale — in short: this trait
/// is a typed marker-plus-contract that names the junction model and the
/// pair of FK columns joining `Self` and `Target` through it. The three
/// async methods are **required** (no default bodies) because the typed
/// filter API cannot synthesise a column handle from a `&'static str`
/// generically; hand-written impls (and the Phase 3 Task 7 `many_to_many!`
/// macro) supply those bodies with the concrete `{Through}Fields` handle.
///
/// # Example
///
/// ```ignore
/// use djogi::prelude::*;
/// use djogi::relation::{ForeignKey, ManyToMany};
///
/// #[model(table = "persons")]
/// #[derive(Debug, Clone)]
/// pub struct Person { pub name: String }
///
/// #[model(table = "groups")]
/// #[derive(Debug, Clone)]
/// pub struct Group { pub name: String }
///
/// #[model(table = "person_groups", through, no_default)]
/// #[derive(Debug, Clone)]
/// pub struct PersonGroup {
/// pub person_id: ForeignKey<Person>,
/// pub group_id: ForeignKey<Group>,
/// pub role: String,
/// }
///
/// impl ManyToMany<Group> for Person {
/// type Through = PersonGroup;
/// const RELATION: &'static str = "groups";
/// fn this_fk() -> &'static str { "person_id" }
/// fn that_fk() -> &'static str { "group_id" }
///
/// async fn related<'ctx>(
/// &'ctx self,
/// ctx: &'ctx mut DjogiContext,
/// ) -> Result<Vec<Group>, DjogiError>
/// {
/// // ... typed-filter body; see module docs.
/// # unimplemented!()
/// }
///
/// async fn add_related<'ctx>(
/// &'ctx self,
/// ctx: &'ctx mut DjogiContext,
/// target: &'ctx Group,
/// extras: PersonGroup,
/// ) -> Result<PersonGroup, DjogiError>
/// {
/// # unimplemented!()
/// }
///
/// async fn remove_related<'ctx>(
/// &'ctx self,
/// ctx: &'ctx mut DjogiContext,
/// target: &'ctx Group,
/// ) -> Result<u64, DjogiError>
/// {
/// # unimplemented!()
/// }
/// }
/// ```