Rust数据库插件pg_bigdecimal的使用:PostgreSQL高精度数值计算与存储解决方案

rust-pg_bigdecimal

一个用于PostgreSQL Numeric类型的Rust数据类型实现(即PostgreSQL文档中"decimal"/"numeric"下列出的类型),用于与Rust的"Postgres"库一起使用。 支持PostgreSQL Numeric值的完整范围。

这个小型的Rust包之所以被创建,是因为目前主要的"Postgres"库没有提供原生数据类型来读取/写入Numeric值。

我们只实现了PostgreSQL Numeric数据类型的传输逻辑。我们没有重写大数操作的整个逻辑, 而是让已经流行的BigDecimal包来实现该逻辑。

具体来说,我们的新Rust数据类型PgNumeric只是一个可选的BigDecimalNone表示PostgreSQL Numeric值NaN,所有Some(..)表示PostgreSQL Numeric数字。

在crates.io上列出。

类似包之间的比较

  • rust-decimal提供了一个rust原生类型,但它表示为一个96位整数数字+缩放位+1个符号位。 这意味着只有(这里引用"只有"是因为它仍然是一个大的整数空间)PostgreSQL Numeric值的一小部分可以被转换。在我们的情况下,这还不够。

安装

在项目目录中运行以下Cargo命令:

cargo add pg_bigdecimal

或者将以下行添加到Cargo.toml中:

pg_bigdecimal = “0.1.5”

文档

docs.rs/pg_bigdecimal/0.1.5

仓库

github.com/tzConnectBerlin/rust-pg_bigdecimal

所有者

Rick Klomp

类别

数据库接口

完整示例demo:

use postgres::{Client, NoTls};
use pg_bigdecimal::PgNumeric;
use std::str::FromStr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 连接到PostgreSQL数据库
    let mut client = Client::connect("host=localhost user=postgres dbname=test", NoTls)?;
    
    // 创建测试表
    client.execute(
        "CREATE TABLE IF NOT EXISTS financial_data (
            id SERIAL PRIMARY KEY,
            amount NUMERIC(20, 8) NOT NULL,
            description TEXT
        )",
        &[],
    )?;
    
    // 插入高精度数值数据
    let amount = PgNumeric::from_str("123456789012345.12345678")?;
    client.execute(
        "INSERT INTO financial_data (amount, description) VALUES ($1, $2)",
        &[&amount, &"高精度金融交易"],
    )?;
    
    // 查询数据
    for row in client.query("SELECT id, amount, description FROM financial_data", &[])? {
        let id: i32 = row.get(0);
        let amount: PgNumeric = row.get(1);
        let description: Option<String> = row.get(2);
        
        println!(
            "ID: {}, Amount: {}, Description: {:?}",
            id, amount, description
        );
    }
    
    // 执行高精度计算
    let result: PgNumeric = client.query_one(
        "SELECT amount * 2 FROM financial_data WHERE id = 1",
        &[],
    )?.get(0);
    
    println!("计算后的金额: {}", result);
    
    Ok(())
}
// Cargo.toml依赖配置
[dependencies]
postgres = "0.19"
pg_bigdecimal = "0.1.5"
bigdecimal = "0.3"

完整示例代码:

