feat(grid): add database value filter
This commit is contained in:
1 parent
190b5b4ad8
commit
4c48263793
19 files changed
+700
-51
No files matched your search
@@ -11,12 +11,14 @@ mod data_grid_tdengine_sql;
|
||||
use data_grid_tdengine_sql::build_tdengine_data_grid_save_statements;
|
||||
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::sql_dialect::quote_table_identifier;
|
||||
use crate::sql_dialect::{quote_table_identifier, table_pagination_strategy, TablePaginationStrategy};
|
||||
use crate::transfer::{format_ch_array_sql_literal, format_pg_array_sql_literal};
|
||||
|
||||
const DBX_ROWID_COLUMN: &str = "__DBX_ROWID";
|
||||
pub(crate) const DBX_NEO4J_ELEMENT_ID_COLUMN: &str = "__DBX_ELEMENT_ID";
|
||||
pub(crate) const DBX_TDENGINE_TBNAME_COLUMN: &str = "tbname";
|
||||
const DATA_GRID_COLUMN_DISTINCT_VALUES_DEFAULT_LIMIT: usize = 1000;
|
||||
const DATA_GRID_COLUMN_DISTINCT_VALUES_MAX_LIMIT: usize = 1000;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -129,6 +131,39 @@ pub struct DataGridColumnValueFilterConditionOptions {
|
||||
pub raw_value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataGridColumnValuesFilterConditionOptions {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub database_type: Option<DatabaseType>,
|
||||
pub column_name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub column_info: Option<DataGridColumnInfo>,
|
||||
#[serde(default)]
|
||||
pub values: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataGridColumnDistinctValuesSqlOptions {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub database_type: Option<DatabaseType>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub schema: Option<String>,
|
||||
pub table_name: String,
|
||||
pub column_name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub column_info: Option<DataGridColumnInfo>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub where_input: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub search_value: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub include_counts: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataGridCountSqlOptions {
|
||||
@@ -387,6 +422,96 @@ pub fn build_data_grid_column_value_filter_condition(
|
||||
Some(format!("{column} = {}", format_grid_sql_literal(&value, options.database_type, options.column_info.as_ref())))
|
||||
}
|
||||
|
||||
pub fn build_data_grid_column_values_filter_condition(
|
||||
options: DataGridColumnValuesFilterConditionOptions,
|
||||
) -> Option<String> {
|
||||
if options.values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let mut has_null = false;
|
||||
let mut literals = Vec::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) {
|
||||
literals.push(literal);
|
||||
}
|
||||
}
|
||||
|
||||
let mut predicates = Vec::new();
|
||||
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})"));
|
||||
}
|
||||
}
|
||||
|
||||
match predicates.len() {
|
||||
0 => None,
|
||||
1 => predicates.into_iter().next(),
|
||||
_ => Some(format!("({})", predicates.join(" OR "))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_data_grid_column_distinct_values_sql(options: DataGridColumnDistinctValuesSqlOptions) -> String {
|
||||
if options.database_type == Some(DatabaseType::Neo4j) {
|
||||
return build_neo4j_data_grid_column_distinct_values_sql(&options);
|
||||
}
|
||||
|
||||
let limit = data_grid_column_distinct_values_limit(options.limit);
|
||||
let table = qualified_table_name(options.database_type, options.schema.as_deref(), &options.table_name);
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let mut predicates = Vec::new();
|
||||
let predicate = crate::sql_dialect::normalize_where_input(options.where_input.as_deref());
|
||||
if !predicate.is_empty() {
|
||||
predicates.push(format!("({predicate})"));
|
||||
}
|
||||
if let Some(search_predicate) = data_grid_column_distinct_values_search_predicate(&options) {
|
||||
predicates.push(search_predicate);
|
||||
}
|
||||
let where_clause =
|
||||
if predicates.is_empty() { String::new() } else { format!(" WHERE {}", predicates.join(" AND ")) };
|
||||
let select_list = if options.include_counts {
|
||||
format!("{column} AS dbx_value, COUNT(*) AS dbx_count")
|
||||
} else {
|
||||
format!("{column} AS dbx_value")
|
||||
};
|
||||
let group_by = format!(" GROUP BY {column}");
|
||||
let order_by = if options.include_counts { " ORDER BY dbx_count DESC, dbx_value" } else { " ORDER BY dbx_value" };
|
||||
let from_clause = format!(" FROM {table}{where_clause}{group_by}{order_by}");
|
||||
|
||||
match table_pagination_strategy(options.database_type) {
|
||||
TablePaginationStrategy::SqlServerTop => format!("SELECT TOP ({limit}) {select_list}{from_clause}"),
|
||||
TablePaginationStrategy::IrisTop => format!("SELECT TOP {limit} {select_list}{from_clause}"),
|
||||
TablePaginationStrategy::InformixFirst => format!("SELECT FIRST {limit} {select_list}{from_clause}"),
|
||||
TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => {
|
||||
format!("SELECT {select_list}{from_clause} FETCH FIRST {limit} ROWS ONLY")
|
||||
}
|
||||
TablePaginationStrategy::Rownum => {
|
||||
let inner = format!("SELECT {select_list}{from_clause}");
|
||||
format!("SELECT * FROM ({inner}) WHERE ROWNUM <= {limit}")
|
||||
}
|
||||
TablePaginationStrategy::AgentMaxRows | TablePaginationStrategy::Unbounded => {
|
||||
format!("SELECT {select_list}{from_clause}")
|
||||
}
|
||||
TablePaginationStrategy::QuestDbLimit | TablePaginationStrategy::LimitOffset => {
|
||||
format!("SELECT {select_list}{from_clause} LIMIT {limit}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_data_grid_count_sql(options: DataGridCountSqlOptions) -> String {
|
||||
let table = qualified_table_name(options.database_type, options.schema.as_deref(), &options.table_name);
|
||||
let predicate = crate::sql_dialect::normalize_where_input(options.where_input.as_deref());
|
||||
@@ -400,6 +525,60 @@ pub fn build_hive_table_properties_sql(options: HiveTablePropertiesSqlOptions) -
|
||||
format!("SHOW TBLPROPERTIES {table} ('{property}')")
|
||||
}
|
||||
|
||||
fn data_grid_column_distinct_values_limit(limit: Option<usize>) -> usize {
|
||||
limit.unwrap_or(DATA_GRID_COLUMN_DISTINCT_VALUES_DEFAULT_LIMIT).clamp(1, DATA_GRID_COLUMN_DISTINCT_VALUES_MAX_LIMIT)
|
||||
}
|
||||
|
||||
fn data_grid_column_distinct_values_search_predicate(
|
||||
options: &DataGridColumnDistinctValuesSqlOptions,
|
||||
) -> Option<String> {
|
||||
let search = options.search_value.as_deref()?.trim();
|
||||
if search.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if !options.column_info.as_ref().map(|column| is_textual_column_type(&column.data_type)).unwrap_or(true)
|
||||
&& !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());
|
||||
return Some(format!(
|
||||
"{column} = {}",
|
||||
format_grid_sql_literal(&value, options.database_type, options.column_info.as_ref())
|
||||
));
|
||||
}
|
||||
let column = column_like_filter_ref(options.database_type, &options.column_name, options.column_info.as_ref());
|
||||
let pattern = Value::String(format!("%{search}%"));
|
||||
Some(format!("{column} LIKE {}", format_grid_sql_literal(&pattern, options.database_type, None)))
|
||||
}
|
||||
|
||||
fn build_neo4j_data_grid_column_distinct_values_sql(options: &DataGridColumnDistinctValuesSqlOptions) -> String {
|
||||
let limit = data_grid_column_distinct_values_limit(options.limit);
|
||||
let label = quote_ident(Some(DatabaseType::Neo4j), &options.table_name);
|
||||
let column = column_filter_ref(Some(DatabaseType::Neo4j), &options.column_name);
|
||||
let mut predicates = Vec::new();
|
||||
let predicate = crate::sql_dialect::normalize_where_input(options.where_input.as_deref());
|
||||
if !predicate.is_empty() {
|
||||
predicates.push(predicate);
|
||||
}
|
||||
if let Some(search) = options.search_value.as_deref().map(str::trim).filter(|value| !value.is_empty()) {
|
||||
predicates.push(format!(
|
||||
"toString({column}) CONTAINS {}",
|
||||
format_grid_sql_literal(&Value::String(search.to_string()), Some(DatabaseType::Neo4j), None)
|
||||
));
|
||||
}
|
||||
let where_clause =
|
||||
if predicates.is_empty() { String::new() } else { format!(" WHERE {}", predicates.join(" AND ")) };
|
||||
if options.include_counts {
|
||||
format!(
|
||||
"MATCH (n:{label}){where_clause} RETURN {column} AS dbx_value, count(*) AS dbx_count ORDER BY dbx_count DESC, dbx_value LIMIT {limit}"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"MATCH (n:{label}){where_clause} RETURN DISTINCT {column} AS dbx_value ORDER BY dbx_value LIMIT {limit}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_data_grid_save(options: &DataGridSaveStatementOptions) -> Option<String> {
|
||||
if let Some(error) = validate_inserted_primary_keys(options) {
|
||||
return Some(error);
|
||||
@@ -1856,6 +2035,90 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_multi_value_filter_conditions() {
|
||||
assert_eq!(
|
||||
build_data_grid_column_values_filter_condition(DataGridColumnValuesFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
column_name: "status".to_string(),
|
||||
column_info: Some(column("status", "varchar", true, None)),
|
||||
values: vec![json!("active"), json!("pending"), Value::Null, json!("active")],
|
||||
})
|
||||
.as_deref(),
|
||||
Some("(\"status\" IS NULL OR \"status\" IN ('active', 'pending'))")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_values_filter_condition(DataGridColumnValuesFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
column_name: "id".to_string(),
|
||||
column_info: Some(column("id", "int", false, None)),
|
||||
values: vec![json!(42)],
|
||||
})
|
||||
.as_deref(),
|
||||
Some("`id` = 42")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_column_distinct_values_sql() {
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
schema: Some("public".to_string()),
|
||||
table_name: "users".to_string(),
|
||||
column_name: "status".to_string(),
|
||||
column_info: Some(column("status", "varchar", true, None)),
|
||||
where_input: Some("WHERE deleted_at IS NULL;".to_string()),
|
||||
search_value: Some("act".to_string()),
|
||||
limit: None,
|
||||
include_counts: true,
|
||||
}),
|
||||
"SELECT \"status\" AS dbx_value, COUNT(*) AS dbx_count FROM \"public\".\"users\" WHERE (deleted_at IS NULL) AND \"status\" LIKE '%act%' GROUP BY \"status\" ORDER BY dbx_count DESC, dbx_value LIMIT 1000"
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::SqlServer),
|
||||
schema: None,
|
||||
table_name: "users".to_string(),
|
||||
column_name: "status".to_string(),
|
||||
column_info: Some(column("status", "nvarchar", true, None)),
|
||||
where_input: None,
|
||||
search_value: None,
|
||||
limit: Some(25),
|
||||
include_counts: false,
|
||||
}),
|
||||
"SELECT TOP (25) [status] AS dbx_value FROM [users] GROUP BY [status] ORDER BY dbx_value"
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::SqlServer),
|
||||
schema: None,
|
||||
table_name: "users".to_string(),
|
||||
column_name: "id".to_string(),
|
||||
column_info: Some(column("id", "int", false, None)),
|
||||
where_input: None,
|
||||
search_value: Some("42".to_string()),
|
||||
limit: Some(25),
|
||||
include_counts: true,
|
||||
}),
|
||||
"SELECT TOP (25) [id] AS dbx_value, COUNT(*) AS dbx_count FROM [users] WHERE [id] = 42 GROUP BY [id] ORDER BY dbx_count DESC, dbx_value"
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
schema: Some("APP".to_string()),
|
||||
table_name: "EVENTS".to_string(),
|
||||
column_name: "KIND".to_string(),
|
||||
column_info: Some(column("KIND", "VARCHAR2", true, None)),
|
||||
where_input: None,
|
||||
search_value: None,
|
||||
limit: Some(10),
|
||||
include_counts: true,
|
||||
}),
|
||||
"SELECT * FROM (SELECT \"KIND\" AS dbx_value, COUNT(*) AS dbx_count FROM \"APP\".\"EVENTS\" GROUP BY \"KIND\" ORDER BY dbx_count DESC, dbx_value) WHERE ROWNUM <= 10"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_grid_count_sql() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user