djogi 0.1.0-alpha.3

Model-first web framework for Rust — web-framework-agnostic core; Axum integration opt-in via the `axum` feature flag
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
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
//! Correlated subqueries, EXISTS predicates, and typed outer-scope
//! column references — the remaining expression-IR surface the plan's
//! Task 5 brief calls out.
//!
//! # What this module ships
//!
//! Three typed wrappers, one macro seal:
//!
//! - [`Subquery<T, V>`] — `(SELECT <col> FROM T::table_name() WHERE ...)`
//!   as a scalar [`Expr<V>`]. Built from a [`QuerySet<T>`] plus a
//!   [`FieldRef<T, V>`] picking the column to project.
//! - [`Exists`] — `EXISTS (SELECT 1 FROM T::table_name() WHERE ...)` as
//!   an [`Expr<bool>`]. Built from a [`QuerySet<T>`] alone; the emitter
//!   always renders `SELECT 1` because EXISTS only cares about row
//!   presence.
//! - [`OuterRef<M, V>`] — correlated reference to a column in the
//!   enclosing query scope. Shape mirrors [`FieldRef<M, V>`] so the
//!   compiler catches value-type mismatches at `eq` / `lt` / arithmetic
//!   composition sites.
//! - [`__macro_support::__make_outer_ref`] — the sealed macro entry
//!   point `{Model}OuterRef::column()` accessors route through (same
//!   identifier-validation pattern as
//!   [`crate::query::field::__macro_support::__make_field_ref`]).
//!
//! # Design: store the queryset's `Q<T>` behind an erased emitter
//!
//! Phase 8eta PR2b — the subquery's `WHERE` predicate is the parent
//! queryset's accumulated `Q<T>` tree, wrapped in an
//! [`crate::expr::node::ErasedSubqueryPredicate`] handle and carried on
//! the [`crate::expr::node::SubqueryNode`] payload. Emission flows
//! through the direct-`Q<T>` SQL walker
//! (`crate::query::sql::emit_q::<T>`), so portable root-field
//! predicates inside a subquery emit through
//! `Model::__djogi_emit_field_predicate` (PR2d's macro override) without
//! ever round-tripping through `q_to_condition`.
//!
//! **Why type-erase rather than parameterise the node?** `SubqueryNode`
//! is a variant inside `ExprNode`, which itself is the type-erased
//! payload behind `Expr<V>`. Threading a `T` parameter through
//! `ExprNode` would propagate the model bound to every `Expr` user.
//! Wrapping the `Q<T>` behind a `dyn SubqueryPredicateEmitter` keeps the
//! expression IR model-parameter-free while preserving the trusted-
//! provenance shape of the inner predicate. The trait object's `emit`
//! method drives `query::sql::emit_q::<T>` from the model-knowing side,
//! inside the constructor that captures `T`.
//!
//! # Why typed `OuterRef<M, V>` (plan Q12 = B)
//!
//! Bare `&'static str` would let the user write
//! `Expr::outer_ref_raw("id").as_expr().eq(other_col.as_expr())` with no
//! check that "id" actually exists on the outer-scope model or that the
//! value type matches. The typed shape makes the compiler catch both:
//! `OuterRef<Account, HeerId>::id()` is constructive evidence that
//! column "id" exists on `Account` as `HeerId`, and `.as_expr()` returns
//! an `Expr<HeerId>` that can only `.eq` another `Expr<HeerId>`.
//!
//! # Macro integration
//!
//! `#[derive(Model)]` emits a `{Model}OuterRef` ZST with one accessor per
//! column, mirroring the `{Model}Fields` pattern. The accessors route
//! through [`__macro_support::__make_outer_ref`] so the column string
//! gets the same validation as [`FieldRef::new`].
//!
//! # Column qualification
//!
//! `OuterRef` exposes two emission modes:
//!
//! - [`OuterRef::as_expr`] — emits the column name **unqualified**. Postgres
//!   resolves an unqualified name against the enclosing query scope when
//!   the inner `FROM` list has no matching column, which covers the common
//!   case. When both tables expose a same-named column (every Djogi model
//!   has `id`, `created_at`, `updated_at`, so this is real), the emission
//!   is ambiguous and Postgres raises `42702`. Use this form when you've
//!   verified the inner / outer scopes share no column names.
//! - [`OuterRef::as_qualified_expr`] — emits `<M::table_name()>.<column>`
//!   so the reference disambiguates to the outer scope unconditionally.
//!   Phase 7-Zero-2 T13b's macro-emitted M2M `EXISTS` predicates use this
//!   form because the through-table and target-table always collide on
//!   framework-column names. Adopters writing correlated subqueries by
//!   hand should reach for this whenever the inner / outer column-name
//!   sets are not provably disjoint.
//!
//! Generalised `parent_table` threading for `select_related + filter_expr`
//! composition (where the outer FROM may be aliased rather than literal
//! `M::table_name()`) is the next step on this surface and remains a
//! Phase 8+ enhancement. See `docs/roadmap/future-work.md` §4.7.

