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
//! End-to-end coverage for the M2 `Model` trait surface as implemented by `Post`.
//!
//! The trait is mostly static metadata plus a tiny primary-key getter, so the
//! bulk of these checks are sync `#[test]`s reading `TABLE`, `FIELDS`, and the
//! `PrimaryKey` associated type. One `#[tokio::test]` at the bottom exercises
//! the `for<'r> FromRow<'r, SqliteRow>` supertrait against a real in-memory
//! SQLite pool, which is the M2 invariant QuerySet terminals rely on to
//! blanket `T: Model` and call `sqlx::query_as_with::<_, T, _>`.
//!
//! Living under `tests/` rather than alongside `src/orm/model.rs` matters for
//! the async case: each file in `tests/` compiles to its own test binary, so
//! the process-wide `OnceLock`s in `umbral_core::db` start empty for every run
//! and can't be polluted by a sibling test that already called `App::build()`.
//! It also keeps the sync trait checks and the async DB check in the same
//! file without splitting them across compilation units.
use umbral_core::db;
use umbral_core::orm::{FieldSpec, Model, Post, SqlType};
/// The `TABLE` constant should be the lowercase struct name. M3's derive will
/// default to `snake_case(<StructName>)`; the hand-written M2 impl pins the
/// `Post` -> `"post"` mapping that the derive has to reproduce.
#[test]
fn post_table_constant_matches_struct_name() {
assert_eq!(Post::TABLE, "post");
}
/// `FIELDS` lists every column in struct declaration order. That order is
/// the contract M5's migration engine relies on for snapshot diffing, so
/// drift here would silently break autodetection later.
#[test]
fn post_has_four_fields_in_declaration_order() {
assert_eq!(
Post::FIELDS.len(),
4,
"Post has exactly four columns: id, title, body, published_at",
);
let names: Vec<&str> = Post::FIELDS.iter().map(|f| f.name).collect();
assert_eq!(
names,
vec!["id", "title", "body", "published_at"],
"FIELDS must mirror struct declaration order",
);
}
/// Exactly one field carries `primary_key: true`, and it's `id`. Models with
/// no PK or multiple PKs are spec violations at M2; this guards the canonical
/// single-column shape.
#[test]
fn post_primary_key_field_is_id() {
let pks: Vec<&FieldSpec> = Post::FIELDS.iter().filter(|f| f.primary_key).collect();
assert_eq!(
pks.len(),
1,
"exactly one field should be marked as the primary key",
);
assert_eq!(pks[0].name, "id", "the primary key column should be 'id'");
}
/// Exactly one field is `nullable: true`, and it's `published_at`. This
/// mirrors the `Option<DateTime<Utc>>` field on the struct and pins the
/// `04-orm-model-and-fields.md` invariant that NULL is reachable only via
/// `Option<T>`.
#[test]
fn post_published_at_is_the_only_nullable_field() {
let nullable: Vec<&FieldSpec> = Post::FIELDS.iter().filter(|f| f.nullable).collect();
assert_eq!(
nullable.len(),
1,
"exactly one field should be nullable (matches the lone Option<T> on Post)",
);
assert_eq!(
nullable[0].name, "published_at",
"the nullable field should be 'published_at'",
);
}
/// Each field's `SqlType` tag should match its Rust type: `id` -> BigInt,
/// `title` / `body` -> Text, `published_at` -> Timestamptz. The dialect
/// rendering is the backend's job; this enum is the abstract classification
/// the system check (M4) and migration engine (M5) reason about.
#[test]
fn post_field_types_are_correct() {
let by_name = |name: &str| {
Post::FIELDS
.iter()
.find(|f| f.name == name)
.unwrap_or_else(|| panic!("Post::FIELDS should contain '{name}'"))
};
assert_eq!(by_name("id").ty, SqlType::BigInt);
assert_eq!(by_name("title").ty, SqlType::Text);
assert_eq!(by_name("body").ty, SqlType::Text);
assert_eq!(by_name("published_at").ty, SqlType::Timestamptz);
}
/// Every field on `Post` declares an empty `supported_backends` slice, which
/// per the trait doc means "all backends." No Postgres-only field types
/// (arrays, JSONB, etc.) are in play at M2, so the system check has nothing
/// to reject.
#[test]
fn post_supported_backends_is_empty_for_every_field() {
for field in Post::FIELDS {
assert!(
field.supported_backends.is_empty(),
"field '{}' should declare no backend restrictions at M2, got {:?}",
field.name,
field.supported_backends,
);
}
}
/// `primary_key()` should return the value of the `id` field on the instance.
/// Exercises the only fn on the trait, not just its constants.
#[test]
fn primary_key_getter_returns_id_field() {
let post = Post {
id: 42,
title: "any title".to_string(),
body: "any body".to_string(),
published_at: None,
};
assert_eq!(
post.primary_key(),
42,
"primary_key() should hand back the id field verbatim",
);
}
/// `<Post as Model>::PrimaryKey` is `i64` at M2 (UUID lands later). The
/// assignment below only compiles if the associated type really is `i64`, so
/// this is a type-level check the compiler enforces; the runtime assert is
/// belt-and-braces.
#[test]
fn primary_key_type_is_i64() {
let post = Post {
id: 7,
title: String::new(),
body: String::new(),
published_at: None,
};
let pk: <Post as Model>::PrimaryKey = post.primary_key();
let pk_as_i64: i64 = pk;
assert_eq!(pk_as_i64, 7);
}
/// `FieldSpec` is documented as `Copy + Eq`. Constructing two equal-by-value
/// instances and comparing them pins the derive down, and the `let copy = ...`
/// line wouldn't compile without `Copy`.
#[test]
fn field_spec_is_copy_and_eq() {
let a = FieldSpec {
name: "x",
ty: SqlType::BigInt,
primary_key: true,
nullable: false,
supported_backends: &[],
fk_target: None,
noform: false,
db_constraint: true,
noedit: false,
is_string_repr: false,
max_length: 0,
choices: &[],
choice_labels: &[],
default: "",
is_multichoice: false,
unique: false,
on_delete: umbral_core::orm::FkAction::NoAction,
on_update: umbral_core::orm::FkAction::NoAction,
index: false,
auto_now_add: false,
auto_now: false,
help: "",
example: "",
widget: None,
min: None,
max: None,
text_format: ::core::option::Option::None,
slug_from: ::core::option::Option::None,
};
let b = FieldSpec {
name: "x",
ty: SqlType::BigInt,
primary_key: true,
nullable: false,
supported_backends: &[],
fk_target: None,
noform: false,
db_constraint: true,
noedit: false,
is_string_repr: false,
max_length: 0,
choices: &[],
choice_labels: &[],
default: "",
is_multichoice: false,
unique: false,
on_delete: umbral_core::orm::FkAction::NoAction,
on_update: umbral_core::orm::FkAction::NoAction,
index: false,
auto_now_add: false,
auto_now: false,
help: "",
example: "",
widget: None,
min: None,
max: None,
text_format: ::core::option::Option::None,
slug_from: ::core::option::Option::None,
};
assert_eq!(
a, b,
"FieldSpecs with identical fields should compare equal"
);
// Copy semantics: `a` stays usable after a move-by-value would have
// consumed it. If `FieldSpec` weren't `Copy` this line would fail to
// compile.
let copy = a;
assert_eq!(copy, a);
}
/// A different field name should make two `FieldSpec`s unequal. The Eq impl
/// would be useless if it ignored fields, so this guards the derive against
/// silently degrading to "always equal."
#[test]
fn field_spec_eq_distinguishes_different_names() {
let a = FieldSpec {
name: "x",
ty: SqlType::BigInt,
primary_key: false,
nullable: false,
supported_backends: &[],
fk_target: None,
noform: false,
db_constraint: true,
noedit: false,
is_string_repr: false,
max_length: 0,
choices: &[],
choice_labels: &[],
default: "",
is_multichoice: false,
unique: false,
on_delete: umbral_core::orm::FkAction::NoAction,
on_update: umbral_core::orm::FkAction::NoAction,
index: false,
auto_now_add: false,
auto_now: false,
help: "",
example: "",
widget: None,
min: None,
max: None,
text_format: ::core::option::Option::None,
slug_from: ::core::option::Option::None,
};
let b = FieldSpec {
name: "y",
ty: SqlType::BigInt,
primary_key: false,
nullable: false,
supported_backends: &[],
fk_target: None,
noform: false,
db_constraint: true,
noedit: false,
is_string_repr: false,
max_length: 0,
choices: &[],
choice_labels: &[],
default: "",
is_multichoice: false,
unique: false,
on_delete: umbral_core::orm::FkAction::NoAction,
on_update: umbral_core::orm::FkAction::NoAction,
index: false,
auto_now_add: false,
auto_now: false,
help: "",
example: "",
widget: None,
min: None,
max: None,
text_format: ::core::option::Option::None,
slug_from: ::core::option::Option::None,
};
assert_ne!(a, b, "FieldSpecs differing in name must not compare equal");
}
/// `Post` satisfies `for<'r> FromRow<'r, SqliteRow>` via its `#[derive(sqlx::FromRow)]`,
/// which is the supertrait `Model` carries so QuerySet terminals can call
/// `sqlx::query_as::<_, T>` over any `T: Model`. This test runs an actual
/// SELECT through `query_as` to verify the row decoding works end-to-end
/// rather than just at the type level.
#[tokio::test]
async fn post_implements_fromrow() {
let pool = db::connect_sqlite("sqlite::memory:")
.await
.expect("in-memory sqlite should always connect");
sqlx::query(
"CREATE TABLE post (\
id INTEGER PRIMARY KEY AUTOINCREMENT,\
title TEXT NOT NULL,\
body TEXT NOT NULL,\
published_at DATETIME\
)",
)
.execute(&pool)
.await
.expect("CREATE TABLE post should succeed on a fresh in-memory database");
sqlx::query("INSERT INTO post (id, title, body, published_at) VALUES (?, ?, ?, ?)")
.bind(1_i64)
.bind("hello")
.bind("world")
.bind::<Option<chrono::DateTime<chrono::Utc>>>(None)
.execute(&pool)
.await
.expect("INSERT into post should succeed");
let row: Post =
sqlx::query_as::<_, Post>("SELECT id, title, body, published_at FROM post WHERE id = ?")
.bind(1_i64)
.fetch_one(&pool)
.await
.expect("query_as::<_, Post> should decode the row via the FromRow supertrait");
assert_eq!(row.id, 1);
assert_eq!(row.title, "hello");
assert_eq!(row.body, "world");
assert!(row.published_at.is_none());
assert_eq!(
row.primary_key(),
1,
"round-tripped row should expose its primary key via the trait method",
);
}