1use 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#[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#[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
229pub fn quote_sql_string_literal(value: &str) -> String {
235 format!("N'{}'", value.replace('\'', "''"))
236}
237
238#[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
250pub trait Entity: Sized + Send + Sync + 'static {
252 fn metadata() -> &'static EntityMetadata;
253}
254
255#[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
268pub 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
308pub 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#[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#[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
358pub 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
376pub trait FromRow: Sized {
378 fn from_row<R: Row>(row: &R) -> Result<Self, OrmError>;
379}
380
381pub trait Insertable<E: Entity> {
383 fn values(&self) -> Vec<ColumnValue>;
384}
385
386pub 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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
743pub enum ReferentialAction {
744 NoAction,
745 Cascade,
746 SetNull,
747 SetDefault,
748}
749
750#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
794pub enum NavigationKind {
795 BelongsTo,
796 HasOne,
797 HasMany,
798 ManyToMany,
799}
800
801#[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#[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}