use chrono::{Local, NaiveDateTime}; use serde::{Deserialize, Serialize}; use serde_json::Value; #[path = "data_grid_neo4j_sql.rs"] mod data_grid_neo4j_sql; use data_grid_neo4j_sql::{build_neo4j_data_grid_rollback_statements, build_neo4j_data_grid_save_statements}; #[path = "data_grid_tdengine_sql.rs"] 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::transfer::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"; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridTableMeta { #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, #[serde(default)] pub primary_keys: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub columns: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DataGridColumnInfo { pub name: String, #[serde(default)] pub data_type: String, #[serde(default)] pub is_nullable: bool, #[serde(default)] pub is_primary_key: bool, #[serde(default)] pub column_default: Option, #[serde(default)] pub extra: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridSaveStatementOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub table_meta: DataGridTableMeta, pub columns: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub source_columns: Option>>, #[serde(default)] pub rows: Vec>, #[serde(default)] pub dirty_rows: Vec<(usize, Vec<(usize, Value)>)>, #[serde(default)] pub deleted_rows: Vec, #[serde(default)] pub new_rows: Vec>, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridCopyUpdateStatementOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub table_meta: DataGridTableMeta, pub columns: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub source_columns: Option>>, #[serde(default)] pub rows: Vec>, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridCopyInsertStatementOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub table_meta: Option, pub columns: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub source_columns: Option>>, #[serde(default)] pub rows: Vec>, #[serde(default)] pub exclude_primary_keys: bool, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum DataGridContextFilterMode { Equals, NotEquals, IsNull, IsNotNull, Like, NotLike, LessThan, GreaterThan, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridContextFilterConditionOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub column_name: String, pub mode: DataGridContextFilterMode, pub value: Value, #[serde(default, skip_serializing_if = "Option::is_none")] pub column_info: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridColumnValueFilterConditionOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub column_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub column_info: Option, pub raw_value: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridCountSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub where_input: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HiveTablePropertiesSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, pub property_name: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DataGridSavePreparation { #[serde(skip_serializing_if = "Option::is_none")] pub validation_error: Option, pub statements: Vec, pub rollback_statements: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub execution_schema: Option, } pub fn prepare_data_grid_save(options: DataGridSaveStatementOptions) -> DataGridSavePreparation { let validation_error = validate_data_grid_save(&options); if validation_error.is_some() { return DataGridSavePreparation { validation_error, statements: Vec::new(), rollback_statements: Vec::new(), execution_schema: data_grid_save_execution_schema(options.database_type, &options.table_meta), }; } DataGridSavePreparation { validation_error: None, statements: build_data_grid_save_statements(&options), rollback_statements: build_data_grid_rollback_statements(&options), execution_schema: data_grid_save_execution_schema(options.database_type, &options.table_meta), } } pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStatementOptions) -> Vec { if matches!(options.database_type, Some(DatabaseType::Neo4j | DatabaseType::Tdengine)) { return Vec::new(); } let primary_keys = &options.table_meta.primary_keys; if primary_keys.is_empty() { return Vec::new(); } let save_columns = effective_copy_columns(options.source_columns.as_deref(), &options.columns); let column_info = options.table_meta.columns.as_deref().unwrap_or(&[]); let primary_key_indexes: Vec> = primary_keys.iter().map(|primary_key| find_column_index(&save_columns, primary_key)).collect(); if primary_key_indexes.iter().any(Option::is_none) { return Vec::new(); } let primary_key_indexes: Vec = primary_key_indexes.into_iter().flatten().collect(); let primary_key_set: Vec = primary_keys.iter().map(|primary_key| normalize_column_name(primary_key)).collect(); let writable_indexes: Vec<(&str, usize)> = save_columns .iter() .enumerate() .filter_map(|(index, column)| Some((column.as_deref()?, index))) .filter(|(column, _)| !primary_key_set.contains(&normalize_column_name(column))) .filter(|(column, _)| !is_oracle_row_id(options.database_type, Some(column))) .collect(); if writable_indexes.is_empty() { return Vec::new(); } let table = qualified_table_name( options.database_type, options.table_meta.schema.as_deref(), &options.table_meta.table_name, ); let mut statements = Vec::new(); for row in &options.rows { if primary_key_indexes.iter().any(|index| row.get(*index).unwrap_or(&Value::Null).is_null()) { continue; } let sets = writable_indexes .iter() .map(|(column, index)| { format!( "{} = {}", quote_ident(options.database_type, column), format_grid_sql_literal( row.get(*index).unwrap_or(&Value::Null), options.database_type, column_info_for(column_info, column) ) ) }) .collect::>() .join(", "); if sets.is_empty() { continue; } let where_clause = primary_keys .iter() .enumerate() .map(|(index, primary_key)| { build_column_predicate( options.database_type, primary_key, row.get(primary_key_indexes[index]).unwrap_or(&Value::Null), column_info_for(column_info, primary_key), ) }) .collect::>() .join(" AND "); statements.push(format!("UPDATE {table} SET {sets} WHERE {where_clause};")); } statements } pub fn build_data_grid_copy_insert_statement(options: DataGridCopyInsertStatementOptions) -> Option { let save_columns = effective_copy_columns(options.source_columns.as_deref(), &options.columns); let column_info = options.table_meta.as_ref().and_then(|meta| meta.columns.as_deref()).unwrap_or(&[]); let primary_key_set: Vec = options .table_meta .as_ref() .map(|meta| meta.primary_keys.iter().map(|primary_key| normalize_column_name(primary_key)).collect()) .unwrap_or_default(); let insertable_columns: Vec<(&str, usize)> = save_columns .iter() .enumerate() .filter_map(|(index, column)| Some((column.as_deref()?, index))) .filter(|(column, _)| !is_oracle_row_id(options.database_type, Some(column))) .collect(); let insert_columns: Vec<(&str, usize)> = insertable_columns .iter() .copied() .filter(|(column, _)| { !options.exclude_primary_keys || !primary_key_set.contains(&normalize_column_name(column)) }) .collect(); if options.exclude_primary_keys && insert_columns.len() == insertable_columns.len() { return None; } if insert_columns.is_empty() || options.rows.is_empty() { return None; } let table = options.table_meta.as_ref().map_or_else( || "table_name".to_string(), |meta| qualified_table_name(options.database_type, meta.schema.as_deref(), &meta.table_name), ); let columns = insert_columns .iter() .map(|(column, _)| quote_ident(options.database_type, column)) .collect::>() .join(", "); let value_rows = options .rows .iter() .map(|row| { format!( "({})", insert_columns .iter() .map(|(column, index)| { format_grid_sql_literal( row.get(*index).unwrap_or(&Value::Null), options.database_type, column_info_for(column_info, column), ) }) .collect::>() .join(", ") ) }) .collect::>(); Some(format!( "INSERT INTO {table} ({columns}) VALUES{}{};", if value_rows.len() == 1 { " " } else { "\n" }, value_rows.join(",\n") )) } pub fn build_data_grid_context_filter_condition(options: DataGridContextFilterConditionOptions) -> Option { let column = column_filter_ref(options.database_type, &options.column_name); let value = &options.value; match options.mode { DataGridContextFilterMode::IsNull => Some(format!("{column} IS NULL")), DataGridContextFilterMode::IsNotNull => Some(format!("{column} IS NOT NULL")), DataGridContextFilterMode::Equals if value.is_null() => Some(format!("{column} IS NULL")), DataGridContextFilterMode::NotEquals if value.is_null() => Some(format!("{column} IS NOT NULL")), DataGridContextFilterMode::Like => Some(format!( "{column} LIKE {}", format_grid_sql_literal( &Value::String(format!("%{}%", value_to_filter_text(value))), options.database_type, None ) )), DataGridContextFilterMode::NotLike => Some(format!( "{column} NOT LIKE {}", format_grid_sql_literal( &Value::String(format!("%{}%", value_to_filter_text(value))), options.database_type, None ) )), DataGridContextFilterMode::LessThan => Some(format!( "{column} < {}", format_grid_sql_literal(value, options.database_type, options.column_info.as_ref()) )), DataGridContextFilterMode::GreaterThan => Some(format!( "{column} > {}", format_grid_sql_literal(value, options.database_type, options.column_info.as_ref()) )), DataGridContextFilterMode::Equals => Some(format!( "{column} = {}", format_grid_sql_literal(value, options.database_type, options.column_info.as_ref()) )), DataGridContextFilterMode::NotEquals => Some(format!( "{column} <> {}", format_grid_sql_literal(value, options.database_type, options.column_info.as_ref()) )), } } pub fn build_data_grid_column_value_filter_condition( options: DataGridColumnValueFilterConditionOptions, ) -> Option { let text = options.raw_value.trim(); if text.is_empty() { return None; } let column = column_filter_ref(options.database_type, &options.column_name); 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()); Some(format!("{column} = {}", format_grid_sql_literal(&value, options.database_type, options.column_info.as_ref()))) } 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()); let where_clause = if predicate.is_empty() { String::new() } else { format!(" WHERE ({predicate})") }; format!("SELECT COUNT(*) AS cnt FROM {table}{where_clause}") } pub fn build_hive_table_properties_sql(options: HiveTablePropertiesSqlOptions) -> String { let table = qualified_table_name(Some(DatabaseType::Hive), options.schema.as_deref(), &options.table_name); let property = options.property_name.replace('\'', "''"); format!("SHOW TBLPROPERTIES {table} ('{property}')") } fn validate_data_grid_save(options: &DataGridSaveStatementOptions) -> Option { if let Some(error) = validate_inserted_primary_keys(options) { return Some(error); } let not_null_columns: Vec = options .table_meta .columns .as_deref() .unwrap_or(&[]) .iter() .filter(|column| { !column.is_nullable && column.column_default.is_none() && !is_auto_generated_column(column) && !is_oracle_row_id(options.database_type, Some(&column.name)) }) .map(|column| normalize_column_name(&column.name)) .collect(); if not_null_columns.is_empty() { return None; } for (_, changes) in &options.dirty_rows { for (column_index, value) in changes { let source_column = effective_column(options, *column_index); if is_null_write_to_not_null_column(options.database_type, ¬_null_columns, source_column, value) { return Some(null_write_error(source_column.unwrap_or_default())); } } } for row in &options.new_rows { for column_index in 0..options.columns.len() { let source_column = effective_column(options, column_index); if is_null_write_to_not_null_column( options.database_type, ¬_null_columns, source_column, row.get(column_index).unwrap_or(&Value::Null), ) { return Some(null_write_error(source_column.unwrap_or_default())); } } } None } fn validate_inserted_primary_keys(options: &DataGridSaveStatementOptions) -> Option { let primary_keys = &options.table_meta.primary_keys; if primary_keys.is_empty() || options.new_rows.is_empty() { return None; } let save_columns = effective_columns(options); let primary_key_indexes: Vec> = primary_keys.iter().map(|primary_key| find_column_index(&save_columns, primary_key)).collect(); if primary_key_indexes.iter().any(Option::is_none) { return None; } let primary_key_indexes: Vec = primary_key_indexes.into_iter().flatten().collect(); let mut existing_keys: Vec = Vec::new(); for row in &options.rows { if let Some(key) = primary_key_value_key(&primary_key_indexes, row) { existing_keys.push(key); } } let mut new_keys: Vec = Vec::new(); for row in &options.new_rows { let Some(key) = primary_key_value_key(&primary_key_indexes, row) else { continue; }; if existing_keys.contains(&key) || new_keys.contains(&key) { return Some(duplicate_primary_key_error( primary_keys, &primary_key_indexes, row, existing_keys.contains(&key), )); } new_keys.push(key); } None } fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Vec { if options.database_type == Some(DatabaseType::Neo4j) { return build_neo4j_data_grid_save_statements(options); } if options.database_type == Some(DatabaseType::Tdengine) { return build_tdengine_data_grid_save_statements(options); } let save_columns = effective_columns(options); let column_info = options.table_meta.columns.as_deref().unwrap_or(&[]); let table = qualified_table_name( options.database_type, options.table_meta.schema.as_deref(), &options.table_meta.table_name, ); let mut statements = Vec::new(); for (row_index, changes) in &options.dirty_rows { let Some(row) = options.rows.get(*row_index) else { continue; }; let sets = changes .iter() .filter_map(|(column_index, value)| { let column = save_columns.get(*column_index)?.as_deref()?; if is_oracle_row_id(options.database_type, Some(column)) { return None; } Some(format!( "{} = {}", quote_ident(options.database_type, column), format_grid_sql_literal(value, options.database_type, column_info_for(column_info, column)) )) }) .collect::>() .join(", "); if sets.is_empty() { continue; } let where_clause = build_primary_key_where( options.database_type, &options.table_meta.primary_keys, &save_columns, row, column_info, ); statements.push(format!("UPDATE {table} SET {sets} WHERE {where_clause};")); } for row_index in &options.deleted_rows { let Some(row) = options.rows.get(*row_index) else { continue; }; let where_clause = build_primary_key_where( options.database_type, &options.table_meta.primary_keys, &save_columns, row, column_info, ); statements.push(format!("DELETE FROM {table} WHERE {where_clause};")); } for row in &options.new_rows { let insert_pairs: Vec<(&str, &Value)> = save_columns .iter() .enumerate() .filter_map(|(index, column)| Some((column.as_deref()?, row.get(index).unwrap_or(&Value::Null)))) .filter(|(column, _)| !is_oracle_row_id(options.database_type, Some(column))) .filter(|(_, value)| !value.is_null()) .collect(); if insert_pairs.is_empty() { continue; } let columns = insert_pairs .iter() .map(|(column, _)| quote_ident(options.database_type, column)) .collect::>() .join(", "); let values = insert_pairs .iter() .map(|(column, value)| { format_grid_sql_literal(value, options.database_type, column_info_for(column_info, column)) }) .collect::>() .join(", "); statements.push(format!("INSERT INTO {table} ({columns}) VALUES ({values});")); } statements } fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -> Vec { if options.database_type == Some(DatabaseType::Neo4j) { return build_neo4j_data_grid_rollback_statements(options); } let save_columns = effective_columns(options); let column_info = options.table_meta.columns.as_deref().unwrap_or(&[]); let table = qualified_table_name( options.database_type, options.table_meta.schema.as_deref(), &options.table_meta.table_name, ); let mut statements = Vec::new(); for row in &options.new_rows { let where_clause = build_row_where(options.database_type, &save_columns, row, column_info); if !where_clause.is_empty() { statements.push(format!("DELETE FROM {table} WHERE {where_clause};")); } } for row_index in &options.deleted_rows { let Some(row) = options.rows.get(*row_index) else { continue; }; let insert_pairs: Vec<(&str, &Value)> = save_columns .iter() .enumerate() .filter_map(|(index, column)| Some((column.as_deref()?, row.get(index).unwrap_or(&Value::Null)))) .filter(|(column, _)| !is_oracle_row_id(options.database_type, Some(column))) .collect(); let columns = insert_pairs .iter() .map(|(column, _)| quote_ident(options.database_type, column)) .collect::>() .join(", "); let values = insert_pairs .iter() .map(|(column, value)| { format_grid_sql_literal(value, options.database_type, column_info_for(column_info, column)) }) .collect::>() .join(", "); statements.push(format!("INSERT INTO {table} ({columns}) VALUES ({values});")); } for (row_index, changes) in &options.dirty_rows { let Some(row) = options.rows.get(*row_index) else { continue; }; let mut after_row = row.clone(); for (column_index, value) in changes { if *column_index < after_row.len() { after_row[*column_index] = value.clone(); } } let writable_changes: Vec<(&(usize, Value), &str)> = changes .iter() .filter_map(|change @ (column_index, _)| { let column = save_columns.get(*column_index)?.as_deref()?; if is_oracle_row_id(options.database_type, Some(column)) { return None; } Some((change, column)) }) .collect(); let sets = writable_changes .iter() .map(|((column_index, _), column)| { format!( "{} = {}", quote_ident(options.database_type, column), format_grid_sql_literal( row.get(*column_index).unwrap_or(&Value::Null), options.database_type, column_info_for(column_info, column) ) ) }) .collect::>() .join(", "); if sets.is_empty() { continue; } let mut predicates = vec![build_primary_key_where( options.database_type, &options.table_meta.primary_keys, &save_columns, &after_row, column_info, )]; predicates.extend(writable_changes.iter().map(|((_, value), column)| { build_column_predicate(options.database_type, column, value, column_info_for(column_info, column)) })); statements.push(format!( "UPDATE {table} SET {sets} WHERE {};", predicates.into_iter().filter(|part| !part.is_empty()).collect::>().join(" AND ") )); } statements } pub(crate) fn effective_columns(options: &DataGridSaveStatementOptions) -> Vec> { match &options.source_columns { Some(source_columns) if source_columns.len() == options.columns.len() => source_columns.clone(), _ => options.columns.iter().map(|column| Some(column.clone())).collect(), } } fn effective_copy_columns(source_columns: Option<&[Option]>, columns: &[String]) -> Vec> { match source_columns { Some(source_columns) if source_columns.len() == columns.len() => source_columns.to_vec(), _ => columns.iter().map(|column| Some(column.clone())).collect(), } } fn effective_column(options: &DataGridSaveStatementOptions, index: usize) -> Option<&str> { match &options.source_columns { Some(source_columns) if source_columns.len() == options.columns.len() => source_columns.get(index)?.as_deref(), _ => options.columns.get(index).map(String::as_str), } } fn data_grid_save_execution_schema( database_type: Option, table_meta: &DataGridTableMeta, ) -> Option { if matches!(database_type, Some(DatabaseType::Neo4j | DatabaseType::Oracle)) { return None; } table_meta.schema.clone() } pub fn normalize_data_grid_save_error(database_type: Option, error: &str) -> String { if database_type == Some(DatabaseType::Hive) && (error.contains("Attempt to do update or delete") || error.contains("Error 10294")) { return "Hive UPDATE/DELETE are not enabled for this table or server. Add rows with INSERT, or enable ACID transactional tables in Hive before editing/deleting existing rows.".to_string(); } error.to_string() } pub fn format_grid_sql_literal( value: &Value, database_type: Option, column_info: Option<&DataGridColumnInfo>, ) -> String { if value.is_null() { return "NULL".to_string(); } if is_mysql_bit_literal_column(database_type, column_info) { if let Some(value) = value.as_bool() { return if value { "1" } else { "0" }.to_string(); } if let Some(number) = value.as_number() { return number.to_string(); } if let Some(text) = value.as_str().and_then(format_mysql_bit_literal_text) { return text; } } if let Some(value) = value.as_bool() { return if value { "TRUE" } else { "FALSE" }.to_string(); } if let Some(number) = value.as_number() { return number.to_string(); } if let Some(arr) = value.as_array() { return format_pg_array_sql_literal(arr); } let text = value.as_str().map_or_else(|| value.to_string(), ToString::to_string); if text.is_empty() { return if database_type == Some(DatabaseType::SqlServer) { "N''" } else { "''" }.to_string(); } let literal_text = if database_type == Some(DatabaseType::Tdengine) { format_tdengine_timestamp_literal_text(&text) } else if is_mysql_datetime_literal_database(database_type) && column_info.map(|column| is_temporal_column_type(&column.data_type)).unwrap_or(true) { format_mysql_temporal_literal_text(&text, column_info.map(|column| column.data_type.as_str())) } else { text }; let escaped = format!("'{}'", literal_text.replace('\\', "\\\\").replace('\'', "''")); if database_type == Some(DatabaseType::SqlServer) { format!("N{escaped}") } else { escaped } } fn is_mysql_bit_literal_column(database_type: Option, column_info: Option<&DataGridColumnInfo>) -> bool { is_mysql_datetime_literal_database(database_type) && column_info.map(|column| is_bit_column_type(&column.data_type)).unwrap_or(false) } fn is_bit_column_type(data_type: &str) -> bool { let lower = data_type.to_ascii_lowercase(); lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == "bit") } fn format_mysql_bit_literal_text(text: &str) -> Option { let trimmed = text.trim(); if trimmed.eq_ignore_ascii_case("true") { return Some("1".to_string()); } if trimmed.eq_ignore_ascii_case("false") { return Some("0".to_string()); } if trimmed.chars().all(|ch| ch.is_ascii_digit()) && !trimmed.is_empty() { return Some(if trimmed.len() == 1 { trimmed.to_string() } else if trimmed.chars().all(|ch| matches!(ch, '0' | '1')) { format!("b'{trimmed}'") } else { trimmed.to_string() }); } let lower = trimmed.to_ascii_lowercase(); if lower.starts_with("b'") && trimmed.ends_with('\'') { let bits = &trimmed[2..trimmed.len() - 1]; if !bits.is_empty() && bits.chars().all(|ch| matches!(ch, '0' | '1')) { return Some(format!("b'{bits}'")); } } None } fn is_mysql_datetime_literal_database(database_type: Option) -> bool { matches!( database_type, Some( DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::Goldendb | DatabaseType::Sundb ) ) } fn format_mysql_temporal_literal_text(text: &str, data_type: Option<&str>) -> String { let Some(captures) = regex_like_rfc3339(text) else { return text.to_string(); }; match temporal_column_kind(data_type) { Some("date") => captures.date, Some("time") => { format!("{}{}", captures.time, normalize_mysql_fractional_seconds(captures.fraction.as_deref())) } _ => format!( "{} {}{}", captures.date, captures.time, normalize_mysql_fractional_seconds(captures.fraction.as_deref()) ), } } struct Rfc3339Parts { date: String, time: String, fraction: Option, } fn regex_like_rfc3339(text: &str) -> Option { let bytes = text.as_bytes(); if bytes.len() < 20 || bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { return None; } let separator = *bytes.get(10)?; if separator != b'T' && separator != b' ' { return None; } if bytes.get(13) != Some(&b':') || bytes.get(16) != Some(&b':') { return None; } let date = &text[0..10]; let time = &text[11..19]; let rest = &text[19..]; let (fraction, zone) = if let Some(rest) = rest.strip_prefix('.') { let digit_count = rest.chars().take_while(|ch| ch.is_ascii_digit()).count(); if digit_count == 0 || digit_count > 9 { return None; } (Some(format!(".{}", &rest[..digit_count])), &rest[digit_count..]) } else { (None, rest) }; if zone == "Z" || zone == "z" || is_timezone_offset(zone) { Some(Rfc3339Parts { date: date.to_string(), time: time.to_string(), fraction }) } else { None } } fn is_timezone_offset(value: &str) -> bool { let bytes = value.as_bytes(); bytes.len() == 6 && matches!(bytes[0], b'+' | b'-') && bytes[3] == b':' && bytes[1].is_ascii_digit() && bytes[2].is_ascii_digit() && bytes[4].is_ascii_digit() && bytes[5].is_ascii_digit() } fn normalize_mysql_fractional_seconds(fraction: Option<&str>) -> String { match fraction { Some(fraction) if fraction.len() > 7 => fraction[..7].to_string(), Some(fraction) => fraction.to_string(), None => String::new(), } } fn is_temporal_column_type(data_type: &str) -> bool { temporal_column_kind(Some(data_type)).is_some() } fn temporal_column_kind(data_type: Option<&str>) -> Option<&'static str> { let base = data_type.unwrap_or("").trim().to_ascii_lowercase().split(['(', ':', ' ']).next().unwrap_or("").to_string(); match base.as_str() { "date" => Some("date"), "time" => Some("time"), "datetime" | "timestamp" => Some("datetime"), _ => None, } } fn format_tdengine_timestamp_literal_text(text: &str) -> String { let Some((date, time, fraction)) = parse_tdengine_timestamp(text) else { return text.to_string(); }; format!( "{date}T{time}{}{suffix}", normalize_fractional_seconds(fraction.as_deref()), suffix = local_timezone_offset_suffix(text) ) } fn parse_tdengine_timestamp(text: &str) -> Option<(String, String, Option)> { if text.len() < 19 { return None; } let bytes = text.as_bytes(); if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') || bytes.get(10) != Some(&b' ') { return None; } if bytes.get(13) != Some(&b':') || bytes.get(16) != Some(&b':') { return None; } let date = text[0..10].to_string(); let time = text[11..19].to_string(); let rest = &text[19..]; if rest.is_empty() { return Some((date, time, None)); } let fraction = rest.strip_prefix('.')?; if fraction.is_empty() || fraction.len() > 9 || !fraction.chars().all(|ch| ch.is_ascii_digit()) { return None; } Some((date, time, Some(format!(".{fraction}")))) } fn normalize_fractional_seconds(fraction: Option<&str>) -> String { match fraction { Some(fraction) if fraction.len() >= 4 => fraction[..4].to_string(), Some(fraction) => format!("{fraction:0<4}"), None => ".000".to_string(), } } fn local_timezone_offset_suffix(text: &str) -> String { let naive = NaiveDateTime::parse_from_str(&text.replace(' ', "T"), "%Y-%m-%dT%H:%M:%S%.f").ok(); let offset_minutes = naive .and_then(|dt| { let local = dt.and_local_timezone(Local).earliest()?; Some(local.offset().local_minus_utc() / -60) }) .unwrap_or_else(|| Local::now().offset().local_minus_utc() / -60); let sign = if offset_minutes <= 0 { "+" } else { "-" }; let abs = offset_minutes.abs(); format!("{sign}{:02}:{:02}", abs / 60, abs % 60) } fn build_primary_key_where( database_type: Option, primary_keys: &[String], columns: &[Option], row: &[Value], column_info: &[DataGridColumnInfo], ) -> String { if primary_keys.is_empty() && uses_keyless_row_predicate(database_type) { return build_row_where(database_type, columns, row, column_info); } primary_keys .iter() .map(|primary_key| { let value = row .get( columns .iter() .position(|column| column.as_deref() == Some(primary_key.as_str())) .unwrap_or(usize::MAX), ) .unwrap_or(&Value::Null); format!( "{} = {}", predicate_ident(database_type, primary_key), format_grid_sql_literal(value, database_type, column_info_for(column_info, primary_key)) ) }) .collect::>() .join(" AND ") } fn build_row_where( database_type: Option, columns: &[Option], row: &[Value], column_info: &[DataGridColumnInfo], ) -> String { columns .iter() .enumerate() .filter_map(|(index, column)| { let column = column.as_deref()?; if is_oracle_row_id(database_type, Some(column)) { return None; } Some(build_column_predicate( database_type, column, row.get(index).unwrap_or(&Value::Null), column_info_for(column_info, column), )) }) .collect::>() .join(" AND ") } fn build_column_predicate( database_type: Option, column: &str, value: &Value, column_info: Option<&DataGridColumnInfo>, ) -> String { let ident = predicate_ident(database_type, column); if value.is_null() { format!("{ident} IS NULL") } else { format!("{ident} = {}", format_grid_sql_literal(value, database_type, column_info)) } } fn is_oracle_row_id(database_type: Option, name: Option<&str>) -> bool { database_type == Some(DatabaseType::Oracle) && name.is_some_and(|name| name.eq_ignore_ascii_case(DBX_ROWID_COLUMN)) } pub(crate) fn is_neo4j_element_id(database_type: Option, name: Option<&str>) -> bool { database_type == Some(DatabaseType::Neo4j) && name == Some(DBX_NEO4J_ELEMENT_ID_COLUMN) } fn is_auto_generated_column(column: &DataGridColumnInfo) -> bool { column .extra .as_deref() .unwrap_or("") .to_ascii_lowercase() .split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_') .any(|part| matches!(part, "auto_increment" | "autoincrement" | "identity")) } fn is_null_write_to_not_null_column( database_type: Option, not_null_columns: &[String], column: Option<&str>, value: &Value, ) -> bool { let Some(column) = column else { return false; }; if is_oracle_row_id(database_type, Some(column)) || is_neo4j_element_id(database_type, Some(column)) { return false; } value.is_null() && not_null_columns.iter().any(|not_null| not_null == &normalize_column_name(column)) } fn find_column_index(columns: &[Option], target: &str) -> Option { let normalized_target = normalize_column_name(target); columns .iter() .position(|column| column.as_deref().map(normalize_column_name).unwrap_or_default() == normalized_target) } fn primary_key_value_key(primary_key_indexes: &[usize], row: &[Value]) -> Option { let values: Vec = primary_key_indexes.iter().map(|index| row.get(*index).cloned().unwrap_or(Value::Null)).collect(); if values.iter().any(Value::is_null) { return None; } serde_json::to_string(&values).ok() } fn duplicate_primary_key_error( primary_keys: &[String], primary_key_indexes: &[usize], row: &[Value], matches_existing_row: bool, ) -> String { let key_summary = primary_keys .iter() .enumerate() .map(|(index, primary_key)| { format!( "{} = {}", primary_key, format_key_value_for_message(row.get(primary_key_indexes[index]).unwrap_or(&Value::Null)) ) }) .collect::>() .join(", "); let source = if matches_existing_row { "the existing primary key" } else { "another new row's primary key" }; format!("New row duplicates {source} ({key_summary}). Change the key before saving.") } fn format_key_value_for_message(value: &Value) -> String { if value.is_null() { return "NULL".to_string(); } if let Some(value) = value.as_str() { return format!("\"{}\"", value.replace('"', "\\\"")); } value.to_string() } fn normalize_column_name(name: &str) -> String { name.to_ascii_uppercase() } fn null_write_error(column: &str) -> String { format!("Column \"{column}\" does not allow NULL.") } fn predicate_ident(database_type: Option, name: &str) -> String { if is_oracle_row_id(database_type, Some(name)) { "ROWIDTOCHAR(ROWID)".to_string() } else { quote_ident(database_type, name) } } pub(crate) fn quote_ident(database_type: Option, name: &str) -> String { quote_table_identifier(database_type, name) } pub(crate) fn qualified_table_name( database_type: Option, schema: Option<&str>, table_name: &str, ) -> String { crate::sql_dialect::qualified_table_name(database_type, schema, table_name) } fn column_filter_ref(database_type: Option, column_name: &str) -> String { let quoted = quote_ident(database_type, column_name); if database_type == Some(DatabaseType::Neo4j) { format!("n.{quoted}") } else { quoted } } fn value_to_filter_text(value: &Value) -> String { if let Some(value) = value.as_str() { value.to_string() } else if value.is_null() { String::new() } else { value.to_string() } } fn parse_typed_filter_value(text: &str, 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") { return Value::Bool(true); } if is_boolean_type(&data_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) { if let Ok(number) = unquoted.parse::() { return Value::Number(number); } } Value::String(unquoted) } fn unwrap_matching_quotes(text: &str) -> String { let mut chars = text.chars(); let Some(first) = chars.next() else { return String::new(); }; let Some(last) = text.chars().last() else { return String::new(); }; if text.len() >= 2 && ((first == '\'' && last == '\'') || (first == '"' && last == '"')) { text[1..text.len() - 1].to_string() } else { text.to_string() } } fn is_numeric_type(data_type: &str) -> bool { let lower = data_type.to_ascii_lowercase(); [ "int", "integer", "bigint", "smallint", "tinyint", "mediumint", "serial", "number", "numeric", "decimal", "float", "double", "real", "money", ] .iter() .any(|part| lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == *part)) } fn is_boolean_type(data_type: &str) -> bool { let lower = data_type.to_ascii_lowercase(); lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| matches!(token, "bool" | "boolean" | "bit")) } fn is_numeric_literal(text: &str) -> bool { if text.trim() != text || text.is_empty() { return false; } text.parse::().is_ok_and(f64::is_finite) && text.chars().all(|ch| ch.is_ascii_digit() || matches!(ch, '+' | '-' | '.' | 'e' | 'E')) && text.chars().any(|ch| ch.is_ascii_digit()) } fn uses_keyless_row_predicate(database_type: Option) -> bool { matches!( database_type, Some( DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::Sqlite | DatabaseType::DuckDb | DatabaseType::SqlServer | DatabaseType::Oracle | DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::Redshift | DatabaseType::Dameng | DatabaseType::Gaussdb | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Vastbase | DatabaseType::Goldendb | DatabaseType::Yashandb | DatabaseType::Databricks | DatabaseType::SapHana | DatabaseType::Teradata | DatabaseType::Vertica | DatabaseType::Firebird | DatabaseType::Exasol | DatabaseType::OpenGauss | DatabaseType::OceanbaseOracle | DatabaseType::Gbase | DatabaseType::Access | DatabaseType::H2 | DatabaseType::Snowflake | DatabaseType::Db2 | DatabaseType::Informix | DatabaseType::Bigquery | DatabaseType::Sundb | DatabaseType::Hive | DatabaseType::Iris ) ) } pub(crate) fn column_info_for<'a>(columns: &'a [DataGridColumnInfo], name: &str) -> Option<&'a DataGridColumnInfo> { let normalized = normalize_column_name(name); columns.iter().find(|column| normalize_column_name(&column.name) == normalized) } #[cfg(test)] mod tests { use serde_json::json; use super::*; fn column(name: &str, data_type: &str, nullable: bool, extra: Option<&str>) -> DataGridColumnInfo { DataGridColumnInfo { name: name.to_string(), data_type: data_type.to_string(), is_nullable: nullable, is_primary_key: false, column_default: None, extra: extra.map(ToString::to_string), } } #[test] fn builds_copy_update_statements() { let statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions { database_type: Some(DatabaseType::Postgres), table_meta: DataGridTableMeta { schema: Some("public".to_string()), table_name: "users".to_string(), primary_keys: vec!["id".to_string()], columns: None, }, columns: vec!["id".to_string(), "name".to_string(), "status".to_string()], source_columns: None, rows: vec![vec![json!(1), json!("Ada"), json!("active")]], }); assert_eq!( statements, vec!["UPDATE \"public\".\"users\" SET \"name\" = 'Ada', \"status\" = 'active' WHERE \"id\" = 1;"] ); } #[test] fn builds_copy_insert_statement_without_primary_keys() { let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions { database_type: Some(DatabaseType::Mysql), table_meta: Some(DataGridTableMeta { schema: None, table_name: "users".to_string(), primary_keys: vec!["id".to_string()], columns: None, }), columns: vec!["id".to_string(), "login_name".to_string(), "display_name".to_string()], source_columns: None, rows: vec![vec![json!(1), json!("ada"), json!("Ada")], vec![json!(2), json!("linus"), json!("Linus")]], exclude_primary_keys: true, }); assert_eq!( statement.as_deref(), Some("INSERT INTO `users` (`login_name`, `display_name`) VALUES\n('ada', 'Ada'),\n('linus', 'Linus');") ); } #[test] fn builds_filter_conditions() { assert_eq!( build_data_grid_column_value_filter_condition(DataGridColumnValueFilterConditionOptions { database_type: Some(DatabaseType::Mysql), column_name: "id".to_string(), column_info: Some(column("id", "int", false, None)), raw_value: "49436".to_string(), }) .as_deref(), Some("`id` = 49436") ); assert_eq!( build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions { database_type: Some(DatabaseType::Postgres), column_name: "status".to_string(), mode: DataGridContextFilterMode::Like, value: json!("active"), column_info: Some(column("status", "varchar", true, None)), }) .as_deref(), Some("\"status\" LIKE '%active%'") ); } #[test] fn builds_grid_count_sql() { assert_eq!( build_data_grid_count_sql(DataGridCountSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "users".to_string(), where_input: Some("WHERE active = true;".to_string()), }), "SELECT COUNT(*) AS cnt FROM \"public\".\"users\" WHERE (active = true)" ); } #[test] fn builds_hive_table_properties_sql() { assert_eq!( build_hive_table_properties_sql(HiveTablePropertiesSqlOptions { schema: Some("default".to_string()), table_name: "events".to_string(), property_name: "transactional".to_string(), }), "SHOW TBLPROPERTIES `events` ('transactional')" ); } #[test] fn formats_temporal_copy_literals() { assert_eq!( format_grid_sql_literal(&json!("2026-05-12T00:00:00+00:00"), Some(DatabaseType::Mysql), None), "'2026-05-12 00:00:00'" ); assert_eq!( format_grid_sql_literal(&json!("2026-05-12T00:00:00.123456Z"), Some(DatabaseType::Mysql), None), "'2026-05-12 00:00:00.123456'" ); } #[test] fn formats_mysql_bit_literals_without_string_quotes() { let bit = column("flag", "bit(1)", true, None); let bit_string = column("flags", "bit(8)", true, None); assert_eq!(format_grid_sql_literal(&json!("0"), Some(DatabaseType::Mysql), Some(&bit)), "0"); assert_eq!(format_grid_sql_literal(&json!("1"), Some(DatabaseType::Mysql), Some(&bit)), "1"); assert_eq!(format_grid_sql_literal(&json!(true), Some(DatabaseType::Mysql), Some(&bit)), "1"); assert_eq!( format_grid_sql_literal(&json!("10101010"), Some(DatabaseType::Mysql), Some(&bit_string)), "b'10101010'" ); assert_eq!(format_grid_sql_literal(&json!("0"), Some(DatabaseType::Postgres), Some(&bit)), "'0'"); } #[test] fn prepares_sqlserver_save_statements() { let result = prepare_data_grid_save(DataGridSaveStatementOptions { database_type: Some(DatabaseType::SqlServer), table_meta: DataGridTableMeta { schema: Some("game".to_string()), table_name: "player states".to_string(), primary_keys: vec!["role id".to_string()], columns: None, }, columns: vec!["role id".to_string(), "state".to_string(), "updated at".to_string()], source_columns: None, rows: vec![vec![json!(42), json!("old"), json!("2026-05-03")]], dirty_rows: vec![(0, vec![(1, json!("ready")), (2, json!("2026-05-04"))])], deleted_rows: vec![0], new_rows: vec![vec![json!(43), json!("new"), json!("2026-05-05")]], }); assert_eq!( result.statements, vec![ "UPDATE [game].[player states] SET [state] = N'ready', [updated at] = N'2026-05-04' WHERE [role id] = 42;", "DELETE FROM [game].[player states] WHERE [role id] = 42;", "INSERT INTO [game].[player states] ([role id], [state], [updated at]) VALUES (43, N'new', N'2026-05-05');", ] ); } #[test] fn skips_expression_only_source_columns() { let result = prepare_data_grid_save(DataGridSaveStatementOptions { database_type: Some(DatabaseType::Postgres), table_meta: DataGridTableMeta { schema: Some("public".to_string()), table_name: "ihli_data".to_string(), primary_keys: vec!["iso3".to_string(), "year".to_string()], columns: None, }, columns: vec!["iso3".to_string(), "year".to_string(), "country_name".to_string(), "score".to_string()], source_columns: Some(vec![ Some("iso3".to_string()), Some("year".to_string()), Some("country_name".to_string()), None, ]), rows: vec![vec![json!("LUX"), json!(2007), json!("Luxembourg"), json!(50242.1)]], dirty_rows: vec![(0, vec![(2, json!("Luxembourg City")), (3, json!(999))])], deleted_rows: vec![], new_rows: vec![vec![json!("USA"), json!(2008), json!("United States"), json!(43000)]], }); assert_eq!( result.statements, vec![ r#"UPDATE "public"."ihli_data" SET "country_name" = 'Luxembourg City' WHERE "iso3" = 'LUX' AND "year" = 2007;"#, r#"INSERT INTO "public"."ihli_data" ("iso3", "year", "country_name") VALUES ('USA', 2008, 'United States');"#, ] ); } #[test] fn formats_mysql_temporal_columns_by_target_type() { let result = prepare_data_grid_save(DataGridSaveStatementOptions { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { schema: None, table_name: "policies".to_string(), primary_keys: vec!["id".to_string()], columns: Some(vec![ column("id", "int", false, None), column("insurance_start_time", "datetime", true, None), column("raw_text", "varchar(64)", true, None), column("coverage_day", "date", true, None), column("start_clock", "time", true, None), ]), }, columns: vec![ "id".to_string(), "insurance_start_time".to_string(), "raw_text".to_string(), "coverage_day".to_string(), "start_clock".to_string(), ], source_columns: None, rows: vec![vec![ json!(1), json!("2026-05-12T00:00:00+00:00"), json!("old"), json!("2026-05-12T00:00:00+00:00"), json!("2026-05-12T09:30:45+00:00"), ]], dirty_rows: vec![( 0, vec![ (1, json!("2026-05-12T00:00:00+00:00")), (2, json!("2026-05-12T00:00:00+00:00")), (3, json!("2026-05-12T00:00:00+00:00")), (4, json!("2026-05-12T09:30:45+00:00")), ], )], deleted_rows: vec![], new_rows: vec![], }); assert_eq!( result.statements, vec!["UPDATE `policies` SET `insurance_start_time` = '2026-05-12 00:00:00', `raw_text` = '2026-05-12T00:00:00+00:00', `coverage_day` = '2026-05-12', `start_clock` = '09:30:45' WHERE `id` = 1;"] ); } #[test] fn validates_duplicate_inserted_primary_keys() { let result = prepare_data_grid_save(DataGridSaveStatementOptions { database_type: Some(DatabaseType::Postgres), table_meta: DataGridTableMeta { schema: None, table_name: "education_data".to_string(), primary_keys: vec!["country_code".to_string(), "year".to_string()], columns: None, }, columns: vec!["country_code".to_string(), "year".to_string(), "value".to_string()], source_columns: None, rows: vec![vec![json!("ALB"), json!(2021), json!(0.812)]], dirty_rows: vec![], deleted_rows: vec![], new_rows: vec![vec![json!("ALB"), json!(2021), json!(0.913)]], }); assert_eq!( result.validation_error, Some(r#"New row duplicates the existing primary key (country_code = "ALB", year = 2021). Change the key before saving."#.to_string()) ); assert!(result.statements.is_empty()); } }