use postgres::{Client, NoTls, error::Error};
use pg_bigdecimal::PgNumeric;
use std::str::FromStr;
use bigdecimal::BigDecimal;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 建立数据库连接
    let mut client = Client::connect("host=localhost user=postgres password=your_password dbname=test_db", NoTls)?;
    
    // 创建数据表用于存储高精度数值
    client.execute(
        "CREATE TABLE IF NOT EXISTS precision_numbers (
            id SERIAL PRIMARY KEY,
            value NUMERIC(30, 15) NOT NULL,
            note VARCHAR(100)
        )",
        &[],
    )?;
    
    // 准备要插入的高精度数值
    let numeric_value1 = PgNumeric::from_str("987654321098765.123456789012345")?;
    let numeric_value2 = PgNumeric::from_str("-123456789012345.678901234567890")?;
    
    // 插入多条高精度数值记录
    client.execute(
        "INSERT INTO precision_numbers (value, note) VALUES ($1, $2)",
        &[&numeric_value1, &"正数高精度测试"],
    )?;
    
    client.execute(
        "INSERT INTO precision_numbers (value, note) VALUES ($1, $2)",
        &[&numeric_value2, &"负数高精度测试"],
    )?;
    
    // 查询并显示所有数据
    println!("=== 查询所有数据 ===");
    for row in client.query("SELECT id, value, note FROM precision_numbers", &[])? {
        let id: i32 = row.get(0);
        let value: PgNumeric = row.get(1);
        let note: Option<String> = row.get(2);
        
        println!("ID: {}, Value: {}, Note: {:?}", id, value, note);
    }
    
    // 执行复杂计算查询
    println!("\n=== 执行高精度计算 ===");
    let calculation_result: PgNumeric = client.query_one(
        "SELECT SUM(value * 1.5) FROM precision_numbers",
        &[],
    )?.get(0);
    
    println!("计算结果: {}", calculation_result);
    
    // 处理NaN情况示例
    println!("\n=== 处理NaN值 ===");
    client.execute(
        "INSERT INTO precision_numbers (value, note) VALUES ('NaN'::numeric, $1)",
        &[&"NaN值测试"],
    )?;
    
    let nan_result: PgNumeric = client.query_one(
        "SELECT value FROM precision_numbers WHERE note = 'NaN值测试'",
        &[],
    )?.get(0);
    
    match nan_result {
        PgNumeric::None => println!("检测到NaN值"),
        PgNumeric::Some(val) => println!("数值: {}", val),
    }
    
    // 清理测试数据
    client.execute("DELETE FROM precision_numbers", &[])?;
    
    Ok(())
}
// Cargo.toml依赖配置
[dependencies]
postgres = "0.19"
pg_bigdecimal = "0.1.5"
bigdecimal = "0.3"
serde = { version = "1.0", features = ["derive"] }  # 可选:用于序列化支持

1 回复

Rust数据库插件pg_bigdecimal的使用:PostgreSQL高精度数值计算与存储解决方案

插件概述

pg_bigdecimal是一个专为Rust和PostgreSQL设计的高精度数值计算插件,它使用Rust的bigdecimal库提供任意精度的十进制数值处理能力,解决了PostgreSQL原生数值类型在金融计算、科学计算等场景中的精度限制问题。

核心特性

  • 支持任意精度的十进制数值存储和计算
  • 与PostgreSQL NUMERIC类型无缝集成
  • 提供精确的数值运算,避免浮点数精度误差
  • 支持Rust与PostgreSQL之间的类型自动转换

安装方法

在Cargo.toml中添加依赖:

[dependencies]
bigdecimal = "0.4"
tokio-postgres = { version = "0.7", features = ["with-bigdecimal-0_4"] }

基本使用方法

1. 数据库连接配置

use tokio_postgres::{NoTls, Client};
use bigdecimal::BigDecimal;

async fn connect() -> Result<Client, Box<dyn std::error::Error>> {
    let (client, connection) = tokio_postgres::connect(
        "host=localhost user=postgres dbname=test",
        NoTls,
    ).await?;
    
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
    
    Ok(client)
}

2. 创建支持bigdecimal的表

CREATE TABLE financial_records (
    id SERIAL PRIMARY KEY,
    amount NUMERIC(30, 15) NOT NULL,
    description TEXT
);

3. 插入高精度数值

use bigdecimal::BigDecimal;
use std::str::FromStr;

async fn insert_record(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
    let amount = BigDecimal::from_str("123456789012345.123456789012345")?;
    
    client.execute(
        "INSERT INTO financial_records (amount, description) VALUES ($1, $2)",
        &[&amount, &"高精度测试金额"]
    ).await?;
    
    Ok(())
}

4. 查询和处理高精度数据

async fn query_records(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
    for row in client.query("SELECT id, amount, description FROM financial_records", &[]).await? {
        let id: i32 = row.get(0);
        let amount: BigDecimal = row.get(1);
        let description: &str = row.get(2);
        
        println!("记录 {}: {} - {}", id, amount, description);
        
        // 进行高精度计算
        let doubled = &amount * BigDecimal::from(2);
        println!("金额翻倍: {}", doubled);
    }
    
    Ok(())
}

5. 复杂数值运算示例

