diff --git a/crates/dbx-core/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs index 0d9ba47b6..9ad1acceb 100644 --- a/crates/dbx-core/src/db/postgres.rs +++ b/crates/dbx-core/src/db/postgres.rs @@ -523,7 +523,7 @@ pub(crate) fn pg_value_to_json_classified(row: &Row, idx: usize, col_type: PgCol } serde_json::Value::Null } - PgColType::Bool => row.try_get::<_, bool>(idx).map(serde_json::Value::Bool).unwrap_or(serde_json::Value::Null), + PgColType::Bool => pg_bool_value_to_json(row, idx), PgColType::Interval => row .try_get::<_, PgInterval>(idx) .map(|interval| serde_json::Value::String(format_pg_interval(interval))) @@ -2636,11 +2636,42 @@ fn parse_enum_values_from_row(row: &Row, index: usize) -> Option> { serde_json::from_str::>(&raw).ok() } +/// Decode a boolean column to JSON, tolerating databases (e.g. GaussDB) that +/// encode booleans as the ASCII bytes `t` (0x74) / `f` (0x66) in the binary +/// protocol instead of the standard PostgreSQL 0x00 / 0x01. +fn pg_bool_value_to_json(row: &Row, idx: usize) -> serde_json::Value { + if let Some(v) = pg_row_try_bool(row, idx) { + return serde_json::Value::Bool(v); + } + serde_json::Value::Null +} + +/// Map raw boolean bytes to a Rust `bool`. +/// +/// Standard PostgreSQL binary uses `[0x00]` / `[0x01]`; GaussDB sends the ASCII +/// text representation `[b't']` / `[b'f']` instead. +fn decode_bool_bytes(raw: &[u8]) -> Option { + match raw { + [0x00] => Some(false), + [0x01] => Some(true), + [b't'] | [b'T'] => Some(true), + [b'f'] | [b'F'] => Some(false), + _ => None, + } +} + +fn decode_bool_candidates(raw: Option<&[u8]>, standard: Option) -> Option { + raw.and_then(decode_bool_bytes).or(standard) +} + /// Read a boolean column from a PostgreSQL row, tolerating databases that /// encode booleans as integers (0/1) or text ('t'/'f') instead of the standard /// `bool` OID. Returns `None` when the column is NULL or truly unreadable. fn pg_row_try_bool(row: &Row, idx: usize) -> Option { - if let Ok(v) = row.try_get::<_, bool>(idx) { + // GaussDB encodes boolean as ASCII 't' (0x74) / 'f' (0x66) in binary. + let raw = row.try_get::<_, PgRawBytes>(idx).ok(); + let standard = row.try_get::<_, bool>(idx).ok(); + if let Some(v) = decode_bool_candidates(raw.as_ref().map(|value| value.0.as_slice()), standard) { return Some(v); } if let Ok(v) = row.try_get::<_, i32>(idx) { @@ -4501,6 +4532,33 @@ mod tests { assert_eq!(raw.0, vec![0x01, 0xAB, 0xFF]); } + #[test] + fn decode_bool_bytes_handles_standard_and_gaussdb_encodings() { + // Standard PostgreSQL binary boolean: 0x00 / 0x01 + assert_eq!(decode_bool_bytes(&[0x00]), Some(false)); + assert_eq!(decode_bool_bytes(&[0x01]), Some(true)); + // GaussDB binary boolean: ASCII 't' (0x74) / 'f' (0x66) + assert_eq!(decode_bool_bytes(&[0x74]), Some(true)); + assert_eq!(decode_bool_bytes(&[0x66]), Some(false)); + assert_eq!(decode_bool_bytes(b"t"), Some(true)); + assert_eq!(decode_bool_bytes(b"f"), Some(false)); + assert_eq!(decode_bool_bytes(b"T"), Some(true)); + assert_eq!(decode_bool_bytes(b"F"), Some(false)); + // Unrecognized encodings return None + assert_eq!(decode_bool_bytes(&[0x02]), None); + assert_eq!(decode_bool_bytes(&[0x74, 0x66]), None); + assert_eq!(decode_bool_bytes(&[]), None); + } + + #[test] + fn raw_gaussdb_boolean_takes_precedence_over_standard_decoder() { + assert_eq!(decode_bool_candidates(Some(b"f"), Some(true)), Some(false)); + assert_eq!(decode_bool_candidates(Some(b"t"), Some(true)), Some(true)); + assert_eq!(decode_bool_candidates(Some(&[0x00]), Some(true)), Some(false)); + assert_eq!(decode_bool_candidates(Some(&[0x01]), Some(false)), Some(true)); + assert_eq!(decode_bool_candidates(Some(&[0x02]), Some(false)), Some(false)); + } + #[test] fn postgres_foreign_keys_sql_selects_referential_actions() { let sql = postgres_foreign_keys_sql();