use crate::expr::Expr;
use crate::expr::node::{ErasedSubqueryPredicate, ExprNode, SubqueryNode};
use crate::model::Model;
// Phase 8eta PR3: `Subquery::new` no longer demands a `FieldRef<T, V>`
// — it accepts any `IntoSqlField<T, V>` so post-flip root accessors
// (`DjogiField<T, V>`) compose without explicit unwrapping. The unit
// tests under `mod tests` construct `FieldRef` values directly via
// their own scoped imports — no top-level use is needed.
use crate::query::queryset::QuerySet;
use std::any::TypeId;
use std::marker::PhantomData;

// ── Subquery<T, V> ────────────────────────────────────────────────────
//
// Scalar subquery — (SELECT <col> FROM T::table_name() WHERE ...).
// The SQL is parenthesised at emission time so the subquery can appear
// in any `Expr<V>` position.

/// Typed scalar subquery — lowers to `(SELECT <col> FROM <table>
/// WHERE <cond>)` when emitted.
///
/// `T` pins the source model (so the emitter knows which table to
/// SELECT from) and `V` pins the column's Rust type (so the scalar
/// returned by the subquery slots into `Expr<V>` positions without an
/// explicit cast). Both are phantom-typed; the node itself carries only
/// the untyped [`SubqueryNode`] payload.
///
/// `#[must_use]` — a dropped `Subquery` is usually a mistake; the user
/// likely meant to feed it to `.as_expr()` and thence to
/// [`crate::query::QuerySet::filter_expr`] / an arithmetic composition.
///
/// Construction: [`Subquery::new`] takes the source queryset and the
/// projected column. No other public constructor — the `SubqueryNode`
/// inside is crate-private so downstream code cannot fabricate a
/// subquery with a mismatched column / type pair.
#[must_use = "subqueries are lazy — drop one and the predicate is silently omitted"]
pub struct Subquery<T: Model, V> {
    // Untyped payload — the emitter walks this directly. Typed `T`/`V`
    // live only in the phantom markers.
    node: SubqueryNode,
    // Covariant phantom tags — mirror the pattern on
    // [`crate::expr::Expr`] / [`FieldRef`] so the wrapper is
    // `Send + Sync` regardless of `T`/`V`'s own markers.
    _phantom: PhantomData<fn() -> (T, V)>,
}

impl<T: Model, V> Subquery<T, V> {
    /// Build a scalar subquery from a source queryset and the column to
    /// project.
    ///
    /// The `qs` parameter supplies the `FROM <table>` source and the
    /// correlated `WHERE` predicate; the `column` parameter picks which
    /// scalar the subquery returns. `T` on both arguments is the same
    /// model, so the compiler rejects a column ref taken from a
    /// different model's `Fields` handle.
    ///
    /// # How the `WHERE` clause flows
    ///
    /// `qs.condition` is cloned verbatim into the
    /// [`SubqueryNode::where_clause`] slot — every Phase 2 lookup op the
    /// caller composed via `filter` / `filter_expr` is preserved. The
    /// clone is structural (the condition tree is shallow `Vec` / `Box`
    /// / enum variants); typical correlated-subquery call sites build
    /// the queryset fresh, so there is nothing to share-ownership with.
    ///
    /// `Condition::True` (the "no filter" identity) is stored as
    /// `Some(True)` and the emitter renders it as `WHERE TRUE`. This
    /// matches the existing `push_where` shim's behaviour — a vacuous
    /// predicate is rare in correlated subqueries, so the emission
    /// overhead is not worth a special case here.
    ///
    /// # What this ignores about `qs`
    ///
    /// A queryset carries ordering, limit/offset, distinct mode, and
    /// prefetch / select_related registrations. The subquery emitter
    /// consumes only the table name and condition tree; ordering and
    /// pagination are ignored because Postgres' scalar-subquery context
    /// does not use them (a scalar subquery must return a single value;
    /// `ORDER BY ... LIMIT 1` would be the usual way to force that but
    /// is out of scope for Task 5). Callers who need a deterministic
    /// scalar from a multi-row source should use `ctx.raw_scalar`
    /// until Phase 5 extends this surface.
    /// PR3: accepts both legacy `FieldRef<T, V>` and the post-flip root
    /// accessor return type `DjogiField<T, V>` through the sealed
    /// [`IntoSqlField`](crate::query::field::IntoSqlField) bridge.
    /// Subquery `SELECT <col>` is a SQL-only emission boundary — the
    /// projected column metadata flows through unchanged.
    pub fn new<S>(qs: QuerySet<T>, column: S) -> Self
    where
        S: crate::query::field::IntoSqlField<T, V>,
    {
        Subquery {
            node: SubqueryNode {
                table: T::table_name(),
                select_column: Some(column.into_sql_field().column()),
                // Phase 8eta PR2b: store the queryset's `Q<T>` behind an
                // erased emitter handle so portable root-field predicates
                // emit through `query::sql::emit_q` without round-tripping
                // through `q_to_condition`. Vacuous-truth queries (no
                // filters chained) collapse to `None` so the emitter
                // skips the `WHERE` clause entirely — same logical
                // shape Subquery emission produced before the rewrite.
                where_clause: q_to_subquery_opt::<T>(qs.condition),
            },
            _phantom: PhantomData,
        }
    }

