Skip to main content

umbral_core/orm/
post.rs

1//! The hardcoded `Post` model.
2//!
3//! M1 ships exactly one model so the QuerySet machinery has something
4//! concrete to query against. Per CLAUDE.md M1: "QuerySet builder → SQL
5//! for one hard-coded model (no macros)." The model is intentionally
6//! tiny: an autoincrement primary key, two text columns, and a nullable
7//! datetime column. That covers the basic field-type repertoire (i64,
8//! String, Option<DateTime<Utc>>) the column module needs to demonstrate.
9//!
10//! At M2 the `Model` trait gets extracted from this concrete shape; at M3
11//! the trait impl gets generated from a `#[derive(Model)]` on the struct.
12//! The struct itself is the eventual target both abstractions converge on.
13
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16
17/// A blog post. The M1 hardcoded model.
18///
19/// The struct derives `sqlx::FromRow` so a sea-query SELECT can be
20/// executed via `sqlx::query_as::<_, Post>(...)` and rows come back
21/// already typed. This is the M1 stand-in for the M3 derive macro that
22/// will eventually generate the same impl.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow)]
24pub struct Post {
25    pub id: i64,
26    pub title: String,
27    pub body: String,
28    pub published_at: Option<DateTime<Utc>>,
29}
30
31impl Post {
32    /// Entry point for queries: `Post::objects().filter(...).fetch().await`.
33    ///
34    /// Returns a `Manager<Post>` which in turn produces `QuerySet<Post>`s
35    /// via its chainable methods. See `docs/specs/03-orm-querysets.md`.
36    pub fn objects() -> crate::orm::Manager<Post> {
37        crate::orm::Manager::new()
38    }
39}
40
41/// The hand-written `Model` impl. M3 will generate this exact impl from
42/// `#[derive(Model)]` on the struct above.
43///
44/// `Post::TABLE` and `Post::FIELDS` are reached via the trait (not as
45/// inherent consts); call sites use `<Post as Model>::TABLE` or just
46/// `Post::TABLE` when the trait is in scope.
47impl crate::orm::Model for Post {
48    type PrimaryKey = i64;
49
50    const NAME: &'static str = "Post";
51
52    const TABLE: &'static str = "post";
53
54    const FIELDS: &'static [crate::orm::FieldSpec] = &[
55        crate::orm::FieldSpec {
56            name: "id",
57            ty: crate::orm::SqlType::BigInt,
58            primary_key: true,
59            nullable: false,
60            supported_backends: &[],
61            fk_target: None,
62            noform: false,
63            db_constraint: true,
64            noedit: false,
65            is_string_repr: false,
66            max_length: 0,
67            choices: &[],
68            choice_labels: &[],
69            default: "",
70            is_multichoice: false,
71            unique: false,
72            on_delete: crate::orm::FkAction::NoAction,
73            on_update: crate::orm::FkAction::NoAction,
74            index: false,
75            auto_now_add: false,
76            auto_now: false,
77            help: "",
78            example: "",
79            widget: None,
80            min: None,
81            max: None,
82            text_format: ::core::option::Option::None,
83            slug_from: ::core::option::Option::None,
84        },
85        crate::orm::FieldSpec {
86            name: "title",
87            ty: crate::orm::SqlType::Text,
88            primary_key: false,
89            nullable: false,
90            supported_backends: &[],
91            fk_target: None,
92            noform: false,
93            db_constraint: true,
94            noedit: false,
95            is_string_repr: false,
96            max_length: 0,
97            choices: &[],
98            choice_labels: &[],
99            default: "",
100            is_multichoice: false,
101            unique: false,
102            on_delete: crate::orm::FkAction::NoAction,
103            on_update: crate::orm::FkAction::NoAction,
104            index: false,
105            auto_now_add: false,
106            auto_now: false,
107            help: "",
108            example: "",
109            widget: None,
110            min: None,
111            max: None,
112            text_format: ::core::option::Option::None,
113            slug_from: ::core::option::Option::None,
114        },
115        crate::orm::FieldSpec {
116            name: "body",
117            ty: crate::orm::SqlType::Text,
118            primary_key: false,
119            nullable: false,
120            supported_backends: &[],
121            fk_target: None,
122            noform: false,
123            db_constraint: true,
124            noedit: false,
125            is_string_repr: false,
126            max_length: 0,
127            choices: &[],
128            choice_labels: &[],
129            default: "",
130            is_multichoice: false,
131            unique: false,
132            on_delete: crate::orm::FkAction::NoAction,
133            on_update: crate::orm::FkAction::NoAction,
134            index: false,
135            auto_now_add: false,
136            auto_now: false,
137            help: "",
138            example: "",
139            widget: None,
140            min: None,
141            max: None,
142            text_format: ::core::option::Option::None,
143            slug_from: ::core::option::Option::None,
144        },
145        crate::orm::FieldSpec {
146            name: "published_at",
147            ty: crate::orm::SqlType::Timestamptz,
148            primary_key: false,
149            nullable: true,
150            supported_backends: &[],
151            fk_target: None,
152            noform: false,
153            db_constraint: true,
154            noedit: false,
155            is_string_repr: false,
156            max_length: 0,
157            choices: &[],
158            choice_labels: &[],
159            default: "",
160            is_multichoice: false,
161            unique: false,
162            on_delete: crate::orm::FkAction::NoAction,
163            on_update: crate::orm::FkAction::NoAction,
164            index: false,
165            auto_now_add: false,
166            auto_now: false,
167            help: "",
168            example: "",
169            widget: None,
170            min: None,
171            max: None,
172            text_format: ::core::option::Option::None,
173            slug_from: ::core::option::Option::None,
174        },
175    ];
176
177    fn primary_key(&self) -> i64 {
178        self.id
179    }
180}
181
182/// `Post` has no FK fields, so `HydrateRelated` is a no-op.
183impl crate::orm::HydrateRelated for Post {
184    fn fk_id_for(&self, _field_name: &str) -> Option<serde_json::Value> {
185        None
186    }
187    fn hydrate_fk(&mut self, _field_name: &str, _row: &serde_json::Value) {}
188}
189
190/// The sibling column module.
191///
192/// Each column constant here is the typed handle used in `filter` /
193/// `order_by` predicates: `post::ID.eq(2)`, `post::PUBLISHED_AT.is_not_null()`,
194/// etc. M3 will generate this module from the `#[derive(Model)]` on the
195/// struct above.
196///
197/// The double-`post` path (`umbral_core::orm::post::post::ID`) reads oddly
198/// but matches the spec's `<model>.rs` file + sibling `mod <model>` of
199/// column constants convention. clippy's `module_inception` lint is
200/// silenced because the pattern is intentional.
201#[allow(clippy::module_inception)]
202pub mod post {
203    use super::Post;
204    use crate::orm::column::{IntCol, NullableDateTimeCol, StrCol};
205
206    pub const ID: IntCol<Post> = IntCol::new("id");
207    pub const TITLE: StrCol<Post> = StrCol::new("title");
208    pub const BODY: StrCol<Post> = StrCol::new("body");
209    pub const PUBLISHED_AT: NullableDateTimeCol<Post> = NullableDateTimeCol::new("published_at");
210}