304 lines
10 KiB
Rust
304 lines
10 KiB
Rust
use serde_json::Value;
|
|
|
|
use super::*;
|
|
use crate::models::connection::DatabaseType;
|
|
|
|
pub(super) fn build_tdengine_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Vec<String> {
|
|
let save_columns = effective_columns(options);
|
|
let mut statements = Vec::new();
|
|
let has_tbname = save_columns
|
|
.iter()
|
|
.any(|column| column.as_deref().is_some_and(|column| column.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN)));
|
|
let requires_tbname = options
|
|
.table_meta
|
|
.primary_keys
|
|
.iter()
|
|
.any(|primary_key| primary_key.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN));
|
|
|
|
if has_tbname || !requires_tbname {
|
|
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();
|
|
}
|
|
}
|
|
if let Some(statement) = build_tdengine_insert_statement(options, &save_columns, &after_row) {
|
|
statements.push(statement);
|
|
}
|
|
}
|
|
}
|
|
|
|
for row_index in &options.deleted_rows {
|
|
let Some(row) = options.rows.get(*row_index) else {
|
|
continue;
|
|
};
|
|
if let Some(statement) = build_tdengine_delete_statement(options, &save_columns, row) {
|
|
statements.push(statement);
|
|
}
|
|
}
|
|
|
|
for row in &options.new_rows {
|
|
if let Some(statement) = build_tdengine_insert_statement(options, &save_columns, row) {
|
|
statements.push(statement);
|
|
}
|
|
}
|
|
|
|
statements
|
|
}
|
|
|
|
pub(super) fn build_tdengine_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -> Vec<String> {
|
|
if has_tdengine_composite_primary_key(options) && !options.new_rows.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let save_columns = effective_columns(options);
|
|
let mut statements = Vec::new();
|
|
|
|
for row in &options.new_rows {
|
|
if let Some(statement) = build_tdengine_delete_statement(options, &save_columns, row) {
|
|
statements.push(statement);
|
|
}
|
|
}
|
|
|
|
for row_index in &options.deleted_rows {
|
|
let Some(row) = options.rows.get(*row_index) else {
|
|
continue;
|
|
};
|
|
if let Some(statement) = build_tdengine_insert_statement(options, &save_columns, row) {
|
|
statements.push(statement);
|
|
}
|
|
}
|
|
|
|
for (row_index, _) in &options.dirty_rows {
|
|
let Some(row) = options.rows.get(*row_index) else {
|
|
continue;
|
|
};
|
|
if let Some(statement) = build_tdengine_insert_statement(options, &save_columns, row) {
|
|
statements.push(statement);
|
|
}
|
|
}
|
|
|
|
statements
|
|
}
|
|
|
|
pub(super) fn validate_tdengine_inserted_rows(options: &DataGridSaveStatementOptions) -> Option<String> {
|
|
if options.database_type != Some(DatabaseType::Tdengine) || options.new_rows.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let requires_tbname = options
|
|
.table_meta
|
|
.primary_keys
|
|
.iter()
|
|
.any(|primary_key| primary_key.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN));
|
|
if !requires_tbname {
|
|
return None;
|
|
}
|
|
|
|
let save_columns = effective_columns(options);
|
|
if options
|
|
.new_rows
|
|
.iter()
|
|
.any(|row| tdengine_tbname_value(&save_columns, row).is_none_or(|tbname| tbname.trim().is_empty()))
|
|
{
|
|
return Some(tdengine_insert_identity_error());
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub(super) fn validate_tdengine_existing_rows(options: &DataGridSaveStatementOptions) -> Option<String> {
|
|
if options.database_type != Some(DatabaseType::Tdengine)
|
|
|| (options.deleted_rows.is_empty() && options.dirty_rows.is_empty())
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if has_tdengine_composite_primary_key(options) && !options.deleted_rows.is_empty() {
|
|
return Some(tdengine_composite_key_delete_error());
|
|
}
|
|
|
|
let save_columns = effective_columns(options);
|
|
let primary_keys = tdengine_row_primary_keys(options);
|
|
let requires_tbname = options
|
|
.table_meta
|
|
.primary_keys
|
|
.iter()
|
|
.any(|primary_key| primary_key.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN));
|
|
if (requires_tbname
|
|
&& !save_columns.iter().any(|column| {
|
|
column.as_deref().is_some_and(|column| column.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN))
|
|
}))
|
|
|| primary_keys.is_empty()
|
|
|| primary_keys
|
|
.iter()
|
|
.any(|primary_key| find_column_index(options.database_type, &save_columns, primary_key).is_none())
|
|
{
|
|
return Some(tdengine_row_identity_error());
|
|
}
|
|
|
|
if options.dirty_rows.iter().any(|(_, changes)| {
|
|
changes.iter().any(|(column_index, _)| {
|
|
save_columns.get(*column_index).and_then(Option::as_deref).is_some_and(|column| {
|
|
options.table_meta.primary_keys.iter().any(|primary_key| primary_key.eq_ignore_ascii_case(column))
|
|
})
|
|
})
|
|
}) {
|
|
return Some(tdengine_row_identity_readonly_error());
|
|
}
|
|
|
|
for row_index in
|
|
options.deleted_rows.iter().copied().chain(options.dirty_rows.iter().map(|(row_index, _)| *row_index))
|
|
{
|
|
let Some(row) = options.rows.get(row_index) else {
|
|
return Some(tdengine_row_identity_error());
|
|
};
|
|
if (requires_tbname && tdengine_tbname_value(&save_columns, row).is_none_or(|tbname| tbname.trim().is_empty()))
|
|
|| primary_keys.iter().any(|primary_key| {
|
|
find_column_index(options.database_type, &save_columns, primary_key)
|
|
.and_then(|index| row.get(index))
|
|
.is_none_or(Value::is_null)
|
|
})
|
|
{
|
|
return Some(tdengine_row_identity_error());
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn build_tdengine_delete_statement(
|
|
options: &DataGridSaveStatementOptions,
|
|
save_columns: &[Option<String>],
|
|
row: &[Value],
|
|
) -> Option<String> {
|
|
if has_tdengine_composite_primary_key(options) {
|
|
return None;
|
|
}
|
|
|
|
let table_name = tdengine_tbname_value(save_columns, row).unwrap_or_else(|| options.table_meta.table_name.clone());
|
|
let primary_keys = tdengine_row_primary_keys(options);
|
|
let where_clause = build_primary_key_where(
|
|
Some(DatabaseType::Tdengine),
|
|
&primary_keys,
|
|
save_columns,
|
|
row,
|
|
options.table_meta.columns.as_deref().unwrap_or(&[]),
|
|
None,
|
|
);
|
|
if where_clause.is_empty() {
|
|
return None;
|
|
}
|
|
let table = qualified_table_name(Some(DatabaseType::Tdengine), options.table_meta.schema.as_deref(), &table_name);
|
|
Some(data_grid_statement(
|
|
Some(DatabaseType::Tdengine),
|
|
data_grid_delete_sql(Some(DatabaseType::Tdengine), &table, &where_clause),
|
|
))
|
|
}
|
|
|
|
fn tdengine_row_primary_keys(options: &DataGridSaveStatementOptions) -> Vec<String> {
|
|
options
|
|
.table_meta
|
|
.primary_keys
|
|
.iter()
|
|
.filter(|primary_key| !primary_key.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN))
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
fn has_tdengine_composite_primary_key(options: &DataGridSaveStatementOptions) -> bool {
|
|
tdengine_row_primary_keys(options).len() > 1
|
|
}
|
|
|
|
fn tdengine_row_identity_error() -> String {
|
|
"TDengine row editing requires all row identifier columns in the result.".to_string()
|
|
}
|
|
|
|
fn tdengine_row_identity_readonly_error() -> String {
|
|
"TDengine row identifier columns cannot be edited.".to_string()
|
|
}
|
|
|
|
fn tdengine_composite_key_delete_error() -> String {
|
|
"TDengine tables with composite keys do not support row deletion.".to_string()
|
|
}
|
|
|
|
fn tdengine_insert_identity_error() -> String {
|
|
"TDengine STABLE inserts require a child table name (tbname).".to_string()
|
|
}
|
|
|
|
fn build_tdengine_insert_statement(
|
|
options: &DataGridSaveStatementOptions,
|
|
save_columns: &[Option<String>],
|
|
row: &[Value],
|
|
) -> Option<String> {
|
|
let tbname = tdengine_tbname_value(save_columns, row);
|
|
let table = qualified_table_name(
|
|
Some(DatabaseType::Tdengine),
|
|
options.table_meta.schema.as_deref(),
|
|
&options.table_meta.table_name,
|
|
);
|
|
let tag_columns = tdengine_tag_columns(options.table_meta.columns.as_deref());
|
|
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(|(_, value)| !value.is_null())
|
|
.filter(|(column, _)| {
|
|
tdengine_can_insert_column(column, &options.table_meta.table_name, tbname.as_deref(), &tag_columns)
|
|
})
|
|
.collect();
|
|
if insert_pairs.is_empty() {
|
|
return None;
|
|
}
|
|
let columns = insert_pairs
|
|
.iter()
|
|
.map(|(column, _)| quote_ident(Some(DatabaseType::Tdengine), column))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let values = insert_pairs
|
|
.iter()
|
|
.map(|(_, value)| format_grid_sql_literal(value, Some(DatabaseType::Tdengine), None))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
Some(format!("INSERT INTO {table} ({columns}) VALUES ({values});"))
|
|
}
|
|
|
|
fn tdengine_tbname_value(save_columns: &[Option<String>], row: &[Value]) -> Option<String> {
|
|
let index = save_columns.iter().position(|column| {
|
|
column.as_deref().is_some_and(|column| column.eq_ignore_ascii_case(DBX_TDENGINE_TBNAME_COLUMN))
|
|
})?;
|
|
let value = row.get(index)?;
|
|
if value.is_null() {
|
|
None
|
|
} else if let Some(value) = value.as_str() {
|
|
Some(value.to_string())
|
|
} else {
|
|
Some(value.to_string())
|
|
}
|
|
}
|
|
|
|
fn tdengine_can_insert_column(column: &str, table_name: &str, tbname: Option<&str>, tag_columns: &[String]) -> bool {
|
|
let normalized = column.to_ascii_lowercase();
|
|
let target_is_child_table = tbname.is_none_or(|tbname| tbname == table_name);
|
|
if !target_is_child_table {
|
|
return true;
|
|
}
|
|
if normalized == DBX_TDENGINE_TBNAME_COLUMN {
|
|
return false;
|
|
}
|
|
!tag_columns.iter().any(|tag| tag == &normalized)
|
|
}
|
|
|
|
fn tdengine_tag_columns(columns: Option<&[DataGridColumnInfo]>) -> Vec<String> {
|
|
columns
|
|
.unwrap_or(&[])
|
|
.iter()
|
|
.filter(|column| column.extra.as_deref().unwrap_or("").to_ascii_lowercase().contains("tag"))
|
|
.map(|column| column.name.to_ascii_lowercase())
|
|
.collect()
|
|
}
|