use sea_query::Value as SeaValue;
use serde_json::Value as JsonValue;
use crate::orm::SqlType;
#[derive(Debug)]
pub enum WriteError {
RequiredFieldMissing { field: String },
BlankNotAllowed { field: String },
ForeignKeyNotFound {
field: String,
target_table: String,
value: serde_json::Value,
},
UniqueViolation {
field: Option<String>,
value: Option<serde_json::Value>,
},
NotNullViolation { field: Option<String> },
CheckViolation { constraint: Option<String> },
ForeignKeyViolation { field: Option<String> },
Multiple { errors: Vec<WriteError> },
TypeMismatch {
field: String,
expected: SqlType,
got: String,
},
Validator { field: String, message: String },
NotAnObject,
SerializeFailed(serde_json::Error),
Sqlx(sqlx::Error),
UnknownColumn { field: String },
}
impl WriteError {
pub fn field_errors(&self) -> std::collections::BTreeMap<String, Vec<String>> {
let mut out: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
self.collect_field_errors(&mut out);
out
}
fn collect_field_errors(&self, out: &mut std::collections::BTreeMap<String, Vec<String>>) {
use WriteError::*;
match self {
RequiredFieldMissing { field } => {
out.entry(field.clone())
.or_default()
.push("This field is required.".to_string());
}
BlankNotAllowed { field } => {
out.entry(field.clone())
.or_default()
.push("This field cannot be blank.".to_string());
}
ForeignKeyNotFound {
field,
target_table,
value,
} => {
let value_repr = repr_json_value(value);
out.insert(
field.clone(),
vec![format!(
"Referenced {target_table} row with id={value_repr} not found."
)],
);
}
UniqueViolation {
field: Some(col),
value,
} => {
let value_repr = value.as_ref().map(repr_json_value);
let msg = match value_repr {
Some(v) => format!("A row with {col}={v} already exists."),
None => "A row with this value already exists.".to_string(),
};
out.insert(col.clone(), vec![msg]);
}
NotNullViolation { field: Some(col) } => {
out.entry(col.clone())
.or_default()
.push("This field is required.".to_string());
}
ForeignKeyViolation { field: Some(col) } => {
out.insert(
col.clone(),
vec!["Referenced row does not exist.".to_string()],
);
}
TypeMismatch {
field,
expected,
got,
} => {
out.entry(field.clone())
.or_default()
.push(format!("Expected `{expected:?}`, got `{got}`."));
}
Validator { field, message } => {
if !field.is_empty() {
out.entry(field.clone()).or_default().push(message.clone());
}
}
UnknownColumn { field } => {
out.entry(field.clone())
.or_default()
.push(format!("Unknown column `{field}` on this model."));
}
Multiple { errors } => {
for e in errors {
e.collect_field_errors(out);
}
}
_ => {
}
}
}
pub fn non_field_errors(&self) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
self.collect_non_field_errors(&mut out);
out
}
fn collect_non_field_errors(&self, out: &mut Vec<String>) {
use WriteError::*;
match self {
UniqueViolation { field: None, .. } => {
out.push("A row with one or more of these values already exists.".into());
}
NotNullViolation { field: None } => {
out.push("A required field is missing.".into());
}
ForeignKeyViolation { field: None } => {
out.push("One or more foreign-key fields reference rows that don't exist.".into());
}
CheckViolation { constraint } => {
let msg = match constraint {
Some(c) => format!("Check constraint `{c}` failed."),
None => "A check constraint failed.".to_string(),
};
out.push(msg);
}
Validator { field, message } if field.is_empty() => {
out.push(message.clone());
}
Multiple { errors } => {
for e in errors {
e.collect_non_field_errors(out);
}
}
_ => {}
}
}
pub fn code(&self) -> &'static str {
use WriteError::*;
match self {
RequiredFieldMissing { .. } | BlankNotAllowed { .. } | NotNullViolation { .. } => {
"required_field"
}
ForeignKeyNotFound { .. } | ForeignKeyViolation { .. } => "fk_constraint",
UniqueViolation { .. } => "unique_constraint",
CheckViolation { .. } => "check_constraint",
TypeMismatch { .. } => "type_mismatch",
Validator { .. } => "validator_failed",
Multiple { .. } => "validation_error",
UnknownColumn { .. } => "unknown_column",
NotAnObject => "not_an_object",
SerializeFailed(_) => "serialize_failed",
Sqlx(_) => "database_error",
}
}
pub fn is_validation(&self) -> bool {
use WriteError::*;
!matches!(self, Sqlx(_) | SerializeFailed(_) | NotAnObject)
}
}
fn repr_json_value(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => format!("'{s}'"),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => "null".to_string(),
_ => serde_json::to_string(v).unwrap_or_else(|_| "(?)".to_string()),
}
}
impl std::fmt::Display for WriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WriteError::RequiredFieldMissing { field } => write!(
f,
"umbral::orm::write: required field `{field}` is missing or null"
),
WriteError::BlankNotAllowed { field } => {
write!(f, "umbral::orm::write: field `{field}` cannot be blank")
}
WriteError::ForeignKeyNotFound {
field,
target_table,
value,
} => write!(
f,
"umbral::orm::write: field `{field}` references `{target_table}` row with id={} which does not exist",
repr_json_value(value),
),
WriteError::UniqueViolation { field, value } => match (field, value) {
(Some(f_), Some(v)) => write!(
f,
"umbral::orm::write: unique constraint on `{f_}`={} violated",
repr_json_value(v),
),
(Some(f_), None) => {
write!(f, "umbral::orm::write: unique constraint on `{f_}` violated")
}
_ => write!(f, "umbral::orm::write: unique constraint violated"),
},
WriteError::NotNullViolation { field } => match field {
Some(f_) => write!(f, "umbral::orm::write: NOT NULL on `{f_}` violated"),
None => write!(f, "umbral::orm::write: NOT NULL violation"),
},
WriteError::CheckViolation { constraint } => match constraint {
Some(c) => write!(f, "umbral::orm::write: CHECK `{c}` violated"),
None => write!(f, "umbral::orm::write: CHECK constraint violated"),
},
WriteError::ForeignKeyViolation { field } => match field {
Some(f_) => write!(
f,
"umbral::orm::write: foreign-key constraint on `{f_}` violated"
),
None => write!(f, "umbral::orm::write: foreign-key constraint violated"),
},
WriteError::Multiple { errors } => {
write!(f, "umbral::orm::write: {} validation error(s)", errors.len())
}
WriteError::TypeMismatch {
field,
expected,
got,
} => write!(
f,
"umbral::orm::write: field `{field}` expected `{expected:?}`, got `{got}`",
),
WriteError::Validator { field, message } => {
write!(f, "umbral::orm::write: field `{field}` {message}")
}
WriteError::NotAnObject => write!(
f,
"umbral::orm::write: model didn't serialize to a JSON object — make sure your struct uses a flat field layout",
),
WriteError::SerializeFailed(e) => write!(f, "umbral::orm::write: serialize: {e}"),
WriteError::Sqlx(e) => write!(f, "umbral::orm::write: sqlx: {e}"),
WriteError::UnknownColumn { field } => {
write!(f, "umbral::orm::write: unknown column `{field}` on model")
}
}
}
}
impl std::error::Error for WriteError {}
impl From<sqlx::Error> for WriteError {
fn from(e: sqlx::Error) -> Self {
Self::Sqlx(e)
}
}
impl From<serde_json::Error> for WriteError {
fn from(e: serde_json::Error) -> Self {
Self::SerializeFailed(e)
}
}
pub fn json_to_sea_value(
sql_type: SqlType,
value: &JsonValue,
nullable: bool,
field_name: &str,
fk_target_pk: Option<SqlType>,
) -> Result<SeaValue, WriteError> {
if value.is_null() {
if !nullable {
return Err(WriteError::RequiredFieldMissing {
field: field_name.to_string(),
});
}
return Ok(null_for(sql_type));
}
match sql_type {
SqlType::Boolean => coerce_bool(value, field_name),
SqlType::SmallInt | SqlType::Integer => {
coerce_i32(value, field_name).map(|v| SeaValue::Int(Some(v)))
}
SqlType::BigInt => coerce_i64(value, field_name).map(|v| SeaValue::BigInt(Some(v))),
SqlType::ForeignKey => match fk_target_pk {
Some(SqlType::Text) => {
coerce_string(value, field_name).map(|s| SeaValue::String(Some(Box::new(s))))
}
Some(SqlType::Uuid) => match value {
JsonValue::String(s) => uuid::Uuid::parse_str(s)
.map(|u| SeaValue::Uuid(Some(Box::new(u))))
.map_err(|_| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Uuid,
got: s.clone(),
}),
_ => coerce_i64(value, field_name).map(|v| SeaValue::BigInt(Some(v))),
},
_ => coerce_i64(value, field_name).map(|v| SeaValue::BigInt(Some(v))),
},
SqlType::Real => coerce_f32(value, field_name).map(|v| SeaValue::Float(Some(v))),
SqlType::Double => coerce_f64(value, field_name).map(|v| SeaValue::Double(Some(v))),
SqlType::Text => {
coerce_string(value, field_name).map(|s| SeaValue::String(Some(Box::new(s))))
}
SqlType::Date => {
let s = coerce_string(value, field_name)?;
let d = chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
WriteError::TypeMismatch {
field: field_name.to_string(),
expected: sql_type,
got: format!("{value:?}"),
}
})?;
Ok(SeaValue::ChronoDate(Some(Box::new(d))))
}
SqlType::Time => {
let s = coerce_string(value, field_name)?;
let t = chrono::NaiveTime::parse_from_str(&s, "%H:%M:%S")
.or_else(|_| chrono::NaiveTime::parse_from_str(&s, "%H:%M"))
.map_err(|_| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: sql_type,
got: format!("{value:?}"),
})?;
Ok(SeaValue::ChronoTime(Some(Box::new(t))))
}
SqlType::Timestamptz => {
let s = coerce_string(value, field_name)?;
let dt = chrono::DateTime::parse_from_rfc3339(&s)
.map(|d| d.with_timezone(&chrono::Utc))
.or_else(|_| {
chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S")
.or_else(|_| chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M"))
.map(|naive| {
crate::timezone::naive_local_to_utc(naive)
.unwrap_or_else(|| naive.and_utc())
})
})
.map_err(|_| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: sql_type,
got: format!("{value:?}"),
})?;
Ok(SeaValue::ChronoDateTimeUtc(Some(Box::new(dt))))
}
SqlType::Uuid => {
let s = coerce_string(value, field_name)?;
let u = uuid::Uuid::parse_str(&s).map_err(|_| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: sql_type,
got: format!("{value:?}"),
})?;
Ok(SeaValue::Uuid(Some(Box::new(u))))
}
SqlType::Json => {
Ok(SeaValue::Json(Some(Box::new(value.clone()))))
}
SqlType::Array(_)
| SqlType::Inet
| SqlType::Cidr
| SqlType::MacAddr
| SqlType::Xml
| SqlType::Ltree
| SqlType::Bit
| SqlType::FullText => Ok(SeaValue::String(Some(Box::new(coerce_string(
value, field_name,
)?)))),
SqlType::Bytes => {
coerce_bytes(value, field_name).map(|b| SeaValue::Bytes(Some(Box::new(b))))
}
SqlType::Decimal => coerce_decimal(value, field_name),
}
}
fn coerce_decimal(value: &JsonValue, field_name: &str) -> Result<SeaValue, WriteError> {
use std::str::FromStr;
let parsed: Option<rust_decimal::Decimal> = match value {
JsonValue::String(s) => rust_decimal::Decimal::from_str(s).ok(),
JsonValue::Number(n) => rust_decimal::Decimal::from_str(&n.to_string()).ok(),
_ => None,
};
parsed
.map(|d| SeaValue::Decimal(Some(Box::new(d))))
.ok_or_else(|| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Decimal,
got: format!("{value:?}"),
})
}
fn coerce_bytes(value: &JsonValue, field_name: &str) -> Result<Vec<u8>, WriteError> {
if let Some(arr) = value.as_array() {
let mut out = Vec::with_capacity(arr.len());
for v in arr {
let n = v.as_u64().ok_or_else(|| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Bytes,
got: format!("{v:?}"),
})?;
if n > 255 {
return Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Bytes,
got: format!("element {v} out of u8 range"),
});
}
out.push(n as u8);
}
return Ok(out);
}
if let Some(s) = value.as_str() {
if s.len() % 2 != 0 {
return Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Bytes,
got: "hex string has odd length".to_string(),
});
}
let mut out = Vec::with_capacity(s.len() / 2);
for chunk in s.as_bytes().chunks(2) {
let high = hex_nibble(chunk[0]).ok_or_else(|| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Bytes,
got: format!("non-hex char `{}`", chunk[0] as char),
})?;
let low = hex_nibble(chunk[1]).ok_or_else(|| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Bytes,
got: format!("non-hex char `{}`", chunk[1] as char),
})?;
out.push((high << 4) | low);
}
return Ok(out);
}
Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Bytes,
got: format!("{value:?}"),
})
}
fn hex_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(10 + b - b'a'),
b'A'..=b'F' => Some(10 + b - b'A'),
_ => None,
}
}
pub fn slugify(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut last_was_dash = true; for c in s.chars() {
if c.is_ascii_alphanumeric() {
for low in c.to_lowercase() {
out.push(low);
}
last_was_dash = false;
} else if !last_was_dash {
out.push('-');
last_was_dash = true;
}
}
while out.ends_with('-') {
out.pop();
}
out
}
pub fn apply_slug_from(
fields: &[crate::migrate::Column],
body: &mut serde_json::Map<String, serde_json::Value>,
is_update: bool,
) {
for col in fields {
let Some(source) = col.slug_from.as_deref() else {
continue;
};
let explicit = body
.get(&col.name)
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false);
if explicit {
continue;
}
let source_value = body.get(source).and_then(|v| v.as_str()).unwrap_or("");
if source_value.is_empty() {
continue;
}
if is_update && !body.contains_key(source) {
continue;
}
let slug = slugify(source_value);
if slug.is_empty() {
continue;
}
body.insert(col.name.clone(), serde_json::Value::String(slug));
}
}
pub fn now_for_column(sql_type: SqlType) -> SeaValue {
let now = chrono::Utc::now();
match sql_type {
SqlType::Timestamptz => SeaValue::ChronoDateTimeUtc(Some(Box::new(now))),
SqlType::Date => SeaValue::ChronoDate(Some(Box::new(now.date_naive()))),
SqlType::Time => SeaValue::ChronoTime(Some(Box::new(now.time()))),
_ => null_for(sql_type),
}
}
pub(crate) fn null_for(sql_type: SqlType) -> SeaValue {
match sql_type {
SqlType::Boolean => SeaValue::Bool(None),
SqlType::SmallInt | SqlType::Integer => SeaValue::Int(None),
SqlType::BigInt | SqlType::ForeignKey => SeaValue::BigInt(None),
SqlType::Real => SeaValue::Float(None),
SqlType::Double => SeaValue::Double(None),
SqlType::Text => SeaValue::String(None),
SqlType::Json => SeaValue::Json(None),
SqlType::Date => SeaValue::ChronoDate(None),
SqlType::Time => SeaValue::ChronoTime(None),
SqlType::Timestamptz => SeaValue::ChronoDateTimeUtc(None),
SqlType::Uuid => SeaValue::Uuid(None),
SqlType::Array(_)
| SqlType::Inet
| SqlType::Cidr
| SqlType::MacAddr
| SqlType::Xml
| SqlType::Ltree
| SqlType::Bit
| SqlType::FullText => SeaValue::String(None),
SqlType::Bytes => SeaValue::Bytes(None),
SqlType::Decimal => SeaValue::Decimal(None),
}
}
fn coerce_bool(value: &JsonValue, field_name: &str) -> Result<SeaValue, WriteError> {
match value {
JsonValue::Bool(b) => Ok(SeaValue::Bool(Some(*b))),
JsonValue::String(s) => match s.as_str() {
"true" | "1" | "yes" | "on" => Ok(SeaValue::Bool(Some(true))),
"false" | "0" | "no" | "off" | "" => Ok(SeaValue::Bool(Some(false))),
_ => Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Boolean,
got: format!("{value:?}"),
}),
},
JsonValue::Number(n) => Ok(SeaValue::Bool(Some(n.as_i64() != Some(0)))),
_ => Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Boolean,
got: format!("{value:?}"),
}),
}
}
fn coerce_i32(value: &JsonValue, field_name: &str) -> Result<i32, WriteError> {
match value {
JsonValue::Number(n) => n
.as_i64()
.and_then(|i| i32::try_from(i).ok())
.ok_or_else(|| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Integer,
got: format!("{value:?}"),
}),
JsonValue::String(s) => s.parse::<i32>().map_err(|_| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Integer,
got: s.clone(),
}),
_ => Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Integer,
got: format!("{value:?}"),
}),
}
}
fn coerce_i64(value: &JsonValue, field_name: &str) -> Result<i64, WriteError> {
match value {
JsonValue::Number(n) => n.as_i64().ok_or_else(|| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::BigInt,
got: format!("{value:?}"),
}),
JsonValue::String(s) => s.parse::<i64>().map_err(|_| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::BigInt,
got: s.clone(),
}),
_ => Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::BigInt,
got: format!("{value:?}"),
}),
}
}
fn coerce_f32(value: &JsonValue, field_name: &str) -> Result<f32, WriteError> {
coerce_f64(value, field_name).map(|v| v as f32)
}
fn coerce_f64(value: &JsonValue, field_name: &str) -> Result<f64, WriteError> {
match value {
JsonValue::Number(n) => n.as_f64().ok_or_else(|| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Double,
got: format!("{value:?}"),
}),
JsonValue::String(s) => s.parse::<f64>().map_err(|_| WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Double,
got: s.clone(),
}),
_ => Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Double,
got: format!("{value:?}"),
}),
}
}
fn coerce_string(value: &JsonValue, field_name: &str) -> Result<String, WriteError> {
match value {
JsonValue::String(s) => Ok(s.clone()),
JsonValue::Number(n) => Ok(n.to_string()),
JsonValue::Bool(b) => Ok(b.to_string()),
_ => Err(WriteError::TypeMismatch {
field: field_name.to_string(),
expected: SqlType::Text,
got: format!("{value:?}"),
}),
}
}
#[derive(Debug)]
pub enum SaveError {
NoPrimaryKey,
Write(WriteError),
}
impl std::fmt::Display for SaveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SaveError::NoPrimaryKey => write!(
f,
"umbral::orm::save: model has no primary key — cannot determine INSERT vs UPDATE"
),
SaveError::Write(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for SaveError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
SaveError::Write(e) => Some(e),
_ => None,
}
}
}
impl From<WriteError> for SaveError {
fn from(e: WriteError) -> Self {
Self::Write(e)
}
}
pub fn is_default_pk(sql_type: SqlType, value: &JsonValue) -> bool {
match (sql_type, value) {
(SqlType::SmallInt | SqlType::Integer | SqlType::BigInt, JsonValue::Number(n)) => {
n.as_i64() == Some(0) || n.as_u64() == Some(0)
}
(SqlType::Uuid, JsonValue::String(s)) => {
s == "00000000-0000-0000-0000-000000000000" || s.is_empty()
}
(SqlType::Text, JsonValue::String(s)) => s.is_empty(),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn json_to_sea_value_passes_basic_types() {
let v = json_to_sea_value(SqlType::Integer, &json!(42), false, "x", None).unwrap();
assert!(matches!(v, SeaValue::Int(Some(42))));
let v = json_to_sea_value(SqlType::BigInt, &json!(42), false, "x", None).unwrap();
assert!(matches!(v, SeaValue::BigInt(Some(42))));
let v = json_to_sea_value(SqlType::Text, &json!("hi"), false, "x", None).unwrap();
assert!(matches!(v, SeaValue::String(Some(_))));
let v = json_to_sea_value(SqlType::Boolean, &json!(true), false, "x", None).unwrap();
assert!(matches!(v, SeaValue::Bool(Some(true))));
let v =
json_to_sea_value(SqlType::Json, &json!({ "nested": true }), false, "x", None).unwrap();
assert!(matches!(v, SeaValue::Json(Some(_))));
}
#[test]
fn json_to_sea_value_coerces_string_booleans() {
let v = json_to_sea_value(SqlType::Boolean, &json!("true"), false, "x", None).unwrap();
assert!(matches!(v, SeaValue::Bool(Some(true))));
let v = json_to_sea_value(SqlType::Boolean, &json!("0"), false, "x", None).unwrap();
assert!(matches!(v, SeaValue::Bool(Some(false))));
}
#[test]
fn json_to_sea_value_rejects_null_on_required_field() {
let err = json_to_sea_value(SqlType::Integer, &json!(null), false, "x", None).unwrap_err();
assert!(matches!(err, WriteError::RequiredFieldMissing { .. }));
}
#[test]
fn json_to_sea_value_accepts_null_on_nullable_field() {
let v = json_to_sea_value(SqlType::Integer, &json!(null), true, "x", None).unwrap();
assert!(matches!(v, SeaValue::Int(None)));
let v = json_to_sea_value(SqlType::Json, &json!(null), true, "x", None).unwrap();
assert!(matches!(v, SeaValue::Json(None)));
}
#[test]
fn json_to_sea_value_accepts_datetime_local_form_shape() {
let v = json_to_sea_value(
SqlType::Timestamptz,
&json!("2026-06-03T22:24:00Z"),
false,
"x",
None,
)
.unwrap();
let SeaValue::ChronoDateTimeUtc(Some(dt)) = v else {
panic!("expected ChronoDateTimeUtc");
};
assert_eq!(dt.to_rfc3339(), "2026-06-03T22:24:00+00:00");
let v = json_to_sea_value(
SqlType::Timestamptz,
&json!("2026-06-03T22:24:00"),
false,
"x",
None,
)
.unwrap();
let SeaValue::ChronoDateTimeUtc(Some(dt)) = v else {
panic!("expected ChronoDateTimeUtc");
};
assert_eq!(dt.to_rfc3339(), "2026-06-03T22:24:00+00:00");
let v = json_to_sea_value(
SqlType::Timestamptz,
&json!("2026-06-03T22:24"),
false,
"x",
None,
)
.unwrap();
let SeaValue::ChronoDateTimeUtc(Some(dt)) = v else {
panic!("expected ChronoDateTimeUtc");
};
assert_eq!(dt.to_rfc3339(), "2026-06-03T22:24:00+00:00");
let err = json_to_sea_value(SqlType::Timestamptz, &json!("not a date"), false, "x", None)
.unwrap_err();
assert!(matches!(err, WriteError::TypeMismatch { .. }));
}
#[test]
fn is_default_pk_recognises_zero_int_and_nil_uuid() {
assert!(is_default_pk(SqlType::Integer, &json!(0)));
assert!(is_default_pk(SqlType::BigInt, &json!(0)));
assert!(!is_default_pk(SqlType::BigInt, &json!(42)));
assert!(is_default_pk(
SqlType::Uuid,
&json!("00000000-0000-0000-0000-000000000000")
));
assert!(!is_default_pk(SqlType::Uuid, &json!("not-zero")));
}
}