Skip to main content

ormdantic_core/
lib.rs

1//! Shared primitives used by Ormdantic's Rust crates.
2//!
3//! ```
4//! use ormdantic_core::{BackendFeature, FeatureSet, Identifier, QualifiedName};
5//!
6//! let table = Identifier::new("coffee")?;
7//! assert_eq!(table.as_str(), "coffee");
8//! assert!(Identifier::new("not-a-valid-identifier").is_err());
9//!
10//! let qualified = QualifiedName::with_schema("public", "coffee")?;
11//! assert_eq!(qualified.to_string(), "public.coffee");
12//!
13//! let features = FeatureSet::new([BackendFeature::Returning, BackendFeature::Returning]);
14//! assert_eq!(features.features(), &[BackendFeature::Returning]);
15//!
16//! # Ok::<(), ormdantic_core::OrmdanticError>(())
17//! ```
18
19use std::fmt::{Display, Formatter};
20
21pub type OrmdanticResult<T> = Result<T, OrmdanticError>;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct TableId(pub usize);
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct ColumnId(pub usize);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct RelationshipId(pub usize);
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct Identifier(String);
34
35impl Identifier {
36    pub fn new(value: impl Into<String>) -> OrmdanticResult<Self> {
37        let value = value.into();
38        if is_valid_identifier(&value) {
39            Ok(Self(value))
40        } else {
41            Err(OrmdanticError::InvalidIdentifier { identifier: value })
42        }
43    }
44
45    pub fn unchecked(value: impl Into<String>) -> Self {
46        Self(value.into())
47    }
48
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52
53    pub fn into_string(self) -> String {
54        self.0
55    }
56}
57
58impl Display for Identifier {
59    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
60        formatter.write_str(&self.0)
61    }
62}
63
64impl From<Identifier> for String {
65    fn from(value: Identifier) -> Self {
66        value.0
67    }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub struct QualifiedName {
72    schema: Option<Identifier>,
73    name: Identifier,
74}
75
76impl QualifiedName {
77    pub fn new(name: impl Into<String>) -> OrmdanticResult<Self> {
78        Ok(Self {
79            schema: None,
80            name: Identifier::new(name)?,
81        })
82    }
83
84    pub fn with_schema(
85        schema: impl Into<String>,
86        name: impl Into<String>,
87    ) -> OrmdanticResult<Self> {
88        Ok(Self {
89            schema: Some(Identifier::new(schema)?),
90            name: Identifier::new(name)?,
91        })
92    }
93
94    pub fn unchecked(schema: Option<impl Into<String>>, name: impl Into<String>) -> Self {
95        Self {
96            schema: schema.map(Identifier::unchecked),
97            name: Identifier::unchecked(name),
98        }
99    }
100
101    pub fn schema(&self) -> Option<&Identifier> {
102        self.schema.as_ref()
103    }
104
105    pub fn name(&self) -> &Identifier {
106        &self.name
107    }
108}
109
110impl Display for QualifiedName {
111    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
112        if let Some(schema) = &self.schema {
113            write!(formatter, "{schema}.{}", self.name)
114        } else {
115            write!(formatter, "{}", self.name)
116        }
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub enum BackendFeature {
122    Ctes,
123    Windows,
124    Savepoints,
125    TransactionalDdl,
126    PartialIndexes,
127    ExpressionIndexes,
128    NativeEnum,
129    NativeJson,
130    NativeUuid,
131    AlterColumn,
132    Returning,
133}
134
135#[derive(Debug, Clone, Default, PartialEq, Eq)]
136pub struct FeatureSet {
137    features: Vec<BackendFeature>,
138}
139
140impl FeatureSet {
141    pub fn new(features: impl IntoIterator<Item = BackendFeature>) -> Self {
142        let mut features = features.into_iter().collect::<Vec<_>>();
143        features.sort_by_key(|feature| *feature as u8);
144        features.dedup();
145        Self { features }
146    }
147
148    pub fn contains(&self, feature: BackendFeature) -> bool {
149        self.features.contains(&feature)
150    }
151
152    pub fn insert(&mut self, feature: BackendFeature) {
153        if !self.contains(feature) {
154            self.features.push(feature);
155            self.features.sort_by_key(|feature| *feature as u8);
156        }
157    }
158
159    pub fn features(&self) -> &[BackendFeature] {
160        &self.features
161    }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165pub enum IsolationLevel {
166    ReadUncommitted,
167    ReadCommitted,
168    RepeatableRead,
169    Serializable,
170    Snapshot,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub enum TransactionAccessMode {
175    ReadWrite,
176    ReadOnly,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180pub enum DeferrableMode {
181    Deferrable,
182    NotDeferrable,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct TransactionOptions {
187    isolation_level: Option<IsolationLevel>,
188    access_mode: TransactionAccessMode,
189    deferrable_mode: Option<DeferrableMode>,
190}
191
192impl Default for TransactionOptions {
193    fn default() -> Self {
194        Self {
195            isolation_level: None,
196            access_mode: TransactionAccessMode::ReadWrite,
197            deferrable_mode: None,
198        }
199    }
200}
201
202impl TransactionOptions {
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    pub fn with_isolation_level(mut self, isolation_level: IsolationLevel) -> Self {
208        self.isolation_level = Some(isolation_level);
209        self
210    }
211
212    pub fn read_only(mut self) -> Self {
213        self.access_mode = TransactionAccessMode::ReadOnly;
214        self
215    }
216
217    pub fn with_access_mode(mut self, access_mode: TransactionAccessMode) -> Self {
218        self.access_mode = access_mode;
219        self
220    }
221
222    pub fn with_deferrable_mode(mut self, deferrable_mode: DeferrableMode) -> Self {
223        self.deferrable_mode = Some(deferrable_mode);
224        self
225    }
226
227    pub fn isolation_level(&self) -> Option<IsolationLevel> {
228        self.isolation_level
229    }
230
231    pub fn access_mode(&self) -> TransactionAccessMode {
232        self.access_mode
233    }
234
235    pub fn deferrable_mode(&self) -> Option<DeferrableMode> {
236        self.deferrable_mode
237    }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Hash)]
241pub struct SavepointName(Identifier);
242
243impl SavepointName {
244    pub fn new(name: impl Into<String>) -> OrmdanticResult<Self> {
245        Ok(Self(Identifier::new(name)?))
246    }
247
248    pub fn as_str(&self) -> &str {
249        self.0.as_str()
250    }
251}
252
253impl Display for SavepointName {
254    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
255        Display::fmt(&self.0, formatter)
256    }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260pub enum EventKind {
261    BeforeExecute,
262    AfterExecute,
263    BeforeCommit,
264    AfterCommit,
265    AfterRollback,
266    BeforeFlush,
267    AfterFlush,
268    BeforeMigration,
269    AfterMigration,
270    BeforeReflection,
271    AfterReflection,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct EventPayload {
276    kind: EventKind,
277    target: Option<String>,
278    message: Option<String>,
279}
280
281impl EventPayload {
282    pub fn new(kind: EventKind) -> Self {
283        Self {
284            kind,
285            target: None,
286            message: None,
287        }
288    }
289
290    pub fn with_target(mut self, target: impl Into<String>) -> Self {
291        self.target = Some(target.into());
292        self
293    }
294
295    pub fn with_message(mut self, message: impl Into<String>) -> Self {
296        self.message = Some(message.into());
297        self
298    }
299
300    pub fn kind(&self) -> EventKind {
301        self.kind
302    }
303
304    pub fn target(&self) -> Option<&str> {
305        self.target.as_deref()
306    }
307
308    pub fn message(&self) -> Option<&str> {
309        self.message.as_deref()
310    }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Hash)]
314pub struct IdentityKey {
315    model_key: String,
316    primary_key: Vec<String>,
317}
318
319impl IdentityKey {
320    pub fn new(model_key: impl Into<String>, primary_key: Vec<String>) -> Self {
321        Self {
322            model_key: model_key.into(),
323            primary_key,
324        }
325    }
326
327    pub fn model_key(&self) -> &str {
328        &self.model_key
329    }
330
331    pub fn primary_key(&self) -> &[String] {
332        &self.primary_key
333    }
334}
335
336#[derive(Debug, Clone, PartialEq, Eq, Hash)]
337pub struct RevisionId(String);
338
339impl RevisionId {
340    pub fn new(value: impl Into<String>) -> OrmdanticResult<Self> {
341        let value = value.into();
342        if value.trim().is_empty() {
343            Err(OrmdanticError::MigrationError {
344                message: "revision id cannot be empty".to_string(),
345            })
346        } else {
347            Ok(Self(value))
348        }
349    }
350
351    pub fn as_str(&self) -> &str {
352        &self.0
353    }
354}
355
356impl Display for RevisionId {
357    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
358        formatter.write_str(&self.0)
359    }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
363pub enum ExecutionErrorKind {
364    Connection,
365    Syntax,
366    UniqueViolation,
367    ForeignKeyViolation,
368    NotNullViolation,
369    CheckViolation,
370    SerializationFailure,
371    Timeout,
372    PermissionDenied,
373    Unknown,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
377pub enum OrmdanticError {
378    MissingPrimaryKeyAlias {
379        tablename: String,
380        primary_key: String,
381    },
382    DuplicateTable {
383        tablename: String,
384    },
385    DuplicateColumn {
386        tablename: String,
387        column: String,
388    },
389    MissingPrimaryKey {
390        tablename: String,
391        primary_key: String,
392    },
393    UnknownTable {
394        tablename: String,
395    },
396    InvalidRelationship {
397        table: String,
398        field: String,
399        target_table: String,
400    },
401    UnsupportedDialect {
402        dialect: String,
403    },
404    InvalidIdentifier {
405        identifier: String,
406    },
407    UnsupportedFeature {
408        feature: String,
409        dialect: String,
410    },
411    TransactionError {
412        message: String,
413    },
414    ReflectionError {
415        message: String,
416    },
417    MigrationError {
418        message: String,
419    },
420    SchemaDiffError {
421        message: String,
422    },
423    UnitOfWorkError {
424        message: String,
425    },
426    EventError {
427        message: String,
428    },
429    ExecutionError {
430        kind: ExecutionErrorKind,
431        message: String,
432    },
433    SqlCompile {
434        message: String,
435    },
436}
437
438impl Display for OrmdanticError {
439    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
440        match self {
441            Self::MissingPrimaryKeyAlias {
442                tablename,
443                primary_key,
444            } => write!(
445                formatter,
446                "primary key column '{tablename}\\{primary_key}' was not found"
447            ),
448            Self::DuplicateTable { tablename } => {
449                write!(formatter, "table '{tablename}' is already registered")
450            }
451            Self::DuplicateColumn { tablename, column } => {
452                write!(
453                    formatter,
454                    "column '{tablename}.{column}' is already registered"
455                )
456            }
457            Self::MissingPrimaryKey {
458                tablename,
459                primary_key,
460            } => write!(
461                formatter,
462                "table '{tablename}' does not define primary key column '{primary_key}'"
463            ),
464            Self::UnknownTable { tablename } => {
465                write!(formatter, "table '{tablename}' is not registered")
466            }
467            Self::InvalidRelationship {
468                table,
469                field,
470                target_table,
471            } => write!(
472                formatter,
473                "relationship '{table}.{field}' targets unknown table '{target_table}'"
474            ),
475            Self::UnsupportedDialect { dialect } => {
476                write!(formatter, "dialect '{dialect}' is not supported")
477            }
478            Self::InvalidIdentifier { identifier } => {
479                write!(formatter, "identifier '{identifier}' is not valid")
480            }
481            Self::UnsupportedFeature { feature, dialect } => {
482                write!(
483                    formatter,
484                    "feature '{feature}' is not supported by dialect '{dialect}'"
485                )
486            }
487            Self::TransactionError { message } => write!(formatter, "transaction error: {message}"),
488            Self::ReflectionError { message } => write!(formatter, "reflection error: {message}"),
489            Self::MigrationError { message } => write!(formatter, "migration error: {message}"),
490            Self::SchemaDiffError { message } => write!(formatter, "schema diff error: {message}"),
491            Self::UnitOfWorkError { message } => write!(formatter, "unit of work error: {message}"),
492            Self::EventError { message } => write!(formatter, "event error: {message}"),
493            Self::ExecutionError { message, .. } => write!(formatter, "execution error: {message}"),
494            Self::SqlCompile { message } => write!(formatter, "{message}"),
495        }
496    }
497}
498
499impl std::error::Error for OrmdanticError {}
500
501fn is_valid_identifier(value: &str) -> bool {
502    let mut chars = value.chars();
503    let Some(first) = chars.next() else {
504        return false;
505    };
506    (first == '_' || first.is_ascii_alphabetic())
507        && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
508}
509
510#[cfg(test)]
511mod tests {
512    use super::{
513        BackendFeature, FeatureSet, Identifier, IsolationLevel, SavepointName, TransactionOptions,
514    };
515
516    #[test]
517    fn validates_identifiers() {
518        assert_eq!(
519            Identifier::new("valid_name").unwrap().as_str(),
520            "valid_name"
521        );
522        assert!(Identifier::new("1_invalid").is_err());
523    }
524
525    #[test]
526    fn stores_backend_features() {
527        let mut features = FeatureSet::new([BackendFeature::Ctes]);
528        features.insert(BackendFeature::Savepoints);
529
530        assert!(features.contains(BackendFeature::Ctes));
531        assert!(features.contains(BackendFeature::Savepoints));
532    }
533
534    #[test]
535    fn builds_transaction_options_and_savepoints() {
536        let options = TransactionOptions::new().with_isolation_level(IsolationLevel::Serializable);
537        let savepoint = SavepointName::new("sp_1").unwrap();
538
539        assert_eq!(
540            options.isolation_level(),
541            Some(IsolationLevel::Serializable)
542        );
543        assert_eq!(savepoint.as_str(), "sp_1");
544    }
545}