    /// Promote this subquery to a typed [`Expr<V>`] for use in
    /// `filter_expr`, `set_expr`, or any other expression-IR consumer.
    ///
    /// The phantom `V` on `Subquery<T, V>` projects directly onto the
    /// resulting `Expr<V>` — so a subquery that selects a `HeerId`
    /// column produces an `Expr<HeerId>` that can `.eq` another
    /// `Expr<HeerId>`, and nothing else. Type discipline all the way
    /// through.
    pub fn as_expr(self) -> Expr<V> {
        Expr::from_node(ExprNode::Subquery(Box::new(self.node)))
    }
}

// ── Exists ────────────────────────────────────────────────────────────
//
// EXISTS (SELECT 1 FROM T::table_name() WHERE ...) — boolean predicate.
// No `V` parameter on the typed surface because EXISTS always yields a
// boolean (`Expr<bool>`) regardless of what columns the inner queryset
// selected.

/// Typed `EXISTS (...)` predicate — lowers to `EXISTS (SELECT 1 FROM
/// <table> WHERE <cond>)` when emitted.
///
/// `T` pins the source model at construction time (so the emitter
/// knows which table to SELECT from); the typed surface drops `T`
/// after `.as_expr()` because the `Expr<bool>` result is type-erased
/// (every `Exists` produces the same `Expr<bool>` type regardless of
/// source model).
///
/// `#[must_use]` — a dropped `Exists` is usually a mistake; the user
/// likely meant to feed it to `.as_expr()` and thence to
/// [`crate::query::QuerySet::filter_expr`].
#[must_use = "EXISTS predicates are lazy — drop one and the filter is silently omitted"]
pub struct Exists {
    /// Untyped payload. `select_column` is always `None` for this
    /// variant; the emitter special-cases that arm to render
    /// `SELECT 1`.
    node: SubqueryNode,
}

impl Exists {
    /// Build an `EXISTS (...)` predicate from a source queryset.
    ///
    /// Drops the queryset's `T` parameter at construction because the
    /// emitted SQL only needs the table name + condition tree — every
    /// `Exists` carries an `Expr<bool>` result regardless of the inner
    /// model. Callers who want to express "at least one row matches"
    /// over a correlated predicate use this as the idiomatic shape.
    ///
    /// # Why `select_column = None`
    ///
    /// Postgres' EXISTS only evaluates the subquery to the point of
    /// finding (or failing to find) a single row; the selected columns
    /// are never materialised. `SELECT 1` is the idiomatic stand-in —
    /// it tells the planner "any row will do" without binding a
    /// specific column name that might later be renamed or dropped.
    /// Routing through `select_column = None` at the node level keeps
    /// the rendering decision ("`1` vs a column") inside the emitter,
    /// not the typed builder.
    ///
    /// # What this ignores about `qs`
    ///
    /// Mirrors the [`Subquery::new`] rule: only the table name and
    /// condition tree flow into the emitted SQL. Ordering, limit/offset,
    /// distinct mode, prefetch, and select_related registrations on the
    /// inner queryset are silently dropped. For EXISTS this is almost
    /// never a functional problem — the predicate is satisfied by the
    /// existence of any matching row, so `ORDER BY` and `LIMIT` are
    /// vestigial — but it is worth knowing that `Exists::new(qs.limit(1))`
    /// does not constrain the planner's row search at all. Callers who
    /// need row-bounded EXISTS semantics should use `ctx.raw_execute`
    /// until Phase 5 widens this surface.
    pub fn new<T: Model>(qs: QuerySet<T>) -> Self {
        Exists {
            node: SubqueryNode {
                table: T::table_name(),
                select_column: None,
                // Phase 8eta PR2b: store the queryset's `Q<T>` behind
                // an erased emitter handle. EXISTS still emits `SELECT
                // 1` from the inner subquery; only the WHERE body
                // changes shape vs the pre-flip `Option<Condition>`
                // storage.
                where_clause: q_to_subquery_opt::<T>(qs.condition),
            },
        }
    }

