1use sea_query::Value as SeaValue;
37use serde_json::Value as JsonValue;
38
39use crate::orm::SqlType;
40
41#[derive(Debug)]
48pub enum WriteError {
49 RequiredFieldMissing { field: String },
52 BlankNotAllowed { field: String },
56 ForeignKeyNotFound {
61 field: String,
62 target_table: String,
63 value: serde_json::Value,
64 },
65 UniqueViolation {
71 field: Option<String>,
72 value: Option<serde_json::Value>,
73 },
74 NotNullViolation { field: Option<String> },
77 CheckViolation { constraint: Option<String> },
81 ForeignKeyViolation { field: Option<String> },
85 Multiple { errors: Vec<WriteError> },
89 TypeMismatch {
92 field: String,
93 expected: SqlType,
94 got: String,
95 },
96 Validator { field: String, message: String },
99 NotAnObject,
102 SerializeFailed(serde_json::Error),
106 Sqlx(sqlx::Error),
108 UnknownColumn { field: String },
111}
112
113impl WriteError {
114 pub fn field_errors(&self) -> std::collections::BTreeMap<String, Vec<String>> {
121 let mut out: std::collections::BTreeMap<String, Vec<String>> =
122 std::collections::BTreeMap::new();
123 self.collect_field_errors(&mut out);
124 out
125 }
126
127 fn collect_field_errors(&self, out: &mut std::collections::BTreeMap<String, Vec<String>>) {
128 use WriteError::*;
129 match self {
130 RequiredFieldMissing { field } => {
131 out.entry(field.clone())
132 .or_default()
133 .push("This field is required.".to_string());
134 }
135 BlankNotAllowed { field } => {
136 out.entry(field.clone())
137 .or_default()
138 .push("This field cannot be blank.".to_string());
139 }
140 ForeignKeyNotFound {
141 field,
142 target_table,
143 value,
144 } => {
145 let value_repr = repr_json_value(value);
146 out.insert(
147 field.clone(),
148 vec![format!(
149 "Referenced {target_table} row with id={value_repr} not found."
150 )],
151 );
152 }
153 UniqueViolation {
154 field: Some(col),
155 value,
156 } => {
157 let value_repr = value.as_ref().map(repr_json_value);
158 let msg = match value_repr {
159 Some(v) => format!("A row with {col}={v} already exists."),
160 None => "A row with this value already exists.".to_string(),
161 };
162 out.insert(col.clone(), vec![msg]);
163 }
164 NotNullViolation { field: Some(col) } => {
165 out.entry(col.clone())
166 .or_default()
167 .push("This field is required.".to_string());
168 }
169 ForeignKeyViolation { field: Some(col) } => {
170 out.insert(
171 col.clone(),
172 vec!["Referenced row does not exist.".to_string()],
173 );
174 }
175 TypeMismatch {
176 field,
177 expected,
178 got,
179 } => {
180 out.entry(field.clone())
181 .or_default()
182 .push(format!("Expected `{expected:?}`, got `{got}`."));
183 }
184 Validator { field, message } => {
185 if !field.is_empty() {
192 out.entry(field.clone()).or_default().push(message.clone());
193 }
194 }
195 UnknownColumn { field } => {
196 out.entry(field.clone())
197 .or_default()
198 .push(format!("Unknown column `{field}` on this model."));
199 }
200 Multiple { errors } => {
201 for e in errors {
202 e.collect_field_errors(out);
203 }
204 }
205 _ => {
206 }
211 }
212 }
213
214 pub fn non_field_errors(&self) -> Vec<String> {
218 let mut out: Vec<String> = Vec::new();
219 self.collect_non_field_errors(&mut out);
220 out
221 }
222
223 fn collect_non_field_errors(&self, out: &mut Vec<String>) {
224 use WriteError::*;
225 match self {
226 UniqueViolation { field: None, .. } => {
227 out.push("A row with one or more of these values already exists.".into());
228 }
229 NotNullViolation { field: None } => {
230 out.push("A required field is missing.".into());
231 }
232 ForeignKeyViolation { field: None } => {
233 out.push("One or more foreign-key fields reference rows that don't exist.".into());
234 }
235 CheckViolation { constraint } => {
236 let msg = match constraint {
237 Some(c) => format!("Check constraint `{c}` failed."),
238 None => "A check constraint failed.".to_string(),
239 };
240 out.push(msg);
241 }
242 Validator { field, message } if field.is_empty() => {
245 out.push(message.clone());
246 }
247 Multiple { errors } => {
248 for e in errors {
249 e.collect_non_field_errors(out);
250 }
251 }
252 _ => {}
253 }
254 }
255
256 pub fn code(&self) -> &'static str {
260 use WriteError::*;
261 match self {
262 RequiredFieldMissing { .. } | BlankNotAllowed { .. } | NotNullViolation { .. } => {
263 "required_field"
264 }
265 ForeignKeyNotFound { .. } | ForeignKeyViolation { .. } => "fk_constraint",
266 UniqueViolation { .. } => "unique_constraint",
267 CheckViolation { .. } => "check_constraint",
268 TypeMismatch { .. } => "type_mismatch",
269 Validator { .. } => "validator_failed",
270 Multiple { .. } => "validation_error",
271 UnknownColumn { .. } => "unknown_column",
272 NotAnObject => "not_an_object",
273 SerializeFailed(_) => "serialize_failed",
274 Sqlx(_) => "database_error",
275 }
276 }
277
278 pub fn is_validation(&self) -> bool {
283 use WriteError::*;
284 !matches!(self, Sqlx(_) | SerializeFailed(_) | NotAnObject)
285 }
286}
287
288fn repr_json_value(v: &serde_json::Value) -> String {
292 match v {
293 serde_json::Value::String(s) => format!("'{s}'"),
294 serde_json::Value::Number(n) => n.to_string(),
295 serde_json::Value::Bool(b) => b.to_string(),
296 serde_json::Value::Null => "null".to_string(),
297 _ => serde_json::to_string(v).unwrap_or_else(|_| "(?)".to_string()),
298 }
299}
300
301impl std::fmt::Display for WriteError {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 match self {
304 WriteError::RequiredFieldMissing { field } => write!(
305 f,
306 "umbral::orm::write: required field `{field}` is missing or null"
307 ),
308 WriteError::BlankNotAllowed { field } => {
309 write!(f, "umbral::orm::write: field `{field}` cannot be blank")
310 }
311 WriteError::ForeignKeyNotFound {
312 field,
313 target_table,
314 value,
315 } => write!(
316 f,
317 "umbral::orm::write: field `{field}` references `{target_table}` row with id={} which does not exist",
318 repr_json_value(value),
319 ),
320 WriteError::UniqueViolation { field, value } => match (field, value) {
321 (Some(f_), Some(v)) => write!(
322 f,
323 "umbral::orm::write: unique constraint on `{f_}`={} violated",
324 repr_json_value(v),
325 ),
326 (Some(f_), None) => {
327 write!(f, "umbral::orm::write: unique constraint on `{f_}` violated")
328 }
329 _ => write!(f, "umbral::orm::write: unique constraint violated"),
330 },
331 WriteError::NotNullViolation { field } => match field {
332 Some(f_) => write!(f, "umbral::orm::write: NOT NULL on `{f_}` violated"),
333 None => write!(f, "umbral::orm::write: NOT NULL violation"),
334 },
335 WriteError::CheckViolation { constraint } => match constraint {
336 Some(c) => write!(f, "umbral::orm::write: CHECK `{c}` violated"),
337 None => write!(f, "umbral::orm::write: CHECK constraint violated"),
338 },
339 WriteError::ForeignKeyViolation { field } => match field {
340 Some(f_) => write!(
341 f,
342 "umbral::orm::write: foreign-key constraint on `{f_}` violated"
343 ),
344 None => write!(f, "umbral::orm::write: foreign-key constraint violated"),
345 },
346 WriteError::Multiple { errors } => {
347 write!(f, "umbral::orm::write: {} validation error(s)", errors.len())
348 }
349 WriteError::TypeMismatch {
350 field,
351 expected,
352 got,
353 } => write!(
354 f,
355 "umbral::orm::write: field `{field}` expected `{expected:?}`, got `{got}`",
356 ),
357 WriteError::Validator { field, message } => {
358 write!(f, "umbral::orm::write: field `{field}` {message}")
359 }
360 WriteError::NotAnObject => write!(
361 f,
362 "umbral::orm::write: model didn't serialize to a JSON object — make sure your struct uses a flat field layout",
363 ),
364 WriteError::SerializeFailed(e) => write!(f, "umbral::orm::write: serialize: {e}"),
365 WriteError::Sqlx(e) => write!(f, "umbral::orm::write: sqlx: {e}"),
366 WriteError::UnknownColumn { field } => {
367 write!(f, "umbral::orm::write: unknown column `{field}` on model")
368 }
369 }
370 }
371}
372
373impl std::error::Error for WriteError {}
374
375impl From<sqlx::Error> for WriteError {
376 fn from(e: sqlx::Error) -> Self {
377 Self::Sqlx(e)
378 }
379}
380
381impl From<serde_json::Error> for WriteError {
382 fn from(e: serde_json::Error) -> Self {
383 Self::SerializeFailed(e)
384 }
385}
386
387pub fn json_to_sea_value(
409 sql_type: SqlType,
410 value: &JsonValue,
411 nullable: bool,
412 field_name: &str,
413 fk_target_pk: Option<SqlType>,
414) -> Result<SeaValue, WriteError> {
415 if value.is_null() {
417 if !nullable {
418 return Err(WriteError::RequiredFieldMissing {
419 field: field_name.to_string(),
420 });
421 }
422 return Ok(null_for(sql_type));
423 }
424
425 match sql_type {
426 SqlType::Boolean => coerce_bool(value, field_name),
427 SqlType::SmallInt | SqlType::Integer => {
428 coerce_i32(value, field_name).map(|v| SeaValue::Int(Some(v)))
429 }
430 SqlType::BigInt => coerce_i64(value, field_name).map(|v| SeaValue::BigInt(Some(v))),
431 SqlType::ForeignKey => match fk_target_pk {
447 Some(SqlType::Text) => {
448 coerce_string(value, field_name).map(|s| SeaValue::String(Some(Box::new(s))))
449 }
450 Some(SqlType::Uuid) => match value {
451 JsonValue::String(s) => uuid::Uuid::parse_str(s)
452 .map(|u| SeaValue::Uuid(Some(Box::new(u))))
453 .map_err(|_| WriteError::TypeMismatch {
454 field: field_name.to_string(),
455 expected: SqlType::Uuid,
456 got: s.clone(),
457 }),
458 _ => coerce_i64(value, field_name).map(|v| SeaValue::BigInt(Some(v))),
459 },
460 _ => coerce_i64(value, field_name).map(|v| SeaValue::BigInt(Some(v))),
461 },
462 SqlType::Real => coerce_f32(value, field_name).map(|v| SeaValue::Float(Some(v))),
463 SqlType::Double => coerce_f64(value, field_name).map(|v| SeaValue::Double(Some(v))),
464 SqlType::Text => {
465 coerce_string(value, field_name).map(|s| SeaValue::String(Some(Box::new(s))))
466 }
467 SqlType::Date => {
468 let s = coerce_string(value, field_name)?;
469 let d = chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
470 WriteError::TypeMismatch {
471 field: field_name.to_string(),
472 expected: sql_type,
473 got: format!("{value:?}"),
474 }
475 })?;
476 Ok(SeaValue::ChronoDate(Some(Box::new(d))))
477 }
478 SqlType::Time => {
479 let s = coerce_string(value, field_name)?;
480 let t = chrono::NaiveTime::parse_from_str(&s, "%H:%M:%S")
481 .or_else(|_| chrono::NaiveTime::parse_from_str(&s, "%H:%M"))
482 .map_err(|_| WriteError::TypeMismatch {
483 field: field_name.to_string(),
484 expected: sql_type,
485 got: format!("{value:?}"),
486 })?;
487 Ok(SeaValue::ChronoTime(Some(Box::new(t))))
488 }
489 SqlType::Timestamptz => {
490 let s = coerce_string(value, field_name)?;
491 let dt = chrono::DateTime::parse_from_rfc3339(&s)
507 .map(|d| d.with_timezone(&chrono::Utc))
508 .or_else(|_| {
509 chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S")
510 .or_else(|_| chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M"))
511 .map(|naive| {
512 crate::timezone::naive_local_to_utc(naive)
513 .unwrap_or_else(|| naive.and_utc())
514 })
515 })
516 .map_err(|_| WriteError::TypeMismatch {
517 field: field_name.to_string(),
518 expected: sql_type,
519 got: format!("{value:?}"),
520 })?;
521 Ok(SeaValue::ChronoDateTimeUtc(Some(Box::new(dt))))
522 }
523 SqlType::Uuid => {
524 let s = coerce_string(value, field_name)?;
525 let u = uuid::Uuid::parse_str(&s).map_err(|_| WriteError::TypeMismatch {
526 field: field_name.to_string(),
527 expected: sql_type,
528 got: format!("{value:?}"),
529 })?;
530 Ok(SeaValue::Uuid(Some(Box::new(u))))
531 }
532 SqlType::Json => {
533 Ok(SeaValue::Json(Some(Box::new(value.clone()))))
536 }
537 SqlType::Array(_)
542 | SqlType::Inet
543 | SqlType::Cidr
544 | SqlType::MacAddr
545 | SqlType::Xml
549 | SqlType::Ltree
550 | SqlType::Bit
551 | SqlType::FullText => Ok(SeaValue::String(Some(Box::new(coerce_string(
552 value, field_name,
553 )?)))),
554 SqlType::Bytes => {
560 coerce_bytes(value, field_name).map(|b| SeaValue::Bytes(Some(Box::new(b))))
561 }
562 SqlType::Decimal => coerce_decimal(value, field_name),
568 }
569}
570
571fn coerce_decimal(value: &JsonValue, field_name: &str) -> Result<SeaValue, WriteError> {
572 use std::str::FromStr;
573 let parsed: Option<rust_decimal::Decimal> = match value {
579 JsonValue::String(s) => rust_decimal::Decimal::from_str(s).ok(),
580 JsonValue::Number(n) => rust_decimal::Decimal::from_str(&n.to_string()).ok(),
581 _ => None,
582 };
583 parsed
584 .map(|d| SeaValue::Decimal(Some(Box::new(d))))
585 .ok_or_else(|| WriteError::TypeMismatch {
586 field: field_name.to_string(),
587 expected: SqlType::Decimal,
588 got: format!("{value:?}"),
589 })
590}
591
592fn coerce_bytes(value: &JsonValue, field_name: &str) -> Result<Vec<u8>, WriteError> {
596 if let Some(arr) = value.as_array() {
597 let mut out = Vec::with_capacity(arr.len());
598 for v in arr {
599 let n = v.as_u64().ok_or_else(|| WriteError::TypeMismatch {
600 field: field_name.to_string(),
601 expected: SqlType::Bytes,
602 got: format!("{v:?}"),
603 })?;
604 if n > 255 {
605 return Err(WriteError::TypeMismatch {
606 field: field_name.to_string(),
607 expected: SqlType::Bytes,
608 got: format!("element {v} out of u8 range"),
609 });
610 }
611 out.push(n as u8);
612 }
613 return Ok(out);
614 }
615 if let Some(s) = value.as_str() {
616 if s.len() % 2 != 0 {
617 return Err(WriteError::TypeMismatch {
618 field: field_name.to_string(),
619 expected: SqlType::Bytes,
620 got: "hex string has odd length".to_string(),
621 });
622 }
623 let mut out = Vec::with_capacity(s.len() / 2);
624 for chunk in s.as_bytes().chunks(2) {
625 let high = hex_nibble(chunk[0]).ok_or_else(|| WriteError::TypeMismatch {
626 field: field_name.to_string(),
627 expected: SqlType::Bytes,
628 got: format!("non-hex char `{}`", chunk[0] as char),
629 })?;
630 let low = hex_nibble(chunk[1]).ok_or_else(|| WriteError::TypeMismatch {
631 field: field_name.to_string(),
632 expected: SqlType::Bytes,
633 got: format!("non-hex char `{}`", chunk[1] as char),
634 })?;
635 out.push((high << 4) | low);
636 }
637 return Ok(out);
638 }
639 Err(WriteError::TypeMismatch {
640 field: field_name.to_string(),
641 expected: SqlType::Bytes,
642 got: format!("{value:?}"),
643 })
644}
645
646fn hex_nibble(b: u8) -> Option<u8> {
647 match b {
648 b'0'..=b'9' => Some(b - b'0'),
649 b'a'..=b'f' => Some(10 + b - b'a'),
650 b'A'..=b'F' => Some(10 + b - b'A'),
651 _ => None,
652 }
653}
654
655pub fn slugify(s: &str) -> String {
678 let mut out = String::with_capacity(s.len());
679 let mut last_was_dash = true; for c in s.chars() {
681 if c.is_ascii_alphanumeric() {
682 for low in c.to_lowercase() {
683 out.push(low);
684 }
685 last_was_dash = false;
686 } else if !last_was_dash {
687 out.push('-');
688 last_was_dash = true;
689 }
690 }
691 while out.ends_with('-') {
693 out.pop();
694 }
695 out
696}
697
698pub fn apply_slug_from(
708 fields: &[crate::migrate::Column],
709 body: &mut serde_json::Map<String, serde_json::Value>,
710 is_update: bool,
711) {
712 for col in fields {
713 let Some(source) = col.slug_from.as_deref() else {
714 continue;
715 };
716 let explicit = body
718 .get(&col.name)
719 .and_then(|v| v.as_str())
720 .map(|s| !s.is_empty())
721 .unwrap_or(false);
722 if explicit {
723 continue;
724 }
725 let source_value = body.get(source).and_then(|v| v.as_str()).unwrap_or("");
727 if source_value.is_empty() {
728 continue;
729 }
730 if is_update && !body.contains_key(source) {
731 continue;
732 }
733 let slug = slugify(source_value);
734 if slug.is_empty() {
735 continue;
736 }
737 body.insert(col.name.clone(), serde_json::Value::String(slug));
738 }
739}
740
741pub fn now_for_column(sql_type: SqlType) -> SeaValue {
742 let now = chrono::Utc::now();
743 match sql_type {
744 SqlType::Timestamptz => SeaValue::ChronoDateTimeUtc(Some(Box::new(now))),
745 SqlType::Date => SeaValue::ChronoDate(Some(Box::new(now.date_naive()))),
746 SqlType::Time => SeaValue::ChronoTime(Some(Box::new(now.time()))),
747 _ => null_for(sql_type),
748 }
749}
750
751pub(crate) fn null_for(sql_type: SqlType) -> SeaValue {
755 match sql_type {
756 SqlType::Boolean => SeaValue::Bool(None),
757 SqlType::SmallInt | SqlType::Integer => SeaValue::Int(None),
758 SqlType::BigInt | SqlType::ForeignKey => SeaValue::BigInt(None),
759 SqlType::Real => SeaValue::Float(None),
760 SqlType::Double => SeaValue::Double(None),
761 SqlType::Text => SeaValue::String(None),
762 SqlType::Json => SeaValue::Json(None),
763 SqlType::Date => SeaValue::ChronoDate(None),
764 SqlType::Time => SeaValue::ChronoTime(None),
765 SqlType::Timestamptz => SeaValue::ChronoDateTimeUtc(None),
766 SqlType::Uuid => SeaValue::Uuid(None),
767 SqlType::Array(_)
768 | SqlType::Inet
769 | SqlType::Cidr
770 | SqlType::MacAddr
771 | SqlType::Xml
772 | SqlType::Ltree
773 | SqlType::Bit
774 | SqlType::FullText => SeaValue::String(None),
775 SqlType::Bytes => SeaValue::Bytes(None),
776 SqlType::Decimal => SeaValue::Decimal(None),
777 }
778}
779
780fn coerce_bool(value: &JsonValue, field_name: &str) -> Result<SeaValue, WriteError> {
781 match value {
782 JsonValue::Bool(b) => Ok(SeaValue::Bool(Some(*b))),
783 JsonValue::String(s) => match s.as_str() {
784 "true" | "1" | "yes" | "on" => Ok(SeaValue::Bool(Some(true))),
785 "false" | "0" | "no" | "off" | "" => Ok(SeaValue::Bool(Some(false))),
786 _ => Err(WriteError::TypeMismatch {
787 field: field_name.to_string(),
788 expected: SqlType::Boolean,
789 got: format!("{value:?}"),
790 }),
791 },
792 JsonValue::Number(n) => Ok(SeaValue::Bool(Some(n.as_i64() != Some(0)))),
793 _ => Err(WriteError::TypeMismatch {
794 field: field_name.to_string(),
795 expected: SqlType::Boolean,
796 got: format!("{value:?}"),
797 }),
798 }
799}
800
801fn coerce_i32(value: &JsonValue, field_name: &str) -> Result<i32, WriteError> {
802 match value {
803 JsonValue::Number(n) => n
804 .as_i64()
805 .and_then(|i| i32::try_from(i).ok())
806 .ok_or_else(|| WriteError::TypeMismatch {
807 field: field_name.to_string(),
808 expected: SqlType::Integer,
809 got: format!("{value:?}"),
810 }),
811 JsonValue::String(s) => s.parse::<i32>().map_err(|_| WriteError::TypeMismatch {
812 field: field_name.to_string(),
813 expected: SqlType::Integer,
814 got: s.clone(),
815 }),
816 _ => Err(WriteError::TypeMismatch {
817 field: field_name.to_string(),
818 expected: SqlType::Integer,
819 got: format!("{value:?}"),
820 }),
821 }
822}
823
824fn coerce_i64(value: &JsonValue, field_name: &str) -> Result<i64, WriteError> {
825 match value {
826 JsonValue::Number(n) => n.as_i64().ok_or_else(|| WriteError::TypeMismatch {
827 field: field_name.to_string(),
828 expected: SqlType::BigInt,
829 got: format!("{value:?}"),
830 }),
831 JsonValue::String(s) => s.parse::<i64>().map_err(|_| WriteError::TypeMismatch {
832 field: field_name.to_string(),
833 expected: SqlType::BigInt,
834 got: s.clone(),
835 }),
836 _ => Err(WriteError::TypeMismatch {
837 field: field_name.to_string(),
838 expected: SqlType::BigInt,
839 got: format!("{value:?}"),
840 }),
841 }
842}
843
844fn coerce_f32(value: &JsonValue, field_name: &str) -> Result<f32, WriteError> {
845 coerce_f64(value, field_name).map(|v| v as f32)
846}
847
848fn coerce_f64(value: &JsonValue, field_name: &str) -> Result<f64, WriteError> {
849 match value {
850 JsonValue::Number(n) => n.as_f64().ok_or_else(|| WriteError::TypeMismatch {
851 field: field_name.to_string(),
852 expected: SqlType::Double,
853 got: format!("{value:?}"),
854 }),
855 JsonValue::String(s) => s.parse::<f64>().map_err(|_| WriteError::TypeMismatch {
856 field: field_name.to_string(),
857 expected: SqlType::Double,
858 got: s.clone(),
859 }),
860 _ => Err(WriteError::TypeMismatch {
861 field: field_name.to_string(),
862 expected: SqlType::Double,
863 got: format!("{value:?}"),
864 }),
865 }
866}
867
868fn coerce_string(value: &JsonValue, field_name: &str) -> Result<String, WriteError> {
869 match value {
870 JsonValue::String(s) => Ok(s.clone()),
871 JsonValue::Number(n) => Ok(n.to_string()),
872 JsonValue::Bool(b) => Ok(b.to_string()),
873 _ => Err(WriteError::TypeMismatch {
874 field: field_name.to_string(),
875 expected: SqlType::Text,
876 got: format!("{value:?}"),
877 }),
878 }
879}
880
881#[derive(Debug)]
887pub enum SaveError {
888 NoPrimaryKey,
892 Write(WriteError),
895}
896
897impl std::fmt::Display for SaveError {
898 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
899 match self {
900 SaveError::NoPrimaryKey => write!(
901 f,
902 "umbral::orm::save: model has no primary key — cannot determine INSERT vs UPDATE"
903 ),
904 SaveError::Write(e) => write!(f, "{e}"),
905 }
906 }
907}
908
909impl std::error::Error for SaveError {
910 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
911 match self {
912 SaveError::Write(e) => Some(e),
913 _ => None,
914 }
915 }
916}
917
918impl From<WriteError> for SaveError {
919 fn from(e: WriteError) -> Self {
920 Self::Write(e)
921 }
922}
923
924pub fn is_default_pk(sql_type: SqlType, value: &JsonValue) -> bool {
934 match (sql_type, value) {
935 (SqlType::SmallInt | SqlType::Integer | SqlType::BigInt, JsonValue::Number(n)) => {
936 n.as_i64() == Some(0) || n.as_u64() == Some(0)
937 }
938 (SqlType::Uuid, JsonValue::String(s)) => {
939 s == "00000000-0000-0000-0000-000000000000" || s.is_empty()
940 }
941 (SqlType::Text, JsonValue::String(s)) => s.is_empty(),
942 _ => false,
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use serde_json::json;
950
951 #[test]
952 fn json_to_sea_value_passes_basic_types() {
953 let v = json_to_sea_value(SqlType::Integer, &json!(42), false, "x", None).unwrap();
954 assert!(matches!(v, SeaValue::Int(Some(42))));
955 let v = json_to_sea_value(SqlType::BigInt, &json!(42), false, "x", None).unwrap();
956 assert!(matches!(v, SeaValue::BigInt(Some(42))));
957 let v = json_to_sea_value(SqlType::Text, &json!("hi"), false, "x", None).unwrap();
958 assert!(matches!(v, SeaValue::String(Some(_))));
959 let v = json_to_sea_value(SqlType::Boolean, &json!(true), false, "x", None).unwrap();
960 assert!(matches!(v, SeaValue::Bool(Some(true))));
961 let v =
962 json_to_sea_value(SqlType::Json, &json!({ "nested": true }), false, "x", None).unwrap();
963 assert!(matches!(v, SeaValue::Json(Some(_))));
964 }
965
966 #[test]
967 fn json_to_sea_value_coerces_string_booleans() {
968 let v = json_to_sea_value(SqlType::Boolean, &json!("true"), false, "x", None).unwrap();
969 assert!(matches!(v, SeaValue::Bool(Some(true))));
970 let v = json_to_sea_value(SqlType::Boolean, &json!("0"), false, "x", None).unwrap();
971 assert!(matches!(v, SeaValue::Bool(Some(false))));
972 }
973
974 #[test]
975 fn json_to_sea_value_rejects_null_on_required_field() {
976 let err = json_to_sea_value(SqlType::Integer, &json!(null), false, "x", None).unwrap_err();
977 assert!(matches!(err, WriteError::RequiredFieldMissing { .. }));
978 }
979
980 #[test]
981 fn json_to_sea_value_accepts_null_on_nullable_field() {
982 let v = json_to_sea_value(SqlType::Integer, &json!(null), true, "x", None).unwrap();
983 assert!(matches!(v, SeaValue::Int(None)));
984 let v = json_to_sea_value(SqlType::Json, &json!(null), true, "x", None).unwrap();
985 assert!(matches!(v, SeaValue::Json(None)));
986 }
987
988 #[test]
989 fn json_to_sea_value_accepts_datetime_local_form_shape() {
990 let v = json_to_sea_value(
992 SqlType::Timestamptz,
993 &json!("2026-06-03T22:24:00Z"),
994 false,
995 "x",
996 None,
997 )
998 .unwrap();
999 let SeaValue::ChronoDateTimeUtc(Some(dt)) = v else {
1000 panic!("expected ChronoDateTimeUtc");
1001 };
1002 assert_eq!(dt.to_rfc3339(), "2026-06-03T22:24:00+00:00");
1003
1004 let v = json_to_sea_value(
1006 SqlType::Timestamptz,
1007 &json!("2026-06-03T22:24:00"),
1008 false,
1009 "x",
1010 None,
1011 )
1012 .unwrap();
1013 let SeaValue::ChronoDateTimeUtc(Some(dt)) = v else {
1014 panic!("expected ChronoDateTimeUtc");
1015 };
1016 assert_eq!(dt.to_rfc3339(), "2026-06-03T22:24:00+00:00");
1017
1018 let v = json_to_sea_value(
1022 SqlType::Timestamptz,
1023 &json!("2026-06-03T22:24"),
1024 false,
1025 "x",
1026 None,
1027 )
1028 .unwrap();
1029 let SeaValue::ChronoDateTimeUtc(Some(dt)) = v else {
1030 panic!("expected ChronoDateTimeUtc");
1031 };
1032 assert_eq!(dt.to_rfc3339(), "2026-06-03T22:24:00+00:00");
1033
1034 let err = json_to_sea_value(SqlType::Timestamptz, &json!("not a date"), false, "x", None)
1036 .unwrap_err();
1037 assert!(matches!(err, WriteError::TypeMismatch { .. }));
1038 }
1039
1040 #[test]
1041 fn is_default_pk_recognises_zero_int_and_nil_uuid() {
1042 assert!(is_default_pk(SqlType::Integer, &json!(0)));
1043 assert!(is_default_pk(SqlType::BigInt, &json!(0)));
1044 assert!(!is_default_pk(SqlType::BigInt, &json!(42)));
1045 assert!(is_default_pk(
1046 SqlType::Uuid,
1047 &json!("00000000-0000-0000-0000-000000000000")
1048 ));
1049 assert!(!is_default_pk(SqlType::Uuid, &json!("not-zero")));
1050 }
1051}