fix(grid): chunk Oracle CLOB literals in data grid SQL
This commit is contained in:
1 parent
d8e3d041cc
commit
566bc87014
1 file changed
+128
@@ -35,6 +35,8 @@ const DATA_GRID_COLUMN_DISTINCT_VALUES_DEFAULT_LIMIT: usize = 1000;
|
||||
const DATA_GRID_COLUMN_DISTINCT_VALUES_MAX_LIMIT: usize = 1000;
|
||||
const MYSQL_DATA_GRID_BATCH_MAX_ROWS: usize = 500;
|
||||
const MYSQL_DATA_GRID_BATCH_TARGET_SQL_BYTES: usize = 256 * 1024;
|
||||
const ORACLE_SQL_LITERAL_MAX_BYTES: usize = 4000;
|
||||
const ORACLE_LOB_LITERAL_CHUNK_BYTES: usize = 3900;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
@@ -1917,12 +1919,68 @@ fn format_grid_assignment_sql_literal(
|
||||
database_type: Option<DatabaseType>,
|
||||
column_info: Option<&DataGridColumnInfo>,
|
||||
) -> String {
|
||||
if matches!(database_type, Some(DatabaseType::Oracle | DatabaseType::OceanbaseOracle)) {
|
||||
if let Some(constructor) = column_info.and_then(|column| oracle_character_lob_constructor(&column.data_type)) {
|
||||
if let Some(text) = value.as_str() {
|
||||
return format_oracle_lob_assignment_literal(text, constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.is_array() || value.is_object()) && is_json_document_column(column_info) {
|
||||
return format_grid_sql_literal(&Value::String(value.to_string()), database_type, column_info);
|
||||
}
|
||||
format_grid_sql_literal(value, database_type, column_info)
|
||||
}
|
||||
|
||||
fn format_oracle_lob_assignment_literal(text: &str, constructor: &str) -> String {
|
||||
// Keep each Oracle SQL literal below the parser limit while preserving a CLOB result.
|
||||
let escaped_len = text.chars().map(oracle_sql_literal_char_len).sum::<usize>();
|
||||
if escaped_len <= ORACLE_SQL_LITERAL_MAX_BYTES {
|
||||
let mut escaped_text = String::with_capacity(escaped_len);
|
||||
append_oracle_sql_literal_characters(&mut escaped_text, text);
|
||||
return format!("'{escaped_text}'");
|
||||
}
|
||||
|
||||
let mut chunks = Vec::new();
|
||||
let mut current = String::with_capacity(ORACLE_LOB_LITERAL_CHUNK_BYTES);
|
||||
let mut current_len = 0;
|
||||
for ch in text.chars() {
|
||||
let char_len = oracle_sql_literal_char_len(ch);
|
||||
if current_len > 0 && current_len + char_len > ORACLE_LOB_LITERAL_CHUNK_BYTES {
|
||||
chunks.push(format!("{constructor}('{current}')"));
|
||||
current = String::with_capacity(ORACLE_LOB_LITERAL_CHUNK_BYTES);
|
||||
current_len = 0;
|
||||
}
|
||||
append_oracle_sql_literal_char(&mut current, ch);
|
||||
current_len += char_len;
|
||||
}
|
||||
if current_len > 0 {
|
||||
chunks.push(format!("{constructor}('{current}')"));
|
||||
}
|
||||
chunks.join(" || ")
|
||||
}
|
||||
|
||||
fn oracle_sql_literal_char_len(ch: char) -> usize {
|
||||
match ch {
|
||||
'\\' | '\'' => 2,
|
||||
_ => ch.len_utf8(),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_oracle_sql_literal_characters(output: &mut String, text: &str) {
|
||||
for ch in text.chars() {
|
||||
append_oracle_sql_literal_char(output, ch);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_oracle_sql_literal_char(output: &mut String, ch: char) {
|
||||
match ch {
|
||||
'\\' => output.push_str("\\\\"),
|
||||
'\'' => output.push_str("''"),
|
||||
_ => output.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_json_document_column(column_info: Option<&DataGridColumnInfo>) -> bool {
|
||||
column_info.is_some_and(|column| {
|
||||
let data_type = column.data_type.trim();
|
||||
@@ -2820,6 +2878,17 @@ fn is_oracle_lob_type(data_type: &str) -> bool {
|
||||
|| lower.starts_with("character large object")
|
||||
}
|
||||
|
||||
fn oracle_character_lob_constructor(data_type: &str) -> Option<&'static str> {
|
||||
let lower = data_type.trim().trim_matches('"').to_ascii_lowercase();
|
||||
let base = lower.split(['(', ':', ' ']).next().unwrap_or("");
|
||||
match base {
|
||||
"clob" => Some("TO_CLOB"),
|
||||
"nclob" => Some("TO_NCLOB"),
|
||||
_ if lower.starts_with("character large object") => Some("TO_CLOB"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_oracle_row_id(database_type: Option<DatabaseType>, name: Option<&str>) -> bool {
|
||||
uses_oracle_row_id(database_type) && name.is_some_and(|name| name.eq_ignore_ascii_case(DBX_ROWID_COLUMN))
|
||||
}
|
||||
@@ -5078,6 +5147,65 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_oracle_clob_update_without_oversized_string_literals() {
|
||||
let clob = column("body", "CLOB", true, None);
|
||||
let large_value = "x".repeat(4205);
|
||||
let literal = format_grid_assignment_sql_literal(&json!(large_value), Some(DatabaseType::Oracle), Some(&clob));
|
||||
let chunks = literal
|
||||
.split("TO_CLOB('")
|
||||
.skip(1)
|
||||
.map(|chunk| chunk.split("')").next().unwrap_or_default())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(chunks.len(), 2);
|
||||
assert!(chunks.iter().all(|chunk| chunk.len() <= ORACLE_LOB_LITERAL_CHUNK_BYTES));
|
||||
assert_eq!(chunks[0].len(), ORACLE_LOB_LITERAL_CHUNK_BYTES);
|
||||
assert_eq!(chunks[1].len(), 4205 - ORACLE_LOB_LITERAL_CHUNK_BYTES);
|
||||
assert_eq!(
|
||||
format_grid_assignment_sql_literal(&json!("short"), Some(DatabaseType::Oracle), Some(&clob)),
|
||||
"'short'"
|
||||
);
|
||||
let nclob = column("body", "NCLOB", true, None);
|
||||
assert!(format_grid_assignment_sql_literal(&json!(large_value), Some(DatabaseType::Oracle), Some(&nclob))
|
||||
.starts_with("TO_NCLOB('"));
|
||||
let special_value = format!("{}'\\", "x".repeat(3998));
|
||||
let special_literal =
|
||||
format_grid_assignment_sql_literal(&json!(special_value), Some(DatabaseType::Oracle), Some(&clob));
|
||||
assert!(special_literal.starts_with("TO_CLOB('"));
|
||||
assert!(special_literal.contains("''"));
|
||||
assert!(special_literal.contains("\\\\"));
|
||||
let varchar = column("body", "VARCHAR2(5000)", true, None);
|
||||
let varchar_literal =
|
||||
format_grid_assignment_sql_literal(&json!(large_value), Some(DatabaseType::Oracle), Some(&varchar));
|
||||
assert!(varchar_literal.starts_with("'"));
|
||||
assert!(!varchar_literal.contains("TO_CLOB("));
|
||||
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
identifier_quote: Some("\"".to_string()),
|
||||
table_meta: DataGridTableMeta {
|
||||
catalog: None,
|
||||
database: None,
|
||||
schema: Some("APP".to_string()),
|
||||
table_name: "DOCUMENTS".to_string(),
|
||||
primary_keys: vec!["ID".to_string()],
|
||||
columns: Some(vec![column("ID", "NUMBER", false, None), clob]),
|
||||
},
|
||||
columns: vec!["ID".to_string(), "BODY".to_string()],
|
||||
source_columns: None,
|
||||
rows: vec![vec![json!(1), json!("old")]],
|
||||
dirty_rows: vec![(0, vec![(1, json!(large_value))])],
|
||||
deleted_rows: vec![],
|
||||
new_rows: vec![],
|
||||
});
|
||||
|
||||
assert_eq!(result.validation_error, None);
|
||||
assert_eq!(result.statements.len(), 1);
|
||||
assert!(result.statements[0].contains("\"BODY\" = TO_CLOB('"));
|
||||
assert!(result.statements[0].contains("WHERE \"ID\" = 1;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_numeric_string_literals_for_numeric_columns_without_quotes() {
|
||||
let number = column("amount", "NUMBER(20,0)", true, None);
|
||||
|
||||
Reference in new issue
Block a user