Repository: sfu-db/connector-x
Pull request #806: Support PostgreSQL numeric type to decimal
Author: login: holicc
Created: 2025-05-15T11:29:19Z | Merged: 2025-05-21T17:27:08Z

[#751](https://github.com/sfu-db/connector-x/issues/751)


Diff:
diff --git a/connectorx-python/connectorx/tests/test_arrow.py b/connectorx-python/connectorx/tests/test_arrow.py
index 606c1db0c0..735cb85e6c 100644
--- a/connectorx-python/connectorx/tests/test_arrow.py
+++ b/connectorx-python/connectorx/tests/test_arrow.py
@@ -4,6 +4,7 @@
 import pytest
 from pandas.testing import assert_frame_equal
 import datetime
+from decimal import localcontext, Decimal
 
 from .. import read_sql
 
@@ -43,110 +44,115 @@ def test_arrow(postgres_url: str) -> None:
     df.sort_values(by="test_int", inplace=True, ignore_index=True)
     assert_frame_equal(df, expected, check_names=True)
 
+def decimal_s10(val):
+    return Decimal(val).quantize(Decimal("0.0000000001"))
+
 def test_arrow_type(postgres_url: str) -> None:
     query = "SELECT test_date, test_timestamp, test_timestamptz, test_int2, test_int4, test_int8, test_float4, test_float8, test_numeric, test_bpchar, test_char, test_varchar, test_uuid, test_time, test_bytea, test_json, test_jsonb, test_ltree, test_name FROM test_types"
     df = read_sql(postgres_url, query, return_type="arrow")
     df = df.to_pandas(date_as_object=False)
     df.sort_values(by="test_int2", inplace=True, ignore_index=True)
-    expected = pd.DataFrame(
-        index=range(5),
-        data={
-            "test_date": pd.Series(
-                ["1970-01-01", "2000-02-28", "2038-01-18", "1901-12-14", None], dtype="datetime64[ms]"
-            ),
-            "test_timestamp": pd.Series(
-                [
-                    "1970-01-01 00:00:01",
-                    "2000-02-28 12:00:10",
-                    "2038-01-18 23:59:59",
-                    "1901-12-14 00:00:00.062547",
-                    None,
-                ],
-                dtype="datetime64[us]",
-            ),
-            "test_timestamptz": pd.Series(
-                [
-                    "1970-01-01 00:00:01+00:00",
-                    "2000-02-28 12:00:10-04:00",
-                    "2038-01-18 23:59:59+08:00",
-                    "1901-12-14 00:00:00.062547-12:00",
-                    None,
-                ],
-                dtype="datetime64[us, UTC]",
-            ),
-            "test_int2": pd.Series([-32768, 0, 1, 32767], dtype="int16"),
-            "test_int4": pd.Series([0, 1, -2147483648, 2147483647], dtype="int32"),
-            "test_int8": pd.Series(
-                [-9223372036854775808, 0, 9223372036854775807, 1], dtype="float64"
-            ),
-            "test_float4": pd.Series(
-                [-1.1, 0.00, 2.123456, -12345.1, None], dtype="float32"
-            ),
-            "test_float8": pd.Series(
-                [-1.1, 0.00, 2.12345678901, -12345678901.1, None], dtype="float64"
-            ),
-            "test_numeric": pd.Series([0.01, 521.34, 0, -1.123e2, None], dtype="float64"),
-            "test_bpchar": pd.Series(["👨‍🍳  ", "bb   ", "     ", "ddddd", None], dtype="object"),
-            "test_char": pd.Series(["a", "ಠ", "😃", "@", None], dtype="object"),
-            "test_varchar": pd.Series(["abcdefghij", "", "👨‍🍳👨‍🍳👨‍🍳👨", "@", None], dtype="object"),
-            "test_uuid": pd.Series(
-                [
-                    "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
-                    "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
-                    "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
-                    "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
-                    None,
-                ],
-                dtype="object",
-            ),
-            "test_time": pd.Series(
-                [
-                    datetime.time(8, 12, 40),
-                    datetime.time(18, 30),
-                    datetime.time(23, 0, 10),
-                    datetime.time(0, 0, 59, 62547),
-                    None,
-                ],
-                dtype="object",
-            ),
-            "test_bytea": pd.Series(
-                [
-                    b'\x08',
-                    b"\xd0\x97\xd0\xb4\xd1\x80\xd0\xb0\xcc\x81\xd0\xb2\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd0\xb9\xd1\x82\xd0\xb5",
-                    b"",
-                    b"\xf0\x9f\x98\x9c",
-                    None
-                ],
-                dtype="object",
-            ),
-            "test_json": pd.Series(
-                [
-                    '{"customer":"John Doe","items":{"product":"Beer","qty":6}}',
-                    '{"customer":"Lily Bush","items":{"product":"Diaper","qty":24}}',
-                    '{"customer":"Josh William","items":{"product":"Toy Car","qty":1}}',
-                    '{}',
-                    None,
-                ],
-                dtype="object",
-            ),
-            "test_jsonb": pd.Series(
-                [
-                    '{"customer":"John Doe","items":{"product":"Beer","qty":6}}',
-                    '{"customer":"Lily Bush","items":{"product":"Diaper","qty":24}}',
-                    '{"customer":"Josh William","items":{"product":"Toy Car","qty":1}}',
-                    '{}',
-                    None,
-                ],
-                dtype="object",
-            ),
-            "test_ltree": pd.Series(
-                ["A.B.C.D", "A.B.E", "A", "", None], dtype="object"
-            ),
-            "test_name": pd.Series(
-                ["0", "21", "someName", "101203203-1212323-22131235", None]
-            )
-            
-        },
-    )
+    with localcontext() as ctx:
+        ctx.prec = 38
+        expected = pd.DataFrame(
+            index=range(5),
+            data={
+                "test_date": pd.Series(
+                    ["1970-01-01", "2000-02-28", "2038-01-18", "1901-12-14", None], dtype="datetime64[ms]"
+                ),
+                "test_timestamp": pd.Series(
+                    [
+                        "1970-01-01 00:00:01",
+                        "2000-02-28 12:00:10",
+                        "2038-01-18 23:59:59",
+                        "1901-12-14 00:00:00.062547",
+                        None,
+                    ],
+                    dtype="datetime64[us]",
+                ),
+                "test_timestamptz": pd.Series(
+                    [
+                        "1970-01-01 00:00:01+00:00",
+                        "2000-02-28 12:00:10-04:00",
+                        "2038-01-18 23:59:59+08:00",
+                        "1901-12-14 00:00:00.062547-12:00",
+                        None,
+                    ],
+                    dtype="datetime64[us, UTC]",
+                ),
+                "test_int2": pd.Series([-32768, 0, 1, 32767], dtype="int16"),
+                "test_int4": pd.Series([0, 1, -2147483648, 2147483647], dtype="int32"),
+                "test_int8": pd.Series(
+                    [-9223372036854775808, 0, 9223372036854775807, 1], dtype="float64"
+                ),
+                "test_float4": pd.Series(
+                    [-1.1, 0.00, 2.123456, -12345.1, None], dtype="float32"
+                ),
+                "test_float8": pd.Series(
+                    [-1.1, 0.00, 2.12345678901, -12345678901.1, None], dtype="float64"
+                ),
+                "test_numeric": pd.Series([decimal_s10(0.01), decimal_s10(521.34), decimal_s10(0), decimal_s10(-1.123e2), None], dtype="object"),
+                "test_bpchar": pd.Series(["👨‍🍳  ", "bb   ", "     ", "ddddd", None], dtype="object"),
+                "test_char": pd.Series(["a", "ಠ", "😃", "@", None], dtype="object"),
+                "test_varchar": pd.Series(["abcdefghij", "", "👨‍🍳👨‍🍳👨‍🍳👨", "@", None], dtype="object"),
+                "test_uuid": pd.Series(
+                    [
+                        "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
+                        "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
+                        "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
+                        "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
+                        None,
+                    ],
+                    dtype="object",
+                ),
+                "test_time": pd.Series(
+                    [
+                        datetime.time(8, 12, 40),
+                        datetime.time(18, 30),
+                        datetime.time(23, 0, 10),
+                        datetime.time(0, 0, 59, 62547),
+                        None,
+                    ],
+                    dtype="object",
+                ),
+                "test_bytea": pd.Series(
+                    [
+                        b'\x08',
+                        b"\xd0\x97\xd0\xb4\xd1\x80\xd0\xb0\xcc\x81\xd0\xb2\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd0\xb9\xd1\x82\xd0\xb5",
+                        b"",
+                        b"\xf0\x9f\x98\x9c",
+                        None
+                    ],
+                    dtype="object",
+                ),
+                "test_json": pd.Series(
+                    [
+                        '{"customer":"John Doe","items":{"product":"Beer","qty":6}}',
+                        '{"customer":"Lily Bush","items":{"product":"Diaper","qty":24}}',
+                        '{"customer":"Josh William","items":{"product":"Toy Car","qty":1}}',
+                        '{}',
+                        None,
+                    ],
+                    dtype="object",
+                ),
+                "test_jsonb": pd.Series(
+                    [
+                        '{"customer":"John Doe","items":{"product":"Beer","qty":6}}',
+                        '{"customer":"Lily Bush","items":{"product":"Diaper","qty":24}}',
+                        '{"customer":"Josh William","items":{"product":"Toy Car","qty":1}}',
+                        '{}',
+                        None,
+                    ],
+                    dtype="object",
+                ),
+                "test_ltree": pd.Series(
+                    ["A.B.C.D", "A.B.E", "A", "", None], dtype="object"
+                ),
+                "test_name": pd.Series(
+                    ["0", "21", "someName", "101203203-1212323-22131235", None]
+                )
+                
+            },
+        )
 
     assert_frame_equal(df, expected, check_names=True)
\ No newline at end of file
diff --git a/connectorx/src/constants.rs b/connectorx/src/constants.rs
index bcdd779089..30df391995 100644
--- a/connectorx/src/constants.rs
+++ b/connectorx/src/constants.rs
@@ -1,3 +1,16 @@
+#[cfg(feature = "dst_arrow")]
+use arrow::datatypes::DataType as ArrowDataType;
+
+#[cfg(feature = "dst_arrow")]
+pub const DEFAULT_ARROW_DECIMAL_PRECISION: u8 = 38;
+
+#[cfg(feature = "dst_arrow")]
+pub const DEFAULT_ARROW_DECIMAL_SCALE: i8 = 10;
+
+#[cfg(feature = "dst_arrow")]
+pub const DEFAULT_ARROW_DECIMAL: ArrowDataType =
+    ArrowDataType::Decimal128(DEFAULT_ARROW_DECIMAL_PRECISION, DEFAULT_ARROW_DECIMAL_SCALE);
+
 #[cfg(feature = "dst_arrow")]
 pub(crate) const SECONDS_IN_DAY: i64 = 86_400;
 
diff --git a/connectorx/src/destinations/arrow/arrow_assoc.rs b/connectorx/src/destinations/arrow/arrow_assoc.rs
index e71a61aa1f..f9b40c370d 100644
--- a/connectorx/src/destinations/arrow/arrow_assoc.rs
+++ b/connectorx/src/destinations/arrow/arrow_assoc.rs
@@ -2,10 +2,13 @@ use super::{
     errors::{ArrowDestinationError, Result},
     typesystem::{DateTimeWrapperMicro, NaiveDateTimeWrapperMicro, NaiveTimeWrapperMicro},
 };
-use crate::constants::SECONDS_IN_DAY;
+use crate::{
+    constants::{DEFAULT_ARROW_DECIMAL, DEFAULT_ARROW_DECIMAL_SCALE, SECONDS_IN_DAY},
+    utils::decimal_to_i128,
+};
 use arrow::array::{
-    ArrayBuilder, BooleanBuilder, Date32Builder, Float32Builder, Float64Builder, Int16Builder,
-    Int32Builder, Int64Builder, LargeBinaryBuilder, LargeListBuilder, StringBuilder,
+    ArrayBuilder, BooleanBuilder, Date32Builder, Decimal128Builder, Float32Builder, Float64Builder,
+    Int16Builder, Int32Builder, Int64Builder, LargeBinaryBuilder, LargeListBuilder, StringBuilder,
     Time64MicrosecondBuilder, Time64NanosecondBuilder, TimestampMicrosecondBuilder,
     TimestampNanosecondBuilder, UInt16Builder, UInt32Builder, UInt64Builder,
 };
@@ -13,6 +16,7 @@ use arrow::datatypes::Field;
 use arrow::datatypes::{DataType as ArrowDataType, TimeUnit};
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc};
 use fehler::throws;