    /// Promote this EXISTS predicate to a typed [`Expr<bool>`] for use
    /// in [`crate::query::QuerySet::filter_expr`] or a nested boolean
    /// composition.
    pub fn as_expr(self) -> Expr<bool> {
        Expr::from_node(ExprNode::Exists(Box::new(self.node)))
    }
}

// ── OuterRef<M, V> ────────────────────────────────────────────────────
//
// Outer-scope column reference — shape mirrors `FieldRef<M, V>` so the
// compiler enforces the same value-type discipline on correlated
// predicates that Phase 2's literal-RHS filters already enjoy.

/// Typed reference to a column in the enclosing query scope — the
/// outer-scope counterpart to [`FieldRef<M, V>`].
///
/// Inside a correlated subquery (the typical site for `OuterRef`), an
/// unqualified column name resolves against the enclosing `FROM`
/// clause when the inner query's `FROM` list has no matching column.
/// `OuterRef` is the typed handle for that reference — it carries the
/// column name plus phantom markers that bind it to a specific model
/// (`M`) and a specific value type (`V`), so `.as_expr()` produces an
/// `Expr<V>` that only composes with same-`V` operands.
///
/// # Limitation: unqualified emission
///
/// `as_expr()` renders the column name without a table qualifier. When
/// the inner and outer scopes both expose a same-named column (every
/// Djogi model has `id`, `created_at`, `updated_at`, so this is real
/// for intra-model correlated subqueries), Postgres raises `42702
/// column reference "X" is ambiguous`. Workarounds:
///
/// - Correlate on tables whose bare column names do not collide.
/// - Use `ctx.raw_execute` / `ctx.raw_scalar` for explicitly-aliased
///   correlations.
///
/// The qualified form (carrying an outer-table alias) is deferred
/// alongside the broader `parent_table` threading needed for
/// `select_related + filter_expr` composition — flagged in the
/// [`crate::expr::sql`] module header and on [`OuterRef::as_expr`].
///
/// # Construction
///
/// Users do not call [`OuterRef::new`] directly. The
/// `#[derive(Model)]` macro emits a `{Model}OuterRef` ZST helper with
/// one accessor per column:
///
/// ```ignore
/// // Emitted by the macro for `struct Account { balance: i64 }`:
/// impl AccountOuterRef {
///     pub fn id() -> OuterRef<Account, HeerId> { /* ... */ }
///     pub fn balance() -> OuterRef<Account, i64> { /* ... */ }
///     pub fn created_at() -> OuterRef<Account, DateTime> { /* ... */ }
///     pub fn updated_at() -> OuterRef<Account, DateTime> { /* ... */ }
/// }
/// ```
///
/// The accessors route through [`__macro_support::__make_outer_ref`]
/// so every column string is validated via
/// [`crate::ident::assert_plain_ident`] at construction time.
pub struct OuterRef<M: Model, V> {
    /// Macro-baked column name — validated by
    /// [`__macro_support::__make_outer_ref`] at construction.
    column: &'static str,
    /// Covariant `M` tag — ties the ref to one model so that mixing
    /// `AccountOuterRef::id()` with a `Post`-correlated subquery is a
    /// compile error, not a runtime SQL error.
    _m: PhantomData<fn() -> M>,
    /// Covariant `V` tag — pins the column's value type so the
    /// produced `Expr<V>` only composes with same-`V` operands.
    _v: PhantomData<fn() -> V>,
}

impl<M: Model, V> Copy for OuterRef<M, V> {}
impl<M: Model, V> Clone for OuterRef<M, V> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<M: Model, V> std::fmt::Debug for OuterRef<M, V> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "OuterRef({})", self.column)
    }
}

