Skip to main content

radixdb_orm/
record.rs

1//! Record state, reference keys, and mutation safety.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::marker::PhantomData;
5
6use serde::{Deserialize, Serialize};
7
8use crate::*;
9
10macro_rules! typed_expression_methods {
11    () => {
12        pub fn eq(self, value: impl Into<Expr>) -> Expr {
13            self.expr().eq(value)
14        }
15        pub fn ne(self, value: impl Into<Expr>) -> Expr {
16            self.expr().ne(value)
17        }
18        pub fn lt(self, value: impl Into<Expr>) -> Expr {
19            self.expr().lt(value)
20        }
21        pub fn lte(self, value: impl Into<Expr>) -> Expr {
22            self.expr().lte(value)
23        }
24        pub fn gt(self, value: impl Into<Expr>) -> Expr {
25            self.expr().gt(value)
26        }
27        pub fn gte(self, value: impl Into<Expr>) -> Expr {
28            self.expr().gte(value)
29        }
30        pub fn is_null(self) -> Expr {
31            self.expr().is_null()
32        }
33        pub fn is_not_null(self) -> Expr {
34            self.expr().is_not_null()
35        }
36        pub fn between(self, lower: impl Into<Expr>, upper: impl Into<Expr>) -> Expr {
37            self.expr().between(lower, upper)
38        }
39        pub fn not_between(self, lower: impl Into<Expr>, upper: impl Into<Expr>) -> Expr {
40            self.expr().not_between(lower, upper)
41        }
42        pub fn in_(self, values: impl IntoIterator<Item = impl Into<Expr>>) -> Expr {
43            self.expr().in_list(values)
44        }
45        pub fn not_in(self, values: impl IntoIterator<Item = impl Into<Expr>>) -> Expr {
46            self.expr().not_in_list(values)
47        }
48        pub fn in_subquery(self, query: QueryBuilder) -> Expr {
49            self.expr().in_subquery(query)
50        }
51        pub fn not_in_subquery(self, query: QueryBuilder) -> Expr {
52            self.expr().not_in_subquery(query)
53        }
54        pub fn like(self, value: impl Into<Expr>) -> Expr {
55            self.expr().like(value)
56        }
57        pub fn not_like(self, value: impl Into<Expr>) -> Expr {
58            self.expr().not_like(value)
59        }
60        pub fn regexp(self, value: impl Into<Expr>) -> Expr {
61            self.expr().regexp(value)
62        }
63        pub fn glob(self, value: impl Into<Expr>) -> Expr {
64            self.expr().glob(value)
65        }
66        pub fn is_distinct_from(self, value: impl Into<Expr>) -> Expr {
67            self.expr().is_distinct_from(value)
68        }
69        pub fn is_not_distinct_from(self, value: impl Into<Expr>) -> Expr {
70            self.expr().is_not_distinct_from(value)
71        }
72        #[allow(clippy::should_implement_trait)]
73        pub fn add(self, value: impl Into<Expr>) -> Expr {
74            self.expr().add(value)
75        }
76        #[allow(clippy::should_implement_trait)]
77        pub fn sub(self, value: impl Into<Expr>) -> Expr {
78            self.expr().sub(value)
79        }
80        #[allow(clippy::should_implement_trait)]
81        pub fn mul(self, value: impl Into<Expr>) -> Expr {
82            self.expr().mul(value)
83        }
84        #[allow(clippy::should_implement_trait)]
85        pub fn div(self, value: impl Into<Expr>) -> Expr {
86            self.expr().div(value)
87        }
88        pub fn modulo(self, value: impl Into<Expr>) -> Expr {
89            self.expr().modulo(value)
90        }
91        pub fn cast(self, data_type: DataTypeDescriptor) -> Expr {
92            self.expr().cast(data_type)
93        }
94        pub fn alias(self, alias: impl Into<String>) -> Projection {
95            self.expr().alias(alias)
96        }
97        pub fn projection(self) -> Projection {
98            self.expr().projection()
99        }
100        pub fn asc(self) -> Order {
101            self.expr().asc()
102        }
103        pub fn desc(self) -> Order {
104            self.expr().desc()
105        }
106    };
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct TypedColumn<T, Owner = ()> {
111    pub table: &'static str,
112    pub name: &'static str,
113    pub data_type: DataTypeDescriptor,
114    pub nullable: bool,
115    marker: PhantomData<fn() -> (T, Owner)>,
116}
117
118impl<T, Owner> TypedColumn<T, Owner> {
119    pub const fn new(
120        table: &'static str,
121        name: &'static str,
122        data_type: DataTypeDescriptor,
123        nullable: bool,
124    ) -> Self {
125        Self {
126            table,
127            name,
128            data_type,
129            nullable,
130            marker: PhantomData,
131        }
132    }
133    pub fn expr(self) -> Expr {
134        Expr::qualified(self.table, self.name)
135    }
136
137    typed_expression_methods!();
138}
139
140impl<T, Owner> From<TypedColumn<T, Owner>> for Expr {
141    fn from(value: TypedColumn<T, Owner>) -> Self {
142        value.expr()
143    }
144}
145
146impl<T, Owner> From<&TypedColumn<T, Owner>> for Expr {
147    fn from(value: &TypedColumn<T, Owner>) -> Self {
148        Expr::qualified(value.table, value.name)
149    }
150}
151
152impl<T, Owner> From<TypedColumn<T, Owner>> for String {
153    fn from(value: TypedColumn<T, Owner>) -> Self {
154        value.name.to_string()
155    }
156}
157
158/// Statically rooted read-only navigation path. Each additional `field()`
159/// appends one schema-validated reference edge; the server remains the final
160/// authority for catalog compatibility and rejects stale generated paths.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct TypedNavigation<T, Root = ()> {
163    root: &'static str,
164    path: Vec<&'static str>,
165    marker: PhantomData<fn() -> (T, Root)>,
166}
167
168impl<Source, Target: GeneratedEntity> TypedColumn<Reference<Target>, Source> {
169    pub fn field<U>(self, target: TypedColumn<U, Target>) -> TypedNavigation<U, Source> {
170        TypedNavigation {
171            root: self.table,
172            path: vec![self.name, target.name],
173            marker: PhantomData,
174        }
175    }
176}
177
178impl<Root, Target: GeneratedEntity> TypedNavigation<Reference<Target>, Root> {
179    pub fn field<U>(mut self, target: TypedColumn<U, Target>) -> TypedNavigation<U, Root> {
180        self.path.push(target.name);
181        TypedNavigation {
182            root: self.root,
183            path: self.path,
184            marker: PhantomData,
185        }
186    }
187}
188
189impl<T, Root> TypedNavigation<T, Root> {
190    pub fn expr(self) -> Expr {
191        Expr::navigation(self.root, self.path)
192    }
193
194    typed_expression_methods!();
195}
196
197impl<T, Root> From<TypedNavigation<T, Root>> for Expr {
198    fn from(value: TypedNavigation<T, Root>) -> Self {
199        value.expr()
200    }
201}
202
203pub trait GeneratedEntity {
204    type Record;
205    const TABLE: &'static str;
206    const CATALOG_ID: &'static str;
207    const SCHEMA_FINGERPRINT: &'static str;
208}
209
210/// A generated one-column PRIMARY KEY or UNIQUE NOT NULL descriptor.
211///
212/// The owner type binds the key to one generated entity, while the encoder
213/// preserves the exact RadixDB value tag. References can therefore be created
214/// only through a key that codegen proved valid against the saved descriptor.
215#[derive(Debug, Clone)]
216pub struct TypedKey<Owner, T> {
217    column: TypedColumn<T, Owner>,
218    primary: bool,
219    encoder: fn(T) -> TypedValue,
220}
221
222impl<Owner, T> TypedKey<Owner, T> {
223    pub const fn new(
224        column: TypedColumn<T, Owner>,
225        primary: bool,
226        encoder: fn(T) -> TypedValue,
227    ) -> Self {
228        Self {
229            column,
230            primary,
231            encoder,
232        }
233    }
234
235    pub fn column(&self) -> &TypedColumn<T, Owner> {
236        &self.column
237    }
238
239    pub const fn is_primary(&self) -> bool {
240        self.primary
241    }
242
243    pub fn reference(&self, key: T) -> Reference<Owner> {
244        Reference::new(self.column.table, self.column.name, (self.encoder)(key))
245    }
246}
247
248/// Runtime bridge implemented by deterministic generated record types.
249/// Execution remains transport-owned; generated code only converts between
250/// its typed fields and the canonical [`DynamicRecord`].
251pub trait GeneratedRecord: Sized {
252    type Entity: GeneratedEntity<Record = Self>;
253
254    fn to_dynamic(
255        &self,
256        descriptor: &TableDescriptor,
257    ) -> Result<DynamicRecord, GeneratedRecordError>;
258
259    fn apply_dynamic(&mut self, record: &DynamicRecord) -> Result<(), GeneratedRecordError>;
260}
261
262#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
263#[error("generated schema changed: expected {expected}, actual {actual}")]
264pub struct SchemaChanged {
265    pub expected: String,
266    pub actual: String,
267}
268
269#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
270pub enum GeneratedRecordError {
271    #[error(transparent)]
272    Schema(#[from] SchemaChanged),
273    #[error(transparent)]
274    Record(#[from] RecordError),
275}
276
277pub fn ensure_schema_fingerprint(expected: &str, actual: &str) -> Result<(), SchemaChanged> {
278    if expected == actual {
279        Ok(())
280    } else {
281        Err(SchemaChanged {
282            expected: expected.to_string(),
283            actual: actual.to_string(),
284        })
285    }
286}
287
288#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
289#[serde(tag = "state", rename_all = "snake_case")]
290pub enum FieldValue<T> {
291    Omitted,
292    Value { value: T },
293    Null { data_type: DataTypeDescriptor },
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct FieldState<T> {
298    value: FieldValue<T>,
299    dirty: bool,
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    declared_type: Option<DataTypeDescriptor>,
302}
303
304impl<T> Default for FieldState<T> {
305    fn default() -> Self {
306        Self::omitted()
307    }
308}
309
310impl<T> FieldState<T> {
311    pub const fn omitted() -> Self {
312        Self {
313            value: FieldValue::Omitted,
314            dirty: false,
315            declared_type: None,
316        }
317    }
318    pub fn typed(data_type: DataTypeDescriptor) -> Self {
319        Self {
320            value: FieldValue::Omitted,
321            dirty: false,
322            declared_type: Some(data_type),
323        }
324    }
325    pub fn value(&self) -> &FieldValue<T> {
326        &self.value
327    }
328    pub fn is_dirty(&self) -> bool {
329        self.dirty
330    }
331    pub fn is_omitted(&self) -> bool {
332        matches!(self.value, FieldValue::Omitted)
333    }
334    pub fn set(&mut self, value: impl Into<T>) {
335        self.value = FieldValue::Value {
336            value: value.into(),
337        };
338        self.dirty = true;
339    }
340    pub fn set_null(&mut self) {
341        self.value = FieldValue::Null {
342            data_type: self
343                .declared_type
344                .clone()
345                .unwrap_or(DataTypeDescriptor::Null),
346        };
347        self.dirty = true;
348    }
349    pub fn set_null_as(&mut self, data_type: DataTypeDescriptor) {
350        self.declared_type = Some(data_type.clone());
351        self.value = FieldValue::Null { data_type };
352        self.dirty = true;
353    }
354    /// Remove the field from the next INSERT/UPDATE. This also clears a pending
355    /// dirty transition; it never writes SQL NULL implicitly.
356    pub fn unset(&mut self) {
357        self.value = FieldValue::Omitted;
358        self.dirty = false;
359    }
360    pub fn hydrate(&mut self, value: FieldValue<T>) {
361        if let FieldValue::Null { data_type } = &value {
362            self.declared_type = Some(data_type.clone());
363        }
364        self.value = value;
365        self.dirty = false;
366    }
367    pub fn mark_clean(&mut self) {
368        self.dirty = false;
369    }
370}
371
372#[derive(Debug, Clone, PartialEq, Serialize)]
373pub struct Reference<T> {
374    target_table: String,
375    target_column: String,
376    key: TypedValue,
377    #[serde(skip)]
378    marker: PhantomData<T>,
379}
380
381/// Runtime counterpart of [`Reference<T>`] for descriptor-driven clients.
382/// It carries only a validated target key; it never owns or loads a row.
383#[derive(Debug, Clone, PartialEq, Serialize)]
384pub struct DynamicReference {
385    target_table: String,
386    target_column: String,
387    key: TypedValue,
388}
389
390impl DynamicReference {
391    pub(crate) fn new(
392        target_table: impl Into<String>,
393        target_column: impl Into<String>,
394        key: TypedValue,
395    ) -> Self {
396        Self {
397            target_table: target_table.into(),
398            target_column: target_column.into(),
399            key,
400        }
401    }
402
403    pub fn key(&self) -> &TypedValue {
404        &self.key
405    }
406
407    pub fn target_table(&self) -> &str {
408        &self.target_table
409    }
410
411    pub fn target_column(&self) -> &str {
412        &self.target_column
413    }
414
415    pub fn to_json(&self) -> Result<String, serde_json::Error> {
416        serde_json::to_string(self)
417    }
418}
419
420impl<T> Reference<T> {
421    pub(crate) fn new(
422        target_table: impl Into<String>,
423        target_column: impl Into<String>,
424        key: TypedValue,
425    ) -> Self {
426        Self {
427            target_table: target_table.into(),
428            target_column: target_column.into(),
429            key,
430            marker: PhantomData,
431        }
432    }
433    pub fn key(&self) -> &TypedValue {
434        &self.key
435    }
436
437    pub fn target_table(&self) -> &str {
438        &self.target_table
439    }
440
441    pub fn target_column(&self) -> &str {
442        &self.target_column
443    }
444
445    pub fn to_json(&self) -> Result<String, serde_json::Error> {
446        serde_json::to_string(self)
447    }
448}
449
450impl<T> From<Reference<T>> for TypedValue {
451    fn from(reference: Reference<T>) -> Self {
452        reference.key
453    }
454}
455
456impl<T> From<Reference<T>> for Expr {
457    fn from(reference: Reference<T>) -> Self {
458        Expr::value(reference.key)
459    }
460}
461
462#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
463pub enum RecordError {
464    #[error(transparent)]
465    Build(#[from] BuilderError),
466    #[error("unknown record field '{0}'")]
467    UnknownField(String),
468    #[error("table '{0}' has no primary key")]
469    MissingPrimaryKey(String),
470    #[error("record primary-key field '{0}' is omitted or NULL")]
471    MissingPrimaryKeyValue(String),
472    #[error("record UPDATE has no dirty non-primary-key fields")]
473    EmptyUpdate,
474    #[error("reference target must be a one-column PRIMARY KEY or UNIQUE NOT NULL key")]
475    UnsupportedReferenceTarget,
476    #[error("field '{field}' expects {expected:?}, got {actual:?}")]
477    ValueTypeMismatch {
478        field: String,
479        expected: DataTypeDescriptor,
480        actual: DataTypeDescriptor,
481    },
482    #[error("NULL reference keys are not valid; use an explicit typed NULL on the source field")]
483    NullReferenceKey,
484    #[error(
485        "field '{table}.{column}' is not a one-column reference to '{target_table}.{target_column}'"
486    )]
487    ReferenceSourceMismatch {
488        table: String,
489        column: String,
490        target_table: String,
491        target_column: String,
492    },
493    #[error("hydrated row is missing expected field '{0}'")]
494    IncompleteHydration(String),
495    #[error("hydrated field '{field}' does not match generated type {expected:?}")]
496    HydrationTypeMismatch {
497        field: String,
498        expected: DataTypeDescriptor,
499    },
500    #[error("generated query expected one row but returned none")]
501    ExpectedOneRow,
502    #[error("generated query expected at most one row but returned {0}")]
503    ExpectedAtMostOneRow(usize),
504}
505
506#[derive(Debug, Clone, PartialEq)]
507pub struct GeneratedQuery<R> {
508    builder: QueryBuilder,
509    marker: PhantomData<fn() -> R>,
510}
511
512impl<R> GeneratedQuery<R> {
513    pub fn new(builder: QueryBuilder) -> Self {
514        Self {
515            builder,
516            marker: PhantomData,
517        }
518    }
519
520    pub fn builder(&self) -> &QueryBuilder {
521        &self.builder
522    }
523
524    pub fn document(&self) -> Result<IrDocument, BuilderError> {
525        self.builder.document()
526    }
527
528    pub fn to_json(&self) -> Result<String, BuilderJsonError> {
529        self.builder.to_json()
530    }
531
532    pub fn to_sql(&self) -> Result<CompiledStatement, BuilderSqlError> {
533        self.builder.to_sql()
534    }
535}
536
537impl<R: GeneratedRecord + Default> GeneratedQuery<R> {
538    pub fn all<S>(self, session: S) -> Result<Vec<R>, S::Error>
539    where
540        S: OrmGeneratedQuerySession,
541        S::Error: From<BuilderError>,
542    {
543        let document = self.document().map_err(S::Error::from)?;
544        session.query_generated_records(&document)
545    }
546
547    pub fn one<S>(self, session: S) -> Result<R, S::Error>
548    where
549        S: OrmGeneratedQuerySession,
550        S::Error: From<RecordError> + From<BuilderError>,
551    {
552        let mut rows = self.all(session)?;
553        match rows.len() {
554            1 => Ok(rows.pop().expect("one row")),
555            0 => Err(RecordError::ExpectedOneRow.into()),
556            count => Err(RecordError::ExpectedAtMostOneRow(count).into()),
557        }
558    }
559
560    pub fn optional<S>(self, session: S) -> Result<Option<R>, S::Error>
561    where
562        S: OrmGeneratedQuerySession,
563        S::Error: From<RecordError> + From<BuilderError>,
564    {
565        let mut rows = self.all(session)?;
566        match rows.len() {
567            0 => Ok(None),
568            1 => Ok(rows.pop()),
569            count => Err(RecordError::ExpectedAtMostOneRow(count).into()),
570        }
571    }
572
573    pub async fn all_async<S>(self, session: &mut S) -> Result<Vec<R>, S::Error>
574    where
575        S: AsyncOrmGeneratedQuerySession,
576        S::Error: From<BuilderError>,
577    {
578        let document = self.document().map_err(S::Error::from)?;
579        session.query_generated_records_async(&document).await
580    }
581
582    pub async fn one_async<S>(self, session: &mut S) -> Result<R, S::Error>
583    where
584        S: AsyncOrmGeneratedQuerySession,
585        S::Error: From<RecordError> + From<BuilderError>,
586    {
587        let mut rows = self.all_async(session).await?;
588        match rows.len() {
589            1 => Ok(rows.pop().expect("one row")),
590            0 => Err(RecordError::ExpectedOneRow.into()),
591            count => Err(RecordError::ExpectedAtMostOneRow(count).into()),
592        }
593    }
594
595    pub async fn optional_async<S>(self, session: &mut S) -> Result<Option<R>, S::Error>
596    where
597        S: AsyncOrmGeneratedQuerySession,
598        S::Error: From<RecordError> + From<BuilderError>,
599    {
600        let mut rows = self.all_async(session).await?;
601        match rows.len() {
602            0 => Ok(None),
603            1 => Ok(rows.pop()),
604            count => Err(RecordError::ExpectedAtMostOneRow(count).into()),
605        }
606    }
607}
608
609#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
610pub struct DynamicRecord {
611    descriptor: TableDescriptor,
612    fields: BTreeMap<String, FieldState<TypedValue>>,
613}
614
615impl DynamicRecord {
616    pub fn new(descriptor: TableDescriptor) -> Self {
617        let fields = descriptor
618            .columns
619            .iter()
620            .map(|column| (column.name.clone(), FieldState::omitted()))
621            .collect();
622        Self { descriptor, fields }
623    }
624    pub fn descriptor(&self) -> &TableDescriptor {
625        &self.descriptor
626    }
627    pub fn field(&self, name: &str) -> Result<&FieldState<TypedValue>, RecordError> {
628        self.fields
629            .get(name)
630            .ok_or_else(|| RecordError::UnknownField(name.to_string()))
631    }
632    pub fn field_mut(&mut self, name: &str) -> Result<&mut FieldState<TypedValue>, RecordError> {
633        self.fields
634            .get_mut(name)
635            .ok_or_else(|| RecordError::UnknownField(name.to_string()))
636    }
637    pub fn set(&mut self, name: &str, value: TypedValue) -> Result<(), RecordError> {
638        let column = self.column_descriptor(name)?;
639        if !typed_value_matches(&value, &column.data_type) {
640            return Err(RecordError::ValueTypeMismatch {
641                field: name.to_string(),
642                expected: column.data_type.clone(),
643                actual: value.data_type(),
644            });
645        }
646        self.field_mut(name)?.set(value);
647        Ok(())
648    }
649    pub fn set_reference(
650        &mut self,
651        name: &str,
652        reference: &DynamicReference,
653    ) -> Result<(), RecordError> {
654        let matches = self.descriptor.constraints.iter().any(|constraint| {
655            let ConstraintDefinition::ForeignKey {
656                columns,
657                referenced_table,
658                referenced_columns,
659                ..
660            } = &constraint.definition
661            else {
662                return false;
663            };
664            columns.as_slice() == [name]
665                && referenced_table == &reference.target_table
666                && referenced_columns.as_slice() == [reference.target_column.as_str()]
667        });
668        if !matches {
669            return Err(RecordError::ReferenceSourceMismatch {
670                table: self.descriptor.name.clone(),
671                column: name.to_string(),
672                target_table: reference.target_table.clone(),
673                target_column: reference.target_column.clone(),
674            });
675        }
676        self.set(name, reference.key.clone())
677    }
678    pub fn set_null(&mut self, name: &str) -> Result<(), RecordError> {
679        let data_type = self
680            .descriptor
681            .columns
682            .iter()
683            .find(|column| column.name == name)
684            .map(|column| column.data_type.clone())
685            .ok_or_else(|| RecordError::UnknownField(name.to_string()))?;
686        self.field_mut(name)?.set_null_as(data_type);
687        Ok(())
688    }
689    pub fn unset(&mut self, name: &str) -> Result<(), RecordError> {
690        self.field_mut(name)?.unset();
691        Ok(())
692    }
693
694    /// Install a value read from the server without marking it dirty.
695    /// Generated facades use this to preserve PK and clean-field state while
696    /// converting back into the shared dynamic mutation owner.
697    pub fn hydrate(&mut self, name: &str, value: TypedValue) -> Result<(), RecordError> {
698        let column = self.column_descriptor(name)?;
699        let hydrated = match value {
700            TypedValue::Null(data_type) if data_type == column.data_type => {
701                FieldValue::Null { data_type }
702            }
703            TypedValue::Null(data_type) => {
704                return Err(RecordError::ValueTypeMismatch {
705                    field: name.to_string(),
706                    expected: column.data_type.clone(),
707                    actual: data_type,
708                });
709            }
710            value if typed_value_matches(&value, &column.data_type) => FieldValue::Value { value },
711            value => {
712                return Err(RecordError::ValueTypeMismatch {
713                    field: name.to_string(),
714                    expected: column.data_type.clone(),
715                    actual: value.data_type(),
716                });
717            }
718        };
719        self.field_mut(name)?.hydrate(hydrated);
720        Ok(())
721    }
722    pub fn is_dirty(&self) -> bool {
723        self.fields.values().any(FieldState::is_dirty)
724    }
725
726    pub fn insert<S: OrmRecordSession>(&mut self, session: S) -> Result<(), S::Error> {
727        session.mutate_record(self, RecordMutation::Insert)
728    }
729
730    pub fn save<S: OrmRecordSession>(&mut self, session: S) -> Result<(), S::Error> {
731        session.mutate_record(self, RecordMutation::Save)
732    }
733
734    pub fn update<S: OrmRecordSession>(&mut self, session: S) -> Result<(), S::Error> {
735        session.mutate_record(self, RecordMutation::Update)
736    }
737
738    pub fn delete<S: OrmRecordSession>(&mut self, session: S) -> Result<(), S::Error> {
739        session.mutate_record(self, RecordMutation::Delete)
740    }
741
742    pub async fn insert_async<S: AsyncOrmRecordSession>(
743        &mut self,
744        session: &mut S,
745    ) -> Result<(), S::Error> {
746        session
747            .mutate_record_async(self, RecordMutation::Insert)
748            .await
749    }
750
751    pub async fn save_async<S: AsyncOrmRecordSession>(
752        &mut self,
753        session: &mut S,
754    ) -> Result<(), S::Error> {
755        session
756            .mutate_record_async(self, RecordMutation::Save)
757            .await
758    }
759
760    pub async fn update_async<S: AsyncOrmRecordSession>(
761        &mut self,
762        session: &mut S,
763    ) -> Result<(), S::Error> {
764        session
765            .mutate_record_async(self, RecordMutation::Update)
766            .await
767    }
768
769    pub async fn delete_async<S: AsyncOrmRecordSession>(
770        &mut self,
771        session: &mut S,
772    ) -> Result<(), S::Error> {
773        session
774            .mutate_record_async(self, RecordMutation::Delete)
775            .await
776    }
777
778    pub fn insert_document(&self) -> Result<IrDocument, RecordError> {
779        let (columns, values) = self.present_fields(false)?;
780        Ok(IrDocument::new(Operation::Insert {
781            statement: Insert {
782                table: self.descriptor.name.clone(),
783                columns,
784                rows: vec![values],
785                source: None,
786                returning: return_all(),
787            },
788        }))
789    }
790
791    pub fn save_document(&self) -> Result<IrDocument, RecordError> {
792        let primary_key = self.primary_key_columns()?;
793        self.primary_key_predicate(&primary_key)?;
794        let (columns, values) = self.present_fields(false)?;
795        let primary: BTreeSet<_> = primary_key.iter().cloned().collect();
796        let assignments = self
797            .fields
798            .iter()
799            .filter(|(column, state)| state.is_dirty() && !primary.contains(*column))
800            .map(|(column, _)| Assignment {
801                column: column.clone(),
802                value: Expression::Column {
803                    column: ColumnRef::qualified("excluded", column.clone()),
804                },
805            })
806            .collect();
807        Ok(IrDocument::new(Operation::Upsert {
808            statement: Upsert {
809                insert: Insert {
810                    table: self.descriptor.name.clone(),
811                    columns,
812                    rows: vec![values],
813                    source: None,
814                    returning: return_all(),
815                },
816                conflict_columns: primary_key,
817                assignments,
818            },
819        }))
820    }
821
822    pub fn update_document(&self) -> Result<IrDocument, RecordError> {
823        let primary_key = self.primary_key_columns()?;
824        let filter = self.primary_key_predicate(&primary_key)?;
825        let primary: BTreeSet<_> = primary_key.iter().cloned().collect();
826        let assignments: Vec<_> = self
827            .fields
828            .iter()
829            .filter(|(name, state)| state.is_dirty() && !primary.contains(*name))
830            .filter_map(|(name, state)| {
831                field_expression(state).map(|value| Assignment {
832                    column: name.clone(),
833                    value,
834                })
835            })
836            .collect();
837        if assignments.is_empty() {
838            return Err(RecordError::EmptyUpdate);
839        }
840        Ok(IrDocument::new(Operation::Update {
841            statement: Update {
842                table: self.descriptor.name.clone(),
843                alias: None,
844                assignments,
845                from: None,
846                filter: Some(filter),
847                returning: return_all(),
848            },
849        }))
850    }
851
852    pub fn delete_document(&self) -> Result<IrDocument, RecordError> {
853        let primary_key = self.primary_key_columns()?;
854        let filter = self.primary_key_predicate(&primary_key)?;
855        Ok(IrDocument::new(Operation::Delete {
856            statement: Delete {
857                table: self.descriptor.name.clone(),
858                alias: None,
859                using: None,
860                filter: Some(filter),
861                all_rows: false,
862                returning: Vec::new(),
863            },
864        }))
865    }
866
867    /// Replace the local record only after the server returned a complete
868    /// `RETURNING *` row. Callers keep dirty state untouched on every error.
869    pub fn apply_returning(
870        &mut self,
871        values: BTreeMap<String, TypedValue>,
872    ) -> Result<(), RecordError> {
873        for column in &self.descriptor.columns {
874            if !values.contains_key(&column.name) {
875                return Err(RecordError::IncompleteHydration(column.name.clone()));
876            }
877        }
878        for column in &self.descriptor.columns {
879            let value = values.get(&column.name).expect("checked complete").clone();
880            let hydrated = match value {
881                TypedValue::Null(data_type) if data_type == column.data_type => {
882                    FieldValue::Null { data_type }
883                }
884                TypedValue::Null(data_type) => {
885                    return Err(RecordError::ValueTypeMismatch {
886                        field: column.name.clone(),
887                        expected: column.data_type.clone(),
888                        actual: data_type,
889                    });
890                }
891                value if typed_value_matches(&value, &column.data_type) => {
892                    FieldValue::Value { value }
893                }
894                value => {
895                    return Err(RecordError::ValueTypeMismatch {
896                        field: column.name.clone(),
897                        expected: column.data_type.clone(),
898                        actual: value.data_type(),
899                    });
900                }
901            };
902            self.fields
903                .get_mut(&column.name)
904                .expect("descriptor owns field")
905                .hydrate(hydrated);
906        }
907        Ok(())
908    }
909
910    fn column_descriptor(&self, name: &str) -> Result<&ColumnDescriptor, RecordError> {
911        self.descriptor
912            .columns
913            .iter()
914            .find(|column| column.name == name)
915            .ok_or_else(|| RecordError::UnknownField(name.to_string()))
916    }
917
918    fn present_fields(
919        &self,
920        dirty_only: bool,
921    ) -> Result<(Vec<String>, Vec<Expression>), RecordError> {
922        let mut columns = Vec::new();
923        let mut values = Vec::new();
924        for column in &self.descriptor.columns {
925            let state = self
926                .fields
927                .get(&column.name)
928                .expect("descriptor owns field");
929            if dirty_only && !state.is_dirty() {
930                continue;
931            }
932            if let Some(value) = field_expression(state) {
933                columns.push(column.name.clone());
934                values.push(value);
935            }
936        }
937        if columns.is_empty() {
938            return Err(RecordError::EmptyUpdate);
939        }
940        Ok((columns, values))
941    }
942
943    fn primary_key_columns(&self) -> Result<Vec<String>, RecordError> {
944        self.descriptor
945            .constraints
946            .iter()
947            .find_map(|constraint| match &constraint.definition {
948                crate::ConstraintDefinition::PrimaryKey { columns } => Some(columns.clone()),
949                _ => None,
950            })
951            .filter(|columns| !columns.is_empty())
952            .ok_or_else(|| RecordError::MissingPrimaryKey(self.descriptor.name.clone()))
953    }
954
955    fn primary_key_predicate(&self, columns: &[String]) -> Result<Expression, RecordError> {
956        let mut predicates = Vec::new();
957        for column in columns {
958            let state = self
959                .fields
960                .get(column)
961                .ok_or_else(|| RecordError::UnknownField(column.clone()))?;
962            let FieldValue::Value { value } = state.value() else {
963                return Err(RecordError::MissingPrimaryKeyValue(column.clone()));
964            };
965            predicates.push(Expression::Binary {
966                left: Box::new(Expression::column(column)),
967                operator: BinaryOperator::Eq,
968                right: Box::new(Expression::literal(value.clone())),
969            });
970        }
971        Ok(predicates
972            .into_iter()
973            .reduce(|left, right| Expression::Binary {
974                left: Box::new(left),
975                operator: BinaryOperator::And,
976                right: Box::new(right),
977            })
978            .expect("primary key is nonempty"))
979    }
980}
981
982/// Decode one canonical dynamic field into a generated Rust field while
983/// preserving omitted/NULL/clean state exactly.
984pub fn decode_generated_field<T: GeneratedValue>(
985    field: &str,
986    state: &FieldState<TypedValue>,
987    expected: &DataTypeDescriptor,
988) -> Result<FieldValue<T>, RecordError> {
989    match state.value() {
990        FieldValue::Omitted => Ok(FieldValue::Omitted),
991        FieldValue::Null { data_type } if data_type == expected => Ok(FieldValue::Null {
992            data_type: data_type.clone(),
993        }),
994        FieldValue::Null { .. } => Err(RecordError::HydrationTypeMismatch {
995            field: field.to_string(),
996            expected: expected.clone(),
997        }),
998        FieldValue::Value { value } => T::decode(value, expected)
999            .map(|value| FieldValue::Value { value })
1000            .map_err(|_| RecordError::HydrationTypeMismatch {
1001                field: field.to_string(),
1002                expected: expected.clone(),
1003            }),
1004    }
1005}
1006
1007pub fn decode_generated_reference_field<T>(
1008    field: &str,
1009    state: &FieldState<TypedValue>,
1010    expected: &DataTypeDescriptor,
1011    target_table: &str,
1012    target_column: &str,
1013) -> Result<FieldValue<Reference<T>>, RecordError> {
1014    match state.value() {
1015        FieldValue::Omitted => Ok(FieldValue::Omitted),
1016        FieldValue::Null { data_type } if data_type == expected => Ok(FieldValue::Null {
1017            data_type: data_type.clone(),
1018        }),
1019        FieldValue::Null { .. } => Err(RecordError::HydrationTypeMismatch {
1020            field: field.to_string(),
1021            expected: expected.clone(),
1022        }),
1023        FieldValue::Value { value } if typed_value_matches(value, expected) => {
1024            Ok(FieldValue::Value {
1025                value: Reference::new(target_table, target_column, value.clone()),
1026            })
1027        }
1028        FieldValue::Value { .. } => Err(RecordError::HydrationTypeMismatch {
1029            field: field.to_string(),
1030            expected: expected.clone(),
1031        }),
1032    }
1033}
1034
1035pub(crate) fn typed_value_matches(value: &TypedValue, expected: &DataTypeDescriptor) -> bool {
1036    matches!(
1037        (value, expected),
1038        (TypedValue::Integer(_), DataTypeDescriptor::Integer)
1039            | (TypedValue::Float(_), DataTypeDescriptor::Float)
1040            | (TypedValue::Text(_), DataTypeDescriptor::Text)
1041            | (TypedValue::Boolean(_), DataTypeDescriptor::Boolean)
1042            | (TypedValue::Timestamp(_), DataTypeDescriptor::Timestamp)
1043            | (TypedValue::Date(_), DataTypeDescriptor::Date)
1044            | (TypedValue::Json(_), DataTypeDescriptor::Json)
1045            | (TypedValue::Uuid(_), DataTypeDescriptor::Uuid)
1046            | (TypedValue::Bytes(_), DataTypeDescriptor::Bytes)
1047            | (TypedValue::Decimal(_), DataTypeDescriptor::Decimal { .. })
1048            | (TypedValue::Vector(_), DataTypeDescriptor::Vector { .. })
1049    )
1050}
1051
1052#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
1053#[error("typed value does not match the generated field type")]
1054pub struct GeneratedValueDecodeError;
1055
1056pub trait GeneratedValue: Sized {
1057    fn decode(
1058        value: &TypedValue,
1059        expected: &DataTypeDescriptor,
1060    ) -> Result<Self, GeneratedValueDecodeError>;
1061}
1062
1063impl GeneratedValue for i64 {
1064    fn decode(
1065        value: &TypedValue,
1066        expected: &DataTypeDescriptor,
1067    ) -> Result<Self, GeneratedValueDecodeError> {
1068        match (value, expected) {
1069            (TypedValue::Integer(value), DataTypeDescriptor::Integer) => Ok(*value),
1070            _ => Err(GeneratedValueDecodeError),
1071        }
1072    }
1073}
1074
1075impl GeneratedValue for f64 {
1076    fn decode(
1077        value: &TypedValue,
1078        expected: &DataTypeDescriptor,
1079    ) -> Result<Self, GeneratedValueDecodeError> {
1080        match (value, expected) {
1081            (TypedValue::Float(value), DataTypeDescriptor::Float) => Ok(value.as_f64()),
1082            _ => Err(GeneratedValueDecodeError),
1083        }
1084    }
1085}
1086
1087impl GeneratedValue for bool {
1088    fn decode(
1089        value: &TypedValue,
1090        expected: &DataTypeDescriptor,
1091    ) -> Result<Self, GeneratedValueDecodeError> {
1092        match (value, expected) {
1093            (TypedValue::Boolean(value), DataTypeDescriptor::Boolean) => Ok(*value),
1094            _ => Err(GeneratedValueDecodeError),
1095        }
1096    }
1097}
1098
1099impl GeneratedValue for serde_json::Value {
1100    fn decode(
1101        value: &TypedValue,
1102        expected: &DataTypeDescriptor,
1103    ) -> Result<Self, GeneratedValueDecodeError> {
1104        match (value, expected) {
1105            (TypedValue::Json(value), DataTypeDescriptor::Json) => Ok(value.clone()),
1106            _ => Err(GeneratedValueDecodeError),
1107        }
1108    }
1109}
1110
1111impl GeneratedValue for Vec<f32> {
1112    fn decode(
1113        value: &TypedValue,
1114        expected: &DataTypeDescriptor,
1115    ) -> Result<Self, GeneratedValueDecodeError> {
1116        match (value, expected) {
1117            (TypedValue::Vector(value), DataTypeDescriptor::Vector { dimensions })
1118                if value.len() == usize::from(*dimensions) =>
1119            {
1120                Ok(value.clone())
1121            }
1122            _ => Err(GeneratedValueDecodeError),
1123        }
1124    }
1125}
1126
1127impl GeneratedValue for String {
1128    fn decode(
1129        value: &TypedValue,
1130        expected: &DataTypeDescriptor,
1131    ) -> Result<Self, GeneratedValueDecodeError> {
1132        match (value, expected) {
1133            (TypedValue::Text(value), DataTypeDescriptor::Text)
1134            | (TypedValue::Timestamp(value), DataTypeDescriptor::Timestamp)
1135            | (TypedValue::Date(value), DataTypeDescriptor::Date)
1136            | (TypedValue::Uuid(value), DataTypeDescriptor::Uuid)
1137            | (TypedValue::Bytes(value), DataTypeDescriptor::Bytes)
1138            | (TypedValue::Decimal(value), DataTypeDescriptor::Decimal { .. }) => Ok(value.clone()),
1139            _ => Err(GeneratedValueDecodeError),
1140        }
1141    }
1142}
1143
1144pub fn validate_reference_target(table: &TableDescriptor, column: &str) -> Result<(), RecordError> {
1145    let target = table
1146        .columns
1147        .iter()
1148        .find(|candidate| candidate.name == column)
1149        .ok_or(RecordError::UnsupportedReferenceTarget)?;
1150    if target.nullable {
1151        return Err(RecordError::UnsupportedReferenceTarget);
1152    }
1153    let valid = table
1154        .constraints
1155        .iter()
1156        .any(|constraint| match &constraint.definition {
1157            crate::ConstraintDefinition::PrimaryKey { columns }
1158            | crate::ConstraintDefinition::Unique { columns, .. } => columns.as_slice() == [column],
1159            _ => false,
1160        });
1161    if valid {
1162        Ok(())
1163    } else {
1164        Err(RecordError::UnsupportedReferenceTarget)
1165    }
1166}
1167
1168fn field_expression(state: &FieldState<TypedValue>) -> Option<Expression> {
1169    match state.value() {
1170        FieldValue::Omitted => None,
1171        FieldValue::Value { value } => Some(Expression::literal(value.clone())),
1172        FieldValue::Null { data_type } => {
1173            Some(Expression::literal(TypedValue::Null(data_type.clone())))
1174        }
1175    }
1176}
1177
1178fn return_all() -> Vec<Projection> {
1179    vec![Projection {
1180        expression: Expression::Star { relation: None },
1181        alias: None,
1182    }]
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187    use super::*;
1188
1189    #[derive(Debug, Default, Clone, PartialEq)]
1190    struct TestGeneratedRecord;
1191
1192    struct TestGeneratedEntity;
1193
1194    struct EmployeeEntity;
1195    struct DepartmentEntity;
1196
1197    impl GeneratedEntity for TestGeneratedEntity {
1198        type Record = TestGeneratedRecord;
1199        const TABLE: &'static str = "people";
1200        const CATALOG_ID: &'static str = "test";
1201        const SCHEMA_FINGERPRINT: &'static str = "test";
1202    }
1203
1204    impl GeneratedEntity for EmployeeEntity {
1205        type Record = TestGeneratedRecord;
1206        const TABLE: &'static str = "employees";
1207        const CATALOG_ID: &'static str = "employees";
1208        const SCHEMA_FINGERPRINT: &'static str = "employees";
1209    }
1210
1211    impl GeneratedEntity for DepartmentEntity {
1212        type Record = TestGeneratedRecord;
1213        const TABLE: &'static str = "departments";
1214        const CATALOG_ID: &'static str = "departments";
1215        const SCHEMA_FINGERPRINT: &'static str = "departments";
1216    }
1217
1218    impl GeneratedRecord for TestGeneratedRecord {
1219        type Entity = TestGeneratedEntity;
1220
1221        fn to_dynamic(
1222            &self,
1223            _descriptor: &TableDescriptor,
1224        ) -> Result<DynamicRecord, GeneratedRecordError> {
1225            unreachable!("query mock does not mutate records")
1226        }
1227
1228        fn apply_dynamic(&mut self, _record: &DynamicRecord) -> Result<(), GeneratedRecordError> {
1229            unreachable!("query mock returns already decoded records")
1230        }
1231    }
1232
1233    struct MockGeneratedQuerySession(Vec<TestGeneratedRecord>);
1234
1235    impl OrmGeneratedQuerySession for MockGeneratedQuerySession {
1236        type Error = RecordError;
1237
1238        fn query_generated_records<R: GeneratedRecord + Default>(
1239            self,
1240            _document: &IrDocument,
1241        ) -> Result<Vec<R>, Self::Error> {
1242            Ok(self.0.into_iter().map(|_| R::default()).collect())
1243        }
1244    }
1245
1246    fn descriptor() -> TableDescriptor {
1247        TableDescriptor {
1248            catalog_id: "00000000-0000-0000-0000-000000000001".to_string(),
1249            name: "people".to_string(),
1250            schema_generation: 1,
1251            fingerprint: "f".to_string(),
1252            created_at: "x".to_string(),
1253            updated_at: "x".to_string(),
1254            columns: vec![
1255                ColumnDescriptor {
1256                    ordinal: 0,
1257                    name: "id".to_string(),
1258                    data_type: DataTypeDescriptor::Integer,
1259                    nullable: false,
1260                    auto_increment: false,
1261                    default_expression: None,
1262                    extensions: BTreeMap::new(),
1263                },
1264                ColumnDescriptor {
1265                    ordinal: 1,
1266                    name: "name".to_string(),
1267                    data_type: DataTypeDescriptor::Text,
1268                    nullable: true,
1269                    auto_increment: false,
1270                    default_expression: None,
1271                    extensions: BTreeMap::new(),
1272                },
1273                ColumnDescriptor {
1274                    ordinal: 2,
1275                    name: "note".to_string(),
1276                    data_type: DataTypeDescriptor::Text,
1277                    nullable: true,
1278                    auto_increment: false,
1279                    default_expression: None,
1280                    extensions: BTreeMap::new(),
1281                },
1282            ],
1283            constraints: vec![ConstraintDescriptor {
1284                id: 1,
1285                name: "pk_people".to_string(),
1286                definition: crate::ConstraintDefinition::PrimaryKey {
1287                    columns: vec!["id".to_string()],
1288                },
1289            }],
1290            indexes: Vec::new(),
1291            extensions: BTreeMap::new(),
1292        }
1293    }
1294
1295    #[test]
1296    fn record_distinguishes_omitted_null_value_and_keeps_dirty_until_hydration() {
1297        let mut record = DynamicRecord::new(descriptor());
1298        record.set("id", TypedValue::Integer(7)).unwrap();
1299        record.set_null("name").unwrap();
1300        let insert = record.insert_document().unwrap().to_sql().unwrap();
1301        assert!(insert.sql.ends_with("RETURNING *"));
1302        assert_eq!(insert.parameters.len(), 2);
1303        assert!(record.is_dirty());
1304
1305        record.unset("name").unwrap();
1306        assert!(record.update_document().is_err());
1307        record
1308            .set("name", TypedValue::Text("Alice".to_string()))
1309            .unwrap();
1310        let update = record.update_document().unwrap().to_sql().unwrap();
1311        assert_eq!(
1312            update.parameters,
1313            vec![
1314                TypedValue::Text("Alice".to_string()),
1315                TypedValue::Integer(7)
1316            ]
1317        );
1318        assert!(record.is_dirty());
1319
1320        record
1321            .apply_returning(BTreeMap::from([
1322                ("id".to_string(), TypedValue::Integer(7)),
1323                ("name".to_string(), TypedValue::Text("Alice".to_string())),
1324                ("note".to_string(), TypedValue::Text("clean".to_string())),
1325            ]))
1326            .unwrap();
1327        assert!(!record.is_dirty());
1328
1329        record
1330            .set("name", TypedValue::Text("Changed".to_string()))
1331            .unwrap();
1332        let save = record.save_document().unwrap().to_sql().unwrap();
1333        assert!(save
1334            .sql
1335            .contains("DO UPDATE SET \"name\" = \"excluded\".\"name\""));
1336        assert!(!save.sql.contains("SET \"note\""));
1337    }
1338
1339    #[test]
1340    fn generated_query_cardinality_is_fail_closed() {
1341        let query = || {
1342            GeneratedQuery::<TestGeneratedRecord>::new(
1343                QueryBuilder::from_relation(crate::table("people")).select([Expr::star()]),
1344            )
1345        };
1346        assert_eq!(
1347            query()
1348                .one(MockGeneratedQuerySession(vec![TestGeneratedRecord]))
1349                .unwrap(),
1350            TestGeneratedRecord
1351        );
1352        assert!(matches!(
1353            query().one(MockGeneratedQuerySession(Vec::new())),
1354            Err(RecordError::ExpectedOneRow)
1355        ));
1356        assert!(matches!(
1357            query().optional(MockGeneratedQuerySession(vec![
1358                TestGeneratedRecord,
1359                TestGeneratedRecord,
1360            ])),
1361            Err(RecordError::ExpectedAtMostOneRow(2))
1362        ));
1363        let invalid = GeneratedQuery::<TestGeneratedRecord>::new(QueryBuilder::from_relation(
1364            crate::table("people"),
1365        ));
1366        assert!(matches!(
1367            invalid.all(MockGeneratedQuerySession(Vec::new())),
1368            Err(RecordError::Build(BuilderError::EmptyProjection))
1369        ));
1370    }
1371
1372    #[test]
1373    fn reference_targets_require_one_nonnullable_primary_or_unique_column() {
1374        let mut table = descriptor();
1375        assert!(validate_reference_target(&table, "id").is_ok());
1376        assert!(matches!(
1377            validate_reference_target(&table, "name"),
1378            Err(RecordError::UnsupportedReferenceTarget)
1379        ));
1380
1381        table.columns[1].nullable = false;
1382        table.constraints.push(ConstraintDescriptor {
1383            id: 2,
1384            name: "uq_people_name".to_string(),
1385            definition: crate::ConstraintDefinition::Unique {
1386                columns: vec!["name".to_string()],
1387                owned_index: "uq_people_name".to_string(),
1388            },
1389        });
1390        assert!(validate_reference_target(&table, "name").is_ok());
1391        table.constraints[1].definition = crate::ConstraintDefinition::Unique {
1392            columns: vec!["name".to_string(), "note".to_string()],
1393            owned_index: "uq_people_name_note".to_string(),
1394        };
1395        assert!(matches!(
1396            validate_reference_target(&table, "name"),
1397            Err(RecordError::UnsupportedReferenceTarget)
1398        ));
1399    }
1400
1401    #[test]
1402    fn dynamic_references_are_key_only_typed_and_source_checked() {
1403        let target = descriptor();
1404        let target_entity = DynamicEntity::new(target.clone());
1405        let reference = target_entity
1406            .reference("id", TypedValue::Integer(7))
1407            .unwrap();
1408        assert_eq!(reference.key(), &TypedValue::Integer(7));
1409        assert_eq!(reference.target_table(), "people");
1410        assert_eq!(reference.target_column(), "id");
1411        assert_eq!(
1412            serde_json::from_str::<serde_json::Value>(&reference.to_json().unwrap()).unwrap(),
1413            serde_json::json!({
1414                "target_table": "people",
1415                "target_column": "id",
1416                "key": { "type": "integer", "value": 7 }
1417            })
1418        );
1419        assert!(matches!(
1420            target_entity.reference("name", TypedValue::Text("x".to_string())),
1421            Err(RecordError::UnsupportedReferenceTarget)
1422        ));
1423        assert!(matches!(
1424            target_entity.reference("id", TypedValue::Float(7.0.into())),
1425            Err(RecordError::ValueTypeMismatch { .. })
1426        ));
1427
1428        let mut source = descriptor();
1429        source.name = "documents".to_string();
1430        source.constraints.push(ConstraintDescriptor {
1431            id: 2,
1432            name: "fk_documents_id___people".to_string(),
1433            definition: ConstraintDefinition::ForeignKey {
1434                columns: vec!["id".to_string()],
1435                referenced_table: "people".to_string(),
1436                referenced_columns: vec!["id".to_string()],
1437                on_delete: ForeignKeyActionDescriptor::Restrict,
1438                on_update: ForeignKeyActionDescriptor::Restrict,
1439            },
1440        });
1441        let mut record = DynamicRecord::new(source);
1442        record.set_reference("id", &reference).unwrap();
1443        assert_eq!(
1444            record.field("id").unwrap().value(),
1445            &FieldValue::Value {
1446                value: TypedValue::Integer(7)
1447            }
1448        );
1449
1450        let other = DynamicReference::new("other", "id", TypedValue::Integer(7));
1451        assert!(matches!(
1452            record.set_reference("id", &other),
1453            Err(RecordError::ReferenceSourceMismatch { .. })
1454        ));
1455    }
1456
1457    #[test]
1458    fn typed_columns_cover_predicates_and_transitive_navigation() {
1459        let employee = TypedColumn::<Reference<EmployeeEntity>, TestGeneratedEntity>::new(
1460            "payroll_documents",
1461            "employee_id",
1462            DataTypeDescriptor::Integer,
1463            false,
1464        );
1465        let department = TypedColumn::<Reference<DepartmentEntity>, EmployeeEntity>::new(
1466            "employees",
1467            "department_id",
1468            DataTypeDescriptor::Integer,
1469            false,
1470        );
1471        let department_name = TypedColumn::<String, DepartmentEntity>::new(
1472            "departments",
1473            "name",
1474            DataTypeDescriptor::Text,
1475            false,
1476        );
1477
1478        let path = employee.field(department).field(department_name);
1479        assert_eq!(
1480            path.expr().0,
1481            Expression::Navigation {
1482                root: "payroll_documents".to_string(),
1483                path: vec![
1484                    "employee_id".to_string(),
1485                    "department_id".to_string(),
1486                    "name".to_string(),
1487                ],
1488            }
1489        );
1490
1491        let score = TypedColumn::<i64>::new("people", "score", DataTypeDescriptor::Integer, true);
1492        let query = QueryBuilder::from_relation(crate::table("people"))
1493            .select([score.clone().expr()])
1494            .filter(score.clone().between(10_i64, 20_i64).or(score.is_null()));
1495        let compiled = query.to_sql().unwrap();
1496        assert_eq!(compiled.parameters.len(), 2);
1497        assert!(compiled.sql.contains("BETWEEN $1 AND $2"));
1498    }
1499}