fix(grid): chunk Oracle CLOB literals in data grid SQL

This commit is contained in:
onenewcode authored and GitHub committed 2026-08-24 20:32:19 +08:00
1 parent d8e3d041cc
commit 566bc87014
1 file changed
+128
+128
View File
@@ -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);