impl<M: Model, V> OuterRef<M, V> {
    /// Crate-private constructor. The macro-emitted
    /// `{Model}OuterRef::column()` accessors route through
    /// [`__macro_support::__make_outer_ref`], which validates the
    /// identifier before calling this. Downstream code cannot
    /// fabricate an `OuterRef` with an unvalidated column string —
    /// the seal mirrors [`FieldRef::new`]'s `pub(crate)` visibility.
    ///
    /// `const` so the macro-emitted accessors stay trivially
    /// inlinable — same pattern as [`FieldRef::new`].
    pub(crate) const fn new(column: &'static str) -> Self {
        OuterRef {
            column,
            _m: PhantomData,
            _v: PhantomData,
        }
    }

    /// Promote this outer-scope column ref to a typed [`Expr<V>`] for
    /// use inside a correlated subquery's `filter_expr` closure.
    ///
    /// The emitted SQL is the bare column name — Postgres resolves the
    /// reference against the outer scope when the inner query's `FROM`
    /// list has no matching column. Same-named collisions raise a
    /// runtime error; see the type-level limitation note.
    #[must_use = "OuterRef is inert unless promoted to Expr<V>"]
    pub fn as_expr(self) -> Expr<V> {
        Expr::from_node(ExprNode::OuterRef {
            column: self.column,
        })
    }

    /// Promote this outer-scope column ref to a typed [`Expr<V>`] that
    /// emits as `<M::table_name()>.<column>` instead of the bare column
    /// name.
    ///
    /// Use this whenever the inner subquery and outer scope share a
    /// column name (every Djogi model carries `id` / `created_at` /
    /// `updated_at`, so M2M correlations always collide). The qualified
    /// form lets Postgres resolve the reference unambiguously, sidestepping
    /// the `42702 column reference is ambiguous` failure that the bare
    /// [`Self::as_expr`] form falls into.
    ///
    /// `M::table_name()` is the source of the qualifier — every
    /// `Model::table_name()` is validated by Djogi's identifier rules at
    /// `#[model]` expansion time, so the resulting SQL token is safe to
    /// push without re-validation.
    #[must_use = "OuterRef is inert unless promoted to Expr<V>"]
    pub fn as_qualified_expr(self) -> Expr<V> {
        Expr::from_node(ExprNode::OuterRefColumn {
            table: M::table_name(),
            column: self.column,
        })
    }

    /// Promote this outer-scope column ref to a typed [`Expr<V>`] that
    /// emits as `l.<column>`, using the fixed `l` alias defined by
    /// lateral queries.
    ///
    /// Use this in the inner closure of `join_lateral` or
    /// `left_join_lateral` to correlate the inner query with the
    /// outer query.
    ///
    /// The returned expression is validated when SQL is built:
    ///
    /// - It is only legal inside a lateral-inner lowering scope.
    /// - Its source model `M` must match the actual lateral outer model.
    ///
    /// Out-of-scope or wrong-model usage fails as a typed
    /// `DjogiError::Predicate` before any SQL reaches PostgreSQL.
    #[must_use = "OuterRef is inert unless promoted to Expr<V>"]
    pub fn as_lateral_outer_expr(self) -> Expr<V> {
        Expr::from_node(ExprNode::OuterRefAlias {
            alias: "l",
            column: self.column,
            model_type: TypeId::of::<M>(),
            model_name: std::any::type_name::<M>(),
        })
    }
}

// ── Q<T> → Option<ErasedSubqueryPredicate> helper ─────────────────────
//
// Phase 8eta PR2b — replaces the pre-flip `condition_to_opt` helper.
// Vacuous-truth `Q<T>` queries (e.g. an unfiltered queryset whose
// condition is `Q::Portable(PortablePredicate::True)`) collapse to
// `None` so the subquery emitter skips the `WHERE` clause entirely on
// filter-less subqueries — same observable behaviour as the pre-flip
// `condition_to_opt`. Non-trivial queries are wrapped in an
// `ErasedSubqueryPredicate` handle so subquery emission flows through
// the direct-`Q<T>` SQL walker (`query::sql::emit_q`).

/// Collapse a vacuous `Q<T>` into `None` and otherwise wrap the predicate
/// in an `ErasedSubqueryPredicate` handle. Used by both [`Subquery::new`]
/// and [`Exists::new`] so subquery and EXISTS emission share one
/// identity-folding path.
fn q_to_subquery_opt<T: Model>(q: crate::query::q::Q<T>) -> Option<ErasedSubqueryPredicate> {
    if is_vacuously_true(&q) {
        None
    } else {
        Some(ErasedSubqueryPredicate::from_q::<T>(q))
    }
}

