Files
dbx/crates/dbx-core/src/schema/normalization.rs
T

356 lines
12 KiB
Rust

use crate::db;
use crate::models::connection::{ConnectionConfig, DatabaseType};
use std::collections::HashSet;
pub(super) fn filter_table_infos(
tables: Vec<db::TableInfo>,
filter: Option<&str>,
limit: Option<usize>,
) -> Vec<db::TableInfo> {
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<db::TableInfo>,
filter: Option<&str>,
limit: Option<usize>,
config: Option<&ConnectionConfig>,
) -> Vec<db::TableInfo> {
filter_table_infos(filter_yashandb_recyclebin_tables(tables, config), filter, limit)
}
pub(super) fn filter_yashandb_recyclebin_tables(
tables: Vec<db::TableInfo>,
config: Option<&ConnectionConfig>,
) -> Vec<db::TableInfo> {
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<db::ObjectInfo>,
config: Option<&ConnectionConfig>,
) -> Vec<db::ObjectInfo> {
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<db::ObjectInfo>,
object_types: Option<&[String]>,
) -> Vec<db::ObjectInfo> {
let Some(object_types) = object_types else {
return objects;
};
if object_types.is_empty() {
return objects;
}
let wanted: HashSet<String> =
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<db::ObjectInfo>) -> Vec<db::ObjectInfo> {
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<db::ColumnInfo>) -> Vec<db::ColumnInfo> {
let mut result: Vec<db::ColumnInfo> = 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<String>, candidate: Option<String>) {
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::<Vec<_>>(),
["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::<Vec<_>>(), ["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::<Vec<_>>(), ["ORDERS"]);
}
}