📖
RustでSQLxで扱えるPostgreSQLの型について
目的
ユニークビジョン株式会社 Advent Calendar 2024の12/1の記事です。
RustのSQLxが便利です。コネクションプールが独自に用意されていたり、SQLの結果とstructの変換が簡単だったりします。
PostgreSQLの型がどれだけ使えるか検証してみました。
基本型
基本的な型のマッピングは以下の表になります。
PostgreSQL | Rust |
---|---|
BOOL | bool |
BOOL[] | Vec<bool> |
NULL::bool | Option<bool> |
CHAR | i8 |
SMALLINT | i16 |
INT | i32 |
BIGINT | i64 |
REAL | f32 |
DOUBLE | f64 |
TEXT | String |
BYTEA | Vec<u8> |
INET | std::net::IpAddr |
TIMESTAMP | NaiveDateTime |
TIMESTAMPTZ | DateTime<Utc> |
DATE | NativeDate |
TIME | NativeTime |
JSONB | serde_json::Value |
UUID | uuid::Uuic |
VARBIT | bit_vec::BitVec |
NUMERIC | rust_decimal::Decimal |
OID | sqlx::postgres::types::Oid |
MACADDR | sqlx::types::mac_address::MacAddress |
HSTORE | sqlx::postgres::types::PgHstore |
値の相互変換したコードは以下のようになります。
use bit_vec::BitVec;
use chrono::prelude::*;
use rust_decimal::Decimal;
use serde_json::json;
use sqlx::{
postgres::types::{Oid, PgHstore},
prelude::*,
types::mac_address::MacAddress,
Pool, Postgres,
};
use std::{
net::{IpAddr, Ipv4Addr},
str::FromStr,
};
use uuid::Uuid;
const SQL: &str = r#"
SELECT
$1::BOOL AS bool_val
,$2::BOOL[] AS bool_array_val
,$3::BOOL AS bool_option_some_val
,$4::BOOL AS bool_option_none_val
,$5::"char" AS char_val
,$6::SMALLINT AS smallint_val
,$7::INT AS int_val
,$8::OID AS oid_val
,$9::BIGINT AS bigint_val
,$10::REAL AS real_val
,$11::DOUBLE PRECISION AS double_val
,$12::TEXT AS text_val
,$13::BYTEA AS bytes_val
,$14::INET AS inet_val
,$15::TIMESTAMP AS timestamp_val
,$16::TIMESTAMPTZ AS timestamptz_val
,$17::DATE AS date_val
,$18::TIME AS time_val
,$19::MACADDR AS macaddr_val
,$20::JSONB AS jsonb_val
,$21::UUID AS uuid_val
,$22::VARBIT AS varbit_val
,$23::NUMERIC AS decimal_val
,$24::HSTORE AS hstore_val
"#;
#[derive(FromRow, Debug, PartialEq)]
struct Data {
bool_val: bool,
bool_array_val: Vec<bool>,
bool_option_some_val: Option<bool>,
bool_option_none_val: Option<bool>,
char_val: i8,
smallint_val: i16,
int_val: i32,
oid_val: Oid,
bigint_val: i64,
real_val: f32,
double_val: f64,
text_val: String,
bytes_val: Vec<u8>,
inet_val: IpAddr,
timestamp_val: NaiveDateTime,
timestamptz_val: DateTime<Utc>,
date_val: NaiveDate,
time_val: NaiveTime,
macaddr_val: MacAddress,
jsonb_val: serde_json::Value,
uuid_val: Uuid,
varbit_val: BitVec,
decimal_val: Decimal,
hstore_val: PgHstore,
}
pub async fn execute(pool: &Pool<Postgres>) -> Result<(), sqlx::Error> {
let data = Data {
bool_val: true,
bool_array_val: vec![true, false],
bool_option_some_val: Some(true),
bool_option_none_val: None,
char_val: 1,
smallint_val: 2,
int_val: 3,
oid_val: Oid(4),
bigint_val: 5,
real_val: 6.1,
double_val: 7.1,
text_val: "予定表~①💖ハンカクだ".to_string(),
bytes_val: vec![240, 159, 146, 150],
inet_val: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
timestamp_val: NaiveDateTime::from_str("2001-02-03T04:05:06").unwrap(),
timestamptz_val: Utc.with_ymd_and_hms(2001, 2, 3, 4, 5, 6).unwrap(),
date_val: NaiveDate::from_ymd_opt(2002, 3, 4).unwrap(),
time_val: NaiveTime::from_hms_milli_opt(8, 59, 59, 100).unwrap(),
macaddr_val: MacAddress::new([0x12, 0x34, 0x56, 0xAB, 0xCD, 0xEF]),
jsonb_val: json!({
"name" : "予定表~①💖ハンカクだ",
"age" : 99
}),
uuid_val: Uuid::new_v4(),
varbit_val: {
let mut varbit_val = BitVec::from_elem(10, false);
varbit_val.set(2, true);
varbit_val
},
decimal_val: Decimal::from_str("1.1").unwrap(),
hstore_val: {
let mut val = PgHstore::default();
val.insert("key".to_owned(), Some("value".to_owned()));
val
},
};
let res: Data = sqlx::query_as(SQL)
.bind(&data.bool_val)
.bind(&data.bool_array_val)
.bind(&data.bool_option_some_val)
.bind(&data.bool_option_none_val)
.bind(&data.char_val)
.bind(&data.smallint_val)
.bind(&data.int_val)
.bind(&data.oid_val)
.bind(&data.bigint_val)
.bind(&data.real_val)
.bind(&data.double_val)
.bind(&data.text_val)
.bind(&data.bytes_val)
.bind(&data.inet_val)
.bind(&data.timestamp_val)
.bind(&data.timestamptz_val)
.bind(&data.date_val)
.bind(&data.time_val)
.bind(&data.macaddr_val)
.bind(&data.jsonb_val)
.bind(&data.uuid_val)
.bind(&data.varbit_val)
.bind(&data.decimal_val)
.bind(&data.hstore_val)
.fetch_one(pool)
.await?;
assert_eq!(data, res);
println!("{:?}", data);
Ok(())
}
crate postgresでサポートされているPostGIS用の型は未サポートでした。これらを扱う場合はpostgresを使う必要があります。
- POINT
- BOX
- PATH
複合型
PostgreSQLではTypeやDomainやEnumが定義できます。これらをRustにマッピングしてみます。
PostgreSQLの定義は以下の通りです。
DROP TYPE IF EXISTS type_enum_os CASCADE;
CREATE TYPE type_enum_os AS ENUM (
'linux',
'mac_os',
'windows'
);
DROP DOMAIN IF EXISTS domain_integer CASCADE;
CREATE DOMAIN domain_integer AS BIGINT CHECK(VALUE >= 0);
DROP TYPE IF EXISTS type_sample_get_select_composite CASCADE;
CREATE TYPE type_sample_get_select_composite AS (
os type_enum_os,
cpu_count domain_integer,
memory_size BIGINT
);
CREATE OR REPLACE FUNCTION sample_get_select_composite(
p_value type_sample_get_select_composite DEFAULT NULL
) RETURNS SETOF type_sample_get_select_composite AS $FUNCTION$
DECLARE
BEGIN
RETURN NEXT p_value;
RETURN NEXT p_value;
END;
$FUNCTION$ LANGUAGE plpgsql;
Rustのコードはこちらです。
type_nameを使うことでPostgreSQLに定義されている型と一致させることができます。
ちなみにstructになぜかrenameが使えなかったので少し不便ですがRustとSQLの変数名はsnake_caseなので基本的にはなんとかなると思います。
use sqlx::{prelude::*, Pool, Postgres};
const SQL: &str = r#"
SELECT
t1.*
FROM
sample_get_select_composite(
p_value := $1
) AS t1
"#;
#[derive(Debug, sqlx::Type)]
#[sqlx(type_name = "type_enum_os", rename_all = "snake_case")]
enum TypeEnumOs {
Linux,
MacOs,
Windows,
}
#[derive(sqlx::Type, Debug)]
#[sqlx(transparent)]
struct DomainInteger(i64);
#[derive(sqlx::Type, Debug)]
#[sqlx(transparent)]
struct MemorySizeUnit(i64);
#[derive(FromRow, Debug, sqlx::Type)]
#[sqlx(type_name = "type_sample_get_select_composite")]
struct TypePgGetSelectComposite {
os: TypeEnumOs,
cpu_count: DomainInteger,
memory_size: MemorySizeUnit,
}
pub async fn execute(pool: &Pool<Postgres>) -> Result<(), sqlx::Error> {
let data = TypePgGetSelectComposite {
os: TypeEnumOs::MacOs,
cpu_count: DomainInteger(2),
memory_size: MemorySizeUnit(1024),
};
let row: Vec<TypePgGetSelectComposite> =
sqlx::query_as(SQL).bind(&data).fetch_all(pool).await?;
println!("{:?}", row);
Ok(())
}
おまけ
ストアード・プロシージャー内でログの出力で使われるRAISE文はenv_loggerを用意すればSQLxでも確認できます。
RAISE NOTICE 'Hello World!';
env_logger::init();
Discussion