/// Test whether a `Q<T>` is structurally vacuously TRUE. Mirrors the
/// `query::sql::q_is_vacuously_true` helper (kept module-private there);
/// duplicating the small walker here keeps `expr::subquery` from reaching
/// across crate-private boundaries while the legacy SQL path remains in
/// transition.
fn is_vacuously_true<T: Model>(q: &crate::query::q::Q<T>) -> bool {
    use crate::query::q::{CompoundOp, Q};
    match q {
        Q::Portable(p) => match p.inner_ref() {
            sassi::BasicPredicate::True => true,
            sassi::BasicPredicate::And(parts) => parts
                .iter()
                .all(|c| matches!(c, sassi::BasicPredicate::True)),
            _ => false,
        },
        Q::Condition(c) => c.is_vacuously_true(),
        Q::Compound {
            op: CompoundOp::And,
            parts,
        } => parts.iter().all(is_vacuously_true),
        Q::Negated(inner) => match inner.as_ref() {
            Q::Portable(p) => matches!(p.inner_ref(), sassi::BasicPredicate::False),
            Q::Compound {
                op: CompoundOp::Or,
                parts,
            } => parts.is_empty(),
            _ => false,
        },
        _ => false,
    }
}

/// Macro-only entry points. **Not** part of the stable public API.
///
/// `djogi-macros` emits calls into this module from the `{Model}OuterRef`
/// helper struct that `#[derive(Model)]` expands in the user's crate —
/// the items here are `pub` only so cross-crate codegen can reach them.
/// The double-underscore prefix and `#[doc(hidden)]` marker signal to
/// tooling and reviewers that downstream code must not call these
/// directly; the macro is the sole supported caller.
///
/// The seal closes the identifier-smuggling vector that
/// [`OuterRef::new`]'s `pub(crate)` visibility addresses at the crate
/// boundary: cross-crate code reaches an `OuterRef` only through this
/// validator, same pattern as
/// [`crate::query::field::__macro_support::__make_field_ref`] for
/// [`FieldRef`].
#[doc(hidden)]
pub mod __macro_support {
    use super::OuterRef;
    use crate::ident::assert_plain_ident;
    use crate::model::Model;

    /// Construct an [`OuterRef<M, V>`] from a macro-emitted column
    /// name. The only supported caller is the
    /// `{Model}OuterRef::column()` accessor that `#[derive(Model)]`
    /// emits in the user's crate.
    ///
    /// Panics if `column` violates any rule in
    /// [`crate::ident::assert_plain_ident`]: empty, over 63 bytes,
    /// leading digit, a non-identifier byte, or a reserved Postgres
    /// keyword. Mirrors the seal on
    /// [`crate::query::field::__macro_support::__make_field_ref`]
    /// exactly — the two functions close the same identifier vector on
    /// two parallel surfaces.
    #[doc(hidden)]
    pub fn __make_outer_ref<M: Model, V>(column: &'static str) -> OuterRef<M, V> {
        assert_plain_ident(column, "outer_ref_column");
        OuterRef::new(column)
    }
}

#[cfg(test)]
mod tests {
    //! Emitter shape tests — each construct renders the expected SQL
    //! tokens. Live DB coverage is in
    //! `tests/integration/phase4_transactions_expressions.rs`.

    use super::*;
    use crate::Expr;
    use crate::descriptor::ModelDescriptor;
    use crate::expr::sql::emit_expr;
    use crate::pg::accumulator::SqlAccumulator;
    use crate::query::portable::SqlEmitContext;

