👏
RustとPostgreSQLでUUIDv7
目的
ユニークビジョン株式会社 Advent Calendar 2024のシリーズ2、12/23の記事です。
UUIDv7は2024年5月にRFC9562で標準化されました。
ここではRustとPostgreSQLで取り扱ってみます。
UUIDv7は先頭はUnixTimestampのミリ秒から作成されるため、DBで主キーにするとソートすることができます。それ以外はランダムなので同時に作成しても衝突しにくいです。
コード
Rust
crate uuidを利用します。featuresにv7があります。
use chrono::prelude::*;
use uuid::{NoContext, Timestamp, Uuid};
pub fn uuid_to_utc(uuid: &Uuid) -> Option<DateTime<Utc>> {
uuid.get_timestamp().and_then(|it| {
let (secs, nsecs) = it.to_unix();
Utc.timestamp_opt(secs as i64, nsecs).single()
})
}
pub fn utc_to_uuid(utc: Option<DateTime<Utc>>) -> Uuid {
if let Some(utc) = utc {
let ts = Timestamp::from_unix(
NoContext,
utc.timestamp() as u64,
utc.timestamp_subsec_nanos(),
);
Uuid::new_v7(ts)
} else {
Uuid::now_v7()
}
}
// 現在時間から取得
let uuid = utc_to_uuid(None);
let utc = uuid_to_utc(&uuid);
// 指定時間から取得
let src = Utc.timestamp_opt(1497624119, 123_999_999).single().unwrap();
let uuid = utc_to_uuid(Some(src));
let dst = uuid_to_utc(&uuid).unwrap();
assert_eq!(dst.timestamp(), 1497624119);
// ミリ秒以下切り捨て
assert_eq!(dst.timestamp_subsec_nanos(), 123_000_000);
PostgreSQL
UUIDと時間の相互変更のストアードプロシージャです。
UUIDを作成するコードはUUIDv7 in 33 languagesを参考にしました。
-- UUIDv7を取得する
-- 引数
-- p_now : 現在時間
-- 戻り値
-- UUID
CREATE OR REPLACE FUNCTION uv_uuid_v7(
p_now TIMESTAMPTZ DEFAULT NULL
) RETURNS UUID AS $$
DECLARE
w_epoch BIGINT := (extract(epoch from COALESCE(p_now, now())) * 1000)::BIGINT;
BEGIN
RETURN
-- timestamp
lpad(to_hex((w_epoch >> 16)), 8, '0') || '-' ||
lpad(to_hex((w_epoch & 0xffff)), 4, '0') || '-' ||
-- version / rand_a
lpad(to_hex((0x7000 + (random() * 0x0fff)::int)), 4, '0') || '-' ||
-- variant / rand_b
lpad(to_hex((0x8000 + (random() * 0x3fff)::int)), 4, '0') || '-' ||
-- rand_b
lpad(to_hex((floor(random() * (2^48))::bigint >> 16)), 12, '0');
END;
$$ LANGUAGE plpgsql;
-- UUIDv7から時間を取得する
-- 引数
-- p_uuid : UUID
-- 戻り値
-- UUIDの時間
CREATE OR REPLACE FUNCTION uv_uuid_to_timestamptz(
p_uuid UUID DEFAULT NULL
) RETURNS TIMESTAMPTZ AS $$
DECLARE
w_value BIGINT := ('x'||replace(p_uuid::TEXT, '-', ''))::bit(48)::BIGINT;
BEGIN
RETURN
to_timestamp(w_value / 1000) + ((w_value % 1000) || ' milliseconds')::INTERVAL
;
END;
$$ LANGUAGE plpgsql;
Discussion