👏

RustとPostgreSQLでUUIDv7

2024/12/25に公開

目的

ユニークビジョン株式会社 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