    // Inert local model — only `table_name` matters for emission tests.
    struct Ledger;
    impl crate::model::__sealed::Sealed for Ledger {}
    #[allow(clippy::manual_async_fn)]
    impl Model for Ledger {
        type Pk = i64;
        type Fields = ();
        fn table_name() -> &'static str {
            "ledgers"
        }
        fn pk_value(&self) -> &i64 {
            unreachable!()
        }
        fn descriptor() -> &'static ModelDescriptor {
            unreachable!()
        }
        fn get(
            _ctx: &mut crate::context::DjogiContext,
            _id: i64,
        ) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
            async { unreachable!() }
        }
        fn create(
            _ctx: &mut crate::context::DjogiContext,
            _v: Self,
        ) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
            async { unreachable!() }
        }
        fn save<'ctx>(
            &'ctx mut self,
            _ctx: &'ctx mut crate::context::DjogiContext,
        ) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx
        {
            async { unreachable!() }
        }
        fn delete(
            self,
            _ctx: &mut crate::context::DjogiContext,
        ) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send {
            async { unreachable!() }
        }
        fn refresh_from_db<'ctx>(
            &'ctx self,
            _ctx: &'ctx mut crate::context::DjogiContext,
        ) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx
        {
            async { unreachable!() }
        }
    }

    struct Entry {
        id: i64,
        memo: String,
        active: bool,
    }
    impl crate::model::__sealed::Sealed for Entry {}
    #[allow(clippy::manual_async_fn)]
    impl Model for Entry {
        type Pk = i64;
        type Fields = ();
        fn table_name() -> &'static str {
            "entries"
        }
        fn pk_value(&self) -> &i64 {
            &self.id
        }
        fn descriptor() -> &'static ModelDescriptor {
            unreachable!()
        }
        fn __djogi_emit_field_predicate(
            acc: &mut crate::pg::accumulator::SqlAccumulator,
            field: &crate::types::FieldPredicate<Self>,
            ctx: crate::query::SqlEmitContext,
        ) -> Result<(), crate::query::PortablePredicateError> {
            use crate::query::portable::emit;
            use crate::types::LookupOp;

            match (field.field_name(), field.op()) {
                ("memo", LookupOp::Eq) => {
                    emit::emit_value::<Self, String>(acc, ctx, "memo", " = ", field)
                }
                ("active", LookupOp::Eq) => {
                    emit::emit_value::<Self, bool>(acc, ctx, "active", " = ", field)
                }
                ("memo", op) => Err(crate::query::PortablePredicateError::UnsupportedLookup {
                    field: "memo",
                    op,
                }),
                ("active", op) => Err(crate::query::PortablePredicateError::UnsupportedLookup {
                    field: "active",
                    op,
                }),
                (field, _) => Err(crate::query::PortablePredicateError::UnsupportedField { field }),
            }
        }
        fn get(
            _ctx: &mut crate::context::DjogiContext,
            _id: i64,
        ) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
            async { unreachable!() }
        }
        fn create(
            _ctx: &mut crate::context::DjogiContext,
            _v: Self,
        ) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
            async { unreachable!() }
        }
        fn save<'ctx>(
            &'ctx mut self,
            _ctx: &'ctx mut crate::context::DjogiContext,
        ) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx
        {
            async { unreachable!() }
        }
        fn delete(
            self,
            _ctx: &mut crate::context::DjogiContext,
        ) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send {
            async { unreachable!() }
        }
        fn refresh_from_db<'ctx>(
            &'ctx self,
            _ctx: &'ctx mut crate::context::DjogiContext,
        ) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx
        {
            async { unreachable!() }
        }
    }

    #[test]
    fn exists_no_filter_emits_bare_select_one() {
        // Exists::new(Entry::objects()).as_expr() — no WHERE clause
        // because vacuous-true Q collapses to `None` at construction.
        let qs: QuerySet<Entry> = QuerySet::new();
        let expr = Exists::new(qs).as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("subquery emission");
        let sql = qb.sql();
        assert_eq!(sql.trim(), "EXISTS (SELECT 1 FROM entries)", "got: {sql}");
    }

    #[test]
    fn scalar_subquery_no_filter_emits_without_where() {
        // Subquery::new(Entry::objects(), id_col).as_expr() starts from
        // Q::Portable(True), collapses the erased predicate to None, and
        // emits no WHERE clause.
        use crate::query::field::FieldRef;

        let id_col: FieldRef<Entry, i64> = FieldRef::new("id");
        let qs: QuerySet<Entry> = QuerySet::new();
        let expr = Subquery::new(qs, id_col).as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
        let sql = qb.sql();
        assert_eq!(sql.trim(), "(SELECT id FROM entries)", "got: {sql}");
    }

    #[test]
    fn exists_with_portable_predicate_uses_erased_q_emitter() {
        // A portable Field(_) predicate would panic if Subquery/Exists
        // construction still routed through q_to_condition. This test
        // proves it reaches Entry::__djogi_emit_field_predicate through
        // the erased direct-Q emitter instead.
        use crate::query::field::djogi_field_macro_support::__make_djogi_field;

        let active = __make_djogi_field::<Entry, bool>("active", |row| &row.active);
        let qs: QuerySet<Entry> = QuerySet::new().filter_struct(active.eq(true));
        let expr = Exists::new(qs).as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
        let sql = qb.sql();
        assert_eq!(
            sql.trim(),
            "EXISTS (SELECT 1 FROM entries WHERE active = $1)",
            "got: {sql}"
        );
    }

    #[test]
    fn exists_under_joined_context_keeps_subquery_fields_unqualified() {
        use crate::query::field::djogi_field_macro_support::__make_djogi_field;

        let active = __make_djogi_field::<Entry, bool>("active", |row| &row.active);
        let qs: QuerySet<Entry> = QuerySet::new().filter_struct(active.eq(true));
        let expr = Exists::new(qs).as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::joined("l")).expect("expression emission");
        let sql = qb.sql();
        assert_eq!(
            sql.trim(),
            "EXISTS (SELECT 1 FROM entries WHERE active = $1)",
            "subquery-owned fields must not inherit the outer joined alias: {sql}"
        );
    }

    #[test]
    fn exists_with_correlated_outer_ref() {
        // Exists::new(Entry::objects().filter(|f| <outer predicate>))
        // The outer ref should emit as a bare column name, with the
        // inner column referencing the same bare name. The test here
        // asserts the structural shape — live correlation resolution
        // happens in the Postgres integration test.
        use crate::query::field::FieldRef;
        let inner_col: FieldRef<Entry, i64> = FieldRef::new("ledger_id");
        let outer_ref: OuterRef<Ledger, i64> = OuterRef::new("id");
        let qs: QuerySet<Entry> =
            QuerySet::new().filter_expr(|_| inner_col.as_expr().eq(outer_ref.as_expr()));
        let expr = Exists::new(qs).as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
        let sql = qb.sql();
        assert_eq!(
            sql.trim(),
            "EXISTS (SELECT 1 FROM entries WHERE ledger_id = id)",
            "got: {sql}"
        );
    }

    #[test]
    fn scalar_subquery_emits_select_col() {
        // Subquery::new(Entry::objects().filter(memo eq "x"), id_col).as_expr()
        use crate::query::field::FieldRef;
        let memo: FieldRef<Entry, String> = FieldRef::new("memo");
        let id_col: FieldRef<Entry, i64> = FieldRef::new("id");
        let qs: QuerySet<Entry> = QuerySet::new().filter(|_| memo.eq("opening".to_string()));
        let expr = Subquery::new(qs, id_col).as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
        let sql = qb.sql();
        // One bind for the "opening" literal — assert structural shape
        // with the bind placeholder.
        assert_eq!(
            sql.trim(),
            "(SELECT id FROM entries WHERE memo = $1)",
            "got: {sql}"
        );
    }

    #[test]
    fn scalar_subquery_with_portable_predicate_uses_erased_q_emitter() {
        // Same regression as the EXISTS test, but through scalar
        // Subquery::new. If this constructor ever reintroduces
        // q_to_condition for trusted portable predicates, the
        // BasicPredicate::Field arm panics before SQL emission.
        use crate::query::field::FieldRef;
        use crate::query::field::djogi_field_macro_support::__make_djogi_field;

        let memo = __make_djogi_field::<Entry, String>("memo", |row| &row.memo);
        let id_col: FieldRef<Entry, i64> = FieldRef::new("id");
        let qs: QuerySet<Entry> = QuerySet::new().filter_struct(memo.eq("opening".to_string()));
        let expr = Subquery::new(qs, id_col).as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
        let sql = qb.sql();
        assert_eq!(
            sql.trim(),
            "(SELECT id FROM entries WHERE memo = $1)",
            "got: {sql}"
        );
    }

    #[test]
    fn outer_ref_emits_bare_column() {
        // Baseline check — the outer ref on its own emits just the
        // column name, no qualifier.
        let r: OuterRef<Ledger, i64> = OuterRef::new("id");
        let expr: Expr<i64> = r.as_expr();
        let mut qb = SqlAccumulator::new("");
        emit_expr(&mut qb, &expr.node, SqlEmitContext::root()).expect("expression emission");
        assert_eq!(qb.sql().trim(), "id", "got: {}", qb.sql());
    }

    #[test]
    fn make_outer_ref_seal_validates_identifier() {
        // Identifier validation runs at macro-entry time. A plain
        // column name succeeds; an empty / SQL-metachar payload
        // panics. Full validator coverage lives in `crate::ident`; this
        // test only asserts the seal threads through.
        let result =
            std::panic::catch_unwind(|| __macro_support::__make_outer_ref::<Ledger, i64>("id"));
        assert!(result.is_ok(), "plain column name must pass the seal");

        let result =
            std::panic::catch_unwind(|| __macro_support::__make_outer_ref::<Ledger, i64>("1bad"));
        assert!(result.is_err(), "leading-digit column must panic the seal");

        let result = std::panic::catch_unwind(|| {
            __macro_support::__make_outer_ref::<Ledger, i64>("id) OR 1=1 --")
        });
        assert!(result.is_err(), "SQL-metachar payload must panic the seal");
    }
}