Support array datatype in PostgreSQL (#1132)

* PostgreSQL array (draft)

* Fixup

* Fixup

* Fixup

* Fixup

* Fixup

* Refactoring

* generate entity for Postgres array fields

* Add tests

* Update Cargo.toml

Co-authored-by: Chris Tsang <chris.2y3@outlook.com>
This commit is contained in:
Billy Chan 2022-10-23 18:26:57 +08:00 committed by GitHub
parent 2757190ba4
commit b5b9790252
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 764 additions and 178 deletions

View File

@ -55,7 +55,7 @@ actix-rt = { version = "2.2.0" }
maplit = { version = "^1" }
rust_decimal_macros = { version = "^1" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sea-orm = { path = ".", features = ["mock", "debug-print", "tests-cfg"] }
sea-orm = { path = ".", features = ["mock", "debug-print", "tests-cfg", "postgres-array"] }
pretty_assertions = { version = "^0.7" }
time = { version = "^0.3", features = ["macros"] }
@ -76,6 +76,7 @@ with-chrono = ["chrono", "sea-query/with-chrono", "sea-query-binder?/with-chrono
with-rust_decimal = ["rust_decimal", "sea-query/with-rust_decimal", "sea-query-binder?/with-rust_decimal", "sqlx?/decimal"]
with-uuid = ["uuid", "sea-query/with-uuid", "sea-query-binder?/with-uuid", "sqlx?/uuid"]
with-time = ["time", "sea-query/with-time", "sea-query-binder?/with-time", "sqlx?/time"]
postgres-array = ["sea-query/postgres-array", "sea-query-binder?/postgres-array"]
sqlx-dep = []
sqlx-all = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite"]
sqlx-mysql = ["sqlx-dep", "sea-query-binder/sqlx-mysql", "sqlx/mysql"]

View File

@ -35,7 +35,7 @@ clap = { version = "^3.2", features = ["env", "derive"] }
dotenvy = { version = "^0.15", optional = true }
async-std = { version = "^1.9", features = [ "attributes", "tokio1" ], optional = true }
sea-orm-codegen = { version = "^0.10.0", path = "../sea-orm-codegen", optional = true }
sea-schema = { version = "^0.10.0" }
sea-schema = { version = "^0.10.1" }
sqlx = { version = "^0.6", default-features = false, features = [ "mysql", "postgres" ], optional = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = { version = "0.1" }

View File

@ -28,53 +28,59 @@ impl Column {
}
pub fn get_rs_type(&self, date_time_crate: &DateTimeCrate) -> TokenStream {
#[allow(unreachable_patterns)]
let ident: TokenStream = match &self.col_type {
ColumnType::Char(_)
| ColumnType::String(_)
| ColumnType::Text
| ColumnType::Custom(_) => "String".to_owned(),
ColumnType::TinyInteger(_) => "i8".to_owned(),
ColumnType::SmallInteger(_) => "i16".to_owned(),
ColumnType::Integer(_) => "i32".to_owned(),
ColumnType::BigInteger(_) => "i64".to_owned(),
ColumnType::TinyUnsigned(_) => "u8".to_owned(),
ColumnType::SmallUnsigned(_) => "u16".to_owned(),
ColumnType::Unsigned(_) => "u32".to_owned(),
ColumnType::BigUnsigned(_) => "u64".to_owned(),
ColumnType::Float(_) => "f32".to_owned(),
ColumnType::Double(_) => "f64".to_owned(),
ColumnType::Json | ColumnType::JsonBinary => "Json".to_owned(),
ColumnType::Date => match date_time_crate {
DateTimeCrate::Chrono => "Date".to_owned(),
DateTimeCrate::Time => "TimeDate".to_owned(),
},
ColumnType::Time(_) => match date_time_crate {
DateTimeCrate::Chrono => "Time".to_owned(),
DateTimeCrate::Time => "TimeTime".to_owned(),
},
ColumnType::DateTime(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTime".to_owned(),
DateTimeCrate::Time => "TimeDateTime".to_owned(),
},
ColumnType::Timestamp(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTimeUtc".to_owned(),
// ColumnType::Timpestamp(_) => time::PrimitiveDateTime: https://docs.rs/sqlx/0.3.5/sqlx/postgres/types/index.html#time
DateTimeCrate::Time => "TimeDateTime".to_owned(),
},
ColumnType::TimestampWithTimeZone(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTimeWithTimeZone".to_owned(),
DateTimeCrate::Time => "TimeDateTimeWithTimeZone".to_owned(),
},
ColumnType::Decimal(_) | ColumnType::Money(_) => "Decimal".to_owned(),
ColumnType::Uuid => "Uuid".to_owned(),
ColumnType::Binary(_) | ColumnType::VarBinary(_) => "Vec<u8>".to_owned(),
ColumnType::Boolean => "bool".to_owned(),
ColumnType::Enum { name, .. } => name.to_string().to_camel_case(),
_ => unimplemented!(),
fn write_rs_type(col_type: &ColumnType, date_time_crate: &DateTimeCrate) -> String {
#[allow(unreachable_patterns)]
match col_type {
ColumnType::Char(_)
| ColumnType::String(_)
| ColumnType::Text
| ColumnType::Custom(_) => "String".to_owned(),
ColumnType::TinyInteger(_) => "i8".to_owned(),
ColumnType::SmallInteger(_) => "i16".to_owned(),
ColumnType::Integer(_) => "i32".to_owned(),
ColumnType::BigInteger(_) => "i64".to_owned(),
ColumnType::TinyUnsigned(_) => "u8".to_owned(),
ColumnType::SmallUnsigned(_) => "u16".to_owned(),
ColumnType::Unsigned(_) => "u32".to_owned(),
ColumnType::BigUnsigned(_) => "u64".to_owned(),
ColumnType::Float(_) => "f32".to_owned(),
ColumnType::Double(_) => "f64".to_owned(),
ColumnType::Json | ColumnType::JsonBinary => "Json".to_owned(),
ColumnType::Date => match date_time_crate {
DateTimeCrate::Chrono => "Date".to_owned(),
DateTimeCrate::Time => "TimeDate".to_owned(),
},
ColumnType::Time(_) => match date_time_crate {
DateTimeCrate::Chrono => "Time".to_owned(),
DateTimeCrate::Time => "TimeTime".to_owned(),
},
ColumnType::DateTime(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTime".to_owned(),
DateTimeCrate::Time => "TimeDateTime".to_owned(),
},
ColumnType::Timestamp(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTimeUtc".to_owned(),
// ColumnType::Timpestamp(_) => time::PrimitiveDateTime: https://docs.rs/sqlx/0.3.5/sqlx/postgres/types/index.html#time
DateTimeCrate::Time => "TimeDateTime".to_owned(),
},
ColumnType::TimestampWithTimeZone(_) => match date_time_crate {
DateTimeCrate::Chrono => "DateTimeWithTimeZone".to_owned(),
DateTimeCrate::Time => "TimeDateTimeWithTimeZone".to_owned(),
},
ColumnType::Decimal(_) | ColumnType::Money(_) => "Decimal".to_owned(),
ColumnType::Uuid => "Uuid".to_owned(),
ColumnType::Binary(_) | ColumnType::VarBinary(_) => "Vec<u8>".to_owned(),
ColumnType::Boolean => "bool".to_owned(),
ColumnType::Enum { name, .. } => name.to_string().to_camel_case(),
ColumnType::Array(column_type) => {
format!("Vec<{}>", write_rs_type(column_type, date_time_crate))
}
_ => unimplemented!(),
}
}
.parse()
.unwrap();
let ident: TokenStream = write_rs_type(&self.col_type, date_time_crate)
.parse()
.unwrap();
match self.not_null {
true => quote! { #ident },
false => quote! { Option<#ident> },
@ -97,62 +103,72 @@ impl Column {
}
pub fn get_def(&self) -> TokenStream {
let mut col_def = match &self.col_type {
ColumnType::Char(s) => match s {
Some(s) => quote! { ColumnType::Char(Some(#s)).def() },
None => quote! { ColumnType::Char(None).def() },
},
ColumnType::String(s) => match s {
Some(s) => quote! { ColumnType::String(Some(#s)).def() },
None => quote! { ColumnType::String(None).def() },
},
ColumnType::Text => quote! { ColumnType::Text.def() },
ColumnType::TinyInteger(_) => quote! { ColumnType::TinyInteger.def() },
ColumnType::SmallInteger(_) => quote! { ColumnType::SmallInteger.def() },
ColumnType::Integer(_) => quote! { ColumnType::Integer.def() },
ColumnType::BigInteger(_) => quote! { ColumnType::BigInteger.def() },
ColumnType::TinyUnsigned(_) => quote! { ColumnType::TinyUnsigned.def() },
ColumnType::SmallUnsigned(_) => quote! { ColumnType::SmallUnsigned.def() },
ColumnType::Unsigned(_) => quote! { ColumnType::Unsigned.def() },
ColumnType::BigUnsigned(_) => quote! { ColumnType::BigUnsigned.def() },
ColumnType::Float(_) => quote! { ColumnType::Float.def() },
ColumnType::Double(_) => quote! { ColumnType::Double.def() },
ColumnType::Decimal(s) => match s {
Some((s1, s2)) => quote! { ColumnType::Decimal(Some((#s1, #s2))).def() },
None => quote! { ColumnType::Decimal(None).def() },
},
ColumnType::DateTime(_) => quote! { ColumnType::DateTime.def() },
ColumnType::Timestamp(_) => quote! { ColumnType::Timestamp.def() },
ColumnType::TimestampWithTimeZone(_) => {
quote! { ColumnType::TimestampWithTimeZone.def() }
fn write_col_def(col_type: &ColumnType) -> TokenStream {
match col_type {
ColumnType::Char(s) => match s {
Some(s) => quote! { ColumnType::Char(Some(#s)) },
None => quote! { ColumnType::Char(None) },
},
ColumnType::String(s) => match s {
Some(s) => quote! { ColumnType::String(Some(#s)) },
None => quote! { ColumnType::String(None) },
},
ColumnType::Text => quote! { ColumnType::Text },
ColumnType::TinyInteger(_) => quote! { ColumnType::TinyInteger },
ColumnType::SmallInteger(_) => quote! { ColumnType::SmallInteger },
ColumnType::Integer(_) => quote! { ColumnType::Integer },
ColumnType::BigInteger(_) => quote! { ColumnType::BigInteger },
ColumnType::TinyUnsigned(_) => quote! { ColumnType::TinyUnsigned },
ColumnType::SmallUnsigned(_) => quote! { ColumnType::SmallUnsigned },
ColumnType::Unsigned(_) => quote! { ColumnType::Unsigned },
ColumnType::BigUnsigned(_) => quote! { ColumnType::BigUnsigned },
ColumnType::Float(_) => quote! { ColumnType::Float },
ColumnType::Double(_) => quote! { ColumnType::Double },
ColumnType::Decimal(s) => match s {
Some((s1, s2)) => quote! { ColumnType::Decimal(Some((#s1, #s2))) },
None => quote! { ColumnType::Decimal(None) },
},
ColumnType::DateTime(_) => quote! { ColumnType::DateTime },
ColumnType::Timestamp(_) => quote! { ColumnType::Timestamp },
ColumnType::TimestampWithTimeZone(_) => {
quote! { ColumnType::TimestampWithTimeZone }
}
ColumnType::Time(_) => quote! { ColumnType::Time },
ColumnType::Date => quote! { ColumnType::Date },
ColumnType::Binary(BlobSize::Blob(_)) | ColumnType::VarBinary(_) => {
quote! { ColumnType::Binary }
}
ColumnType::Binary(BlobSize::Tiny) => quote! { ColumnType::TinyBinary },
ColumnType::Binary(BlobSize::Medium) => quote! { ColumnType::MediumBinary },
ColumnType::Binary(BlobSize::Long) => quote! { ColumnType::LongBinary },
ColumnType::Boolean => quote! { ColumnType::Boolean },
ColumnType::Money(s) => match s {
Some((s1, s2)) => quote! { ColumnType::Money(Some((#s1, #s2))) },
None => quote! { ColumnType::Money(None) },
},
ColumnType::Json => quote! { ColumnType::Json },
ColumnType::JsonBinary => quote! { ColumnType::JsonBinary },
ColumnType::Uuid => quote! { ColumnType::Uuid },
ColumnType::Custom(s) => {
let s = s.to_string();
quote! { ColumnType::Custom(#s.to_owned()) }
}
ColumnType::Enum { name, .. } => {
let enum_ident = format_ident!("{}", name.to_string().to_camel_case());
quote! { #enum_ident::db_type() }
}
ColumnType::Array(column_type) => {
let column_type = write_col_def(column_type);
quote! { ColumnType::Array(sea_orm::sea_query::SeaRc::new(#column_type)) }
}
#[allow(unreachable_patterns)]
_ => unimplemented!(),
}
ColumnType::Time(_) => quote! { ColumnType::Time.def() },
ColumnType::Date => quote! { ColumnType::Date.def() },
ColumnType::Binary(BlobSize::Blob(_)) | ColumnType::VarBinary(_) => {
quote! { ColumnType::Binary.def() }
}
ColumnType::Binary(BlobSize::Tiny) => quote! { ColumnType::TinyBinary.def() },
ColumnType::Binary(BlobSize::Medium) => quote! { ColumnType::MediumBinary.def() },
ColumnType::Binary(BlobSize::Long) => quote! { ColumnType::LongBinary.def() },
ColumnType::Boolean => quote! { ColumnType::Boolean.def() },
ColumnType::Money(s) => match s {
Some((s1, s2)) => quote! { ColumnType::Money(Some((#s1, #s2))).def() },
None => quote! { ColumnType::Money(None).def() },
},
ColumnType::Json => quote! { ColumnType::Json.def() },
ColumnType::JsonBinary => quote! { ColumnType::JsonBinary.def() },
ColumnType::Uuid => quote! { ColumnType::Uuid.def() },
ColumnType::Custom(s) => {
let s = s.to_string();
quote! { ColumnType::Custom(#s.to_owned()).def() }
}
ColumnType::Enum { name, .. } => {
let enum_ident = format_ident!("{}", name.to_string().to_camel_case());
quote! { #enum_ident::db_type() }
}
#[allow(unreachable_patterns)]
_ => unimplemented!(),
};
}
let mut col_def = write_col_def(&self.col_type);
col_def.extend(quote! {
.def()
});
if !self.not_null {
col_def.extend(quote! {
.null()

View File

@ -672,7 +672,7 @@ mod tests {
};
use pretty_assertions::assert_eq;
use proc_macro2::TokenStream;
use sea_query::{ColumnType, ForeignKeyAction};
use sea_query::{ColumnType, ForeignKeyAction, SeaRc};
use std::io::{self, BufRead, BufReader, Read};
fn setup() -> Vec<Entity> {
@ -1120,6 +1120,41 @@ mod tests {
name: "id".to_owned(),
}],
},
Entity {
table_name: "collection".to_owned(),
columns: vec![
Column {
name: "id".to_owned(),
col_type: ColumnType::Integer(Some(11)),
auto_increment: true,
not_null: true,
unique: false,
},
Column {
name: "integers".to_owned(),
col_type: ColumnType::Array(SeaRc::new(Box::new(ColumnType::Integer(
None,
)))),
auto_increment: false,
not_null: true,
unique: false,
},
Column {
name: "integers_opt".to_owned(),
col_type: ColumnType::Array(SeaRc::new(Box::new(ColumnType::Integer(
None,
)))),
auto_increment: false,
not_null: false,
unique: false,
},
],
relations: vec![],
conjunct_relations: vec![],
primary_keys: vec![PrimaryKey {
name: "id".to_owned(),
}],
},
]
}
@ -1144,7 +1179,7 @@ mod tests {
#[test]
fn test_gen_expanded_code_blocks() -> io::Result<()> {
let entities = setup();
const ENTITY_FILES: [&str; 8] = [
const ENTITY_FILES: [&str; 9] = [
include_str!("../../tests/expanded/cake.rs"),
include_str!("../../tests/expanded/cake_filling.rs"),
include_str!("../../tests/expanded/filling.rs"),
@ -1153,8 +1188,9 @@ mod tests {
include_str!("../../tests/expanded/rust_keyword.rs"),
include_str!("../../tests/expanded/cake_with_float.rs"),
include_str!("../../tests/expanded/cake_with_double.rs"),
include_str!("../../tests/expanded/collection.rs"),
];
const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 8] = [
const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 9] = [
include_str!("../../tests/expanded_with_schema_name/cake.rs"),
include_str!("../../tests/expanded_with_schema_name/cake_filling.rs"),
include_str!("../../tests/expanded_with_schema_name/filling.rs"),
@ -1163,6 +1199,7 @@ mod tests {
include_str!("../../tests/expanded_with_schema_name/rust_keyword.rs"),
include_str!("../../tests/expanded_with_schema_name/cake_with_float.rs"),
include_str!("../../tests/expanded_with_schema_name/cake_with_double.rs"),
include_str!("../../tests/expanded_with_schema_name/collection.rs"),
];
assert_eq!(entities.len(), ENTITY_FILES.len());
@ -1224,7 +1261,7 @@ mod tests {
#[test]
fn test_gen_compact_code_blocks() -> io::Result<()> {
let entities = setup();
const ENTITY_FILES: [&str; 8] = [
const ENTITY_FILES: [&str; 9] = [
include_str!("../../tests/compact/cake.rs"),
include_str!("../../tests/compact/cake_filling.rs"),
include_str!("../../tests/compact/filling.rs"),
@ -1233,8 +1270,9 @@ mod tests {
include_str!("../../tests/compact/rust_keyword.rs"),
include_str!("../../tests/compact/cake_with_float.rs"),
include_str!("../../tests/compact/cake_with_double.rs"),
include_str!("../../tests/compact/collection.rs"),
];
const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 8] = [
const ENTITY_FILES_WITH_SCHEMA_NAME: [&str; 9] = [
include_str!("../../tests/compact_with_schema_name/cake.rs"),
include_str!("../../tests/compact_with_schema_name/cake_filling.rs"),
include_str!("../../tests/compact_with_schema_name/filling.rs"),
@ -1243,6 +1281,7 @@ mod tests {
include_str!("../../tests/compact_with_schema_name/rust_keyword.rs"),
include_str!("../../tests/compact_with_schema_name/cake_with_float.rs"),
include_str!("../../tests/compact_with_schema_name/cake_with_double.rs"),
include_str!("../../tests/compact_with_schema_name/collection.rs"),
];
assert_eq!(entities.len(), ENTITY_FILES.len());

View File

@ -0,0 +1,17 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "collection")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub integers: Vec<i32> ,
pub integers_opt: Option<Vec<i32> > ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,17 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(schema_name = "schema_name", table_name = "collection")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub integers: Vec<i32> ,
pub integers_opt: Option<Vec<i32> > ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,60 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"collection"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)]
pub struct Model {
pub id: i32,
pub integers: Vec<i32> ,
pub integers_opt: Option<Vec<i32> > ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Integers,
IntegersOpt,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Integers => ColumnType::Array(sea_orm::sea_query::SeaRc::new(ColumnType::Integer)).def(),
Self::IntegersOpt => ColumnType::Array(sea_orm::sea_query::SeaRc::new(ColumnType::Integer)).def().null(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,64 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.10.0
use sea_orm::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn schema_name(&self) -> Option< &str > {
Some("schema_name")
}
fn table_name(&self) -> &str {
"collection"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)]
pub struct Model {
pub id: i32,
pub integers: Vec<i32> ,
pub integers_opt: Option<Vec<i32> > ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Integers,
IntegersOpt,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Integers => ColumnType::Array(sea_orm::sea_query::SeaRc::new(ColumnType::Integer)).def(),
Self::IntegersOpt => ColumnType::Array(sea_orm::sea_query::SeaRc::new(ColumnType::Integer)).def().null(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -711,4 +711,79 @@ mod tests {
Ok(())
}
#[cfg(feature = "postgres-array")]
#[smol_potat::test]
async fn test_postgres_array_1() -> Result<(), DbErr> {
mod collection {
use crate as sea_orm;
use crate::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "collection")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub integers: Vec<i32>,
pub integers_opt: Option<Vec<i32>>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
let db = MockDatabase::new(DbBackend::Postgres)
.append_query_results(vec![vec![
collection::Model {
id: 1,
integers: vec![1, 2, 3],
integers_opt: Some(vec![1, 2, 3]),
},
collection::Model {
id: 2,
integers: vec![],
integers_opt: Some(vec![]),
},
collection::Model {
id: 3,
integers: vec![3, 1, 4],
integers_opt: None,
},
]])
.into_connection();
assert_eq!(
collection::Entity::find().all(&db).await?,
vec![
collection::Model {
id: 1,
integers: vec![1, 2, 3],
integers_opt: Some(vec![1, 2, 3]),
},
collection::Model {
id: 2,
integers: vec![],
integers_opt: Some(vec![]),
},
collection::Model {
id: 3,
integers: vec![3, 1, 4],
integers_opt: None,
},
]
);
assert_eq!(
db.into_transaction_log(),
vec![Transaction::from_sql_and_values(
DbBackend::Postgres,
r#"SELECT "collection"."id", "collection"."integers", "collection"."integers_opt" FROM "collection""#,
vec![]
),]
);
Ok(())
}
}

View File

@ -84,6 +84,8 @@ pub enum ColumnType {
/// Variants of enum
variants: Vec<DynIden>,
},
/// Array of a specific data type (PostgreSQL only)
Array(SeaRc<ColumnType>),
}
impl PartialEq for ColumnType {
@ -398,80 +400,111 @@ impl ColumnDef {
}
impl From<ColumnType> for sea_query::ColumnType {
fn from(col: ColumnType) -> Self {
match col {
ColumnType::Char(s) => sea_query::ColumnType::Char(s),
ColumnType::String(s) => sea_query::ColumnType::String(s),
ColumnType::Text => sea_query::ColumnType::Text,
ColumnType::TinyInteger => sea_query::ColumnType::TinyInteger(None),
ColumnType::SmallInteger => sea_query::ColumnType::SmallInteger(None),
ColumnType::Integer => sea_query::ColumnType::Integer(None),
ColumnType::BigInteger => sea_query::ColumnType::BigInteger(None),
ColumnType::TinyUnsigned => sea_query::ColumnType::TinyUnsigned(None),
ColumnType::SmallUnsigned => sea_query::ColumnType::SmallUnsigned(None),
ColumnType::Unsigned => sea_query::ColumnType::Unsigned(None),
ColumnType::BigUnsigned => sea_query::ColumnType::BigUnsigned(None),
ColumnType::Float => sea_query::ColumnType::Float(None),
ColumnType::Double => sea_query::ColumnType::Double(None),
ColumnType::Decimal(s) => sea_query::ColumnType::Decimal(s),
ColumnType::DateTime => sea_query::ColumnType::DateTime(None),
ColumnType::Timestamp => sea_query::ColumnType::Timestamp(None),
ColumnType::TimestampWithTimeZone => sea_query::ColumnType::TimestampWithTimeZone(None),
ColumnType::Time => sea_query::ColumnType::Time(None),
ColumnType::Date => sea_query::ColumnType::Date,
ColumnType::Binary => sea_query::ColumnType::Binary(sea_query::BlobSize::Blob(None)),
ColumnType::TinyBinary => sea_query::ColumnType::Binary(sea_query::BlobSize::Tiny),
ColumnType::MediumBinary => sea_query::ColumnType::Binary(sea_query::BlobSize::Medium),
ColumnType::LongBinary => sea_query::ColumnType::Binary(sea_query::BlobSize::Long),
ColumnType::Boolean => sea_query::ColumnType::Boolean,
ColumnType::Money(s) => sea_query::ColumnType::Money(s),
ColumnType::Json => sea_query::ColumnType::Json,
ColumnType::JsonBinary => sea_query::ColumnType::JsonBinary,
ColumnType::Custom(s) => {
sea_query::ColumnType::Custom(sea_query::SeaRc::new(sea_query::Alias::new(&s)))
fn from(column_type: ColumnType) -> Self {
fn convert_column_type(column_type: &ColumnType) -> sea_query::ColumnType {
match column_type {
ColumnType::Char(s) => sea_query::ColumnType::Char(*s),
ColumnType::String(s) => sea_query::ColumnType::String(*s),
ColumnType::Text => sea_query::ColumnType::Text,
ColumnType::TinyInteger => sea_query::ColumnType::TinyInteger(None),
ColumnType::SmallInteger => sea_query::ColumnType::SmallInteger(None),
ColumnType::Integer => sea_query::ColumnType::Integer(None),
ColumnType::BigInteger => sea_query::ColumnType::BigInteger(None),
ColumnType::TinyUnsigned => sea_query::ColumnType::TinyUnsigned(None),
ColumnType::SmallUnsigned => sea_query::ColumnType::SmallUnsigned(None),
ColumnType::Unsigned => sea_query::ColumnType::Unsigned(None),
ColumnType::BigUnsigned => sea_query::ColumnType::BigUnsigned(None),
ColumnType::Float => sea_query::ColumnType::Float(None),
ColumnType::Double => sea_query::ColumnType::Double(None),
ColumnType::Decimal(s) => sea_query::ColumnType::Decimal(*s),
ColumnType::DateTime => sea_query::ColumnType::DateTime(None),
ColumnType::Timestamp => sea_query::ColumnType::Timestamp(None),
ColumnType::TimestampWithTimeZone => {
sea_query::ColumnType::TimestampWithTimeZone(None)
}
ColumnType::Time => sea_query::ColumnType::Time(None),
ColumnType::Date => sea_query::ColumnType::Date,
ColumnType::Binary => {
sea_query::ColumnType::Binary(sea_query::BlobSize::Blob(None))
}
ColumnType::TinyBinary => sea_query::ColumnType::Binary(sea_query::BlobSize::Tiny),
ColumnType::MediumBinary => {
sea_query::ColumnType::Binary(sea_query::BlobSize::Medium)
}
ColumnType::LongBinary => sea_query::ColumnType::Binary(sea_query::BlobSize::Long),
ColumnType::Boolean => sea_query::ColumnType::Boolean,
ColumnType::Money(s) => sea_query::ColumnType::Money(*s),
ColumnType::Json => sea_query::ColumnType::Json,
ColumnType::JsonBinary => sea_query::ColumnType::JsonBinary,
ColumnType::Custom(s) => {
sea_query::ColumnType::Custom(sea_query::SeaRc::new(sea_query::Alias::new(s)))
}
ColumnType::Uuid => sea_query::ColumnType::Uuid,
ColumnType::Enum { name, variants } => sea_query::ColumnType::Enum {
name: SeaRc::clone(name),
variants: variants.clone(),
},
ColumnType::Array(column_type) => {
let column_type = convert_column_type(column_type);
sea_query::ColumnType::Array(SeaRc::new(Box::new(column_type)))
}
}
ColumnType::Uuid => sea_query::ColumnType::Uuid,
ColumnType::Enum { name, variants } => sea_query::ColumnType::Enum { name, variants },
}
convert_column_type(&column_type)
}
}
impl From<sea_query::ColumnType> for ColumnType {
fn from(col_type: sea_query::ColumnType) -> Self {
#[allow(unreachable_patterns)]
match col_type {
sea_query::ColumnType::Char(s) => Self::Char(s),
sea_query::ColumnType::String(s) => Self::String(s),
sea_query::ColumnType::Text => Self::Text,
sea_query::ColumnType::TinyInteger(_) => Self::TinyInteger,
sea_query::ColumnType::SmallInteger(_) => Self::SmallInteger,
sea_query::ColumnType::Integer(_) => Self::Integer,
sea_query::ColumnType::BigInteger(_) => Self::BigInteger,
sea_query::ColumnType::TinyUnsigned(_) => Self::TinyUnsigned,
sea_query::ColumnType::SmallUnsigned(_) => Self::SmallUnsigned,
sea_query::ColumnType::Unsigned(_) => Self::Unsigned,
sea_query::ColumnType::BigUnsigned(_) => Self::BigUnsigned,
sea_query::ColumnType::Float(_) => Self::Float,
sea_query::ColumnType::Double(_) => Self::Double,
sea_query::ColumnType::Decimal(s) => Self::Decimal(s),
sea_query::ColumnType::DateTime(_) => Self::DateTime,
sea_query::ColumnType::Timestamp(_) => Self::Timestamp,
sea_query::ColumnType::TimestampWithTimeZone(_) => Self::TimestampWithTimeZone,
sea_query::ColumnType::Time(_) => Self::Time,
sea_query::ColumnType::Date => Self::Date,
sea_query::ColumnType::Binary(sea_query::BlobSize::Blob(_)) => Self::Binary,
sea_query::ColumnType::Binary(sea_query::BlobSize::Tiny) => Self::TinyBinary,
sea_query::ColumnType::Binary(sea_query::BlobSize::Medium) => Self::MediumBinary,
sea_query::ColumnType::Binary(sea_query::BlobSize::Long) => Self::LongBinary,
sea_query::ColumnType::Boolean => Self::Boolean,
sea_query::ColumnType::Money(s) => Self::Money(s),
sea_query::ColumnType::Json => Self::Json,
sea_query::ColumnType::JsonBinary => Self::JsonBinary,
sea_query::ColumnType::Custom(s) => Self::Custom(s.to_string()),
sea_query::ColumnType::Uuid => Self::Uuid,
sea_query::ColumnType::Enum { name, variants } => Self::Enum { name, variants },
_ => unimplemented!(),
fn from(column_type: sea_query::ColumnType) -> Self {
#[allow(clippy::redundant_allocation)]
fn convert_column_type(column_type: &sea_query::ColumnType) -> ColumnType {
#[allow(unreachable_patterns)]
match column_type {
sea_query::ColumnType::Char(s) => ColumnType::Char(*s),
sea_query::ColumnType::String(s) => ColumnType::String(*s),
sea_query::ColumnType::Text => ColumnType::Text,
sea_query::ColumnType::TinyInteger(_) => ColumnType::TinyInteger,
sea_query::ColumnType::SmallInteger(_) => ColumnType::SmallInteger,
sea_query::ColumnType::Integer(_) => ColumnType::Integer,
sea_query::ColumnType::BigInteger(_) => ColumnType::BigInteger,
sea_query::ColumnType::TinyUnsigned(_) => ColumnType::TinyUnsigned,
sea_query::ColumnType::SmallUnsigned(_) => ColumnType::SmallUnsigned,
sea_query::ColumnType::Unsigned(_) => ColumnType::Unsigned,
sea_query::ColumnType::BigUnsigned(_) => ColumnType::BigUnsigned,
sea_query::ColumnType::Float(_) => ColumnType::Float,
sea_query::ColumnType::Double(_) => ColumnType::Double,
sea_query::ColumnType::Decimal(s) => ColumnType::Decimal(*s),
sea_query::ColumnType::DateTime(_) => ColumnType::DateTime,
sea_query::ColumnType::Timestamp(_) => ColumnType::Timestamp,
sea_query::ColumnType::TimestampWithTimeZone(_) => {
ColumnType::TimestampWithTimeZone
}
sea_query::ColumnType::Time(_) => ColumnType::Time,
sea_query::ColumnType::Date => ColumnType::Date,
sea_query::ColumnType::Binary(sea_query::BlobSize::Blob(_)) => ColumnType::Binary,
sea_query::ColumnType::Binary(sea_query::BlobSize::Tiny) => ColumnType::TinyBinary,
sea_query::ColumnType::Binary(sea_query::BlobSize::Medium) => {
ColumnType::MediumBinary
}
sea_query::ColumnType::Binary(sea_query::BlobSize::Long) => ColumnType::LongBinary,
sea_query::ColumnType::Boolean => ColumnType::Boolean,
sea_query::ColumnType::Money(s) => ColumnType::Money(*s),
sea_query::ColumnType::Json => ColumnType::Json,
sea_query::ColumnType::JsonBinary => ColumnType::JsonBinary,
sea_query::ColumnType::Custom(s) => ColumnType::Custom(s.to_string()),
sea_query::ColumnType::Uuid => ColumnType::Uuid,
sea_query::ColumnType::Enum { name, variants } => ColumnType::Enum {
name: SeaRc::clone(name),
variants: variants.clone(),
},
sea_query::ColumnType::Array(column_type) => {
let column_type = convert_column_type(column_type);
ColumnType::Array(SeaRc::new(column_type))
}
_ => unimplemented!(),
}
}
convert_column_type(&column_type)
}
}

View File

@ -444,6 +444,130 @@ impl TryGetable for u32 {
}
}
#[cfg(feature = "postgres-array")]
mod postgres_array {
use super::*;
#[allow(unused_macros)]
macro_rules! try_getable_postgres_array {
( $type: ty ) => {
impl TryGetable for Vec<$type> {
#[allow(unused_variables)]
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError> {
let column = format!("{}{}", pre, col);
match &res.row {
#[cfg(feature = "sqlx-mysql")]
QueryResultRow::SqlxMySql(row) => {
panic!("{} unsupported by sqlx-mysql", stringify!($type))
}
#[cfg(feature = "sqlx-postgres")]
QueryResultRow::SqlxPostgres(row) => {
use sqlx::Row;
row.try_get::<Option<Vec<$type>>, _>(column.as_str())
.map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))
.and_then(|opt| opt.ok_or(TryGetError::Null(column)))
}
#[cfg(feature = "sqlx-sqlite")]
QueryResultRow::SqlxSqlite(_) => {
panic!("{} unsupported by sqlx-sqlite", stringify!($type))
}
#[cfg(feature = "mock")]
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
debug_print!("{:#?}", e.to_string());
TryGetError::Null(column)
}),
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
}
}
};
}
try_getable_postgres_array!(bool);
try_getable_postgres_array!(i8);
try_getable_postgres_array!(i16);
try_getable_postgres_array!(i32);
try_getable_postgres_array!(i64);
try_getable_postgres_array!(f32);
try_getable_postgres_array!(f64);
try_getable_postgres_array!(String);
#[cfg(feature = "with-json")]
try_getable_postgres_array!(serde_json::Value);
#[cfg(feature = "with-chrono")]
try_getable_postgres_array!(chrono::NaiveDate);
#[cfg(feature = "with-chrono")]
try_getable_postgres_array!(chrono::NaiveTime);
#[cfg(feature = "with-chrono")]
try_getable_postgres_array!(chrono::NaiveDateTime);
#[cfg(feature = "with-chrono")]
try_getable_postgres_array!(chrono::DateTime<chrono::FixedOffset>);
#[cfg(feature = "with-chrono")]
try_getable_postgres_array!(chrono::DateTime<chrono::Utc>);
#[cfg(feature = "with-chrono")]
try_getable_postgres_array!(chrono::DateTime<chrono::Local>);
#[cfg(feature = "with-time")]
try_getable_postgres_array!(time::Date);
#[cfg(feature = "with-time")]
try_getable_postgres_array!(time::Time);
#[cfg(feature = "with-time")]
try_getable_postgres_array!(time::PrimitiveDateTime);
#[cfg(feature = "with-time")]
try_getable_postgres_array!(time::OffsetDateTime);
#[cfg(feature = "with-rust_decimal")]
try_getable_postgres_array!(rust_decimal::Decimal);
#[cfg(feature = "with-uuid")]
try_getable_postgres_array!(uuid::Uuid);
impl TryGetable for Vec<u32> {
#[allow(unused_variables)]
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError> {
let column = format!("{}{}", pre, col);
match &res.row {
#[cfg(feature = "sqlx-mysql")]
QueryResultRow::SqlxMySql(row) => {
panic!("{} unsupported by sqlx-mysql", stringify!($type))
}
#[cfg(feature = "sqlx-postgres")]
QueryResultRow::SqlxPostgres(row) => {
use sqlx::postgres::types::Oid;
// Since 0.6.0, SQLx has dropped direct mapping from PostgreSQL's OID to Rust's `u32`;
// Instead, `u32` was wrapped by a `sqlx::Oid`.
use sqlx::Row;
row.try_get::<Option<Vec<Oid>>, _>(column.as_str())
.map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))
.and_then(|opt| opt.ok_or(TryGetError::Null(column)))
.map(|oids| oids.into_iter().map(|oid| oid.0).collect())
}
#[cfg(feature = "sqlx-sqlite")]
QueryResultRow::SqlxSqlite(_) => {
panic!("{} unsupported by sqlx-sqlite", stringify!($type))
}
#[cfg(feature = "mock")]
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
debug_print!("{:#?}", e.to_string());
TryGetError::Null(column)
}),
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
}
}
}
// TryGetableMany //
/// Perform a query on multiple columns

View File

@ -41,7 +41,7 @@ pub async fn create_and_update(db: &DatabaseConnection) -> Result<(), DbErr> {
};
let update_res = Entity::update(updated_active_model.clone())
.filter(Column::Id.eq(vec![1, 2, 4]))
.filter(Column::Id.eq(vec![1_u8, 2_u8, 4_u8])) // annotate it as Vec<u8> explicitly
.exec(db)
.await;
@ -53,7 +53,7 @@ pub async fn create_and_update(db: &DatabaseConnection) -> Result<(), DbErr> {
);
let update_res = Entity::update(updated_active_model)
.filter(Column::Id.eq(vec![1, 2, 3]))
.filter(Column::Id.eq(vec![1_u8, 2_u8, 3_u8])) // annotate it as Vec<u8> explicitly
.exec(db)
.await?;
@ -67,7 +67,7 @@ pub async fn create_and_update(db: &DatabaseConnection) -> Result<(), DbErr> {
assert_eq!(
Entity::find()
.filter(Column::Id.eq(vec![1, 2, 3]))
.filter(Column::Id.eq(vec![1_u8, 2_u8, 3_u8])) // annotate it as Vec<u8> explicitly
.one(db)
.await?,
Some(Model {

95
tests/collection_tests.rs Normal file
View File

@ -0,0 +1,95 @@
pub mod common;
pub use common::{features::*, setup::*, TestContext};
use pretty_assertions::assert_eq;
use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection};
#[sea_orm_macros::test]
#[cfg(all(feature = "sqlx-postgres", feature = "postgres-array"))]
async fn main() -> Result<(), DbErr> {
let ctx = TestContext::new("collection_tests").await;
create_tables(&ctx.db).await?;
insert_collection(&ctx.db).await?;
update_collection(&ctx.db).await?;
ctx.delete().await;
Ok(())
}
pub async fn insert_collection(db: &DatabaseConnection) -> Result<(), DbErr> {
use collection::*;
assert_eq!(
Model {
id: 1,
integers: vec![1, 2, 3],
integers_opt: Some(vec![1, 2, 3]),
}
.into_active_model()
.insert(db)
.await?,
Model {
id: 1,
integers: vec![1, 2, 3],
integers_opt: Some(vec![1, 2, 3]),
}
);
assert_eq!(
Model {
id: 2,
integers: vec![10, 9],
integers_opt: None,
}
.into_active_model()
.insert(db)
.await?,
Model {
id: 2,
integers: vec![10, 9],
integers_opt: None,
}
);
assert_eq!(
Model {
id: 3,
integers: vec![],
integers_opt: Some(vec![]),
}
.into_active_model()
.insert(db)
.await?,
Model {
id: 3,
integers: vec![],
integers_opt: Some(vec![]),
}
);
Ok(())
}
pub async fn update_collection(db: &DatabaseConnection) -> Result<(), DbErr> {
use collection::*;
let model = Entity::find_by_id(1).one(db).await?.unwrap();
ActiveModel {
integers: Set(vec![4, 5, 6]),
integers_opt: Set(Some(vec![4, 5, 6])),
..model.into_active_model()
}
.update(db)
.await?;
ActiveModel {
id: Unchanged(3),
integers: Set(vec![3, 1, 4]),
integers_opt: Set(None),
}
.update(db)
.await?;
Ok(())
}

View File

@ -0,0 +1,15 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "collection")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub integers: Vec<i32>,
pub integers_opt: Option<Vec<i32>>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -2,6 +2,7 @@ pub mod active_enum;
pub mod active_enum_child;
pub mod applog;
pub mod byte_primary_key;
pub mod collection;
pub mod custom_active_model;
pub mod insert_default;
pub mod json_struct;
@ -18,6 +19,7 @@ pub use active_enum::Entity as ActiveEnum;
pub use active_enum_child::Entity as ActiveEnumChild;
pub use applog::Entity as Applog;
pub use byte_primary_key::Entity as BytePrimaryKey;
pub use collection::Entity as Collection;
pub use insert_default::Entity as InsertDefault;
pub use json_struct::Entity as JsonStruct;
pub use json_vec::Entity as JsonVec;

View File

@ -42,6 +42,10 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> {
create_active_enum_child_table(db).await?;
create_insert_default_table(db).await?;
if DbBackend::Postgres == db_backend {
create_collection_table(db).await?;
}
Ok(())
}
@ -326,3 +330,27 @@ pub async fn create_json_struct_table(db: &DbConn) -> Result<ExecResult, DbErr>
create_table(db, &stmt, JsonStruct).await
}
pub async fn create_collection_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(collection::Entity)
.col(
ColumnDef::new(collection::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(collection::Column::Integers)
.array(sea_query::ColumnType::Integer(None))
.not_null(),
)
.col(
ColumnDef::new(collection::Column::IntegersOpt)
.array(sea_query::ColumnType::Integer(None)),
)
.to_owned();
create_table(db, &stmt, Collection).await
}