13 个版本

新增 0.2.0 2024 年 8 月 20 日
0.1.13 2024 年 7 月 13 日
0.0.999 2024 年 7 月 10 日

文本处理 中排名 #238

每月下载量 43

MIT 许可证

35KB
408 行(不包括注释)

UPID

发音为 YOO-pid

也称为通用唯一前缀按字典顺序排序的标识符

这是 UPID 的规范和 Python 实现。

UPID 基于 ULID,但进行了一些修改,灵感来自 这篇文章Stripe IDs

核心思想是指定一个有意义的 前缀,存储在 128 位的 UUID 形状的槽中。因此,UPID 是 可读的(如 Stripe ID),但仍然高效地存储、排序和索引。

UPID 允许最多 4 个字符 的前缀(如果少于 4 个字符,则将右填充),包含一个非环绕的时间戳,精度约为 250 毫秒,以及 64 位的熵。

这是一个 Python 中的 UPID 示例

upid("user")            # user_2accvpp5guht4dts56je5a

以及 Rust 中的实现

UPID::new("user")      // user_2accvpp5guht4dts56je5a

以及在 Postgres 中的实现

CREATE TABLE users (id upid NOT NULL DEFAULT gen_upid('user') PRIMARY KEY);
INSERT INTO users DEFAULT VALUES;
SELECT id FROM users;  -- user_2accvpp5guht4dts56je5a

-- this also works
SELECT id FROM users WHERE id = 'user_2accvpp5guht4dts56je5a';

与服务器代码兼容,无需额外工作

with psycopg.connect("postgresql://...") as conn:
    res = conn.execute("SELECT id FROM users").fetchone()
    print(res)          # user_2accvpp5guht4dts56je5a

演示

您可以在 upid.rdrn.me 上尝试它。

实现

如果您没有时间看 ASCII 艺术字,您可以跳到好东西

语言 链接
Python 在此存储库中(向下滚动)
Postgres 在此存储库中(向下滚动)
Rust 在此存储库中(向下滚动)
TypeScript carderne/upid-ts

规范

相对于 ULID 的主要变化

  1. 使用修改后的 Crockford's base32 形式,使用小写并包含整个字母表(以增加前缀的灵活性)。
  2. 不允许大写/小写解码互换。
  3. 文本编码仍然是每 base32 字符 5 位。
  4. 分配给前缀 20 位
  5. 分配给时间戳 40 位(从 48 位减少),在二进制中首先放置以进行排序
  6. 随机数 64 位(从 80 位减少)
  7. 版本指定器 4 位
    user       2accvpp5      guht4dts56je5       a
   |----|     |--------|    |-------------|   |-----|
   prefix       time            random        version     total
   4 chars      8 chars         13 chars      1 char      26 chars
       \________/________________|___________    |
               /                 |           \   |
              /                  |            \  |
           40 bits            64 bits         24 bits    128 bits
           5 bytes            8 bytes         3 bytes     16 bytes
           time               random      prefix+version

二进制布局

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            time_high                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    time_low   |                     random                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             random                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     random    |                  prefix_and_version           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

冲突

相对于 ULID,时间精度从 48 位减少到 40 位(保留最高有效位,因此溢出仍不会发生在 10889 年),随机数从 80 位减少到 64 位。

在40位精度下,时间戳大约为250毫秒。为了在64位随机性的情况下有50%的概率发生冲突,您需要在每250毫秒窗口内生成大约40亿个条目

Python实现

本节的目的是尽可能简单明了地传达规范的核心工作原理。当前的Python实现完全基于mdomke/python-ulid

安装

pip install upid

用法

从CLI运行

python -m upid user

在程序中使用

from upid import upid
upid("user")

或更明确地

from upid import UPID
UPID.from_prefix("user")

或指定您自己的时间戳或日期时间

import time, datetime
UPID.from_prefix_and_milliseconds("user", milliseconds)
UPID.from_prefix_and_datetime("user", datetime.datetime.now())

字符串之间的转换

u = UPID.from_str("user_2accvpp5guht4dts56je5a")
u.to_str()        # user_2a...

输出内容

u.prefix     # user
u.datetime   # 2024-07-07 ...

转换为其他格式

int(u)       # 2079795568564925668398930358940603766
u.hex        # 01908dd6a3669b912738191ea3d61576
u.to_uuid()  # UUID('01908dd6-a366-9b91-2738-191ea3d61576')

开发

代码和测试位于py/目录中。使用Rye进行开发(安装说明在链接中)。

# can be run from the repo root
rye sync
rye run all  # or fmt/lint/check/test

如果您只是想浏览一下,pip也应该可以

pip install -e .

如果发现错误或改进,请提交PR!

Rust实现

当前的Rust实现基于dylanhart/ulid-rs,但使用与Python实现相同的base32查找方法。

安装

cargo add upid

用法

use upid::Upid;
Upid::new("user");

或指定您自己的时间戳或日期时间

use std::time::SystemTime;
Upid::from_prefix_and_milliseconds("user", 1720366572288);
Upid::from_prefix_and_datetime("user", SystemTime::now());

字符串之间的转换

let u = Upid::from_string("user_2accvpp5guht4dts56je5a");
u.to_string();

输出内容

u.prefix();       // user
u.datetime();     // 2024-07-07 ...
u.milliseconds(); // 17203...

转换为其他格式

u.to_bytes();

开发

代码和测试位于upid_rs/目录中。

cd upid_rs
cargo check  # or fmt/clippy/build/test/run

如果发现错误或改进,请提交PR!

Postgres扩展

还有一个基于Rust实现的Postgres扩展,使用pgrx,并基于类似的扩展pksunkara/pgx_ulid

安装

最简单的方法是尝试Docker镜像carderne/postgres-upid:16,目前为arm64和amd64架构,但仅适用于Postgres 16

docker run -e POSTGRES_HOST_AUTH_METHOD=trust -p 5432:5432 carderne/postgres-upid:16

您还可以从Releases页面获取Linux .deb。这是为Postgres 16和amd64构建的。

一旦推出alpha版本,将会有更多架构和版本。

用法

CREATE EXTENSION upid_pg;

CREATE TABLE users (
    id   upid NOT NULL DEFAULT gen_upid('user') PRIMARY KEY,
    name text NOT NULL
);

INSERT INTO users (name) VALUES('Bob');

SELECT * FROM users;
--              id              | name
-- -----------------------------+------
--  user_2accvpp5guht4dts56je5a | Bob

您可以获取原始的bytea数据,或前缀或时间戳

SELECT upid_to_bytea(id) FROM users;
-- \x019...

SELECT upid_to_prefix(id) FROM users;
-- 'user'

SELECT upid_to_timestamp(id) FROM users;
-- 2024-07-07 ...

或将UPID转换为常规的Postgres UUID

SELECT upid_to_uuid(gen_upid('user'));

或反向操作(尽管前缀和时间戳将不再有意义)

select upid_from_uuid(gen_random_uuid());

开发

如果您想将其安装到另一个Postgres中,您需要安装pgrx并遵循其安装说明。类似以下命令

cd upid_pg
cargo install --locked cargo-pgrx
cargo pgrx init
cargo pgrx install

一些cargo命令与常规操作相同

cargo check  # or fmt/clippy

但构建、测试和运行必须通过pgrx进行。这将将其编译到Postgres安装中,并允许在其中进行交互式会话和测试。

cargo pgrx test pg16
# or       run
# or       install

依赖关系

~250–370KB