From 070fe9602d915ab6ebbf05eae33f097217699f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=A3=9E?= <1835698775@qq.com> Date: Sun, 20 Sep 2026 18:55:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(db):=20=E5=A4=96=E9=83=A8=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E6=8A=8A=E6=95=B0=E5=80=BC=20cell=20=E5=8F=91=E6=88=90?= =?UTF-8?q?=E5=8D=81=E8=BF=9B=E5=88=B6=E6=96=87=E6=9C=AC=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=95=B4=E8=A1=A8=E8=AF=BB=E5=8F=96=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - extension-protocol 新增 cell_number 读取器:CellValue 的 i64 / u64 / f64 `value` 序列化仍只发 JSON number,读取时接受等值的十进制 / 浮点文本; 无法解释的文本("abc"、给 u64 传 "4.5")仍报错,不静默降级成别的类型 - 修 issue #249 / #207:MySQL 文本协议驱动按声明类型把 BIGINT UNSIGNED 列还原成十进制文本({"type":"u64","value":"4"}),宿主严格按 number 解析 时报 `invalid type: string "4", expected u64`,整张表都读不出来 - 三个读取器都处理 serde_json 的 arbitrary_precision 分支:该 feature 下浮点 与超范围整数走私有 number map 进 visit_map,漏掉会把 1.5 误判成对象 验证:cargo test -p extension-protocol(212+ 全绿)、cargo test -p db(1302 全绿)、 cargo clippy -p extension-protocol -p db --all-targets 无新增告警; 摘掉 u64 读取器后 db 与 protocol 两个回归测试均复现 issue 原始报错。 --- crates/db/src/ipc/connection.rs | 17 ++ crates/extension-protocol/src/cell_number.rs | 277 +++++++++++++++++++ crates/extension-protocol/src/lib.rs | 1 + crates/extension-protocol/src/row.rs | 77 ++++++ 4 files changed, 372 insertions(+) create mode 100644 crates/extension-protocol/src/cell_number.rs diff --git a/crates/db/src/ipc/connection.rs b/crates/db/src/ipc/connection.rs index 62fe1d6e5..c1045dd82 100644 --- a/crates/db/src/ipc/connection.rs +++ b/crates/db/src/ipc/connection.rs @@ -1121,6 +1121,23 @@ mod tests { assert_eq!(Some(25), max_rows_to_u64(Some(25))); } + #[test] + fn cursor_fetch_accepts_unsigned_cells_encoded_as_decimal_text() { + // 复现 issue #249/#207:MySQL 文本协议驱动把 `BIGINT UNSIGNED` 列按十进制 + // 文本发送,宿主曾在此处以 `invalid type: string "4", expected u64` 让整张 + // 表都读不出来。 + let fetched: CursorFetchOutput = serde_json::from_value( + json!({"rows": [[{"type": "u64", "value": "4"}]], "done": true}), + ) + .expect("decimal-text u64 cell must decode"); + + assert_eq!(fetched.rows, vec![vec![CellValue::U64 { value: 4 }]]); + assert_eq!( + crate::ipc::value_adapter::cell_to_cell_state(fetched.rows[0][0].clone()).unwrap(), + db_value::CellState::Decoded(db_value::DbValue::Unsigned("4".to_string())) + ); + } + #[test] fn is_query_sql_handles_common_prefixes() { assert!(is_query_sql("SELECT 1")); diff --git a/crates/extension-protocol/src/cell_number.rs b/crates/extension-protocol/src/cell_number.rs new file mode 100644 index 000000000..b477f9c18 --- /dev/null +++ b/crates/extension-protocol/src/cell_number.rs @@ -0,0 +1,277 @@ +//! 数值 cell `value` 的宽松读取。 +//! +//! wire 契约里 `i64` / `u64` / `f64` cell 的 `value` 是 JSON number,序列化仍只发 +//! number。但外部驱动是独立进程里的第三方 JSON 生产者:MySQL 文本协议驱动把 +//! `BIGINT UNSIGNED` 列按声明类型还原后以十进制文本发送(见 navop-extensions +//! `dbipc.toCellForKind`),此时宿主若严格按 number 解析就会报 +//! `invalid type: string "4", expected u64`,整张表都读不出来。 +//! +//! 十进制文本与 JSON number 在宿主侧完全等价:`u64::MAX` 这样的极值也不丢精度。 +//! 因此这里在**解析**时接受等值文本;无法解释的文本(例如 `"abc"`)依然报错, +//! 不会被静默降级成别的类型而掩盖驱动问题。 +//! +//! 注意:工作区为 `serde_json` 开了 `arbitrary_precision`,该 feature 下浮点数 +//! (以及超出 `u64` 范围的整数)不走 `visit_f64` / `visit_u64`,而是被包成私有 +//! number map 交给 `visit_map`。所以三个读取器都必须处理 `visit_map`,否则 +//! `1.5` 这样的普通浮点会被误判成"传了个对象"。 + +use std::fmt; + +use serde::de::{Deserializer, Error as DeError, Expected, MapAccess, Unexpected, Visitor}; + +/// serde_json 在 `arbitrary_precision` 下包裹"无法用 u64/i64/f64 表示的数字"用的 +/// 私有 key(见 serde_json `number::TOKEN`,非 pub,只能按字面量比对)。 +const ARBITRARY_PRECISION_TOKEN: &str = "$serde_json::private::Number"; + +/// 数值 cell 的期望描述,用于错误信息。 +struct CellNumberExpected(&'static str); + +impl Expected for CellNumberExpected { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0) + } +} + +const U64_EXPECTED: CellNumberExpected = + CellNumberExpected("an u64 number or a decimal integer string"); +const I64_EXPECTED: CellNumberExpected = + CellNumberExpected("an i64 number or a decimal integer string"); +const F64_EXPECTED: CellNumberExpected = CellNumberExpected("an f64 number or a numeric string"); + +/// 从 `arbitrary_precision` 的私有 number map 里取回原始数字文本。 +/// +/// 非该形态的 map 一律视为类型不符,由调用方报错,不做任何猜测性解析。 +fn precision_number_text<'de, A: MapAccess<'de>>(access: &mut A) -> Result { + let not_a_number = || A::Error::custom("a JSON object is not a cell number"); + match access.next_key::()?.as_deref() { + Some(ARBITRARY_PRECISION_TOKEN) => {} + _ => return Err(not_a_number()), + } + let text = access.next_value::()?; + if access.next_key::()?.is_some() { + return Err(not_a_number()); + } + Ok(text) +} + +/// `CellValue::I64` 的 `value`:number 或十进制整数文本。 +pub(crate) fn i64_value<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(I64Visitor) +} + +/// `CellValue::U64` 的 `value`:number 或十进制整数文本。 +pub(crate) fn u64_value<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(U64Visitor) +} + +/// `CellValue::F64` 的 `value`:number 或浮点/十进制文本。 +pub(crate) fn f64_value<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(F64Visitor) +} + +struct I64Visitor; + +impl<'de> Visitor<'de> for I64Visitor { + type Value = i64; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(I64_EXPECTED.0) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result { + i64::try_from(value) + .map_err(|_| E::invalid_value(Unexpected::Unsigned(value), &I64_EXPECTED)) + } + + fn visit_str(self, value: &str) -> Result { + value + .trim() + .parse::() + .map_err(|_| E::invalid_value(Unexpected::Str(value), &I64_EXPECTED)) + } + + fn visit_map>(self, mut access: A) -> Result { + parse_from_precision_map(&mut access, &I64_EXPECTED, |text| { + text.trim().parse::().ok() + }) + } +} + +struct U64Visitor; + +impl<'de> Visitor<'de> for U64Visitor { + type Value = u64; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(U64_EXPECTED.0) + } + + fn visit_u64(self, value: u64) -> Result { + Ok(value) + } + + fn visit_i64(self, value: i64) -> Result { + u64::try_from(value).map_err(|_| E::invalid_value(Unexpected::Signed(value), &U64_EXPECTED)) + } + + fn visit_str(self, value: &str) -> Result { + value + .trim() + .parse::() + .map_err(|_| E::invalid_value(Unexpected::Str(value), &U64_EXPECTED)) + } + + fn visit_map>(self, mut access: A) -> Result { + parse_from_precision_map(&mut access, &U64_EXPECTED, |text| { + text.trim().parse::().ok() + }) + } +} + +struct F64Visitor; + +impl<'de> Visitor<'de> for F64Visitor { + type Value = f64; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(F64_EXPECTED.0) + } + + fn visit_f64(self, value: f64) -> Result { + Ok(value) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(value as f64) + } + + fn visit_u64(self, value: u64) -> Result { + Ok(value as f64) + } + + fn visit_str(self, value: &str) -> Result { + value + .trim() + .parse::() + .map_err(|_| E::invalid_value(Unexpected::Str(value), &F64_EXPECTED)) + } + + fn visit_map>(self, mut access: A) -> Result { + parse_from_precision_map(&mut access, &F64_EXPECTED, |text| { + text.trim().parse::().ok() + }) + } +} + +/// 把 `arbitrary_precision` 私有 number map 里的数字文本按目标类型解析。 +fn parse_from_precision_map<'de, A, T>( + access: &mut A, + expected: &CellNumberExpected, + parse: impl Fn(&str) -> Option, +) -> Result +where + A: MapAccess<'de>, +{ + let text = precision_number_text(access) + .map_err(|_| A::Error::invalid_type(Unexpected::Map, expected))?; + parse(&text).ok_or_else(|| A::Error::invalid_value(Unexpected::Str(&text), expected)) +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct U64Cell { + #[serde(deserialize_with = "super::u64_value")] + value: u64, + } + + #[derive(Debug, Deserialize)] + struct I64Cell { + #[serde(deserialize_with = "super::i64_value")] + value: i64, + } + + #[derive(Debug, Deserialize)] + struct F64Cell { + #[serde(deserialize_with = "super::f64_value")] + value: f64, + } + + fn parse Deserialize<'de>>(raw: &str) -> Result { + serde_json::from_str(raw) + } + + #[test] + fn unsigned_accepts_number_and_decimal_text() { + assert_eq!(4, parse::(r#"{"value":4}"#).unwrap().value); + assert_eq!(4, parse::(r#"{"value":"4"}"#).unwrap().value); + assert_eq!( + u64::MAX, + parse::(r#"{"value":"18446744073709551615"}"#) + .unwrap() + .value + ); + assert_eq!( + u64::MAX, + parse::(r#"{"value":18446744073709551615}"#) + .unwrap() + .value + ); + } + + #[test] + fn unsigned_rejects_sign_and_non_numeric_text() { + for raw in [ + r#"{"value":"-1"}"#, + r#"{"value":"abc"}"#, + r#"{"value":-1}"#, + r#"{"value":"18446744073709551616"}"#, + r#"{"value":{"a":"4"}}"#, + ] { + assert!( + parse::(raw).is_err(), + "{raw} must not be accepted as an u64 cell value" + ); + } + } + + #[test] + fn signed_accepts_number_and_decimal_text() { + assert_eq!(-42, parse::(r#"{"value":-42}"#).unwrap().value); + assert_eq!(-42, parse::(r#"{"value":"-42"}"#).unwrap().value); + assert_eq!(42, parse::(r#"{"value":42}"#).unwrap().value); + assert!(parse::(r#"{"value":18446744073709551615}"#).is_err()); + assert!(parse::(r#"{"value":"4.5"}"#).is_err()); + } + + #[test] + fn float_accepts_number_and_numeric_text() { + // 浮点走 serde_json 的私有 number map(arbitrary_precision),不是 visit_f64。 + assert_eq!( + 1234567890.1234567, + parse::(r#"{"value":1234567890.1234567}"#) + .unwrap() + .value + ); + assert_eq!(1.5, parse::(r#"{"value":"1.5"}"#).unwrap().value); + assert_eq!(2.0, parse::(r#"{"value":"2"}"#).unwrap().value); + assert_eq!(2.0, parse::(r#"{"value":2}"#).unwrap().value); + assert!(parse::(r#"{"value":"not-a-number"}"#).is_err()); + assert!(parse::(r#"{"value":{"a":1.5}}"#).is_err()); + } +} diff --git a/crates/extension-protocol/src/lib.rs b/crates/extension-protocol/src/lib.rs index da7a952f2..0e15d1fa2 100644 --- a/crates/extension-protocol/src/lib.rs +++ b/crates/extension-protocol/src/lib.rs @@ -31,6 +31,7 @@ //! - [`event`][]: 单向事件通知 pub mod blob; +mod cell_number; pub mod conn; pub mod data; pub mod ddl; diff --git a/crates/extension-protocol/src/row.rs b/crates/extension-protocol/src/row.rs index d9df652ee..e0b4de758 100644 --- a/crates/extension-protocol/src/row.rs +++ b/crates/extension-protocol/src/row.rs @@ -138,6 +138,11 @@ impl ColumnSpec { /// /// 序列化为 `{ "type": "...", "value": ... }` 的 tagged union。 /// `Null` 没有 `value` 字段。 +/// +/// 数值 variant(`i64` / `u64` / `f64`)的 `value` 序列化后是 JSON number,但读取时 +/// 也接受等值的十进制文本——部分第三方驱动(如 MySQL 文本协议实现)习惯把无符号 +/// 整数发成十进制字符串,文本与 number 在宿主侧等价且无损,详见 +/// [`crate::cell_number`]。 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum CellValue { @@ -146,12 +151,15 @@ pub enum CellValue { value: bool, }, I64 { + #[serde(deserialize_with = "crate::cell_number::i64_value")] value: i64, }, U64 { + #[serde(deserialize_with = "crate::cell_number::u64_value")] value: u64, }, F64 { + #[serde(deserialize_with = "crate::cell_number::f64_value")] value: f64, }, /// `decimal` 以字符串保留精度。 @@ -283,6 +291,75 @@ mod tests { assert_eq!(su, r#"{"type":"u64","value":18000000000000000000}"#); } + #[test] + fn numeric_cells_read_equivalent_decimal_text() { + // 驱动把无符号整数发成十进制文本时宿主必须照常读入(issue #249/#207): + // 文本与 JSON number 在宿主侧等价,`u64::MAX` 也不丢精度。 + let parsed: CellValue = serde_json::from_str(r#"{"type":"u64","value":"4"}"#).unwrap(); + assert_eq!(parsed, CellValue::U64 { value: 4 }); + + let row: Row = serde_json::from_str( + r#"[{"type":"u64","value":"18446744073709551615"},{"type":"i64","value":"-42"},{"type":"f64","value":"1.5"}]"#, + ) + .unwrap(); + assert_eq!( + row, + vec![ + CellValue::U64 { value: u64::MAX }, + CellValue::I64 { value: -42 }, + CellValue::F64 { value: 1.5 }, + ] + ); + + // number 写法照旧(浮点在 serde_json 的 arbitrary_precision 下走私有 + // number map,不能因为改读取器而回归)。 + let numbers: Row = serde_json::from_str( + r#"[{"type":"u64","value":4},{"type":"i64","value":-42},{"type":"f64","value":1.5}]"#, + ) + .unwrap(); + assert_eq!( + numbers, + vec![ + CellValue::U64 { value: 4 }, + CellValue::I64 { value: -42 }, + CellValue::F64 { value: 1.5 }, + ] + ); + + // 嵌套容器里的数值 cell 同样走同一条读取路径。 + let array: CellValue = serde_json::from_str( + r#"{"type":"array","element_type":"u64","value":[{"type":"u64","value":"4"}]}"#, + ) + .unwrap(); + assert_eq!( + array, + CellValue::Array { + element_type: ColumnTypeKind::U64, + value: vec![CellValue::U64 { value: 4 }], + } + ); + } + + #[test] + fn numeric_cells_reject_unparsable_text() { + let error = serde_json::from_str::(r#"{"type":"u64","value":"abc"}"#) + .expect_err("非数字文本不能被静默接受"); + assert!( + error + .to_string() + .contains("an u64 number or a decimal integer string"), + "unexpected error: {error}" + ); + } + + #[test] + fn numeric_cells_serialize_back_as_numbers() { + let cell = CellValue::U64 { value: 4 }; + let wire: serde_json::Value = + serde_json::from_str(&serde_json::to_string(&cell).unwrap()).unwrap(); + assert_eq!(wire["value"], serde_json::json!(4)); + } + #[test] fn decimal_uses_string_to_preserve_precision() { let d = CellValue::Decimal {