use bigdecimal::{BigDecimal, FromPrimitive};
use std::ops::{Add, Mul};

async fn complex_calculation(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
    let principal = BigDecimal::from_str("1000000.00")?;
    let rate = BigDecimal::from_f64(0.05).unwrap(); // 5% 年利率
    let years = BigDecimal::from(10);
    
    // 复利计算: A = P(1 + r)^t
    let one = BigDecimal::from(1);
    let base = one.add(&rate);
    let exponent = base.pow(years.to_u32().unwrap());
    let amount = principal.mul(exponent);
    
    println!("10年后的本金加利息: {}", amount);
    
    Ok(())
}

最佳实践

  1. 精度控制:在数据库模式设计中明确指定NUMERIC类型的精度和标度
  2. 错误处理:始终处理BigDecimal解析和计算可能产生的错误
  3. 性能考虑:高精度计算相比原生类型会有性能开销,仅在需要时使用
  4. 序列化:与其他系统交互时注意数值的序列化和反序列化格式

常见问题解决

精度丢失问题

// 错误方式:使用浮点数直接构造
let bad_example = BigDecimal::from_f64(0.1).unwrap(); // 可能产生精度误差

// 正确方式:使用字符串构造
let good_example = BigDecimal::from_str("0.1").unwrap(); // 精确表示

除零错误处理

use bigdecimal::Zero;

let numerator = BigDecimal::from(10);
let denominator = BigDecimal::from(0);

if denominator.is_zero() {
    println!("除数不能为零");
} else {
    let result = numerator / denominator;
    println!("结果: {}", result);
}

完整示例代码

use tokio_postgres::{NoTls, Client};
use bigdecimal::{BigDecimal, FromPrimitive, Zero};
use std::str::FromStr;
use std::ops::{Add, Mul};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. 连接数据库
    let (client, connection) = tokio_postgres::connect(
        "host=localhost user=postgres dbname=test",
        NoTls,
    ).await?;

    // 处理连接任务
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("连接错误: {}", e);
        }
    });

    // 2. 创建表(实际应用中应该在数据库直接执行)
    // CREATE TABLE financial_records (
    //     id SERIAL PRIMARY KEY,
    //     amount NUMERIC(30, 15) NOT NULL,
    //     description TEXT
    // );

    // 3. 插入高精度数值
    let amount = BigDecimal::from_str("123456789012345.123456789012345")?;
    
    client.execute(
        "INSERT INTO financial_records (amount, description) VALUES ($1, $2)",
        &[&amount, &"高精度测试金额"]
    ).await?;

    // 4. 查询和处理数据
    for row in client.query("SELECT id, amount, description FROM financial_records", &[]).await? {
        let id: i32 = row.get(0);
        let amount: BigDecimal = row.get(1);
        let description: &str = row.get(2);
        
        println!("记录 {}: {} - {}", id, amount, description);
        
        // 进行高精度计算
        let doubled = &amount * BigDecimal::from(2);
        println!("金额翻倍: {}", doubled);
    }

    // 5. 复杂数值运算示例
    let principal = BigDecimal::from_str("1000000.00")?;
    let rate = BigDecimal::from_f64(0.05).unwrap(); // 5% 年利率
    let years = BigDecimal::from(10);
    
    // 复利计算: A = P(1 + r)^t
    let one = BigDecimal::from(1);
    let base = one.add(&rate);
    let exponent = base.pow(years.to_u32().unwrap());
    let amount = principal.mul(exponent);
    
    println!("10年后的本金加利息: {}", amount);

    // 6. 错误处理示例
    // 精度丢失问题处理
    let good_example = BigDecimal::from_str("0.1").unwrap(); // 正确方式
    
    // 除零错误处理
    let numerator = BigDecimal::from(10);
    let denominator = BigDecimal::from(0);
    
    if denominator.is_zero() {
        println!("除数不能为零");
    } else {
        let result = numerator / denominator;
        println!("结果: {}", result);
    }

    Ok(())
}

总结

pg_bigdecimal插件为Rust开发者提供了在PostgreSQL中进行高精度数值计算的完整解决方案,特别适用于金融、科学计算等对精度要求极高的场景。通过合理使用该插件,可以避免浮点数精度问题,确保数值计算的准确性。

回到顶部