Skip to content

Commit

Permalink
Implemented 'Array' type for Postgres. (SeaQL#205)
Browse files Browse the repository at this point in the history
* Add a "with-array" option for postgres.

This introduces "Array" as a new Value variant.
It can be derived from all Vec<T> types, where T is a valid Value,
   except for u8, which is already handled by Bytes.

* Fix typo.

Co-authored-by: Chris Tsang <[email protected]>
  • Loading branch information
kev0960 and tyt2y3 authored Jan 2, 2022
1 parent 118646b commit 04c8712
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ postgres-json = ["with-json", "postgres-types/with-serde_json-1"]
postgres-rust_decimal = ["with-rust_decimal", "rust_decimal/db-postgres"]
postgres-bigdecimal = ["with-bigdecimal"]
postgres-uuid = ["with-uuid", "postgres-types/with-uuid-0_8"]
postgres-array = ["with-array", "postgres-types/array-impls"]
rusqlite = []
sqlx-mysql = []
sqlx-postgres = []
Expand All @@ -73,6 +74,7 @@ with-rust_decimal = ["rust_decimal"]
with-bigdecimal = ["bigdecimal"]
with-uuid = ["uuid"]
pg-interval = ["proc-macro2", "quote"]
with-array = []

[[test]]
name = "test-derive"
Expand Down
3 changes: 2 additions & 1 deletion examples/postgres_json/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ sea-query = { path = "../../", features = [
"postgres-json",
"postgres-rust_decimal",
"postgres-uuid",
"postgres-array",
] }
# NOTE: if you are copying this example into your own project, use the following line instead:
# sea-query = { version = "^0", features = [
Expand All @@ -23,4 +24,4 @@ sea-query = { path = "../../", features = [
# "postgres-json",
# "postgres-rust_decimal",
# "postgres-uuid",
# ] }
# ] }
8 changes: 8 additions & 0 deletions examples/postgres_json/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fn main() {
.col(ColumnDef::new(Document::Timestamp).timestamp())
.col(ColumnDef::new(Document::TimestampWithTimeZone).timestamp_with_time_zone())
.col(ColumnDef::new(Document::Decimal).decimal())
.col(ColumnDef::new(Document::Array).array("integer".to_string()))
.build(PostgresQueryBuilder),
]
.join("; ");
Expand All @@ -53,6 +54,7 @@ fn main() {
timestamp_with_time_zone: DateTime::parse_from_rfc3339("2020-01-01T02:02:02+08:00")
.unwrap(),
decimal: Decimal::from_i128_with_scale(3141i128, 3),
array: vec![3, 4, 5, 6],
};
let (sql, values) = Query::insert()
.into_table(Document::Table)
Expand All @@ -62,13 +64,15 @@ fn main() {
Document::Timestamp,
Document::TimestampWithTimeZone,
Document::Decimal,
Document::Array,
])
.values_panic(vec![
document.uuid.into(),
serde_json::to_value(document.json_field).unwrap().into(),
document.timestamp.into(),
document.timestamp_with_time_zone.into(),
document.decimal.into(),
document.array.into(),
])
.build(PostgresQueryBuilder);

Expand All @@ -85,6 +89,7 @@ fn main() {
Document::Timestamp,
Document::TimestampWithTimeZone,
Document::Decimal,
Document::Array,
])
.from(Document::Table)
.order_by(Document::Id, Order::Desc)
Expand All @@ -109,6 +114,7 @@ enum Document {
Timestamp,
TimestampWithTimeZone,
Decimal,
Array,
}

#[derive(Debug)]
Expand All @@ -119,6 +125,7 @@ struct DocumentStruct {
timestamp: NaiveDateTime,
timestamp_with_time_zone: DateTime<FixedOffset>,
decimal: Decimal,
array: Vec<i32>,
}

impl From<Row> for DocumentStruct {
Expand All @@ -130,6 +137,7 @@ impl From<Row> for DocumentStruct {
timestamp: row.get("timestamp"),
timestamp_with_time_zone: row.get("timestamp_with_time_zone"),
decimal: row.get("decimal"),
array: row.get("array"),
}
}
}
1 change: 1 addition & 0 deletions examples/sqlx_postgres/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ sea-query = { path = "../../", features = [
"with-rust_decimal",
"with-bigdecimal",
"with-uuid",
"with-array",
] }
# NOTE: if you are copying this example into your own project, use the following line instead:
# sea-query = { version = "^0", features = [
Expand Down
1 change: 1 addition & 0 deletions src/backend/mysql/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ impl TableBuilder for MysqlQueryBuilder {
ColumnType::Uuid => "binary(16)".into(),
ColumnType::Custom(iden) => iden.to_string(),
ColumnType::Enum(_, variants) => format!("ENUM('{}')", variants.join("', '")),
ColumnType::Array(_) => unimplemented!("Array is not available in MySQL."),
}
)
.unwrap()
Expand Down
1 change: 1 addition & 0 deletions src/backend/postgres/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl TableBuilder for PostgresQueryBuilder {
ColumnType::Json => "json".into(),
ColumnType::JsonBinary => "jsonb".into(),
ColumnType::Uuid => "uuid".into(),
ColumnType::Array(elem_type) => format!("{}[]", elem_type.as_ref().unwrap()),
ColumnType::Custom(iden) => iden.to_string(),
ColumnType::Enum(name, _) => name.into(),
}
Expand Down
12 changes: 12 additions & 0 deletions src/backend/query_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,8 @@ pub trait QueryBuilder: QuotedBuilder {
Value::BigDecimal(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-uuid")]
Value::Uuid(None) => write!(s, "NULL").unwrap(),
#[cfg(feature = "with-array")]
Value::Array(None) => write!(s, "NULL").unwrap(),
Value::Bool(Some(b)) => write!(s, "{}", if *b { "TRUE" } else { "FALSE" }).unwrap(),
Value::TinyInt(Some(v)) => write!(s, "{}", v).unwrap(),
Value::SmallInt(Some(v)) => write!(s, "{}", v).unwrap(),
Expand Down Expand Up @@ -792,6 +794,16 @@ pub trait QueryBuilder: QuotedBuilder {
Value::BigDecimal(Some(v)) => write!(s, "{}", v).unwrap(),
#[cfg(feature = "with-uuid")]
Value::Uuid(Some(v)) => write!(s, "\'{}\'", v.to_string()).unwrap(),
#[cfg(feature = "with-array")]
Value::Array(Some(v)) => write!(
s,
"\'{{{}}}\'",
v.iter()
.map(|element| self.value_to_string(element))
.collect::<Vec<String>>()
.join(",")
)
.unwrap(),
};
s
}
Expand Down
1 change: 1 addition & 0 deletions src/backend/sqlite/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ impl TableBuilder for SqliteQueryBuilder {
ColumnType::Uuid => "text(36)".into(),
ColumnType::Custom(iden) => iden.to_string(),
ColumnType::Enum(_, _) => "text".into(),
ColumnType::Array(_) => unimplemented!("Array is not available in Sqlite."),
}
)
.unwrap()
Expand Down
2 changes: 2 additions & 0 deletions src/driver/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ impl ToSql for Value {
Value::BigDecimal(_) => unimplemented!("Not supported"),
#[cfg(feature = "postgres-uuid")]
Value::Uuid(v) => box_to_sql!(v, uuid::Uuid),
#[cfg(feature = "postgres-array")]
Value::Array(v) => box_to_sql!(v, Vec<Value>),
#[allow(unreachable_patterns)]
_ => unimplemented!(),
}
Expand Down
2 changes: 2 additions & 0 deletions src/driver/sqlx_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ macro_rules! bind_params_sqlx_postgres {
query.bind(value.as_ref_big_decimal())
} else if value.is_uuid() {
query.bind(value.as_ref_uuid())
} else if value.is_array() {
query.bind(value.as_ref_array())
} else {
unimplemented!();
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
//!
//! SQL dialect: `backend-mysql`, `backend-postgres`, `backend-sqlite`
//!
//! Type support: `with-chrono`, `with-json`, `with-rust_decimal`, `with-bigdecimal`, `with-uuid`
//! Type support: `with-chrono`, `with-json`, `with-rust_decimal`, `with-bigdecimal`, `with-uuid`,
//! `with-array`
//!
//! Driver support: `sqlx-mysql`, `sqlx-postgres`, `sqlx-sqlite`,
//! `postgres`, `postgres-*`, `rusqlite`
Expand Down
8 changes: 8 additions & 0 deletions src/table/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub enum ColumnType {
Uuid,
Custom(DynIden),
Enum(String, Vec<String>),
Array(Option<String>),
}

/// All column specification keywords
Expand Down Expand Up @@ -439,6 +440,13 @@ impl ColumnDef {
self
}

/// Set column type as an array with a specified element type.
/// This is only supported on Postgres.
pub fn array(&mut self, elem_type: String) -> &mut Self {
self.types = Some(ColumnType::Array(Some(elem_type)));
self
}

/// Some extra options in custom string
pub fn extra(&mut self, string: String) -> &mut Self {
self.spec.push(ColumnSpec::Extra(string));
Expand Down
129 changes: 129 additions & 0 deletions src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ pub enum Value {
#[cfg(feature = "with-bigdecimal")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-bigdecimal")))]
BigDecimal(Option<Box<BigDecimal>>),

#[cfg(feature = "with-array")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-array")))]
Array(Option<Box<Vec<Value>>>),
}

pub trait ValueType: Sized {
Expand Down Expand Up @@ -346,6 +350,92 @@ mod with_uuid {
type_to_box_value!(Uuid, Uuid, Uuid);
}

#[cfg(feature = "with-array")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-array")))]
mod with_array {
use super::*;

// We only imlement conversion from Vec<T> to Array when T is not u8.
// This is because for u8's case, there is already conversion to Byte defined above.
// TODO When negative trait becomes a stable feature, following code can be much shorter.
pub trait NotU8 {}

impl NotU8 for bool {}
impl NotU8 for i8 {}
impl NotU8 for i16 {}
impl NotU8 for i32 {}
impl NotU8 for i64 {}
impl NotU8 for u16 {}
impl NotU8 for u32 {}
impl NotU8 for u64 {}
impl NotU8 for f32 {}
impl NotU8 for f64 {}
impl NotU8 for String {}

#[cfg(feature = "with-json")]
impl NotU8 for Json {}

#[cfg(feature = "with-chrono")]
impl NotU8 for NaiveDate {}

#[cfg(feature = "with-chrono")]
impl NotU8 for NaiveTime {}

#[cfg(feature = "with-chrono")]
impl NotU8 for NaiveDateTime {}

#[cfg(feature = "with-chrono")]
impl<Tz> NotU8 for DateTime<Tz> where Tz: chrono::TimeZone {}

#[cfg(feature = "with-rust_decimal")]
impl NotU8 for Decimal {}

#[cfg(feature = "with-bigdecimal")]
impl NotU8 for BigDecimal {}

#[cfg(feature = "with-uuid")]
impl NotU8 for Uuid {}

impl<T> From<Vec<T>> for Value
where
T: Into<Value> + NotU8,
{
fn from(x: Vec<T>) -> Value {
Value::Array(Some(Box::new(x.into_iter().map(|e| e.into()).collect())))
}
}

impl<T> Nullable for Vec<T>
where
T: Into<Value> + NotU8,
{
fn null() -> Value {
Value::Array(None)
}
}

impl<T> ValueType for Vec<T>
where
T: NotU8 + ValueType,
{
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
match v {
Value::Array(Some(v)) => Ok(v.into_iter().map(|e| e.unwrap()).collect()),
_ => Err(ValueTypeErr),
}
}

fn type_name() -> String {
stringify!(Vec<T>).to_owned()
}

fn column_type() -> ColumnType {
use ColumnType::*;
Array(None)
}
}
}

#[allow(unused_macros)]
macro_rules! box_to_opt_ref {
( $v: expr ) => {
Expand Down Expand Up @@ -545,6 +635,27 @@ impl Value {
}
}

impl Value {
pub fn is_array(&self) -> bool {
#[cfg(feature = "with-array")]
return matches!(self, Self::Array(_));
#[cfg(not(feature = "with-array"))]
return false;
}

#[cfg(feature = "with-array")]
pub fn as_ref_array(&self) -> Option<&Vec<Value>> {
match self {
Self::Array(v) => box_to_opt_ref!(v),
_ => panic!("not Value::Array"),
}
}
#[cfg(not(feature = "with-array"))]
pub fn as_ref_array(&self) -> Option<&bool> {
panic!("not Value::Array")
}
}

impl IntoIterator for ValueTuple {
type Item = Value;
type IntoIter = std::vec::IntoIter<Self::Item>;
Expand Down Expand Up @@ -835,6 +946,8 @@ pub fn sea_value_to_json_value(value: &Value) -> Json {
Value::BigDecimal(None) => Json::Null,
#[cfg(feature = "with-uuid")]
Value::Uuid(None) => Json::Null,
#[cfg(feature = "with-array")]
Value::Array(None) => Json::Null,
Value::Bool(Some(b)) => Json::Bool(*b),
Value::TinyInt(Some(v)) => (*v).into(),
Value::SmallInt(Some(v)) => (*v).into(),
Expand Down Expand Up @@ -869,6 +982,13 @@ pub fn sea_value_to_json_value(value: &Value) -> Json {
}
#[cfg(feature = "with-uuid")]
Value::Uuid(Some(v)) => Json::String(v.to_string()),
#[cfg(feature = "with-array")]
Value::Array(Some(v)) => Json::Array(
v.as_ref()
.iter()
.map(|v| sea_value_to_json_value(v))
.collect(),
),
}
}

Expand Down Expand Up @@ -1201,4 +1321,13 @@ mod tests {
let out: Decimal = v.unwrap();
assert_eq!(out.to_string(), num);
}

#[test]
#[cfg(feature = "with-array")]
fn test_array_value() {
let array = vec![1, 2, 3, 4, 5];
let v: Value = array.into();
let out: Vec<i32> = v.unwrap();
assert_eq!(out, vec![1, 2, 3, 4, 5]);
}
}

0 comments on commit 04c8712

Please sign in to comment.