From 94f36c57d3ac8bfda91a0f1cb878418bf000f45f Mon Sep 17 00:00:00 2001 From: t8y2 Date: Sat, 3 Oct 2026 14:40:55 +0800 Subject: [PATCH] fix(db2): bound large LOB column previews with SUBSTR to cap memory Refs: #10723 --- .../src/sql_dialect/table_select.rs | 23 ++++++++++++++ .../dbx-sql-dialect/src/sql_dialect/tests.rs | 31 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs b/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs index 5335962450..977c35f655 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs @@ -92,6 +92,18 @@ fn large_value_preview_kind( None } } + Some(DatabaseType::Db2) => { + if matches!(base.as_str(), "blob" | "binary" | "varbinary" | "longvarbinary") { + Some(LargeValuePreviewKind::Binary) + } else if matches!(base.as_str(), "clob" | "dbclob") + || (matches!(base.as_str(), "char" | "character" | "varchar" | "graphic" | "vargraphic") + && declared_data_type_length(data_type).is_some_and(|length| length > preview_size)) + { + Some(LargeValuePreviewKind::Text) + } else { + None + } + } _ => None, } } @@ -149,6 +161,17 @@ fn build_large_value_preview_columns(options: &TableDataSelectSqlOptions) -> Opt (format!("left({quoted}::text, {prefix_size}) AS {quoted}"), "V") } Some(DatabaseType::Postgres) => (format!("left({quoted}, {prefix_size}) AS {quoted}"), "T"), + // DB2 的 SUBSTR 第三参数越界会报 SQL0138N,而 BLOB/CLOB 长度不定,故用 + // CASE WHEN LENGTH(..) 把预览长度夹在实际长度内,对短值不报错;SUBSTR 对 BLOB + // 返回 BLOB、对 CLOB/字符列返回 VARCHAR,agent 侧 BLOB 再转 0x 前缀的 hex 字符串。 + Some(DatabaseType::Db2) if kind == LargeValuePreviewKind::Binary => ( + format!("SUBSTR({quoted}, 1, CASE WHEN LENGTH({quoted}) >= {prefix_size} THEN {prefix_size} ELSE LENGTH({quoted}) END) AS {quoted}"), + "B", + ), + Some(DatabaseType::Db2) => ( + format!("SUBSTR({quoted}, 1, CASE WHEN LENGTH({quoted}) >= {prefix_size} THEN {prefix_size} ELSE LENGTH({quoted}) END) AS {quoted}"), + "T", + ), _ => return None, }; let marker = if database_type == Some(DatabaseType::Mysql) { diff --git a/crates/dbx-sql-dialect/src/sql_dialect/tests.rs b/crates/dbx-sql-dialect/src/sql_dialect/tests.rs index 17c848dcb7..6d5fc56aff 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/tests.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/tests.rs @@ -1034,6 +1034,37 @@ fn builds_postgres_table_data_large_value_previews() { assert!(sql.contains("'T:8192' AS \"__DBX_LARGE_VALUE_BYTES_K_2\"")); } +#[test] +fn builds_db2_table_data_large_value_previews_with_bounded_substr() { + let sql = build_table_data_select_sql(TableDataSelectSqlOptions { + database_type: Some(DatabaseType::Db2), + schema: Some("DB2INST1".to_string()), + table_name: "large_rows".to_string(), + primary_keys: vec!["id".to_string()], + columns: vec!["id".to_string(), "doc".to_string(), "raw_value".to_string()], + column_types: vec!["integer".to_string(), "clob".to_string(), "blob".to_string()], + large_value_preview_size: Some(4096), + limit: Some(100), + ..Default::default() + }); + + // DB2 的 SUBSTR 第三参数越界会报 SQL0138N,故用 CASE WHEN LENGTH(..) 把预览长度 + // 夹在实际长度内:CLOB → 文本预览、BLOB → 二进制预览,都不对短值报错。 + assert!( + sql.contains("SUBSTR(\"doc\", 1, CASE WHEN LENGTH(\"doc\") >= 4097 THEN 4097 ELSE LENGTH(\"doc\") END) AS \"doc\""), + "clob preview sql: {sql}" + ); + assert!(sql.contains("'T:4096' AS "), "clob marker sql: {sql}"); + assert!( + sql.contains("SUBSTR(\"raw_value\", 1, CASE WHEN LENGTH(\"raw_value\") >= 4097 THEN 4097 ELSE LENGTH(\"raw_value\") END) AS \"raw_value\""), + "blob preview sql: {sql}" + ); + assert!(sql.contains("'B:4096' AS "), "blob marker sql: {sql}"); + // 主键列不做预览,且不得生成裸 SUBSTR(col,1,n)(会越界报错) + assert!(!sql.contains("SUBSTR(\"id\""), "pk should not be previewed: {sql}"); + assert!(!sql.contains("SUBSTR(\"doc\", 1, 4097)"), "must not emit unbounded substr: {sql}"); +} + #[test] fn preserves_postgres_array_types_in_large_value_previews() { let sql = build_table_data_select_sql(TableDataSelectSqlOptions {