Skip to main content

sql_orm_core/
lib.rs

1//! Core contracts and shared types for the ORM.
2
3use chrono::{NaiveDate, NaiveDateTime};
4use core::fmt;
5use core::marker::PhantomData;
6use rust_decimal::Decimal;
7use uuid::Uuid;
8
9/// Common error type placeholder for the workspace foundations.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum OrmError {
12    Message(String),
13    ConcurrencyConflict,
14}
15
16impl OrmError {
17    pub fn new(message: impl Into<String>) -> Self {
18        Self::Message(message.into())
19    }
20
21    pub const fn concurrency_conflict() -> Self {
22        Self::ConcurrencyConflict
23    }
24
25    pub fn message(&self) -> &str {
26        match self {
27            Self::Message(message) => message,
28            Self::ConcurrencyConflict => "concurrency conflict",
29        }
30    }
31}
32
33impl fmt::Display for OrmError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str(self.message())
36    }
37}
38
39impl std::error::Error for OrmError {}
40
41/// Minimal crate identity metadata used while the rest of the model is defined.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CrateIdentity {
44    pub name: &'static str,
45    pub responsibility: &'static str,
46}
47
48pub const CRATE_IDENTITY: CrateIdentity = CrateIdentity {
49    name: "sql-orm-core",
50    responsibility: "contracts, metadata, shared types and errors",
51};
52
53/// Stable contract implemented by persisted entities.
54pub trait Entity: Sized + Send + Sync + 'static {
55    fn metadata() -> &'static EntityMetadata;
56}
57
58/// Static metadata exposed by a reusable entity policy.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct EntityPolicyMetadata {
61    pub name: &'static str,
62    pub columns: &'static [ColumnMetadata],
63}
64
65impl EntityPolicyMetadata {
66    pub const fn new(name: &'static str, columns: &'static [ColumnMetadata]) -> Self {
67        Self { name, columns }
68    }
69}
70
71/// Stable contract for reusable code-first policies that contribute normal columns.
72pub trait EntityPolicy: Sized + Send + Sync + 'static {
73    const POLICY_NAME: &'static str;
74    const COLUMN_NAMES: &'static [&'static str] = &[];
75
76    fn columns() -> &'static [ColumnMetadata];
77
78    fn metadata() -> EntityPolicyMetadata {
79        EntityPolicyMetadata::new(Self::POLICY_NAME, Self::columns())
80    }
81}
82
83pub const fn column_name_exists(columns: &[&'static str], column_name: &'static str) -> bool {
84    let mut index = 0;
85    while index < columns.len() {
86        if column_name_eq(columns[index], column_name) {
87            return true;
88        }
89        index += 1;
90    }
91    false
92}
93
94const fn column_name_eq(left: &str, right: &str) -> bool {
95    let left = left.as_bytes();
96    let right = right.as_bytes();
97    if left.len() != right.len() {
98        return false;
99    }
100
101    let mut index = 0;
102    while index < left.len() {
103        if left[index] != right[index] {
104            return false;
105        }
106        index += 1;
107    }
108    true
109}
110
111/// Base Rust <-> SQL Server mapping contract used by row readers and persistence models.
112pub trait SqlTypeMapping: Sized {
113    const SQL_SERVER_TYPE: SqlServerType;
114    const DEFAULT_MAX_LENGTH: Option<u32> = None;
115    const DEFAULT_PRECISION: Option<u8> = None;
116    const DEFAULT_SCALE: Option<u8> = None;
117
118    fn to_sql_value(self) -> SqlValue;
119
120    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError>;
121}
122
123/// Neutral SQL value representation shared across query compilation and execution layers.
124#[derive(Debug, Clone, PartialEq)]
125pub enum SqlValue {
126    Null,
127    TypedNull(SqlServerType),
128    Bool(bool),
129    I32(i32),
130    I64(i64),
131    F64(f64),
132    String(String),
133    Bytes(Vec<u8>),
134    Uuid(Uuid),
135    Decimal(Decimal),
136    Date(NaiveDate),
137    DateTime(NaiveDateTime),
138}
139
140impl SqlValue {
141    pub const fn is_null(&self) -> bool {
142        matches!(self, Self::Null | Self::TypedNull(_))
143    }
144}
145
146/// Column/value pair produced by insert and update models.
147#[derive(Debug, Clone, PartialEq)]
148pub struct ColumnValue {
149    pub column_name: &'static str,
150    pub value: SqlValue,
151}
152
153impl ColumnValue {
154    pub const fn new(column_name: &'static str, value: SqlValue) -> Self {
155        Self { column_name, value }
156    }
157}
158
159/// Row abstraction used by the core mapping contracts without depending on Tiberius.
160pub trait Row {
161    fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError>;
162
163    fn get_required(&self, column: &str) -> Result<SqlValue, OrmError> {
164        self.try_get(column)?
165            .ok_or_else(|| OrmError::new("required column value was not present"))
166    }
167
168    fn try_get_typed<T: SqlTypeMapping>(&self, column: &str) -> Result<Option<T>, OrmError> {
169        self.try_get(column)?.map(T::from_sql_value).transpose()
170    }
171
172    fn get_required_typed<T: SqlTypeMapping>(&self, column: &str) -> Result<T, OrmError> {
173        T::from_sql_value(self.get_required(column)?)
174    }
175}
176
177/// Stable contract for mapping a SQL row into a Rust type.
178pub trait FromRow: Sized {
179    fn from_row<R: Row>(row: &R) -> Result<Self, OrmError>;
180}
181
182/// Stable contract for extracting persisted values for inserts.
183pub trait Insertable<E: Entity> {
184    fn values(&self) -> Vec<ColumnValue>;
185}
186
187/// Stable contract for extracting changed values for updates.
188pub trait Changeset<E: Entity> {
189    fn changes(&self) -> Vec<ColumnValue>;
190
191    fn concurrency_token(&self) -> Result<Option<SqlValue>, OrmError> {
192        Ok(None)
193    }
194}
195
196/// Static column symbol generated for entities and consumed later by the query builder.
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub struct EntityColumn<E: Entity> {
199    rust_field: &'static str,
200    column_name: &'static str,
201    _entity: PhantomData<fn() -> E>,
202}
203
204impl<E: Entity> EntityColumn<E> {
205    pub const fn new(rust_field: &'static str, column_name: &'static str) -> Self {
206        Self {
207            rust_field,
208            column_name,
209            _entity: PhantomData,
210        }
211    }
212
213    pub const fn rust_field(&self) -> &'static str {
214        self.rust_field
215    }
216
217    pub const fn column_name(&self) -> &'static str {
218        self.column_name
219    }
220
221    pub fn entity_metadata(&self) -> &'static EntityMetadata {
222        E::metadata()
223    }
224
225    pub fn metadata(&self) -> &'static ColumnMetadata {
226        E::metadata()
227            .field(self.rust_field)
228            .expect("generated entity column must reference existing metadata")
229    }
230}
231
232/// SQL Server types supported by the metadata layer.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum SqlServerType {
235    BigInt,
236    Int,
237    SmallInt,
238    TinyInt,
239    Bit,
240    UniqueIdentifier,
241    Date,
242    DateTime2,
243    Decimal,
244    Float,
245    Money,
246    NVarChar,
247    VarBinary,
248    RowVersion,
249    Custom(&'static str),
250}
251
252impl SqlTypeMapping for bool {
253    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::Bit;
254
255    fn to_sql_value(self) -> SqlValue {
256        SqlValue::Bool(self)
257    }
258
259    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
260        match value {
261            SqlValue::Bool(value) => Ok(value),
262            _ => Err(OrmError::new("expected bool value")),
263        }
264    }
265}
266
267impl SqlTypeMapping for i32 {
268    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::Int;
269
270    fn to_sql_value(self) -> SqlValue {
271        SqlValue::I32(self)
272    }
273
274    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
275        match value {
276            SqlValue::I32(value) => Ok(value),
277            _ => Err(OrmError::new("expected i32 value")),
278        }
279    }
280}
281
282impl SqlTypeMapping for i64 {
283    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::BigInt;
284
285    fn to_sql_value(self) -> SqlValue {
286        SqlValue::I64(self)
287    }
288
289    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
290        match value {
291            SqlValue::I64(value) => Ok(value),
292            _ => Err(OrmError::new("expected i64 value")),
293        }
294    }
295}
296
297impl SqlTypeMapping for f64 {
298    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::Float;
299
300    fn to_sql_value(self) -> SqlValue {
301        SqlValue::F64(self)
302    }
303
304    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
305        match value {
306            SqlValue::F64(value) => Ok(value),
307            _ => Err(OrmError::new("expected f64 value")),
308        }
309    }
310}
311
312impl SqlTypeMapping for String {
313    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::NVarChar;
314    const DEFAULT_MAX_LENGTH: Option<u32> = Some(255);
315
316    fn to_sql_value(self) -> SqlValue {
317        SqlValue::String(self)
318    }
319
320    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
321        match value {
322            SqlValue::String(value) => Ok(value),
323            _ => Err(OrmError::new("expected string value")),
324        }
325    }
326}
327
328impl SqlTypeMapping for Vec<u8> {
329    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::VarBinary;
330
331    fn to_sql_value(self) -> SqlValue {
332        SqlValue::Bytes(self)
333    }
334
335    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
336        match value {
337            SqlValue::Bytes(value) => Ok(value),
338            _ => Err(OrmError::new("expected bytes value")),
339        }
340    }
341}
342
343impl SqlTypeMapping for Uuid {
344    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::UniqueIdentifier;
345
346    fn to_sql_value(self) -> SqlValue {
347        SqlValue::Uuid(self)
348    }
349
350    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
351        match value {
352            SqlValue::Uuid(value) => Ok(value),
353            _ => Err(OrmError::new("expected uuid value")),
354        }
355    }
356}
357
358impl SqlTypeMapping for Decimal {
359    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::Decimal;
360    const DEFAULT_PRECISION: Option<u8> = Some(18);
361    const DEFAULT_SCALE: Option<u8> = Some(2);
362
363    fn to_sql_value(self) -> SqlValue {
364        SqlValue::Decimal(self)
365    }
366
367    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
368        match value {
369            SqlValue::Decimal(value) => Ok(value),
370            _ => Err(OrmError::new("expected decimal value")),
371        }
372    }
373}
374
375impl SqlTypeMapping for NaiveDate {
376    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::Date;
377
378    fn to_sql_value(self) -> SqlValue {
379        SqlValue::Date(self)
380    }
381
382    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
383        match value {
384            SqlValue::Date(value) => Ok(value),
385            _ => Err(OrmError::new("expected date value")),
386        }
387    }
388}
389
390impl SqlTypeMapping for NaiveDateTime {
391    const SQL_SERVER_TYPE: SqlServerType = SqlServerType::DateTime2;
392
393    fn to_sql_value(self) -> SqlValue {
394        SqlValue::DateTime(self)
395    }
396
397    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
398        match value {
399            SqlValue::DateTime(value) => Ok(value),
400            _ => Err(OrmError::new("expected datetime value")),
401        }
402    }
403}
404
405impl<T> SqlTypeMapping for Option<T>
406where
407    T: SqlTypeMapping,
408{
409    const SQL_SERVER_TYPE: SqlServerType = T::SQL_SERVER_TYPE;
410    const DEFAULT_MAX_LENGTH: Option<u32> = T::DEFAULT_MAX_LENGTH;
411    const DEFAULT_PRECISION: Option<u8> = T::DEFAULT_PRECISION;
412    const DEFAULT_SCALE: Option<u8> = T::DEFAULT_SCALE;
413
414    fn to_sql_value(self) -> SqlValue {
415        self.map(T::to_sql_value)
416            .unwrap_or(SqlValue::TypedNull(T::SQL_SERVER_TYPE))
417    }
418
419    fn from_sql_value(value: SqlValue) -> Result<Self, OrmError> {
420        match value {
421            SqlValue::Null | SqlValue::TypedNull(_) => Ok(None),
422            other => T::from_sql_value(other).map(Some),
423        }
424    }
425}
426
427/// Metadata for SQL Server identity columns.
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429pub struct IdentityMetadata {
430    pub seed: i64,
431    pub increment: i64,
432}
433
434impl IdentityMetadata {
435    pub const fn new(seed: i64, increment: i64) -> Self {
436        Self { seed, increment }
437    }
438}
439
440/// Primary key metadata for an entity.
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
442pub struct PrimaryKeyMetadata {
443    pub name: Option<&'static str>,
444    pub columns: &'static [&'static str],
445}
446
447impl PrimaryKeyMetadata {
448    pub const fn new(name: Option<&'static str>, columns: &'static [&'static str]) -> Self {
449        Self { name, columns }
450    }
451}
452
453/// Per-column metadata generated from entity definitions.
454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub struct ColumnMetadata {
456    pub rust_field: &'static str,
457    pub column_name: &'static str,
458    pub renamed_from: Option<&'static str>,
459    pub sql_type: SqlServerType,
460    pub nullable: bool,
461    pub primary_key: bool,
462    pub identity: Option<IdentityMetadata>,
463    pub default_sql: Option<&'static str>,
464    pub computed_sql: Option<&'static str>,
465    pub rowversion: bool,
466    pub insertable: bool,
467    pub updatable: bool,
468    pub max_length: Option<u32>,
469    pub precision: Option<u8>,
470    pub scale: Option<u8>,
471}
472
473impl ColumnMetadata {
474    pub const fn is_computed(&self) -> bool {
475        self.computed_sql.is_some()
476    }
477}
478
479/// Columns participating in an index and their sort direction.
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub struct IndexColumnMetadata {
482    pub column_name: &'static str,
483    pub descending: bool,
484}
485
486impl IndexColumnMetadata {
487    pub const fn asc(column_name: &'static str) -> Self {
488        Self {
489            column_name,
490            descending: false,
491        }
492    }
493
494    pub const fn desc(column_name: &'static str) -> Self {
495        Self {
496            column_name,
497            descending: true,
498        }
499    }
500}
501
502/// Index metadata attached to an entity.
503#[derive(Debug, Clone, Copy, PartialEq, Eq)]
504pub struct IndexMetadata {
505    pub name: &'static str,
506    pub columns: &'static [IndexColumnMetadata],
507    pub unique: bool,
508}
509
510/// Delete/update behavior for foreign keys.
511#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512pub enum ReferentialAction {
513    NoAction,
514    Cascade,
515    SetNull,
516    SetDefault,
517}
518
519/// Foreign key metadata attached to an entity.
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
521pub struct ForeignKeyMetadata {
522    pub name: &'static str,
523    pub columns: &'static [&'static str],
524    pub referenced_schema: &'static str,
525    pub referenced_table: &'static str,
526    pub referenced_columns: &'static [&'static str],
527    pub on_delete: ReferentialAction,
528    pub on_update: ReferentialAction,
529}
530
531impl ForeignKeyMetadata {
532    pub const fn new(
533        name: &'static str,
534        columns: &'static [&'static str],
535        referenced_schema: &'static str,
536        referenced_table: &'static str,
537        referenced_columns: &'static [&'static str],
538        on_delete: ReferentialAction,
539        on_update: ReferentialAction,
540    ) -> Self {
541        Self {
542            name,
543            columns,
544            referenced_schema,
545            referenced_table,
546            referenced_columns,
547            on_delete,
548            on_update,
549        }
550    }
551
552    pub fn references_table(&self, schema: &str, table: &str) -> bool {
553        self.referenced_schema == schema && self.referenced_table == table
554    }
555
556    pub fn includes_column(&self, column_name: &str) -> bool {
557        self.columns.contains(&column_name)
558    }
559}
560
561/// Relationship direction represented by a navigation property.
562#[derive(Debug, Clone, Copy, PartialEq, Eq)]
563pub enum NavigationKind {
564    BelongsTo,
565    HasOne,
566    HasMany,
567    ManyToMany,
568}
569
570/// Navigation property metadata attached to an entity.
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub struct NavigationMetadata {
573    pub rust_field: &'static str,
574    pub kind: NavigationKind,
575    pub target_rust_name: &'static str,
576    pub target_schema: &'static str,
577    pub target_table: &'static str,
578    pub local_columns: &'static [&'static str],
579    pub target_columns: &'static [&'static str],
580    pub foreign_key_name: Option<&'static str>,
581}
582
583impl NavigationMetadata {
584    #[allow(clippy::too_many_arguments)]
585    pub const fn new(
586        rust_field: &'static str,
587        kind: NavigationKind,
588        target_rust_name: &'static str,
589        target_schema: &'static str,
590        target_table: &'static str,
591        local_columns: &'static [&'static str],
592        target_columns: &'static [&'static str],
593        foreign_key_name: Option<&'static str>,
594    ) -> Self {
595        Self {
596            rust_field,
597            kind,
598            target_rust_name,
599            target_schema,
600            target_table,
601            local_columns,
602            target_columns,
603            foreign_key_name,
604        }
605    }
606
607    pub fn targets_table(&self, schema: &str, table: &str) -> bool {
608        self.target_schema == schema && self.target_table == table
609    }
610
611    pub fn uses_foreign_key(&self, foreign_key_name: &str) -> bool {
612        self.foreign_key_name == Some(foreign_key_name)
613    }
614}
615
616/// Static metadata describing an entity.
617#[derive(Debug, Clone, Copy, PartialEq, Eq)]
618pub struct EntityMetadata {
619    pub rust_name: &'static str,
620    pub schema: &'static str,
621    pub table: &'static str,
622    pub renamed_from: Option<&'static str>,
623    pub columns: &'static [ColumnMetadata],
624    pub primary_key: PrimaryKeyMetadata,
625    pub indexes: &'static [IndexMetadata],
626    pub foreign_keys: &'static [ForeignKeyMetadata],
627    pub navigations: &'static [NavigationMetadata],
628}
629
630impl EntityMetadata {
631    pub fn column(&self, column_name: &str) -> Option<&'static ColumnMetadata> {
632        self.columns
633            .iter()
634            .find(|column| column.column_name == column_name)
635    }
636
637    pub fn field(&self, rust_field: &str) -> Option<&'static ColumnMetadata> {
638        self.columns
639            .iter()
640            .find(|column| column.rust_field == rust_field)
641    }
642
643    pub fn primary_key_columns(&self) -> Vec<&'static ColumnMetadata> {
644        self.primary_key
645            .columns
646            .iter()
647            .filter_map(|column_name| self.column(column_name))
648            .collect()
649    }
650
651    pub fn rowversion_column(&self) -> Option<&'static ColumnMetadata> {
652        self.columns.iter().find(|column| column.rowversion)
653    }
654
655    pub fn foreign_key(&self, name: &str) -> Option<&'static ForeignKeyMetadata> {
656        self.foreign_keys
657            .iter()
658            .find(|foreign_key| foreign_key.name == name)
659    }
660
661    pub fn foreign_keys_for_column(&self, column_name: &str) -> Vec<&'static ForeignKeyMetadata> {
662        self.foreign_keys
663            .iter()
664            .filter(|foreign_key| foreign_key.includes_column(column_name))
665            .collect()
666    }
667
668    pub fn foreign_keys_referencing(
669        &self,
670        schema: &str,
671        table: &str,
672    ) -> Vec<&'static ForeignKeyMetadata> {
673        self.foreign_keys
674            .iter()
675            .filter(|foreign_key| foreign_key.references_table(schema, table))
676            .collect()
677    }
678
679    pub fn navigation(&self, rust_field: &str) -> Option<&'static NavigationMetadata> {
680        self.navigations
681            .iter()
682            .find(|navigation| navigation.rust_field == rust_field)
683    }
684
685    pub fn navigations_by_kind(&self, kind: NavigationKind) -> Vec<&'static NavigationMetadata> {
686        self.navigations
687            .iter()
688            .filter(|navigation| navigation.kind == kind)
689            .collect()
690    }
691
692    pub fn navigations_for_foreign_key(
693        &self,
694        foreign_key_name: &str,
695    ) -> Vec<&'static NavigationMetadata> {
696        self.navigations
697            .iter()
698            .filter(|navigation| navigation.uses_foreign_key(foreign_key_name))
699            .collect()
700    }
701
702    pub fn navigations_targeting(
703        &self,
704        schema: &str,
705        table: &str,
706    ) -> Vec<&'static NavigationMetadata> {
707        self.navigations
708            .iter()
709            .filter(|navigation| navigation.targets_table(schema, table))
710            .collect()
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    use super::{
717        CRATE_IDENTITY, Changeset, ColumnMetadata, ColumnValue, Entity, EntityColumn,
718        EntityMetadata, EntityPolicy, EntityPolicyMetadata, ForeignKeyMetadata, FromRow,
719        IdentityMetadata, IndexColumnMetadata, IndexMetadata, Insertable, NavigationKind,
720        NavigationMetadata, OrmError, PrimaryKeyMetadata, ReferentialAction, Row, SqlServerType,
721        SqlTypeMapping, SqlValue, column_name_exists,
722    };
723    use chrono::{NaiveDate, NaiveDateTime};
724    use rust_decimal::Decimal;
725    use std::collections::BTreeMap;
726    use uuid::Uuid;
727
728    const USER_COLUMNS: [ColumnMetadata; 4] = [
729        ColumnMetadata {
730            rust_field: "tenant_id",
731            column_name: "tenant_id",
732            renamed_from: None,
733            sql_type: SqlServerType::BigInt,
734            nullable: false,
735            primary_key: true,
736            identity: None,
737            default_sql: None,
738            computed_sql: None,
739            rowversion: false,
740            insertable: true,
741            updatable: false,
742            max_length: None,
743            precision: None,
744            scale: None,
745        },
746        ColumnMetadata {
747            rust_field: "id",
748            column_name: "id",
749            renamed_from: None,
750            sql_type: SqlServerType::BigInt,
751            nullable: false,
752            primary_key: true,
753            identity: Some(IdentityMetadata::new(1, 1)),
754            default_sql: None,
755            computed_sql: None,
756            rowversion: false,
757            insertable: false,
758            updatable: false,
759            max_length: None,
760            precision: None,
761            scale: None,
762        },
763        ColumnMetadata {
764            rust_field: "email",
765            column_name: "email",
766            renamed_from: None,
767            sql_type: SqlServerType::NVarChar,
768            nullable: false,
769            primary_key: false,
770            identity: None,
771            default_sql: None,
772            computed_sql: None,
773            rowversion: false,
774            insertable: true,
775            updatable: true,
776            max_length: Some(180),
777            precision: None,
778            scale: None,
779        },
780        ColumnMetadata {
781            rust_field: "version",
782            column_name: "version",
783            renamed_from: None,
784            sql_type: SqlServerType::RowVersion,
785            nullable: false,
786            primary_key: false,
787            identity: None,
788            default_sql: None,
789            computed_sql: None,
790            rowversion: true,
791            insertable: false,
792            updatable: false,
793            max_length: None,
794            precision: None,
795            scale: None,
796        },
797    ];
798
799    const USER_PRIMARY_KEY_COLUMNS: [&str; 2] = ["id", "tenant_id"];
800
801    const USER_INDEXES: [IndexMetadata; 1] = [IndexMetadata {
802        name: "ux_users_email",
803        columns: &[IndexColumnMetadata::asc("email")],
804        unique: true,
805    }];
806
807    const USER_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
808        "fk_users_tenants",
809        &["tenant_id"],
810        "dbo",
811        "tenants",
812        &["id"],
813        ReferentialAction::NoAction,
814        ReferentialAction::NoAction,
815    )];
816
817    const USER_NAVIGATIONS: [NavigationMetadata; 1] = [NavigationMetadata::new(
818        "tenant",
819        NavigationKind::BelongsTo,
820        "Tenant",
821        "dbo",
822        "tenants",
823        &["tenant_id"],
824        &["id"],
825        Some("fk_users_tenants"),
826    )];
827
828    const AUDIT_POLICY_COLUMNS: [ColumnMetadata; 2] = [
829        ColumnMetadata {
830            rust_field: "created_at",
831            column_name: "created_at",
832            renamed_from: None,
833            sql_type: SqlServerType::DateTime2,
834            nullable: false,
835            primary_key: false,
836            identity: None,
837            default_sql: Some("SYSUTCDATETIME()"),
838            computed_sql: None,
839            rowversion: false,
840            insertable: true,
841            updatable: false,
842            max_length: None,
843            precision: None,
844            scale: None,
845        },
846        ColumnMetadata {
847            rust_field: "updated_at",
848            column_name: "updated_at",
849            renamed_from: None,
850            sql_type: SqlServerType::DateTime2,
851            nullable: true,
852            primary_key: false,
853            identity: None,
854            default_sql: None,
855            computed_sql: None,
856            rowversion: false,
857            insertable: true,
858            updatable: true,
859            max_length: None,
860            precision: None,
861            scale: None,
862        },
863    ];
864
865    const USER_METADATA: EntityMetadata = EntityMetadata {
866        rust_name: "User",
867        schema: "dbo",
868        table: "users",
869        renamed_from: None,
870        columns: &USER_COLUMNS,
871        primary_key: PrimaryKeyMetadata::new(Some("pk_users"), &USER_PRIMARY_KEY_COLUMNS),
872        indexes: &USER_INDEXES,
873        foreign_keys: &USER_FOREIGN_KEYS,
874        navigations: &USER_NAVIGATIONS,
875    };
876
877    struct User;
878
879    impl Entity for User {
880        fn metadata() -> &'static EntityMetadata {
881            &USER_METADATA
882        }
883    }
884
885    struct AuditPolicy;
886
887    impl EntityPolicy for AuditPolicy {
888        const POLICY_NAME: &'static str = "audit";
889        const COLUMN_NAMES: &'static [&'static str] = &["created_at", "updated_at"];
890
891        fn columns() -> &'static [ColumnMetadata] {
892            &AUDIT_POLICY_COLUMNS
893        }
894    }
895
896    struct TestRow {
897        values: BTreeMap<&'static str, SqlValue>,
898    }
899
900    impl Row for TestRow {
901        fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
902            Ok(self.values.get(column).cloned())
903        }
904    }
905
906    #[derive(Debug, PartialEq)]
907    struct UserRecord {
908        id: i64,
909        email: String,
910    }
911
912    impl FromRow for UserRecord {
913        fn from_row<R: Row>(row: &R) -> Result<Self, OrmError> {
914            let id = row.get_required_typed::<i64>("id")?;
915            let email = row.get_required_typed::<String>("email")?;
916
917            Ok(Self { id, email })
918        }
919    }
920
921    struct NewUser {
922        email: String,
923    }
924
925    impl Insertable<User> for NewUser {
926        fn values(&self) -> Vec<ColumnValue> {
927            vec![ColumnValue::new(
928                "email",
929                SqlValue::String(self.email.clone()),
930            )]
931        }
932    }
933
934    struct UpdateUser {
935        email: Option<String>,
936    }
937
938    impl Changeset<User> for UpdateUser {
939        fn changes(&self) -> Vec<ColumnValue> {
940            self.email
941                .clone()
942                .map(|email| vec![ColumnValue::new("email", SqlValue::String(email))])
943                .unwrap_or_default()
944        }
945    }
946
947    #[test]
948    fn exposes_foundation_identity() {
949        assert_eq!(CRATE_IDENTITY.name, "sql-orm-core");
950    }
951
952    #[test]
953    fn preserves_error_message() {
954        let error = OrmError::new("foundation");
955        assert_eq!(error.message(), "foundation");
956        assert_eq!(error.to_string(), "foundation");
957    }
958
959    #[test]
960    fn exposes_concurrency_conflict_error() {
961        let error = OrmError::concurrency_conflict();
962        assert_eq!(error, OrmError::ConcurrencyConflict);
963        assert_eq!(error.message(), "concurrency conflict");
964        assert_eq!(error.to_string(), "concurrency conflict");
965    }
966
967    #[test]
968    fn entity_trait_exposes_static_metadata() {
969        let metadata = User::metadata();
970
971        assert_eq!(metadata.rust_name, "User");
972        assert_eq!(metadata.schema, "dbo");
973        assert_eq!(metadata.table, "users");
974        assert_eq!(metadata.primary_key.name, Some("pk_users"));
975        assert_eq!(metadata.indexes.len(), 1);
976        assert_eq!(metadata.foreign_keys.len(), 1);
977        assert_eq!(metadata.navigations.len(), 1);
978        assert_eq!(metadata.primary_key.columns, &["id", "tenant_id"]);
979    }
980
981    #[test]
982    fn entity_policy_exposes_reusable_column_metadata() {
983        let metadata = AuditPolicy::metadata();
984
985        assert_eq!(
986            metadata,
987            EntityPolicyMetadata::new("audit", &AUDIT_POLICY_COLUMNS)
988        );
989        assert_eq!(metadata.columns[0].column_name, "created_at");
990        assert_eq!(metadata.columns[0].default_sql, Some("SYSUTCDATETIME()"));
991        assert!(!metadata.columns[0].primary_key);
992        assert!(metadata.columns[1].nullable);
993        assert!(metadata.columns[1].updatable);
994        assert!(column_name_exists(AuditPolicy::COLUMN_NAMES, "created_at"));
995        assert!(!column_name_exists(AuditPolicy::COLUMN_NAMES, "missing"));
996    }
997
998    #[test]
999    fn metadata_can_lookup_columns_by_field_and_name() {
1000        let metadata = User::metadata();
1001
1002        assert_eq!(metadata.column("email"), metadata.field("email"));
1003        assert_eq!(
1004            metadata.column("version").map(|column| column.sql_type),
1005            Some(SqlServerType::RowVersion)
1006        );
1007        assert!(metadata.column("missing").is_none());
1008    }
1009
1010    #[test]
1011    fn foreign_key_metadata_supports_relationship_lookups() {
1012        let metadata = User::metadata();
1013        let foreign_key = metadata
1014            .foreign_key("fk_users_tenants")
1015            .expect("foreign key metadata");
1016
1017        assert_eq!(foreign_key.columns, &["tenant_id"]);
1018        assert_eq!(foreign_key.referenced_schema, "dbo");
1019        assert_eq!(foreign_key.referenced_table, "tenants");
1020        assert_eq!(foreign_key.referenced_columns, &["id"]);
1021        assert!(foreign_key.references_table("dbo", "tenants"));
1022        assert!(!foreign_key.references_table("sales", "tenants"));
1023        assert!(foreign_key.includes_column("tenant_id"));
1024        assert!(!foreign_key.includes_column("email"));
1025    }
1026
1027    #[test]
1028    fn metadata_can_filter_foreign_keys_by_column_and_target_table() {
1029        let metadata = User::metadata();
1030
1031        let by_column = metadata.foreign_keys_for_column("tenant_id");
1032        assert_eq!(by_column.len(), 1);
1033        assert_eq!(by_column[0].name, "fk_users_tenants");
1034
1035        let by_table = metadata.foreign_keys_referencing("dbo", "tenants");
1036        assert_eq!(by_table.len(), 1);
1037        assert_eq!(by_table[0].name, "fk_users_tenants");
1038
1039        assert!(metadata.foreign_keys_for_column("email").is_empty());
1040        assert!(
1041            metadata
1042                .foreign_keys_referencing("sales", "customers")
1043                .is_empty()
1044        );
1045    }
1046
1047    #[test]
1048    fn navigation_metadata_supports_relationship_lookups() {
1049        let metadata = User::metadata();
1050        let navigation = metadata.navigation("tenant").expect("navigation metadata");
1051
1052        assert_eq!(navigation.kind, NavigationKind::BelongsTo);
1053        assert_eq!(navigation.target_rust_name, "Tenant");
1054        assert_eq!(navigation.target_schema, "dbo");
1055        assert_eq!(navigation.target_table, "tenants");
1056        assert_eq!(navigation.local_columns, &["tenant_id"]);
1057        assert_eq!(navigation.target_columns, &["id"]);
1058        assert_eq!(navigation.foreign_key_name, Some("fk_users_tenants"));
1059        assert!(navigation.targets_table("dbo", "tenants"));
1060        assert!(!navigation.targets_table("sales", "tenants"));
1061        assert!(navigation.uses_foreign_key("fk_users_tenants"));
1062        assert!(!navigation.uses_foreign_key("fk_users_accounts"));
1063
1064        let by_kind = metadata.navigations_by_kind(NavigationKind::BelongsTo);
1065        assert_eq!(by_kind.len(), 1);
1066        assert_eq!(by_kind[0].rust_field, "tenant");
1067
1068        let by_foreign_key = metadata.navigations_for_foreign_key("fk_users_tenants");
1069        assert_eq!(by_foreign_key.len(), 1);
1070        assert_eq!(by_foreign_key[0].rust_field, "tenant");
1071
1072        let by_target = metadata.navigations_targeting("dbo", "tenants");
1073        assert_eq!(by_target.len(), 1);
1074        assert_eq!(by_target[0].rust_field, "tenant");
1075
1076        assert!(metadata.navigation("missing").is_none());
1077        assert!(
1078            metadata
1079                .navigations_by_kind(NavigationKind::HasMany)
1080                .is_empty()
1081        );
1082        assert!(
1083            metadata
1084                .navigations_for_foreign_key("fk_users_missing")
1085                .is_empty()
1086        );
1087        assert!(
1088            metadata
1089                .navigations_targeting("sales", "customers")
1090                .is_empty()
1091        );
1092    }
1093
1094    #[test]
1095    fn metadata_returns_primary_key_columns() {
1096        let metadata = User::metadata();
1097        let columns = metadata.primary_key_columns();
1098
1099        assert_eq!(columns.len(), 2);
1100        assert_eq!(columns[0].column_name, "id");
1101        assert_eq!(columns[1].column_name, "tenant_id");
1102        assert!(columns.iter().all(|column| column.primary_key));
1103    }
1104
1105    #[test]
1106    fn metadata_returns_rowversion_column_when_present() {
1107        let metadata = User::metadata();
1108        let column = metadata.rowversion_column().expect("rowversion column");
1109
1110        assert_eq!(column.column_name, "version");
1111        assert!(column.rowversion);
1112    }
1113
1114    #[test]
1115    fn column_metadata_marks_computed_values() {
1116        let computed = ColumnMetadata {
1117            rust_field: "full_name",
1118            column_name: "full_name",
1119            renamed_from: None,
1120            sql_type: SqlServerType::NVarChar,
1121            nullable: false,
1122            primary_key: false,
1123            identity: None,
1124            default_sql: None,
1125            computed_sql: Some("[first_name] + ' ' + [last_name]"),
1126            rowversion: false,
1127            insertable: false,
1128            updatable: false,
1129            max_length: Some(240),
1130            precision: None,
1131            scale: None,
1132        };
1133
1134        assert!(computed.is_computed());
1135        assert!(!USER_COLUMNS[0].is_computed());
1136    }
1137
1138    #[test]
1139    fn index_columns_preserve_sort_direction() {
1140        let descending = IndexColumnMetadata::desc("created_at");
1141
1142        assert_eq!(
1143            USER_INDEXES[0].columns[0],
1144            IndexColumnMetadata::asc("email")
1145        );
1146        assert!(descending.descending);
1147        assert_eq!(descending.column_name, "created_at");
1148    }
1149
1150    #[test]
1151    fn entity_column_resolves_back_to_column_metadata() {
1152        let column = EntityColumn::<User>::new("email", "email");
1153
1154        assert_eq!(column.rust_field(), "email");
1155        assert_eq!(column.column_name(), "email");
1156        assert_eq!(column.entity_metadata().table, "users");
1157        assert_eq!(column.metadata(), &USER_COLUMNS[2]);
1158    }
1159
1160    #[test]
1161    fn sql_value_and_row_contract_support_basic_mapping() {
1162        let row = TestRow {
1163            values: BTreeMap::from([
1164                ("id", SqlValue::I64(7)),
1165                ("email", SqlValue::String("ana@example.com".to_string())),
1166            ]),
1167        };
1168
1169        let record = UserRecord::from_row(&row).expect("row mapping should succeed");
1170
1171        assert_eq!(
1172            record,
1173            UserRecord {
1174                id: 7,
1175                email: "ana@example.com".to_string(),
1176            }
1177        );
1178    }
1179
1180    #[test]
1181    fn insertable_and_changeset_return_column_values() {
1182        let insert = NewUser {
1183            email: "ana@example.com".to_string(),
1184        };
1185        let changes = UpdateUser {
1186            email: Some("ana.maria@example.com".to_string()),
1187        };
1188
1189        assert_eq!(
1190            insert.values(),
1191            vec![ColumnValue::new(
1192                "email",
1193                SqlValue::String("ana@example.com".to_string())
1194            )]
1195        );
1196        assert_eq!(
1197            changes.changes(),
1198            vec![ColumnValue::new(
1199                "email",
1200                SqlValue::String("ana.maria@example.com".to_string())
1201            )]
1202        );
1203        assert!(UpdateUser { email: None }.changes().is_empty());
1204    }
1205
1206    #[test]
1207    fn sql_type_mapping_exposes_default_sqlserver_conventions() {
1208        assert_eq!(String::SQL_SERVER_TYPE, SqlServerType::NVarChar);
1209        assert_eq!(String::DEFAULT_MAX_LENGTH, Some(255));
1210        assert_eq!(bool::SQL_SERVER_TYPE, SqlServerType::Bit);
1211        assert_eq!(i32::SQL_SERVER_TYPE, SqlServerType::Int);
1212        assert_eq!(i64::SQL_SERVER_TYPE, SqlServerType::BigInt);
1213        assert_eq!(Uuid::SQL_SERVER_TYPE, SqlServerType::UniqueIdentifier);
1214        assert_eq!(NaiveDateTime::SQL_SERVER_TYPE, SqlServerType::DateTime2);
1215        assert_eq!(Decimal::SQL_SERVER_TYPE, SqlServerType::Decimal);
1216        assert_eq!(Decimal::DEFAULT_PRECISION, Some(18));
1217        assert_eq!(Decimal::DEFAULT_SCALE, Some(2));
1218        assert_eq!(Vec::<u8>::SQL_SERVER_TYPE, SqlServerType::VarBinary);
1219        assert_eq!(Option::<String>::SQL_SERVER_TYPE, SqlServerType::NVarChar);
1220        assert_eq!(Option::<String>::DEFAULT_MAX_LENGTH, Some(255));
1221    }
1222
1223    #[test]
1224    fn sql_type_mapping_roundtrips_supported_values() {
1225        let uuid = Uuid::nil();
1226        let date = NaiveDate::from_ymd_opt(2026, 4, 21).expect("valid date");
1227        let datetime = date.and_hms_opt(14, 30, 0).expect("valid datetime");
1228        let decimal = Decimal::new(12345, 2);
1229
1230        assert_eq!(bool::from_sql_value(true.to_sql_value()), Ok(true));
1231        assert_eq!(i32::from_sql_value(42_i32.to_sql_value()), Ok(42));
1232        assert_eq!(i64::from_sql_value(99_i64.to_sql_value()), Ok(99));
1233        assert_eq!(f64::from_sql_value(10.5_f64.to_sql_value()), Ok(10.5));
1234        assert_eq!(
1235            String::from_sql_value("ana@example.com".to_string().to_sql_value()),
1236            Ok("ana@example.com".to_string())
1237        );
1238        assert_eq!(
1239            Vec::<u8>::from_sql_value(vec![1_u8, 2, 3].to_sql_value()),
1240            Ok(vec![1, 2, 3])
1241        );
1242        assert_eq!(Uuid::from_sql_value(uuid.to_sql_value()), Ok(uuid));
1243        assert_eq!(Decimal::from_sql_value(decimal.to_sql_value()), Ok(decimal));
1244        assert_eq!(NaiveDate::from_sql_value(date.to_sql_value()), Ok(date));
1245        assert_eq!(
1246            NaiveDateTime::from_sql_value(datetime.to_sql_value()),
1247            Ok(datetime)
1248        );
1249        assert_eq!(
1250            Option::<i64>::None.to_sql_value(),
1251            SqlValue::TypedNull(SqlServerType::BigInt)
1252        );
1253        assert_eq!(Option::<String>::from_sql_value(SqlValue::Null), Ok(None));
1254        assert_eq!(
1255            Option::<i64>::from_sql_value(SqlValue::TypedNull(SqlServerType::BigInt)),
1256            Ok(None)
1257        );
1258        assert_eq!(
1259            Option::<String>::from_sql_value(SqlValue::String("ana".to_string())),
1260            Ok(Some("ana".to_string()))
1261        );
1262    }
1263}