feat(grid): add database value filter

This commit is contained in:
t8y2 committed 2026-06-29 03:13:35 +08:00
1 parent 190b5b4ad8
commit 4c48263793
19 files changed
+700 -51

No files matched your search

+264 -1
View File
@@ -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!(