use crate::db; use crate::models::connection::{ConnectionConfig, DatabaseType}; use std::collections::HashSet; pub(super) fn filter_table_infos( tables: Vec, filter: Option<&str>, limit: Option, ) -> Vec { let filter = filter.unwrap_or("").to_lowercase(); let limit = limit.unwrap_or(usize::MAX); tables .into_iter() .filter(|table| filter.is_empty() || table.name.to_lowercase().contains(&filter)) .take(limit) .collect() } pub(super) fn filter_table_infos_for_config( tables: Vec, filter: Option<&str>, limit: Option, config: Option<&ConnectionConfig>, ) -> Vec { filter_table_infos(filter_yashandb_recyclebin_tables(tables, config), filter, limit) } pub(super) fn filter_yashandb_recyclebin_tables( tables: Vec, config: Option<&ConnectionConfig>, ) -> Vec { if !is_yashandb_config(config) { return tables; } tables.into_iter().filter(|table| !is_recyclebin_object_name(&table.name)).collect() } pub(super) fn filter_yashandb_recyclebin_objects( objects: Vec, config: Option<&ConnectionConfig>, ) -> Vec { if !is_yashandb_config(config) { return objects; } objects.into_iter().filter(|object| !is_recyclebin_object_name(&object.name)).collect() } fn is_yashandb_config(config: Option<&ConnectionConfig>) -> bool { config.is_some_and(|config| config.db_type == DatabaseType::Yashandb) } fn is_recyclebin_object_name(name: &str) -> bool { name.to_ascii_uppercase().starts_with("BIN$") } pub(super) fn filter_objects_by_types( objects: Vec, object_types: Option<&[String]>, ) -> Vec { let Some(object_types) = object_types else { return objects; }; if object_types.is_empty() { return objects; } let wanted: HashSet = object_types.iter().map(|object_type| normalize_object_info_type(object_type)).collect(); objects.into_iter().filter(|object| wanted.contains(&normalize_object_info_type(&object.object_type))).collect() } fn normalize_object_info_type(object_type: &str) -> String { let value = object_type.to_ascii_uppercase().replace(' ', "_"); if value.contains("PACKAGE_BODY") { "PACKAGE_BODY".to_string() } else if value.contains("PACKAGE") { "PACKAGE".to_string() } else if value.contains("VIEW") { "VIEW".to_string() } else if value.contains("PROC") { "PROCEDURE".to_string() } else if value.contains("FUNC") { "FUNCTION".to_string() } else { "TABLE".to_string() } } pub(super) fn filter_completion_objects(objects: Vec) -> Vec { objects .into_iter() .filter(|object| { let object_type = object.object_type.to_ascii_uppercase(); object_type.contains("PROCEDURE") || object_type.contains("FUNCTION") || object_type.contains("TRIGGER") }) .collect() } pub(super) fn deduplicate_column_infos(columns: Vec) -> Vec { let mut result: Vec = Vec::with_capacity(columns.len()); for column in columns { if let Some(existing) = result.iter_mut().find(|existing| existing.name == column.name) { existing.is_primary_key |= column.is_primary_key; existing.is_nullable &= column.is_nullable; merge_optional_string(&mut existing.column_default, column.column_default); merge_optional_string(&mut existing.extra, column.extra); merge_optional_string(&mut existing.comment, column.comment); if existing.numeric_precision.is_none() { existing.numeric_precision = column.numeric_precision; } if existing.numeric_scale.is_none() { existing.numeric_scale = column.numeric_scale; } if existing.character_maximum_length.is_none() { existing.character_maximum_length = column.character_maximum_length; } if existing.data_type.trim().is_empty() && !column.data_type.trim().is_empty() { existing.data_type = column.data_type; } } else { result.push(column); } } result } fn merge_optional_string(target: &mut Option, candidate: Option) { let Some(candidate) = candidate else { return; }; if candidate.trim().is_empty() { if target.is_none() { *target = Some(candidate); } return; } if target.as_ref().is_none_or(|value| value.trim().is_empty()) { *target = Some(candidate); } } #[cfg(test)] mod tests { use super::*; use crate::models::connection::{default_redis_key_separator, ConnectionConfig}; fn test_connection_config(db_type: DatabaseType) -> ConnectionConfig { ConnectionConfig { id: "test".to_string(), name: "test".to_string(), db_type, driver_profile: None, driver_label: None, url_params: None, agent_java_options: Vec::new(), host: "127.0.0.1".to_string(), port: 5432, username: "user".to_string(), password: "secret".to_string(), database: Some("demo".to_string()), default_schema: None, visible_databases: None, visible_database_patterns: None, visible_schemas: None, show_system_schemas: false, attached_databases: Vec::new(), init_script: None, color: None, transport_layers: Vec::new(), connect_timeout_secs: 5, query_timeout_secs: 30, idle_timeout_secs: 60, keepalive_interval_secs: 0, ssl: false, ca_cert_path: String::new(), client_cert_path: String::new(), client_key_path: String::new(), sysdba: false, oracle_connection_type: None, connection_string: None, redis_connection_mode: None, redis_sentinel_master: String::new(), redis_sentinel_nodes: String::new(), redis_sentinel_username: String::new(), redis_sentinel_password: String::new(), redis_sentinel_tls: false, redis_cluster_nodes: String::new(), redis_key_separator: default_redis_key_separator(), etcd_endpoints: String::new(), gbase_server: String::new(), informix_server: String::new(), external_config: None, jdbc_driver_class: None, jdbc_driver_paths: Vec::new(), one_time: false, save_password: true, read_only: false, is_production: false, production_databases: vec![], database_info: None, } } fn test_column(name: &str, comment: Option<&str>, is_primary_key: bool) -> db::ColumnInfo { db::ColumnInfo { name: name.to_string(), data_type: "VARCHAR".to_string(), is_nullable: true, column_default: None, is_primary_key, extra: None, comment: comment.map(|value| value.to_string()), numeric_precision: None, numeric_scale: None, character_maximum_length: None, ..Default::default() } } #[test] fn deduplicates_columns_and_preserves_later_comment() { let columns = deduplicate_column_infos(vec![ test_column("ID", None, false), test_column("ID", Some("source primary key"), true), test_column("TFBH", Some(""), false), test_column("TFBH", Some("ledger number"), false), ]); assert_eq!(columns.len(), 2); assert_eq!(columns[0].name, "ID"); assert_eq!(columns[0].comment.as_deref(), Some("source primary key")); assert!(columns[0].is_primary_key); assert_eq!(columns[1].name, "TFBH"); assert_eq!(columns[1].comment.as_deref(), Some("ledger number")); } #[test] fn filters_list_objects_by_normalized_object_types() { let objects = vec![ db::ObjectInfo { name: "orders".to_string(), object_type: "BASE TABLE".to_string(), schema: None, valid: None, signature: None, custom_type_kind: None, has_members: None, comment: None, created_at: None, updated_at: None, parent_schema: None, parent_name: None, }, db::ObjectInfo { name: "active_orders".to_string(), object_type: "MATERIALIZED_VIEW".to_string(), schema: None, valid: None, signature: None, custom_type_kind: None, has_members: None, comment: None, created_at: None, updated_at: None, parent_schema: None, parent_name: None, }, db::ObjectInfo { name: "payroll".to_string(), object_type: "PACKAGE BODY".to_string(), schema: None, valid: None, signature: None, custom_type_kind: None, has_members: None, comment: None, created_at: None, updated_at: None, parent_schema: None, parent_name: None, }, ]; let filtered = filter_objects_by_types(objects, Some(&["VIEW".to_string(), "PACKAGE_BODY".to_string()])); assert_eq!( filtered.iter().map(|object| object.name.as_str()).collect::>(), ["active_orders", "payroll"] ); } #[test] fn filters_yashandb_recyclebin_tables() { let tables = vec![ db::TableInfo { name: "USERS".to_string(), table_type: "TABLE".to_string(), comment: None, parent_schema: None, parent_name: None, }, db::TableInfo { name: "BIN$abc123==$0".to_string(), table_type: "TABLE".to_string(), comment: None, parent_schema: None, parent_name: None, }, ]; let filtered = filter_yashandb_recyclebin_tables(tables.clone(), Some(&test_connection_config(DatabaseType::Yashandb))); let oracle = filter_yashandb_recyclebin_tables(tables, Some(&test_connection_config(DatabaseType::Oracle))); assert_eq!(filtered.iter().map(|table| table.name.as_str()).collect::>(), ["USERS"]); assert_eq!(oracle.len(), 2); } #[test] fn filters_yashandb_recyclebin_objects() { let objects = vec![ db::ObjectInfo { name: "ORDERS".to_string(), object_type: "TABLE".to_string(), schema: Some("HR".to_string()), valid: None, signature: None, custom_type_kind: None, has_members: None, comment: None, created_at: None, updated_at: None, parent_schema: None, parent_name: None, }, db::ObjectInfo { name: "bin$deleted".to_string(), object_type: "TABLE".to_string(), schema: Some("HR".to_string()), valid: None, signature: None, custom_type_kind: None, has_members: None, comment: None, created_at: None, updated_at: None, parent_schema: None, parent_name: None, }, ]; let filtered = filter_yashandb_recyclebin_objects(objects, Some(&test_connection_config(DatabaseType::Yashandb))); assert_eq!(filtered.iter().map(|object| object.name.as_str()).collect::>(), ["ORDERS"]); } }