Skip to content

Commit

Permalink
Add empty and nonempty filters in ORM
Browse files Browse the repository at this point in the history
  • Loading branch information
photino committed Feb 18, 2025
1 parent 4662ed5 commit 61f81b6
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 73 deletions.
36 changes: 16 additions & 20 deletions crates/zino-orm/src/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,18 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
} else if self.has_attribute("exact_filter") {
let value = self.encode_value(Some(value));
return format!(r#"{field} = {value}"#);
} else if let Some((min_value, max_value)) = value
.as_str()
.and_then(|value| value.split_once(','))
.filter(|_| self.is_datetime_type())
{
let min_value = self.format_value(min_value);
let max_value = self.format_value(max_value);
return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#);
} else if let Some(value) = value.as_str() {
if value == "null" {
return format!(r#"{field} IS NULL"#);
} else if value == "not_null" {
return format!(r#"{field} IS NOT NULL"#);
} else if let Some((min_value, max_value)) =
value.split_once(',').filter(|_| self.is_datetime_type())
{
let min_value = self.format_value(min_value);
let max_value = self.format_value(max_value);
return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#);
}
}

match type_name {
Expand All @@ -277,11 +281,7 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
"u64" | "i64" | "u32" | "i32" | "u16" | "i16" | "u8" | "i8" | "usize" | "isize"
| "Option<u64>" | "Option<i64>" | "Option<u32>" | "Option<i32>" => {
if let Some(value) = value.as_str() {
if value == "null" {
format!(r#"{field} IS NULL"#)
} else if value == "not_null" {
format!(r#"{field} IS NOT NULL"#)
} else if value == "nonzero" {
if value == "nonzero" {
format!(r#"{field} <> 0"#)
} else if value.contains(',') {
let value = value.split(',').collect::<Vec<_>>().join(",");
Expand All @@ -297,10 +297,10 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
}
"String" | "Option<String>" => {
if let Some(value) = value.as_str() {
if value == "null" {
if value == "empty" {
// either NULL or empty
format!(r#"({field} = '') IS NOT FALSE"#)
} else if value == "not_null" {
} else if value == "nonempty" {
format!(r#"({field} = '') IS FALSE"#)
} else if self.fuzzy_search() {
if value.contains(',') {
Expand Down Expand Up @@ -378,11 +378,7 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
}
"Uuid" | "Option<Uuid>" => {
if let Some(value) = value.as_str() {
if value == "null" {
format!(r#"{field} IS NULL"#)
} else if value == "not_null" {
format!(r#"{field} IS NOT NULL"#)
} else if value.contains(',') {
if value.contains(',') {
let value = value
.split(',')
.map(Query::escape_string)
Expand Down
36 changes: 16 additions & 20 deletions crates/zino-orm/src/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,18 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
} else if self.has_attribute("exact_filter") {
let value = self.encode_value(Some(value));
return format!(r#"{field} = {value}"#);
} else if let Some((min_value, max_value)) = value
.as_str()
.and_then(|value| value.split_once(','))
.filter(|_| self.is_datetime_type())
{
let min_value = self.format_value(min_value);
let max_value = self.format_value(max_value);
return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#);
} else if let Some(value) = value.as_str() {
if value == "null" {
return format!(r#"{field} IS NULL"#);
} else if value == "not_null" {
return format!(r#"{field} IS NOT NULL"#);
} else if let Some((min_value, max_value)) =
value.split_once(',').filter(|_| self.is_datetime_type())
{
let min_value = self.format_value(min_value);
let max_value = self.format_value(max_value);
return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#);
}
}

match type_name {
Expand All @@ -280,11 +284,7 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
"u64" | "i64" | "u32" | "i32" | "u16" | "i16" | "u8" | "i8" | "usize" | "isize"
| "Option<u64>" | "Option<i64>" | "Option<u32>" | "Option<i32>" => {
if let Some(value) = value.as_str() {
if value == "null" {
format!(r#"{field} IS NULL"#)
} else if value == "not_null" {
format!(r#"{field} IS NOT NULL"#)
} else if value == "nonzero" {
if value == "nonzero" {
format!(r#"{field} <> 0"#)
} else if value.contains(',') {
let value = value.split(',').collect::<Vec<_>>().join(",");
Expand All @@ -300,10 +300,10 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
}
"String" | "Option<String>" => {
if let Some(value) = value.as_str() {
if value == "null" {
if value == "empty" {
// either NULL or empty
format!(r#"({field} = '') IS NOT FALSE"#)
} else if value == "not_null" {
} else if value == "nonempty" {
format!(r#"({field} = '') IS FALSE"#)
} else if self.fuzzy_search() {
if value.contains(',') {
Expand Down Expand Up @@ -388,11 +388,7 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
}
"Uuid" | "Option<Uuid>" => {
if let Some(value) = value.as_str() {
if value == "null" {
format!(r#"{field} IS NULL"#)
} else if value == "not_null" {
format!(r#"{field} IS NOT NULL"#)
} else if value.contains(',') {
if value.contains(',') {
let value = value
.split(',')
.map(Query::escape_string)
Expand Down
36 changes: 25 additions & 11 deletions crates/zino-orm/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,21 @@ impl<E: Entity> QueryBuilder<E> {
self
}

/// Adds a field with an alias extracted from a JSON column.
pub fn json_extract(mut self, col: E::Column, path: &str, alias: &str) -> Self {
let col_name = E::format_column(&col);
let field = Query::format_field(&col_name);
let json_field = if cfg!(feature = "orm-postgres") {
let path = path.strip_prefix("$.").unwrap_or(path).replace('.', ", ");
format!(r#"({field} #>> '{{{path}}}')"#)
} else {
format!(r#"json_unquote(json_extract({field}, '{path}'))"#)
};
let field_alias = [alias, ":", &json_field].concat();
self.fields.push(field_alias);
self
}

/// Adds a field with an optional alias for the aggregate function.
pub fn aggregate(mut self, aggregation: Aggregation<E>, alias: Option<&str>) -> Self {
let expr = aggregation.expr();
Expand Down Expand Up @@ -526,25 +541,25 @@ impl<E: Entity> QueryBuilder<E> {
/// Adds a logical `AND` condition for the column which is null.
#[inline]
pub fn and_null(self, col: E::Column) -> Self {
self.push_logical_and(col, "$is", JsonValue::Null)
self.and_filter(col, JsonValue::Null)
}

/// Adds a logical `AND` condition for the column which is not null.
#[inline]
pub fn and_not_null(self, col: E::Column) -> Self {
self.push_logical_and(col, "$is", "not_null".into_sql_value())
self.and_filter(col, "not_null")
}

/// Adds a logical `AND` condition for the column which is an empty string or a null.
#[inline]
pub fn and_empty(self, col: E::Column) -> Self {
self.and_eq(col, "null")
self.and_filter(col, "empty")
}

/// Adds a logical `AND` condition for the column which is not an empty string or a null.
#[inline]
pub fn and_nonempty(self, col: E::Column) -> Self {
self.and_eq(col, "not_null")
self.and_filter(col, "nonempty")
}

/// Adds a logical `AND` condition for the two ranges which overlaps with each other.
Expand Down Expand Up @@ -802,25 +817,25 @@ impl<E: Entity> QueryBuilder<E> {
/// Adds a logical `OR` condition for the column which is null.
#[inline]
pub fn or_null(self, col: E::Column) -> Self {
self.push_logical_or(col, "$is", JsonValue::Null)
self.or_filter(col, JsonValue::Null)
}

/// Adds a logical `OR` condition for the column which is not null.
#[inline]
pub fn or_not_null(self, col: E::Column) -> Self {
self.push_logical_or(col, "$is", "not_null".into_sql_value())
self.or_filter(col, "not_null")
}

/// Adds a logical `OR` condition for the column which is an empty string or a null.
#[inline]
pub fn or_empty(self, col: E::Column) -> Self {
self.or_eq(col, "null")
self.or_filter(col, "empty")
}

/// Adds a logical `OR` condition for the column which is not an empty string or a null.
#[inline]
pub fn or_nonempty(self, col: E::Column) -> Self {
self.or_eq(col, "not_null")
self.or_filter(col, "nonempty")
}

/// Adds a logical `OR` condition for the two ranges which overlaps with each other.
Expand Down Expand Up @@ -1214,7 +1229,8 @@ pub(super) trait QueryExt<DB> {
let key = [M::model_name(), ".", col.name()].concat();
let field = Self::format_field(&key);
if cfg!(feature = "orm-postgres") {
format!(r#"{}->{}"#, field, path.replace('.', "->"))
let path = path.replace('.', ", ");
format!(r#"({field} #> '{{{path}}}')"#)
} else {
format!(r#"json_extract({field}, '$.{path}')"#)
}
Expand Down Expand Up @@ -1290,8 +1306,6 @@ pub(super) trait QueryExt<DB> {
} else {
format!(r#"{field} = {s}"#)
}
} else if s == "nonzero" {
format!(r#"{field} <> 0"#)
} else if let Ok(value) = s.parse::<serde_json::Number>() {
format!(r#"{field} {operator} {value}"#)
} else {
Expand Down
36 changes: 16 additions & 20 deletions crates/zino-orm/src/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,18 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
} else if self.has_attribute("exact_filter") {
let value = self.encode_value(Some(value));
return format!(r#"{field} = {value}"#);
} else if let Some((min_value, max_value)) = value
.as_str()
.and_then(|value| value.split_once(','))
.filter(|_| self.is_datetime_type())
{
let min_value = self.format_value(min_value);
let max_value = self.format_value(max_value);
return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#);
} else if let Some(value) = value.as_str() {
if value == "null" {
return format!(r#"{field} IS NULL"#);
} else if value == "not_null" {
return format!(r#"{field} IS NOT NULL"#);
} else if let Some((min_value, max_value)) =
value.split_once(',').filter(|_| self.is_datetime_type())
{
let min_value = self.format_value(min_value);
let max_value = self.format_value(max_value);
return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#);
}
}

match type_name {
Expand All @@ -253,11 +257,7 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
"u64" | "i64" | "u32" | "i32" | "u16" | "i16" | "u8" | "i8" | "usize" | "isize"
| "Option<u64>" | "Option<i64>" | "Option<u32>" | "Option<i32>" => {
if let Some(value) = value.as_str() {
if value == "null" {
format!(r#"{field} IS NULL"#)
} else if value == "not_null" {
format!(r#"{field} IS NOT NULL"#)
} else if value == "nonzero" {
if value == "nonzero" {
format!(r#"{field} <> 0"#)
} else if value.contains(',') {
let value = value.split(',').collect::<Vec<_>>().join(",");
Expand All @@ -273,10 +273,10 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
}
"String" | "Option<String>" => {
if let Some(value) = value.as_str() {
if value == "null" {
if value == "empty" {
// either NULL or empty
format!(r#"({field} = '') IS NOT FALSE"#)
} else if value == "not_null" {
} else if value == "nonempty" {
format!(r#"({field} = '') IS FALSE"#)
} else if self.fuzzy_search() {
if value.contains(',') {
Expand Down Expand Up @@ -354,11 +354,7 @@ impl EncodeColumn<DatabaseDriver> for Column<'_> {
}
"Uuid" | "Option<Uuid>" => {
if let Some(value) = value.as_str() {
if value == "null" {
format!(r#"{field} IS NULL"#)
} else if value == "not_null" {
format!(r#"{field} IS NOT NULL"#)
} else if value.contains(',') {
if value.contains(',') {
let value = value
.split(',')
.map(Query::escape_string)
Expand Down
4 changes: 2 additions & 2 deletions examples/dioxus-desktop/Dioxus.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ style = []
script = []

[bundle]
identifier = "Zino"
publisher = "Zino"
identifier = "zino"
publisher = "zino-rs"
icon = [
"public/icons/32x32.png",
"public/icons/128x128.png",
Expand Down

0 comments on commit 61f81b6

Please sign in to comment.