Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion sea-orm-codegen/src/entity/base_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use quote::quote;
use sea_query::ColumnType;

use crate::{
Column, ColumnOption, ConjunctRelation, PrimaryKey, Relation, util::escape_rust_keyword,
Column, ColumnOption, ConjunctRelation, PrimaryKey, Relation, entity::column::oxide_range,
util::escape_rust_keyword,
};

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -56,6 +57,14 @@ impl Entity {
.collect()
}

pub fn get_oxide_column_rs_types(&self, opt: &ColumnOption) -> Vec<TokenStream> {
self.columns
.clone()
.into_iter()
.map(|col| col.get_oxide_rs_type(opt))
.collect()
}

pub fn get_column_defs(&self) -> Vec<TokenStream> {
self.columns
.clone()
Expand Down Expand Up @@ -276,6 +285,23 @@ impl Entity {
.map_or(quote! {, Eq}, |_| quote! {})
}

/// As `get_eq_needed`, but also rules out the range element types that are
/// not `Eq`. The oxide format renders ranges as `PgRange` rather than
/// `String`, so a model struct can carry a field that only implements
/// `PartialEq`.
pub fn get_oxide_eq_needed(&self) -> TokenStream {
let has_non_eq_range = self
.columns
.iter()
.filter_map(|column| oxide_range(&column.col_type))
.any(|range| !range.element_is_eq());

match has_non_eq_range {
true => quote! {},
false => self.get_eq_needed(),
}
}

pub fn get_column_serde_attributes(
&self,
serde_skip_deserializing_primary_key: bool,
Expand Down
81 changes: 81 additions & 0 deletions sea-orm-codegen/src/entity/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ impl Column {
}
}

/// The oxide format decodes rows straight into the model struct with
/// `sqlx::FromRow`, so every field has to name a type sqlx can decode from
/// that column's Postgres type. `get_rs_type` renders ranges as `String`,
/// which is only correct for the standard format, where
/// `get_col_type_attrs` pairs it with `select_as = "text"`.
///
/// Range fields are always optional. `PgRange` has no serde support, so
/// `get_oxide_col_type_attrs` skips these fields, and skipping a field
/// requires it to implement `Default` to deserialize — which `Option`
/// provides and `PgRange` does not.
pub fn get_oxide_rs_type(&self, opt: &ColumnOption) -> TokenStream {
let Some(range) = oxide_range(&self.col_type) else {
return self.get_rs_type(opt);
};
let element: TokenStream = range.element_rs_type(opt).parse().unwrap();
quote! { Option<sqlx::postgres::types::PgRange<#element>> }
}

pub fn get_col_type_attrs(&self) -> Option<TokenStream> {
let col_type = match &self.col_type {
ColumnType::Float => Some("Float".to_owned()),
Expand All @@ -133,6 +151,11 @@ impl Column {
}

pub fn get_oxide_col_type_attrs(&self) -> Option<TokenStream> {
if oxide_range(&self.col_type).is_some() {
// sqlx's PgRange implements neither Serialize nor Deserialize.
return quote! { #[serde(skip)] }.into();
}

if !matches!(self.col_type, ColumnType::TimestampWithTimeZone) {
return None;
}
Expand Down Expand Up @@ -323,6 +346,64 @@ impl From<&ColumnDef> for Column {
}
}


/// A Postgres range type. sea-schema surfaces these as `ColumnType::Custom`,
/// since sea-query has no range variant of its own.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OxideRange {
Int4,
Int8,
Num,
Date,
Ts,
TsTz,
}

impl OxideRange {
/// The type sqlx decodes the range's bounds into.
fn element_rs_type(self, opt: &ColumnOption) -> String {
match self {
Self::Int4 => "i32".to_owned(),
Self::Int8 => "i64".to_owned(),
Self::Num => "sqlx::types::BigDecimal".to_owned(),
Self::Date => match opt.date_time_crate {
DateTimeCrate::Chrono => "chrono::NaiveDate".to_owned(),
DateTimeCrate::Time => "time::Date".to_owned(),
},
Self::Ts => match opt.date_time_crate {
DateTimeCrate::Chrono => "chrono::NaiveDateTime".to_owned(),
DateTimeCrate::Time => "time::PrimitiveDateTime".to_owned(),
},
Self::TsTz => match opt.date_time_crate {
DateTimeCrate::Chrono => "chrono::DateTime<chrono::Utc>".to_owned(),
DateTimeCrate::Time => "time::OffsetDateTime".to_owned(),
},
}
}

/// Whether the element type implements `Eq`, and so whether a model struct
/// holding this range can derive it. `BigDecimal` implements only
/// `PartialEq`; every other element type here is `Eq`.
pub fn element_is_eq(self) -> bool {
!matches!(self, Self::Num)
}
}

pub fn oxide_range(col_type: &ColumnType) -> Option<OxideRange> {
let ColumnType::Custom(iden) = col_type else {
return None;
};
match iden.to_string().as_str() {
"int4range" => Some(OxideRange::Int4),
"int8range" => Some(OxideRange::Int8),
"numrange" => Some(OxideRange::Num),
"daterange" => Some(OxideRange::Date),
"tsrange" => Some(OxideRange::Ts),
"tstzrange" => Some(OxideRange::TsTz),
_ => None,
}
}

#[cfg(test)]
mod tests {
use crate::{Column, ColumnOption, DateTimeCrate};
Expand Down
95 changes: 91 additions & 4 deletions sea-orm-codegen/src/entity/writer/oxide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ impl EntityWriter {
.parse()
.unwrap();
let column_names_snake_case = entity.get_column_names_snake_case();
let column_rs_types = entity.get_column_rs_types(column_option);
let if_eq_needed = entity.get_eq_needed();
let column_rs_types = entity.get_oxide_column_rs_types(column_option);
let if_eq_needed = entity.get_oxide_eq_needed();

let primary_keys: Vec<String> = entity
.primary_keys
Expand Down Expand Up @@ -227,8 +227,12 @@ impl EntityWriter {

#[cfg(test)]
mod tests {
use crate::{Column, Entity, EntityWriter};
use sea_query::{ColumnType, RcOrArc};
use crate::{Column, ColumnOption, DateTimeCrate, Entity, EntityWriter};
use sea_query::{Alias, ColumnType, IntoIden, RcOrArc};

fn range_column(name: &str, range: &str) -> Column {
column(name, ColumnType::Custom(Alias::new(range).into_iden()))
}

fn column(name: &str, col_type: ColumnType) -> Column {
Column {
Expand Down Expand Up @@ -283,4 +287,87 @@ mod tests {
]);
assert!(EntityWriter::gen_import_uuid(&entity).is_empty());
}

#[test]
fn range_columns_are_rendered_as_pg_range() {
let opt = ColumnOption::default();
for (range, element) in [
("int4range", "i32"),
("int8range", "i64"),
("numrange", "sqlx :: types :: BigDecimal"),
("daterange", "chrono :: NaiveDate"),
("tsrange", "chrono :: NaiveDateTime"),
("tstzrange", "chrono :: DateTime < chrono :: Utc >"),
] {
assert_eq!(
range_column("r", range).get_oxide_rs_type(&opt).to_string(),
format!("Option < sqlx :: postgres :: types :: PgRange < {element} >>"),
"unexpected type for {range}"
);
}
}

#[test]
fn temporal_range_columns_follow_the_date_time_crate() {
let opt = ColumnOption {
date_time_crate: DateTimeCrate::Time,
..Default::default()
};
assert_eq!(
range_column("r", "tstzrange")
.get_oxide_rs_type(&opt)
.to_string(),
"Option < sqlx :: postgres :: types :: PgRange < time :: OffsetDateTime >>"
);
}

#[test]
fn range_columns_are_optional_even_when_not_null() {
let mut col = range_column("r", "numrange");
col.not_null = true;
assert!(
col.get_oxide_rs_type(&ColumnOption::default())
.to_string()
.starts_with("Option <"),
"PgRange has no Default, so a skipped field has to be optional"
);
}

#[test]
fn other_custom_columns_are_untouched() {
let opt = ColumnOption::default();
assert_eq!(
range_column("t", "tsvector").get_oxide_rs_type(&opt).to_string(),
"String"
);
}

#[test]
fn range_columns_are_skipped_by_serde() {
assert_eq!(
range_column("r", "numrange")
.get_oxide_col_type_attrs()
.expect("expected a serde attribute")
.to_string(),
"# [serde (skip)]"
);
}

#[test]
fn numrange_suppresses_the_eq_derive() {
let entity = entity(vec![
column("id", ColumnType::BigInteger),
range_column("r", "numrange"),
]);
assert!(entity.get_oxide_eq_needed().is_empty());
}

#[test]
fn ranges_with_eq_elements_keep_the_eq_derive() {
let entity = entity(vec![
column("id", ColumnType::BigInteger),
range_column("r", "int8range"),
]);
assert_eq!(entity.get_oxide_eq_needed().to_string(), ", Eq");
}
}