1use crate::{
7 db::Predicate,
8 model::{field::FieldModel, index::IndexModel},
9};
10
11pub type GeneratedCheckConstraintResolver = fn() -> &'static Predicate;
16
17#[derive(Debug)]
26pub struct CheckConstraintModel {
27 name: &'static str,
28 source_sql: &'static str,
29 semantics: GeneratedCheckConstraintResolver,
30}
31
32impl CheckConstraintModel {
33 #[doc(hidden)]
35 #[must_use]
36 pub const fn generated(
37 name: &'static str,
38 source_sql: &'static str,
39 semantics: GeneratedCheckConstraintResolver,
40 ) -> Self {
41 Self {
42 name,
43 source_sql,
44 semantics,
45 }
46 }
47
48 #[must_use]
50 pub const fn name(&self) -> &'static str {
51 self.name
52 }
53
54 #[must_use]
56 pub const fn source_sql(&self) -> &'static str {
57 self.source_sql
58 }
59
60 #[doc(hidden)]
62 #[must_use]
63 pub fn semantics(&self) -> &'static Predicate {
64 (self.semantics)()
65 }
66}
67
68#[derive(Debug)]
77pub struct PrimaryKeyModel {
78 fields: PrimaryKeyModelFields,
79}
80
81impl PrimaryKeyModel {
82 #[must_use]
84 pub const fn scalar(field: &'static FieldModel) -> Self {
85 Self {
86 fields: PrimaryKeyModelFields::Scalar(field),
87 }
88 }
89
90 #[must_use]
96 pub const fn ordered(fields: &'static [&'static FieldModel]) -> Self {
97 assert!(!fields.is_empty(), "primary key model requires fields");
98 Self {
99 fields: PrimaryKeyModelFields::Ordered(fields),
100 }
101 }
102
103 #[must_use]
105 pub const fn len(&self) -> usize {
106 match self.fields {
107 PrimaryKeyModelFields::Scalar(_) => 1,
108 PrimaryKeyModelFields::Ordered(fields) => fields.len(),
109 }
110 }
111
112 #[must_use]
114 pub const fn is_empty(&self) -> bool {
115 self.len() == 0
116 }
117
118 #[must_use]
120 pub const fn is_scalar(&self) -> bool {
121 self.len() == 1
122 }
123
124 #[must_use]
130 pub const fn first_field(&self) -> &'static FieldModel {
131 match self.fields {
132 PrimaryKeyModelFields::Scalar(field) => field,
133 PrimaryKeyModelFields::Ordered(fields) => fields[0],
134 }
135 }
136
137 #[must_use]
139 pub const fn fields(&self) -> PrimaryKeyModelFields {
140 self.fields
141 }
142}
143
144#[derive(Clone, Copy, Debug)]
151pub enum PrimaryKeyModelFields {
152 Scalar(&'static FieldModel),
153 Ordered(&'static [&'static FieldModel]),
154}
155
156impl PrimaryKeyModelFields {
157 #[must_use]
159 pub const fn len(self) -> usize {
160 match self {
161 Self::Scalar(_) => 1,
162 Self::Ordered(fields) => fields.len(),
163 }
164 }
165
166 #[must_use]
168 pub const fn is_empty(self) -> bool {
169 self.len() == 0
170 }
171
172 #[must_use]
174 pub fn get(self, index: usize) -> Option<&'static FieldModel> {
175 match self {
176 Self::Scalar(field) => (index == 0).then_some(field),
177 Self::Ordered(fields) => fields.get(index).copied(),
178 }
179 }
180
181 #[must_use]
183 pub const fn iter(self) -> PrimaryKeyModelFieldIter {
184 PrimaryKeyModelFieldIter {
185 fields: self,
186 index: 0,
187 }
188 }
189}
190
191#[derive(Clone, Debug)]
198pub struct PrimaryKeyModelFieldIter {
199 fields: PrimaryKeyModelFields,
200 index: usize,
201}
202
203impl Iterator for PrimaryKeyModelFieldIter {
204 type Item = &'static FieldModel;
205
206 fn next(&mut self) -> Option<Self::Item> {
207 let item = self.fields.get(self.index)?;
208 self.index += 1;
209 Some(item)
210 }
211}
212
213#[cfg(test)]
214mod primary_key_model_tests {
215 use super::{PrimaryKeyModel, PrimaryKeyModelFields};
216 use crate::model::FieldModel;
217
218 static ID_FIELD: FieldModel = FieldModel::generated("id", crate::model::FieldKind::Nat64);
219 static TENANT_FIELD: FieldModel =
220 FieldModel::generated("tenant_id", crate::model::FieldKind::Nat64);
221 static ORDERED_FIELDS: [&FieldModel; 2] = [&ID_FIELD, &TENANT_FIELD];
222
223 #[test]
224 fn scalar_primary_key_model_exposes_one_field() {
225 let model = PrimaryKeyModel::scalar(&ID_FIELD);
226
227 assert_eq!(model.len(), 1);
228 assert!(model.is_scalar());
229 assert_eq!(model.first_field().name(), "id");
230 assert_eq!(
231 model
232 .fields()
233 .iter()
234 .map(FieldModel::name)
235 .collect::<Vec<_>>(),
236 ["id"]
237 );
238 }
239
240 #[test]
241 fn ordered_primary_key_model_preserves_field_order() {
242 let model = PrimaryKeyModel::ordered(&ORDERED_FIELDS);
243
244 assert_eq!(model.len(), 2);
245 assert!(!model.is_scalar());
246 assert_eq!(model.first_field().name(), "id");
247 assert_eq!(
248 model
249 .fields()
250 .iter()
251 .map(FieldModel::name)
252 .collect::<Vec<_>>(),
253 ["id", "tenant_id"],
254 );
255 std::assert_matches!(model.fields(), PrimaryKeyModelFields::Ordered(_));
256 }
257}
258
259#[derive(Debug)]
268pub struct RelationEdgeModel {
269 name: &'static str,
270 target_path: &'static str,
271 local_fields: &'static [&'static FieldModel],
272}
273
274impl RelationEdgeModel {
275 #[must_use]
278 pub const fn generated(
279 name: &'static str,
280 target_path: &'static str,
281 local_fields: &'static [&'static FieldModel],
282 ) -> Self {
283 Self {
284 name,
285 target_path,
286 local_fields,
287 }
288 }
289
290 #[must_use]
292 pub const fn name(&self) -> &'static str {
293 self.name
294 }
295
296 #[must_use]
298 pub const fn target_path(&self) -> &'static str {
299 self.target_path
300 }
301
302 #[must_use]
304 pub const fn local_fields(&self) -> &'static [&'static FieldModel] {
305 self.local_fields
306 }
307}
308
309#[cfg(test)]
310mod relation_edge_model_tests {
311 use super::RelationEdgeModel;
312 use crate::model::{FieldKind, FieldModel};
313
314 static TENANT_FIELD: FieldModel = FieldModel::generated("tenant_id", FieldKind::Nat64);
315 static USER_FIELD: FieldModel = FieldModel::generated("user_id", FieldKind::Ulid);
316 static LOCAL_FIELDS: [&FieldModel; 2] = [&TENANT_FIELD, &USER_FIELD];
317
318 #[test]
319 fn relation_edge_model_preserves_ordered_local_fields() {
320 let relation = RelationEdgeModel::generated("author", "example::User", &LOCAL_FIELDS);
321
322 assert_eq!(relation.name(), "author");
323 assert_eq!(relation.target_path(), "example::User");
324 assert_eq!(
325 relation
326 .local_fields()
327 .iter()
328 .map(|field| field.name())
329 .collect::<Vec<_>>(),
330 ["tenant_id", "user_id"],
331 );
332 }
333}
334
335#[derive(Debug)]
343pub struct EntityModel {
344 pub(crate) path: &'static str,
346
347 pub(crate) entity_name: &'static str,
349
350 pub(crate) schema_version: u32,
352
353 pub(crate) primary_key: &'static FieldModel,
355
356 pub(crate) primary_key_slot: usize,
358
359 pub(crate) primary_key_model: PrimaryKeyModel,
361
362 pub(crate) fields: &'static [FieldModel],
364
365 pub(crate) indexes: &'static [&'static IndexModel],
367
368 pub(crate) relations: &'static [RelationEdgeModel],
370
371 pub(crate) check_constraints: &'static [CheckConstraintModel],
373}
374
375impl EntityModel {
376 #[must_use]
382 #[doc(hidden)]
383 pub const fn generated(
384 path: &'static str,
385 entity_name: &'static str,
386 schema_version: u32,
387 primary_key: &'static FieldModel,
388 primary_key_slot: usize,
389 fields: &'static [FieldModel],
390 indexes: &'static [&'static IndexModel],
391 ) -> Self {
392 Self {
393 path,
394 entity_name,
395 schema_version,
396 primary_key,
397 primary_key_slot,
398 primary_key_model: PrimaryKeyModel::scalar(primary_key),
399 fields,
400 indexes,
401 relations: &[],
402 check_constraints: &[],
403 }
404 }
405
406 #[must_use]
409 #[doc(hidden)]
410 pub const fn generated_with_primary_key_model(
411 path: &'static str,
412 entity_name: &'static str,
413 schema_version: u32,
414 primary_key_model: PrimaryKeyModel,
415 primary_key_slot: usize,
416 fields: &'static [FieldModel],
417 indexes: &'static [&'static IndexModel],
418 ) -> Self {
419 Self::generated_with_primary_key_model_and_relations(
420 path,
421 entity_name,
422 schema_version,
423 primary_key_model,
424 primary_key_slot,
425 fields,
426 indexes,
427 &[],
428 )
429 }
430
431 #[must_use]
434 #[doc(hidden)]
435 #[expect(
436 clippy::too_many_arguments,
437 reason = "generated entity model construction keeps path, declared version, key, field, index, and relation metadata explicit"
438 )]
439 pub const fn generated_with_primary_key_model_and_relations(
440 path: &'static str,
441 entity_name: &'static str,
442 schema_version: u32,
443 primary_key_model: PrimaryKeyModel,
444 primary_key_slot: usize,
445 fields: &'static [FieldModel],
446 indexes: &'static [&'static IndexModel],
447 relations: &'static [RelationEdgeModel],
448 ) -> Self {
449 Self::generated_with_primary_key_model_relations_and_checks(
450 path,
451 entity_name,
452 schema_version,
453 primary_key_model,
454 primary_key_slot,
455 fields,
456 indexes,
457 relations,
458 &[],
459 )
460 }
461
462 #[doc(hidden)]
464 #[must_use]
465 #[expect(
466 clippy::too_many_arguments,
467 reason = "generated entity metadata keeps each proposal family explicit"
468 )]
469 pub const fn generated_with_primary_key_model_relations_and_checks(
470 path: &'static str,
471 entity_name: &'static str,
472 schema_version: u32,
473 primary_key_model: PrimaryKeyModel,
474 primary_key_slot: usize,
475 fields: &'static [FieldModel],
476 indexes: &'static [&'static IndexModel],
477 relations: &'static [RelationEdgeModel],
478 check_constraints: &'static [CheckConstraintModel],
479 ) -> Self {
480 assert!(
481 schema_version > 0,
482 "generated schema_version must be positive"
483 );
484
485 Self {
486 path,
487 entity_name,
488 schema_version,
489 primary_key: primary_key_model.first_field(),
490 primary_key_slot,
491 primary_key_model,
492 fields,
493 indexes,
494 relations,
495 check_constraints,
496 }
497 }
498
499 #[must_use]
501 pub const fn path(&self) -> &'static str {
502 self.path
503 }
504
505 #[must_use]
507 pub const fn name(&self) -> &'static str {
508 self.entity_name
509 }
510
511 #[must_use]
516 pub const fn declared_schema_version(&self) -> u32 {
517 self.schema_version
518 }
519
520 #[must_use]
522 pub const fn primary_key(&self) -> &'static FieldModel {
523 self.primary_key
524 }
525
526 #[must_use]
528 pub const fn primary_key_model(&self) -> &PrimaryKeyModel {
529 &self.primary_key_model
530 }
531
532 #[must_use]
534 pub fn primary_key_names(&self) -> Vec<&'static str> {
535 self.primary_key_model()
536 .fields()
537 .iter()
538 .map(crate::model::field::FieldModel::name)
539 .collect()
540 }
541
542 #[must_use]
544 pub const fn primary_key_slot(&self) -> usize {
545 self.primary_key_slot
546 }
547
548 #[must_use]
550 pub const fn fields(&self) -> &'static [FieldModel] {
551 self.fields
552 }
553
554 #[must_use]
556 pub const fn indexes(&self) -> &'static [&'static IndexModel] {
557 self.indexes
558 }
559
560 #[must_use]
562 pub const fn relations(&self) -> &'static [RelationEdgeModel] {
563 self.relations
564 }
565
566 #[must_use]
568 pub const fn check_constraints(&self) -> &'static [CheckConstraintModel] {
569 self.check_constraints
570 }
571
572 #[must_use]
574 pub(crate) fn resolve_field_slot(&self, field_name: &str) -> Option<usize> {
575 self.fields
576 .iter()
577 .position(|field| field.name == field_name)
578 }
579}