feat(grid): add list and range filters
This commit is contained in:
1 parent
8710bc9069
commit
66d7b965dc
12 files changed
+802
-44
No files matched your search
@@ -1,6 +1,7 @@
|
||||
use chrono::{Local, NaiveDateTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[path = "data_grid_neo4j_sql.rs"]
|
||||
mod data_grid_neo4j_sql;
|
||||
@@ -126,6 +127,26 @@ pub enum DataGridContextFilterMode {
|
||||
NotLike,
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
In,
|
||||
NotIn,
|
||||
Between,
|
||||
NotBetween,
|
||||
}
|
||||
|
||||
fn supports_data_grid_context_filter_mode(
|
||||
database_type: Option<DatabaseType>,
|
||||
mode: DataGridContextFilterMode,
|
||||
) -> bool {
|
||||
!matches!(
|
||||
(database_type, mode),
|
||||
(
|
||||
Some(DatabaseType::InfluxDb | DatabaseType::Cassandra | DatabaseType::Jdbc),
|
||||
DataGridContextFilterMode::In
|
||||
| DataGridContextFilterMode::NotIn
|
||||
| DataGridContextFilterMode::Between
|
||||
| DataGridContextFilterMode::NotBetween
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -136,6 +157,10 @@ pub struct DataGridContextFilterConditionOptions {
|
||||
pub column_name: String,
|
||||
pub mode: DataGridContextFilterMode,
|
||||
pub value: Value,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub values: Vec<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub end_value: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub column_info: Option<DataGridColumnInfo>,
|
||||
}
|
||||
@@ -413,6 +438,10 @@ pub fn build_data_grid_copy_insert_statement(options: DataGridCopyInsertStatemen
|
||||
}
|
||||
|
||||
pub fn build_data_grid_context_filter_condition(options: DataGridContextFilterConditionOptions) -> Option<String> {
|
||||
if !supports_data_grid_context_filter_mode(options.database_type, options.mode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let like_column = column_like_filter_ref(options.database_type, &options.column_name, options.column_info.as_ref());
|
||||
let value = &options.value;
|
||||
@@ -445,6 +474,36 @@ pub fn build_data_grid_context_filter_condition(options: DataGridContextFilterCo
|
||||
"{column} > {}",
|
||||
format_grid_sql_literal(value, options.database_type, options.column_info.as_ref())
|
||||
)),
|
||||
DataGridContextFilterMode::In => build_data_grid_context_membership_filter_condition(
|
||||
&column,
|
||||
&options.values,
|
||||
options.database_type,
|
||||
options.column_info.as_ref(),
|
||||
false,
|
||||
),
|
||||
DataGridContextFilterMode::NotIn => build_data_grid_context_membership_filter_condition(
|
||||
&column,
|
||||
&options.values,
|
||||
options.database_type,
|
||||
options.column_info.as_ref(),
|
||||
true,
|
||||
),
|
||||
DataGridContextFilterMode::Between => build_data_grid_context_range_filter_condition(
|
||||
&column,
|
||||
value,
|
||||
options.end_value.as_ref(),
|
||||
options.database_type,
|
||||
options.column_info.as_ref(),
|
||||
false,
|
||||
),
|
||||
DataGridContextFilterMode::NotBetween => build_data_grid_context_range_filter_condition(
|
||||
&column,
|
||||
value,
|
||||
options.end_value.as_ref(),
|
||||
options.database_type,
|
||||
options.column_info.as_ref(),
|
||||
true,
|
||||
),
|
||||
DataGridContextFilterMode::Equals => Some(format!(
|
||||
"{column} = {}",
|
||||
format_grid_sql_literal(value, options.database_type, options.column_info.as_ref())
|
||||
@@ -456,6 +515,101 @@ pub fn build_data_grid_context_filter_condition(options: DataGridContextFilterCo
|
||||
}
|
||||
}
|
||||
|
||||
fn build_data_grid_context_membership_filter_condition(
|
||||
column: &str,
|
||||
values: &[Value],
|
||||
database_type: Option<DatabaseType>,
|
||||
column_info: Option<&DataGridColumnInfo>,
|
||||
negated: bool,
|
||||
) -> Option<String> {
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut has_null = false;
|
||||
let mut literals = Vec::new();
|
||||
let mut seen_literals = HashSet::new();
|
||||
for value in values {
|
||||
if value.is_null() {
|
||||
has_null = true;
|
||||
continue;
|
||||
}
|
||||
let literal = format_grid_sql_literal(value, database_type, column_info);
|
||||
if seen_literals.insert(literal.clone()) {
|
||||
literals.push(literal);
|
||||
}
|
||||
}
|
||||
|
||||
let membership = build_membership_predicate(column, &literals, database_type, negated);
|
||||
|
||||
if negated {
|
||||
return match membership {
|
||||
Some(membership) => Some(format!("({column} IS NOT NULL AND {membership})")),
|
||||
None if has_null => Some(format!("{column} IS NOT NULL")),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
match membership {
|
||||
Some(membership) if has_null => Some(format!("({column} IS NULL OR {membership})")),
|
||||
Some(membership) => Some(membership),
|
||||
None if has_null => Some(format!("{column} IS NULL")),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_membership_predicate(
|
||||
column: &str,
|
||||
literals: &[String],
|
||||
database_type: Option<DatabaseType>,
|
||||
negated: bool,
|
||||
) -> Option<String> {
|
||||
if literals.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if database_type == Some(DatabaseType::Neo4j) {
|
||||
let predicate = format!("{column} IN [{}]", literals.join(", "));
|
||||
return Some(if negated { format!("NOT ({predicate})") } else { predicate });
|
||||
}
|
||||
|
||||
let operator = if negated { "NOT IN" } else { "IN" };
|
||||
if database_type != Some(DatabaseType::Oracle) || literals.len() <= 1000 {
|
||||
return Some(format!("{column} {operator} ({})", literals.join(", ")));
|
||||
}
|
||||
|
||||
// Oracle limits each IN expression to 1000 values; preserve NOT IN semantics
|
||||
// by joining its chunks with AND instead of OR.
|
||||
let joiner = if negated { " AND " } else { " OR " };
|
||||
let chunks =
|
||||
literals.chunks(1000).map(|chunk| format!("{column} {operator} ({})", chunk.join(", "))).collect::<Vec<_>>();
|
||||
Some(format!("({})", chunks.join(joiner)))
|
||||
}
|
||||
|
||||
fn build_data_grid_context_range_filter_condition(
|
||||
column: &str,
|
||||
start_value: &Value,
|
||||
end_value: Option<&Value>,
|
||||
database_type: Option<DatabaseType>,
|
||||
column_info: Option<&DataGridColumnInfo>,
|
||||
negated: bool,
|
||||
) -> Option<String> {
|
||||
let end_value = end_value?;
|
||||
if start_value.is_null() || end_value.is_null() {
|
||||
return None;
|
||||
}
|
||||
let start = format_grid_sql_literal(start_value, database_type, column_info);
|
||||
let end = format_grid_sql_literal(end_value, database_type, column_info);
|
||||
if database_type == Some(DatabaseType::Neo4j) {
|
||||
return if negated {
|
||||
Some(format!("({column} < {start} OR {column} > {end})"))
|
||||
} else {
|
||||
Some(format!("({column} >= {start} AND {column} <= {end})"))
|
||||
};
|
||||
}
|
||||
let operator = if negated { "NOT BETWEEN" } else { "BETWEEN" };
|
||||
Some(format!("{column} {operator} {start} AND {end}"))
|
||||
}
|
||||
|
||||
pub fn build_data_grid_column_value_filter_condition(
|
||||
options: DataGridColumnValueFilterConditionOptions,
|
||||
) -> Option<String> {
|
||||
@@ -467,7 +621,7 @@ pub fn build_data_grid_column_value_filter_condition(
|
||||
if text.eq_ignore_ascii_case("null") {
|
||||
return Some(format!("{column} IS NULL"));
|
||||
}
|
||||
let value = parse_typed_filter_value(text, options.column_info.as_ref());
|
||||
let value = parse_typed_filter_value(text, options.database_type, options.column_info.as_ref());
|
||||
Some(format!("{column} = {}", format_grid_sql_literal(&value, options.database_type, options.column_info.as_ref())))
|
||||
}
|
||||
|
||||
@@ -481,13 +635,14 @@ pub fn build_data_grid_column_values_filter_condition(
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let mut has_null = false;
|
||||
let mut literals = Vec::new();
|
||||
let mut seen_literals = HashSet::new();
|
||||
for value in &options.values {
|
||||
if value.is_null() {
|
||||
has_null = true;
|
||||
continue;
|
||||
}
|
||||
let literal = format_grid_sql_literal(value, options.database_type, options.column_info.as_ref());
|
||||
if !literals.contains(&literal) {
|
||||
if seen_literals.insert(literal.clone()) {
|
||||
literals.push(literal);
|
||||
}
|
||||
}
|
||||
@@ -496,15 +651,10 @@ pub fn build_data_grid_column_values_filter_condition(
|
||||
if has_null {
|
||||
predicates.push(format!("{column} IS NULL"));
|
||||
}
|
||||
if !literals.is_empty() {
|
||||
let list = literals.join(", ");
|
||||
if literals.len() == 1 {
|
||||
predicates.push(format!("{column} = {list}"));
|
||||
} else if options.database_type == Some(DatabaseType::Neo4j) {
|
||||
predicates.push(format!("{column} IN [{list}]"));
|
||||
} else {
|
||||
predicates.push(format!("{column} IN ({list})"));
|
||||
}
|
||||
if literals.len() == 1 {
|
||||
predicates.push(format!("{column} = {}", literals[0]));
|
||||
} else if let Some(membership) = build_membership_predicate(&column, &literals, options.database_type, false) {
|
||||
predicates.push(membership);
|
||||
}
|
||||
|
||||
match predicates.len() {
|
||||
@@ -603,7 +753,7 @@ fn data_grid_column_distinct_values_search_predicate(
|
||||
&& !is_postgres_like_pattern_database(options.database_type)
|
||||
{
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let value = parse_typed_filter_value(search, options.column_info.as_ref());
|
||||
let value = parse_typed_filter_value(search, options.database_type, options.column_info.as_ref());
|
||||
return Some(format!(
|
||||
"{column} = {}",
|
||||
format_grid_sql_literal(&value, options.database_type, options.column_info.as_ref())
|
||||
@@ -1119,7 +1269,7 @@ pub fn format_grid_sql_literal(
|
||||
// is a numeric/boolean type rather than a bit-string type like
|
||||
// PostgreSQL's bit(n).
|
||||
if let Some(value) = value.as_bool() {
|
||||
if is_bit_literal_column(column_info) {
|
||||
if is_bit_literal_column(database_type, column_info) {
|
||||
return if value { "1" } else { "0" }.to_string();
|
||||
}
|
||||
return if value { "TRUE" } else { "FALSE" }.to_string();
|
||||
@@ -1190,7 +1340,12 @@ pub fn format_grid_sql_literal(
|
||||
} else {
|
||||
text
|
||||
};
|
||||
let escaped = format!("'{}'", literal_text.replace('\\', "\\\\").replace('\'', "''"));
|
||||
let escaped_text = if database_type == Some(DatabaseType::Neo4j) {
|
||||
literal_text.replace('\\', "\\\\").replace('\'', "\\'")
|
||||
} else {
|
||||
literal_text.replace('\\', "\\\\").replace('\'', "''")
|
||||
};
|
||||
let escaped = format!("'{escaped_text}'");
|
||||
if database_type == Some(DatabaseType::SqlServer) {
|
||||
format!("N{escaped}")
|
||||
} else {
|
||||
@@ -1339,8 +1494,9 @@ fn is_mysql_bit_literal_column(database_type: Option<DatabaseType>, column_info:
|
||||
&& column_info.map(|column| is_bit_column_type(&column.data_type)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_bit_literal_column(column_info: Option<&DataGridColumnInfo>) -> bool {
|
||||
column_info.map(|column| is_bit_column_type(&column.data_type)).unwrap_or(false)
|
||||
fn is_bit_literal_column(database_type: Option<DatabaseType>, column_info: Option<&DataGridColumnInfo>) -> bool {
|
||||
database_type != Some(DatabaseType::Postgres)
|
||||
&& column_info.map(|column| is_bit_column_type(&column.data_type)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_bit_column_type(data_type: &str) -> bool {
|
||||
@@ -1407,10 +1563,10 @@ fn is_geometry_column_type(data_type: &str) -> bool {
|
||||
|
||||
fn manticore_typed_attribute_value(text: &str, column_info: Option<&DataGridColumnInfo>) -> Option<Value> {
|
||||
let data_type = column_info?.data_type.to_ascii_lowercase();
|
||||
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("true") {
|
||||
if is_boolean_type(&data_type, None) && text.eq_ignore_ascii_case("true") {
|
||||
return Some(Value::Bool(true));
|
||||
}
|
||||
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("false") {
|
||||
if is_boolean_type(&data_type, None) && text.eq_ignore_ascii_case("false") {
|
||||
return Some(Value::Bool(false));
|
||||
}
|
||||
if is_numeric_type(&data_type) && is_numeric_literal(text) {
|
||||
@@ -2005,13 +2161,17 @@ fn value_to_filter_text(value: &Value) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_typed_filter_value(text: &str, column_info: Option<&DataGridColumnInfo>) -> Value {
|
||||
fn parse_typed_filter_value(
|
||||
text: &str,
|
||||
database_type: Option<DatabaseType>,
|
||||
column_info: Option<&DataGridColumnInfo>,
|
||||
) -> Value {
|
||||
let unquoted = unwrap_matching_quotes(text);
|
||||
let data_type = column_info.map(|column| column.data_type.to_ascii_lowercase()).unwrap_or_default();
|
||||
if is_boolean_type(&data_type) && unquoted.eq_ignore_ascii_case("true") {
|
||||
if is_boolean_type(&data_type, database_type) && unquoted.eq_ignore_ascii_case("true") {
|
||||
return Value::Bool(true);
|
||||
}
|
||||
if is_boolean_type(&data_type) && unquoted.eq_ignore_ascii_case("false") {
|
||||
if is_boolean_type(&data_type, database_type) && unquoted.eq_ignore_ascii_case("false") {
|
||||
return Value::Bool(false);
|
||||
}
|
||||
if (is_numeric_type(&data_type) || data_type.is_empty()) && is_numeric_literal(&unquoted) {
|
||||
@@ -2059,11 +2219,12 @@ fn is_numeric_type(data_type: &str) -> bool {
|
||||
.any(|part| lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == *part))
|
||||
}
|
||||
|
||||
fn is_boolean_type(data_type: &str) -> bool {
|
||||
fn is_boolean_type(data_type: &str, database_type: Option<DatabaseType>) -> bool {
|
||||
let lower = data_type.to_ascii_lowercase();
|
||||
lower
|
||||
.split(|ch: char| !ch.is_ascii_alphanumeric())
|
||||
.any(|token| matches!(token, "bool" | "boolean" | "bit" | "bitn"))
|
||||
lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| {
|
||||
matches!(token, "bool" | "boolean")
|
||||
|| (matches!(token, "bit" | "bitn") && database_type != Some(DatabaseType::Postgres))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_numeric_literal(text: &str) -> bool {
|
||||
@@ -2401,6 +2562,8 @@ mod tests {
|
||||
column_name: "status".to_string(),
|
||||
mode: DataGridContextFilterMode::Like,
|
||||
value: json!("active"),
|
||||
values: Vec::new(),
|
||||
end_value: None,
|
||||
column_info: Some(column("status", "varchar", true, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
@@ -2412,6 +2575,8 @@ mod tests {
|
||||
column_name: "update_date".to_string(),
|
||||
mode: DataGridContextFilterMode::Like,
|
||||
value: json!("128"),
|
||||
values: Vec::new(),
|
||||
end_value: None,
|
||||
column_info: Some(column("update_date", "bigint", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
@@ -2423,6 +2588,8 @@ mod tests {
|
||||
column_name: "created_at".to_string(),
|
||||
mode: DataGridContextFilterMode::NotLike,
|
||||
value: json!("2026"),
|
||||
values: Vec::new(),
|
||||
end_value: None,
|
||||
column_info: Some(column("created_at", "timestamp without time zone", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
@@ -2440,6 +2607,247 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_membership_and_range_context_filter_conditions() {
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
column_name: "id".to_string(),
|
||||
mode: DataGridContextFilterMode::In,
|
||||
value: Value::Null,
|
||||
values: vec![json!(42), json!(99)],
|
||||
end_value: None,
|
||||
column_info: Some(column("id", "int", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("`id` IN (42, 99)")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
column_name: "status".to_string(),
|
||||
mode: DataGridContextFilterMode::In,
|
||||
value: Value::Null,
|
||||
values: vec![Value::Null, json!("active"), json!("pending"), json!("active")],
|
||||
end_value: None,
|
||||
column_info: Some(column("status", "varchar", true, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("(\"status\" IS NULL OR \"status\" IN ('active', 'pending'))")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
column_name: "status".to_string(),
|
||||
mode: DataGridContextFilterMode::NotIn,
|
||||
value: Value::Null,
|
||||
values: vec![Value::Null, json!("active"), json!("pending")],
|
||||
end_value: None,
|
||||
column_info: Some(column("status", "varchar", true, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("(\"status\" IS NOT NULL AND \"status\" NOT IN ('active', 'pending'))")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Neo4j),
|
||||
column_name: "name".to_string(),
|
||||
mode: DataGridContextFilterMode::In,
|
||||
value: Value::Null,
|
||||
values: vec![json!("O'Reilly"), json!(r"C:\temp")],
|
||||
end_value: None,
|
||||
column_info: Some(column("name", "string", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some(r#"n.`name` IN ['O\'Reilly', 'C:\\temp']"#)
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::SqlServer),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::Between,
|
||||
value: json!(10),
|
||||
values: Vec::new(),
|
||||
end_value: Some(json!(20)),
|
||||
column_info: Some(column("score", "int", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("[score] BETWEEN 10 AND 20")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::SqlServer),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::NotBetween,
|
||||
value: json!(10),
|
||||
values: Vec::new(),
|
||||
end_value: Some(json!(20)),
|
||||
column_info: Some(column("score", "int", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("[score] NOT BETWEEN 10 AND 20")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_neo4j_membership_and_range_context_filter_conditions() {
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Neo4j),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::In,
|
||||
value: Value::Null,
|
||||
values: vec![json!(10), json!(20)],
|
||||
end_value: None,
|
||||
column_info: Some(column("score", "integer", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("n.`score` IN [10, 20]")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Neo4j),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::NotIn,
|
||||
value: Value::Null,
|
||||
values: vec![json!(10), json!(20)],
|
||||
end_value: None,
|
||||
column_info: Some(column("score", "integer", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("(n.`score` IS NOT NULL AND NOT (n.`score` IN [10, 20]))")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Neo4j),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::Between,
|
||||
value: json!(10),
|
||||
values: Vec::new(),
|
||||
end_value: Some(json!(20)),
|
||||
column_info: Some(column("score", "integer", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("(n.`score` >= 10 AND n.`score` <= 20)")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Neo4j),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::NotBetween,
|
||||
value: json!(10),
|
||||
values: Vec::new(),
|
||||
end_value: Some(json!(20)),
|
||||
column_info: Some(column("score", "integer", false, None)),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("(n.`score` < 10 OR n.`score` > 20)")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_emit_membership_or_range_filters_for_unsupported_dialects() {
|
||||
for database_type in [DatabaseType::InfluxDb, DatabaseType::Cassandra, DatabaseType::Jdbc] {
|
||||
for mode in [
|
||||
DataGridContextFilterMode::In,
|
||||
DataGridContextFilterMode::NotIn,
|
||||
DataGridContextFilterMode::Between,
|
||||
DataGridContextFilterMode::NotBetween,
|
||||
] {
|
||||
let (value, values, end_value) = match mode {
|
||||
DataGridContextFilterMode::In | DataGridContextFilterMode::NotIn => {
|
||||
(Value::Null, vec![json!(10), json!(20)], None)
|
||||
}
|
||||
DataGridContextFilterMode::Between | DataGridContextFilterMode::NotBetween => {
|
||||
(json!(10), Vec::new(), Some(json!(20)))
|
||||
}
|
||||
_ => unreachable!("only membership and range modes are tested"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(database_type),
|
||||
column_name: "score".to_string(),
|
||||
mode,
|
||||
value,
|
||||
values,
|
||||
end_value,
|
||||
column_info: Some(column("score", "int", false, None)),
|
||||
}),
|
||||
None,
|
||||
"{database_type:?} must not emit {mode:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_membership_and_range_filters_require_complete_values() {
|
||||
let column_info = Some(column("score", "int", false, None));
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::In,
|
||||
value: Value::Null,
|
||||
values: Vec::new(),
|
||||
end_value: None,
|
||||
column_info: column_info.clone(),
|
||||
}),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::Between,
|
||||
value: json!(10),
|
||||
values: Vec::new(),
|
||||
end_value: None,
|
||||
column_info: column_info.clone(),
|
||||
}),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::Between,
|
||||
value: Value::Null,
|
||||
values: Vec::new(),
|
||||
end_value: Some(json!(20)),
|
||||
column_info: column_info.clone(),
|
||||
}),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
column_name: "score".to_string(),
|
||||
mode: DataGridContextFilterMode::NotBetween,
|
||||
value: json!(10),
|
||||
values: Vec::new(),
|
||||
end_value: Some(Value::Null),
|
||||
column_info,
|
||||
}),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_filter_options_default_new_fields_when_deserializing_old_requests() {
|
||||
let options: DataGridContextFilterConditionOptions = serde_json::from_value(json!({
|
||||
"databaseType": "mysql",
|
||||
"columnName": "id",
|
||||
"mode": "equals",
|
||||
"value": 42,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert!(options.values.is_empty());
|
||||
assert_eq!(options.end_value, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_multi_value_filter_conditions() {
|
||||
assert_eq!(
|
||||
@@ -2464,6 +2872,63 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunks_large_oracle_membership_filters_with_correct_boolean_operator() {
|
||||
let values = (0..=1000).map(|value| json!(value)).collect::<Vec<_>>();
|
||||
let in_condition = build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
column_name: "id".to_string(),
|
||||
mode: DataGridContextFilterMode::In,
|
||||
value: Value::Null,
|
||||
values: values.clone(),
|
||||
end_value: None,
|
||||
column_info: Some(column("id", "number", false, None)),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(in_condition.matches("\"id\" IN (").count(), 2);
|
||||
assert!(in_condition.contains(") OR \"id\" IN ("));
|
||||
|
||||
let not_in_condition = build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
column_name: "id".to_string(),
|
||||
mode: DataGridContextFilterMode::NotIn,
|
||||
value: Value::Null,
|
||||
values,
|
||||
end_value: None,
|
||||
column_info: Some(column("id", "number", false, None)),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(not_in_condition.matches("\"id\" NOT IN (").count(), 2);
|
||||
assert!(not_in_condition.contains(") AND \"id\" NOT IN ("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_postgres_bit_strings_out_of_boolean_literal_handling() {
|
||||
let bit = column("flags", "bit(3)", false, None);
|
||||
let varying = column("flags", "bit varying", false, None);
|
||||
|
||||
assert_eq!(parse_typed_filter_value("true", Some(DatabaseType::Postgres), Some(&bit)), json!("true"));
|
||||
assert_eq!(parse_typed_filter_value("false", Some(DatabaseType::Postgres), Some(&varying)), json!("false"));
|
||||
assert_eq!(
|
||||
build_data_grid_column_value_filter_condition(DataGridColumnValueFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
column_name: "flags".to_string(),
|
||||
column_info: Some(bit),
|
||||
raw_value: "true".to_string(),
|
||||
})
|
||||
.as_deref(),
|
||||
Some("\"flags\" = 'true'")
|
||||
);
|
||||
assert_eq!(
|
||||
format_grid_sql_literal(
|
||||
&json!(true),
|
||||
Some(DatabaseType::SqlServer),
|
||||
Some(&column("flag", "bit", false, None))
|
||||
),
|
||||
"1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_column_distinct_values_sql() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user