+use rust_decimal::Decimal;
 
 /// Associate arrow builder with native type
 pub trait ArrowAssoc {
@@ -71,6 +75,46 @@ impl_arrow_assoc!(f32, ArrowDataType::Float32, Float32Builder);
 impl_arrow_assoc!(f64, ArrowDataType::Float64, Float64Builder);
 impl_arrow_assoc!(bool, ArrowDataType::Boolean, BooleanBuilder);
 
+impl ArrowAssoc for Decimal {
+    type Builder = Decimal128Builder;
+
+    fn builder(nrows: usize) -> Self::Builder {
+        Decimal128Builder::with_capacity(nrows).with_data_type(DEFAULT_ARROW_DECIMAL)
+    }
+
+    fn append(builder: &mut Self::Builder, value: Self) -> Result<()> {
+        builder.append_value(decimal_to_i128(value, DEFAULT_ARROW_DECIMAL_SCALE as u32)?);
+        Ok(())
+    }
+
+    fn field(header: &str) -> Field {
+        Field::new(header, DEFAULT_ARROW_DECIMAL, false)
+    }
+}
+
+impl ArrowAssoc for Option<Decimal> {
+    type Builder = Decimal128Builder;
+
+    fn builder(nrows: usize) -> Self::Builder {
+        Decimal128Builder::with_capacity(nrows).with_data_type(DEFAULT_ARROW_DECIMAL)
+    }
+
+    fn append(builder: &mut Self::Builder, value: Self) -> Result<()> {
+        match value {
+            Some(v) => builder.append_option(Some(decimal_to_i128(
+                v,
+                DEFAULT_ARROW_DECIMAL_SCALE as u32,
+            )?)),
+            None => builder.append_null(),
+        }
+        Ok(())
+    }
+
+    fn field(header: &str) -> Field {
+        Field::new(header, DEFAULT_ARROW_DECIMAL, true)
+    }
+}
+
 impl ArrowAssoc for &str {
     type Builder = StringBuilder;
 
@@ -486,6 +530,93 @@ impl ArrowAssoc for Vec<u8> {
     }
 }
 
+impl ArrowAssoc for Option<Vec<Option<Decimal>>> {
+    type Builder = LargeListBuilder<Decimal128Builder>;
+
+    fn builder(nrows: usize) -> Self::Builder {
+        LargeListBuilder::with_capacity(
+            Decimal128Builder::with_capacity(nrows).with_data_type(DEFAULT_ARROW_DECIMAL),
+            nrows,
+        )
+    }
+
+    fn append(builder: &mut Self::Builder, value: Self) -> Result<()> {
+        match value {
+            Some(vals) => {
+                let mut list = vec![];
+
+                for val in vals {
+                    match val {
+                        Some(v) => {
+                            list.push(Some(decimal_to_i128(
+                                v,
+                                DEFAULT_ARROW_DECIMAL_SCALE as u32,
+                            )?));
+                        }
+                        None => list.push(None),
+                    }
+                }
+
+                builder.append_value(list);
+            }
+            None => builder.append_null(),
+        };
+        Ok(())
+    }
+
+    fn field(header: &str) -> Field {
+        Field::new(
+            header,
+            ArrowDataType::LargeList(std::sync::Arc::new(Field::new_list_field(
+                DEFAULT_ARROW_DECIMAL,
+                true,
+            ))),
+            true,
+        )
+    }
+}
+
+impl ArrowAssoc for Vec<Option<Decimal>> {
+    type Builder = LargeListBuilder<Decimal128Builder>;
+
+    fn builder(nrows: usize) -> Self::Builder {
+        LargeListBuilder::with_capacity(
+            Decimal128Builder::with_capacity(nrows).with_data_type(DEFAULT_ARROW_DECIMAL),
+            nrows,
+        )
+    }
+
+    fn append(builder: &mut Self::Builder, vals: Self) -> Result<()> {
+        let mut list = vec![];
+
+        for val in vals {
+            match val {
+                Some(v) => {
+                    list.push(Some(decimal_to_i128(
+                        v,
+                        DEFAULT_ARROW_DECIMAL_SCALE as u32,
+                    )?));
+                }
+                None => list.push(None),
+            }
+        }
+
+        builder.append_value(list);
+        Ok(())
+    }
+
+    fn field(header: &str) -> Field {
+        Field::new(
+            header,
+            ArrowDataType::LargeList(std::sync::Arc::new(Field::new_list_field(
+                DEFAULT_ARROW_DECIMAL,
+                false,
+            ))),
+            false,
+        )
+    }
+}
+
 macro_rules! impl_arrow_array_assoc {
     ($T:ty, $AT:expr, $B:ident) => {
         impl ArrowAssoc for $T {
diff --git a/connectorx/src/destinations/arrow/typesystem.rs b/connectorx/src/destinations/arrow/typesystem.rs
index 3eae46bb07..2b190c5c66 100644
--- a/connectorx/src/destinations/arrow/typesystem.rs
+++ b/connectorx/src/destinations/arrow/typesystem.rs
@@ -1,5 +1,6 @@
 use crate::impl_typesystem;
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
+use rust_decimal::Decimal;
 
 #[derive(Debug, Clone, Copy)]
 pub struct DateTimeWrapperMicro(pub DateTime<Utc>);
@@ -20,6 +21,7 @@ pub enum ArrowTypeSystem {
     UInt64(bool),
     Float32(bool),
     Float64(bool),
+    Decimal(bool),
     Boolean(bool),
     LargeUtf8(bool),
     LargeBinary(bool),
@@ -40,6 +42,7 @@ pub enum ArrowTypeSystem {
     UInt64Array(bool),
     Float32Array(bool),
     Float64Array(bool),
+    DecimalArray(bool),
 }
 
 impl_typesystem! {
@@ -53,6 +56,7 @@ impl_typesystem! {
         { UInt64          => u64                       }
         { Float32         => f32                       }
         { Float64         => f64                       }
+        { Decimal         => Decimal                   }
         { Boolean         => bool                      }
         { LargeUtf8       => String                    }
         { LargeBinary     => Vec<u8>                   }
@@ -73,5 +77,6 @@ impl_typesystem! {
         { UInt64Array     => Vec<Option<u64>>          }
         { Float32Array    => Vec<Option<f32>>          }
         { Float64Array    => Vec<Option<f64>>          }
+        { DecimalArray    => Vec<Option<Decimal>>      }
     }
 }
diff --git a/connectorx/src/destinations/arrowstream/arrow_assoc.rs b/connectorx/src/destinations/arrowstream/arrow_assoc.rs
index be119a7f5f..b6aece3544 100644
--- a/connectorx/src/destinations/arrowstream/arrow_assoc.rs
+++ b/connectorx/src/destinations/arrowstream/arrow_assoc.rs
@@ -1,15 +1,16 @@
 use super::errors::{ArrowDestinationError, Result};
-use crate::constants::SECONDS_IN_DAY;
+use crate::constants::{DEFAULT_ARROW_DECIMAL, DEFAULT_ARROW_DECIMAL_SCALE, SECONDS_IN_DAY};
+use crate::utils::decimal_to_i128;
 use arrow::array::{
-    ArrayBuilder, BooleanBuilder, Date32Builder, Date64Builder, Float32Builder, Float64Builder,
-    Int32Builder, Int64Builder, LargeBinaryBuilder, StringBuilder, Time64NanosecondBuilder,
-    TimestampNanosecondBuilder, UInt32Builder, UInt64Builder,
+    ArrayBuilder, BooleanBuilder, Date32Builder, Date64Builder, Decimal128Builder, Float32Builder,
+    Float64Builder, Int32Builder, Int64Builder, LargeBinaryBuilder, StringBuilder,
+    Time64NanosecondBuilder, TimestampNanosecondBuilder, UInt32Builder, UInt64Builder,
 };
 use arrow::datatypes::Field;
 use arrow::datatypes::{DataType as ArrowDataType, TimeUnit};
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc};
 use fehler::throws;
-
+use rust_decimal::Decimal;
 /// Associate arrow builder with native type
 pub trait ArrowAssoc {
     type Builder: ArrayBuilder + Send;
@@ -65,6 +66,46 @@ impl_arrow_assoc!(f32, ArrowDataType::Float32, Float32Builder);
 impl_arrow_assoc!(f64, ArrowDataType::Float64, Float64Builder);
 impl_arrow_assoc!(bool, ArrowDataType::Boolean, BooleanBuilder);
 
+impl ArrowAssoc for Decimal {
+    type Builder = Decimal128Builder;
+
+    fn builder(nrows: usize) -> Self::Builder {
+        Decimal128Builder::with_capacity(nrows).with_data_type(DEFAULT_ARROW_DECIMAL)
+    }
+
+    fn append(builder: &mut Self::Builder, value: Self) -> Result<()> {
+        builder.append_value(decimal_to_i128(value, DEFAULT_ARROW_DECIMAL_SCALE as u32)?);
+        Ok(())
+    }
+
+    fn field(header: &str) -> Field {
+        Field::new(header, DEFAULT_ARROW_DECIMAL, false)
+    }
+}
+
+impl ArrowAssoc for Option<Decimal> {
+    type Builder = Decimal128Builder;
+
+    fn builder(nrows: usize) -> Self::Builder {
+        Decimal128Builder::with_capacity(nrows).with_data_type(DEFAULT_ARROW_DECIMAL)
+    }
+
+    fn append(builder: &mut Self::Builder, value: Self) -> Result<()> {
+        match value {
+            Some(v) => builder.append_option(Some(decimal_to_i128(
+                v,
+                DEFAULT_ARROW_DECIMAL_SCALE as u32,
+            )?)),
+            None => builder.append_null(),
+        }
+        Ok(())
+    }
+
+    fn field(header: &str) -> Field {
+        Field::new(header, DEFAULT_ARROW_DECIMAL, true)
+    }
+}
+
 impl ArrowAssoc for &str {
     type Builder = StringBuilder;
 
diff --git a/connectorx/src/destinations/arrowstream/typesystem.rs b/connectorx/src/destinations/arrowstream/typesystem.rs
index a6997a2ba2..61e3d6d7a2 100644
--- a/connectorx/src/destinations/arrowstream/typesystem.rs
+++ b/connectorx/src/destinations/arrowstream/typesystem.rs
@@ -1,5 +1,6 @@
 use crate::impl_typesystem;
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
+use rust_decimal::Decimal;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub enum ArrowTypeSystem {
@@ -9,6 +10,7 @@ pub enum ArrowTypeSystem {
     UInt64(bool),
     Float32(bool),
     Float64(bool),
+    Decimal(bool),
     Boolean(bool),
     LargeUtf8(bool),
     LargeBinary(bool),
@@ -27,6 +29,7 @@ impl_typesystem! {
         { UInt64     => u64           }
         { Float64    => f64           }
         { Float32    => f32           }
+        { Decimal    => Decimal       }
         { Boolean    => bool          }
         { LargeUtf8  => String        }
         { LargeBinary => Vec<u8>      }
diff --git a/connectorx/src/transports/postgres_arrow.rs b/connectorx/src/transports/postgres_arrow.rs
index 558fa7d81b..cc7ceefafe 100644
--- a/connectorx/src/transports/postgres_arrow.rs
+++ b/connectorx/src/transports/postgres_arrow.rs
@@ -46,7 +46,7 @@ macro_rules! impl_postgres_transport {
             mappings = {
                 { Float4[f32]                        => Float32[f32]                           | conversion auto   }
                 { Float8[f64]                        => Float64[f64]                           | conversion auto   }
-                { Numeric[Decimal]                   => Float64[f64]                           | conversion option }
+                { Numeric[Decimal]                   => Decimal[Decimal]                       | conversion auto   }
                 { Int2[i16]                          => Int16[i16]                             | conversion auto   }
                 { Int4[i32]                          => Int32[i32]                             | conversion auto   }
                 { Int8[i64]                          => Int64[i64]                             | conversion auto   }
@@ -73,7 +73,7 @@ macro_rules! impl_postgres_transport {
                 { Int8Array[Vec<Option<i64>>]        => Int64Array[Vec<Option<i64>>]           | conversion auto   }
                 { Float4Array[Vec<Option<f32>>]      => Float32Array[Vec<Option<f32>>]         | conversion auto   }
                 { Float8Array[Vec<Option<f64>>]      => Float64Array[Vec<Option<f64>>]         | conversion auto   }
-                { NumericArray[Vec<Option<Decimal>>] => Float64Array[Vec<Option<f64>>]         | conversion option }
+                { NumericArray[Vec<Option<Decimal>>] => DecimalArray[Vec<Option<Decimal>>]     | conversion auto   }
             }
         );
     }
@@ -126,16 +126,3 @@ impl<P, C> TypeConversion<Value, String> for PostgresArrowTransport<P, C> {
         val.to_string()
     }
 }
-
-impl<P, C> TypeConversion<Vec<Option<Decimal>>, Vec<Option<f64>>> for PostgresArrowTransport<P, C> {
-    fn convert(val: Vec<Option<Decimal>>) -> Vec<Option<f64>> {
-        val.into_iter()
-            .map(|v| {
-                v.map(|v| {
-                    v.to_f64()
-                        .unwrap_or_else(|| panic!("cannot convert decimal {:?} to float64", v))
-                })
-            })
-            .collect()
-    }
-}
diff --git a/connectorx/src/transports/postgres_arrowstream.rs b/connectorx/src/transports/postgres_arrowstream.rs
index 7d1c20d9e3..1449cea924 100644
--- a/connectorx/src/transports/postgres_arrowstream.rs
+++ b/connectorx/src/transports/postgres_arrowstream.rs
@@ -9,7 +9,6 @@ use crate::sources::postgres::{
 };
 use crate::typesystem::TypeConversion;
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
-use num_traits::ToPrimitive;
 use postgres::NoTls;
 use postgres_openssl::MakeTlsConnector;
 use rust_decimal::Decimal;
@@ -43,7 +42,7 @@ macro_rules! impl_postgres_transport {
             mappings = {
                 { Float4[f32]                => Float64[f64]              | conversion auto }
                 { Float8[f64]                => Float64[f64]              | conversion auto }
-                { Numeric[Decimal]           => Float64[f64]              | conversion option }
+                { Numeric[Decimal]           => Decimal[Decimal]          | conversion auto }
                 { Int2[i16]                  => Int64[i64]                | conversion auto }
                 { Int4[i32]                  => Int64[i64]                | conversion auto }
                 { Int8[i64]                  => Int64[i64]                | conversion auto }
@@ -81,13 +80,6 @@ impl<P, C> TypeConversion<Uuid, String> for PostgresArrowTransport<P, C> {
     }
 }
 
-impl<P, C> TypeConversion<Decimal, f64> for PostgresArrowTransport<P, C> {
-    fn convert(val: Decimal) -> f64 {
-        val.to_f64()
-            .unwrap_or_else(|| panic!("cannot convert decimal {:?} to float64", val))
-    }
-}
-
 impl<P, C> TypeConversion<Value, String> for PostgresArrowTransport<P, C> {
     fn convert(val: Value) -> String {
         val.to_string()
diff --git a/connectorx/src/utils.rs b/connectorx/src/utils.rs
index ec3919ebb4..03a7c883dd 100644
--- a/connectorx/src/utils.rs
+++ b/connectorx/src/utils.rs
@@ -1,3 +1,5 @@
+use anyhow::Result;
+use rust_decimal::Decimal;
 use std::ops::{Deref, DerefMut};
 
 pub struct DummyBox<T>(pub T);
@@ -15,3 +17,18 @@ impl<T> DerefMut for DummyBox<T> {
         &mut self.0
     }
 }
+
+pub fn decimal_to_i128(mut v: Decimal, scale: u32) -> Result<i128> {
+    v.rescale(scale);
+
+    let v_scale = v.scale();
+    if v_scale != scale as u32 {
+        return Err(anyhow::anyhow!(
+            "decimal scale is not equal to expected scale, got: {} expected: {}",
+            v_scale,
+            scale
+        ));
+    }
+
+    Ok(v.mantissa())
+}
diff --git a/connectorx/tests/test_postgres.rs b/connectorx/tests/test_postgres.rs
index dca520a45b..2e61cede82 100644
--- a/connectorx/tests/test_postgres.rs
+++ b/connectorx/tests/test_postgres.rs
@@ -1,8 +1,9 @@
 use arrow::{
     array::{
-        BooleanArray, BooleanBuilder, Date32Array, Float32Array, Float64Array, Int16Array,
-        Int32Array, Int64Array, LargeBinaryArray, LargeListArray, LargeListBuilder, StringArray,
-        StringBuilder, Time64MicrosecondArray, TimestampMicrosecondArray,
+        Array, BooleanArray, BooleanBuilder, Date32Array, Decimal128Array, Decimal128Builder,
+        Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, LargeBinaryArray,
+        LargeListArray, LargeListBuilder, StringArray, StringBuilder, Time64MicrosecondArray,
+        TimestampMicrosecondArray,
     },
     datatypes::{Float32Type, Float64Type, Int16Type, Int32Type, Int64Type},
     record_batch::RecordBatch,
@@ -763,18 +764,19 @@ pub fn verify_arrow_type_results(result: Vec<RecordBatch>, protocol: &str) {
 
     // test_numeric
     col += 1;
-    assert!(result[0]
+    let actual = result[0]
         .column(col)
         .as_any()
-        .downcast_ref::<Float64Array>()
-        .unwrap()
-        .eq(&Float64Array::from(vec![
-            Some(0.01),
-            Some(521.34),
-            Some(0.0),
-            Some(-112.3),
-            None,
-        ])));
+        .downcast_ref::<Decimal128Array>()
+        .unwrap();
+    let expected = build_decimal_array(vec![
+        Some(100000000),
+        Some(5213400000000),
+        Some(0),
+        Some(-1123000000000),
+        None,
+    ]);
+    assert_eq!(actual, &expected);
 
     // test_bpchar
     col += 1;
@@ -970,20 +972,28 @@ pub fn verify_arrow_type_results(result: Vec<RecordBatch>, protocol: &str) {
 
     // test_narray
     col += 1;
-    assert!(result[0]
+    let actual = result[0]
         .column(col)
         .as_any()
         .downcast_ref::<LargeListArray>()
-        .unwrap()
-        .eq(&LargeListArray::from_iter_primitive::<Float64Type, _, _>(
-            vec![
-                Some(vec![Some(0.01), Some(521.23)]),
-                Some(vec![Some(0.12), Some(333.33), Some(22.22)]),
-                Some(vec![]),
-                Some(vec![Some(0.0), None, Some(-112.1)]),
-                None,
-            ]
-        )));
+        .unwrap();
+
+    let mut expected = LargeListBuilder::new(
+        Decimal128Builder::new()
+            .with_precision_and_scale(38, 10)
+            .unwrap(),
+    );
+    expected.append_value(vec![Some(100000000), Some(5212300000000)]);
+    expected.append_value(vec![
+        Some(1200000000),
+        Some(3333300000000),
+        Some(222200000000),
+    ]);
+    expected.append_value(vec![]);
+    expected.append_value(vec![Some(0), None, Some(-1121000000000)]);
+    expected.append_null();
+
+    assert!(actual.eq(&expected.finish()));
 
     // test_boolarray (from_iter_primitive not available for boolean)
     col += 1;
@@ -1313,3 +1323,18 @@ fn test_postgres_partitioned_pre_execution_queries() {
         Some(&2252)
     );
 }
+
+fn build_decimal_array(vals: Vec<Option<i128>>) -> Decimal128Array {
+    let mut builder = Decimal128Builder::new()
+        .with_precision_and_scale(38, 10)
+        .unwrap();
+
+    for val in vals {
+        match val {
+            Some(v) => builder.append_value(v),
+            None => builder.append_null(),
+        }
+    }
+
+    builder.finish()
+}


Reviews:
user: login: wangxiaoying
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T22:49:09Z
user: login: wangxiaoying
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T22:54:43Z
user: login: wangxiaoying
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T23:20:35Z

Review comments:
diff_hunk: @@ -43,7 +42,7 @@ macro_rules! impl_postgres_transport {
             mappings = {
                 { Float4[f32]                => Float64[f64]              | conversion auto }
                 { Float8[f64]                => Float64[f64]              | conversion auto }
-                { Numeric[Decimal]           => Float64[f64]              | conversion option }
+                { Numeric[Decimal]           => Decimal[Decimal]          | conversion option }
path: connectorx/src/transports/postgres_arrowstream.rs
user: login: wangxiaoying
body: Use `auto` here directly?
created_at: 2025-05-20T22:49:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +26,46 @@ use postgres::NoTls;
 use std::env;
 use url::Url;
 
+#[test]
+fn test_types_simple_postgres_aa() {
path: connectorx/tests/test_postgres.rs
user: login: wangxiaoying
body: clean up this and the next `test_decimal_128` tests?
created_at: 2025-05-20T22:54:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,6 +72,51 @@ impl_arrow_assoc!(f32, ArrowDataType::Float32, Float32Builder);
 impl_arrow_assoc!(f64, ArrowDataType::Float64, Float64Builder);
 impl_arrow_assoc!(bool, ArrowDataType::Boolean, BooleanBuilder);
 
+const DEFAULT_ARROW_DECIMAL_PRECISION: u8 = 38;
path: connectorx/src/destinations/arrow/arrow_assoc.rs
user: login: wangxiaoying
body: Move these constants to `connectorx/constants.rs` and share them between `arrow` and `arrowstream`?
created_at: 2025-05-20T23:20:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: wangxiaoying
created_at: 2025-05-20T23:18:06Z
body: Hi @holicc , thanks for the PR!

The code looks good to me. I made some simple changes to fix the CI errors. I also left a few comments on some minor issues.
author_association: CONTRIBUTOR
user: login: holicc
created_at: 2025-05-21T05:42:53Z
body: Thank you for taking the time to review the code. I have addressed the issues you pointed out.  :)
author_association: COLLABORATOR