Skip to main content

sql_orm_core/
lib.rs

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