1use 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}