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