Skip to content

Commit 6b55a5c

Browse files
committed
make core rust functions non-erroring
1 parent 2c001d6 commit 6b55a5c

File tree

6 files changed

+90
-35
lines changed

6 files changed

+90
-35
lines changed

README.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ UPID is based on [ULID](https://github.com/ulid/spec) but with some modification
1111
The core idea is that a **meaningful prefix** is specified that is stored in a 128-bit UUID-shaped slot.
1212
Thus a UPID is **human-readable** (like a Stripe ID), but still efficient to store, sort and index.
1313

14-
UPID allows a prefix of up to **4 characters** (will be right-padded if shorter than 4), includes a non-wrapping timestamp with about 300 millisecond precision, and 64 bits of entropy.
14+
UPID allows a prefix of up to **4 characters** (will be right-padded if shorter than 4), includes a non-wrapping timestamp with about 250 millisecond precision, and 64 bits of entropy.
1515

1616
This is a UPID in Python:
1717
```python
@@ -81,7 +81,7 @@ Key changes relative to ULID:
8181
### Collision
8282
Relative to ULID, the time precision is reduced from 48 to 40 bits (keeping the most significant bits, so oveflow still won't occur until 10889 AD), and the randomness reduced from 80 to 64 bits.
8383

84-
The timestamp precision at 40 bits is around 300 milliseconds. In order to have a 50% probability of collision with 64 bits of randomness, you would need to generate around **4 billion items per 100 millisecond window**.
84+
The timestamp precision at 40 bits is around 250 milliseconds. In order to have a 50% probability of collision with 64 bits of randomness, you would need to generate around **4 billion items per 250 millisecond window**.
8585

8686
## Python implementation
8787
This aims to be maximally simple to convey the core working of the spec.
@@ -105,11 +105,19 @@ upid("user")
105105
```
106106

107107
#### Development
108+
Code and tests are in the [py/](./py/) directory. Using [Rye](https://rye.astral.sh/) for development (installation instructions at the link).
109+
108110
```bash
111+
# can be run from the repo root
109112
rye sync
110113
rye run all # or fmt/lint/check/test
111114
```
112115

116+
If you just want to have a look around, pip should also work:
117+
```bash
118+
pip install -e .
119+
```
120+
113121
## Rust implementation
114122
The current Rust implementation is based on [dylanhart/ulid-rs](https://github.com/dylanhart/ulid-rs), but using the same lookup base32 lookup method as the Python implementation.
115123

@@ -125,28 +133,32 @@ Upid::new("user");
125133
```
126134

127135
#### Development
136+
Code and tests are in the [upid_rs/](./upid_rs/) directory.
137+
128138
```bash
129-
cargo check # or fmt/clippy/test/run
139+
cd upid_rs
140+
cargo check # or fmt/clippy/build/test/run
130141
```
131142

132143
## Postgres extension
133144
There is also a Postgres extension built on the Rust implementation, using [pgrx](https://github.com/pgcentralfoundation/pgrx) and based on the very similar extension [pksunkara/pgx_ulid](https://github.com/pksunkara/pgx_ulid).
134145

135146
#### Installation
136-
You will need to install pgrx and follow its installation instructions.
147+
You can try out the Docker image [carderne/postgres-upid:16](https://hub.docker.com/r/carderne/postgres-upid):
148+
```bash
149+
docker run -e POSTGRES_HOST_AUTH_METHOD=trust -p 5432:5432 carderne/postgres-upid:16
150+
```
151+
152+
If you want to install it into another Postgres, you'll install pgrx and follow its [installation instructions](https://github.com/pgcentralfoundation/pgrx/blob/develop/cargo-pgrx/README.md).
137153
Something like this:
138154
```bash
139155
cargo install --locked cargo-pgrx
140156
pgrx init
141157
cd upid_pg
142158
pgrx install
143-
pgrx run
144159
```
145160

146-
Alternatively, you can try out the Docker image `[carderne/postgres-upid:16](https://hub.docker.com/r/carderne/postgres-upid):
147-
```bash
148-
docker run -e POSTGRES_HOST_AUTH_METHOD=trust -p 5432:5432 carderne/postgres-upid:16
149-
```
161+
Installable binaries will come soon.
150162

151163
#### Usage
152164
```sql
@@ -162,6 +174,14 @@ SELECT * FROM users;
162174
```
163175

164176
#### Development
177+
Code and tests are in the [upid_pg/](./upid_pg/) directory.
178+
165179
```bash
166-
cargo pgrx test
180+
cd upid_pg
181+
cargo check # or fmt/clippy
182+
183+
# must test/run/install with pgrx
184+
# this will compile it into a Postgres installation
185+
# and run the tests there, or drop you into a psql prompt
186+
cargo pgrx test # or run/install
167187
```

py/tests/test_core.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,16 @@ def test_datetime_roundtrip() -> None:
5454
got = a.datetime
5555
diff = want - got
5656
assert diff.total_seconds() * consts.MS_PER_SEC < TS_EPS
57+
58+
59+
def test_invalid_prefix() -> None:
60+
# Invalid characters just become 'zzzz'
61+
want = "zzzz"
62+
63+
# even if too long
64+
got = UPID.from_prefix("[0#/]]1,").prefix
65+
assert got == want
66+
67+
# or too short
68+
got = UPID.from_prefix("[0").prefix
69+
assert got == want

py/upid/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class UPID:
2121
"""
2222
The `UPID` contains a 20-bit prefix, 40-bit timestamp and 68 bits of randomness.
2323
24+
The prefix should only contain lower-case latin alphabet characters.
25+
2426
It is usually created using the `upid(prefix: str)` helper function:
2527
2628
upid("user") # UPID(user_3accvpp5_guht4dts56je5w)

upid_pg/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl FromDatum for upid {
8181

8282
#[pg_extern]
8383
fn gen_upid(prefix: &str) -> upid {
84-
upid(InnerUpid::new(prefix).unwrap().0)
84+
upid(InnerUpid::new(prefix).0)
8585
}
8686

8787
#[pg_extern(immutable, parallel_safe)]

upid_rs/src/lib.rs

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,35 @@ pub struct Upid(pub u128);
2727

2828
impl Upid {
2929
/// Creates a new Upid with the provided prefix and current time (UTC)
30+
///
31+
/// The prefix should only contain lower-case latin alphabet characters.
3032
/// # Example
3133
/// ```rust
3234
/// use upid::Upid;
3335
///
3436
/// let my_upid = Upid::new("user");
3537
/// ```
36-
pub fn new(prefix: &str) -> Result<Upid, DecodeError> {
38+
pub fn new(prefix: &str) -> Upid {
3739
Upid::from_prefix(prefix)
3840
}
3941

4042
/// Creates a Upid with the provided prefix and current time (UTC)
43+
///
44+
/// The prefix should only contain lower-case latin alphabet characters.
4145
/// # Example
4246
/// ```rust
4347
/// use upid::Upid;
4448
///
4549
/// let my_upid = Upid::from_prefix("user");
4650
/// ```
47-
pub fn from_prefix(prefix: &str) -> Result<Upid, DecodeError> {
51+
pub fn from_prefix(prefix: &str) -> Upid {
4852
Upid::from_prefix_and_datetime(prefix, now())
4953
}
5054

5155
/// Creates a new Upid with the given prefix and datetime
5256
///
57+
/// The prefix should only contain lower-case latin alphabet characters.
58+
///
5359
/// This will take the maximum of the `[SystemTime]` argument and `[SystemTime::UNIX_EPOCH]`
5460
/// as earlier times are not valid for a Upid timestamp
5561
///
@@ -60,10 +66,7 @@ impl Upid {
6066
///
6167
/// let upid = Upid::from_prefix_and_datetime("user", SystemTime::now());
6268
/// ```
63-
pub fn from_prefix_and_datetime(
64-
prefix: &str,
65-
datetime: SystemTime,
66-
) -> Result<Upid, DecodeError> {
69+
pub fn from_prefix_and_datetime(prefix: &str, datetime: SystemTime) -> Upid {
6770
let milliseconds = datetime
6871
.duration_since(SystemTime::UNIX_EPOCH)
6972
.unwrap_or(Duration::ZERO)
@@ -73,17 +76,16 @@ impl Upid {
7376

7477
/// Creates a new Upid with the given prefix and timestamp in millisecons
7578
///
79+
/// The prefix should only contain lower-case latin alphabet characters.
80+
///
7681
/// # Example
7782
/// ```rust
7883
/// use upid::Upid;
7984
///
8085
/// let ms: u128 = 1720568902000;
8186
/// let upid = Upid::from_prefix_and_milliseconds("user", ms);
8287
/// ```
83-
pub fn from_prefix_and_milliseconds(
84-
prefix: &str,
85-
milliseconds: u128,
86-
) -> Result<Upid, DecodeError> {
88+
pub fn from_prefix_and_milliseconds(prefix: &str, milliseconds: u128) -> Upid {
8789
// cut off the 8 lsb drops precision to 256 ms
8890
// future version could play with this differently
8991
// eg drop 4 bits on each side
@@ -97,15 +99,21 @@ impl Upid {
9799
let prefix = format!("{:z<4}", prefix);
98100
let prefix: String = prefix.chars().take(4).collect();
99101
let prefix = format!("{}{}", prefix, VERSION);
100-
let p = b32::decode_prefix(prefix.as_bytes())?;
102+
103+
// decode_prefix Errors if the last character is past 'j' in the b32 alphabet
104+
// and we control that with the VERSION variable
105+
// If the prefix has characters from outside the alphabet, they will be wrapped into 'z's
106+
// And we have ensured above that it is exactly 5 characters long
107+
let p = b32::decode_prefix(prefix.as_bytes())
108+
.expect("decode_prefix failed with version character overflow");
101109

102110
let res = (time_bits << 88)
103111
| (random << 24)
104112
| ((p[0] as u128) << 16)
105113
| ((p[1] as u128) << 8)
106114
| p[2] as u128;
107115

108-
Ok(Upid(res))
116+
Upid(res)
109117
}
110118

111119
/// Gets the datetime of when this Upid was created accurate to around 300ms
@@ -116,7 +124,7 @@ impl Upid {
116124
/// use upid::Upid;
117125
///
118126
/// let dt = SystemTime::now();
119-
/// let upid = Upid::from_prefix_and_datetime("user", dt).unwrap();
127+
/// let upid = Upid::from_prefix_and_datetime("user", dt);
120128
///
121129
/// assert!(dt + Duration::from_millis(300) >= upid.datetime());
122130
/// ```
@@ -134,7 +142,6 @@ impl Upid {
134142
/// let text = "user_aaccvpp5guht4dts56je5a";
135143
/// let result = Upid::from_string(text);
136144
///
137-
/// assert!(result.is_ok());
138145
/// assert_eq!(&result.unwrap().to_string(), text);
139146
/// ```
140147
pub fn from_string(encoded: &str) -> Result<Upid, DecodeError> {
@@ -151,7 +158,7 @@ impl Upid {
151158
/// use upid::Upid;
152159
///
153160
/// let prefix = "user";
154-
/// let upid = Upid::from_prefix(prefix).unwrap();
161+
/// let upid = Upid::from_prefix(prefix);
155162
///
156163
/// assert_eq!(upid.prefix(), prefix);
157164
/// ```
@@ -168,7 +175,7 @@ impl Upid {
168175
/// use upid::Upid;
169176
///
170177
/// let ms: u128 = 1720568902000;
171-
/// let upid = Upid::from_prefix_and_milliseconds("user", ms).unwrap();
178+
/// let upid = Upid::from_prefix_and_milliseconds("user", ms);
172179
///
173180
/// assert!(ms - u128::from(upid.milliseconds()) < 256);
174181
/// ```
@@ -221,7 +228,7 @@ impl Upid {
221228

222229
impl Default for Upid {
223230
fn default() -> Self {
224-
Upid::new("").unwrap()
231+
Upid::new("")
225232
}
226233
}
227234

@@ -281,7 +288,7 @@ mod tests {
281288

282289
#[test]
283290
fn test_dynamic() {
284-
let upid = Upid::new("user").unwrap();
291+
let upid = Upid::new("user");
285292
let encoded = upid.to_string();
286293
let upid2 = Upid::from_string(&encoded).expect("failed to deserialize");
287294
assert_eq!(upid, upid2);
@@ -290,9 +297,8 @@ mod tests {
290297
#[test]
291298
fn test_order() {
292299
let dt = SystemTime::now();
293-
let upid1 = Upid::from_prefix_and_datetime("user", dt).unwrap();
294-
let upid2 =
295-
Upid::from_prefix_and_datetime("user", dt + Duration::from_millis(300)).unwrap();
300+
let upid1 = Upid::from_prefix_and_datetime("user", dt);
301+
let upid2 = Upid::from_prefix_and_datetime("user", dt + Duration::from_millis(300));
296302
assert!(upid1 < upid2);
297303
}
298304

@@ -303,7 +309,7 @@ mod tests {
303309
.duration_since(SystemTime::UNIX_EPOCH)
304310
.unwrap()
305311
.as_millis();
306-
let upid = Upid::from_prefix_and_milliseconds("user", want).unwrap();
312+
let upid = Upid::from_prefix_and_milliseconds("user", want);
307313
let got = u128::from(upid.milliseconds());
308314

309315
assert!(want - got < EPS);
@@ -312,9 +318,23 @@ mod tests {
312318
#[test]
313319
fn test_datetime() {
314320
let dt = SystemTime::now();
315-
let upid = Upid::from_prefix_and_datetime("user", dt).unwrap();
321+
let upid = Upid::from_prefix_and_datetime("user", dt);
316322

317323
assert!(upid.datetime() <= dt);
318324
assert!(upid.datetime() + Duration::from_millis(300) >= dt);
319325
}
326+
327+
#[test]
328+
fn test_invalid_prefix() {
329+
// Invalid characters just become 'zzzz'
330+
let want = "zzzz";
331+
332+
// even if too long
333+
let got = Upid::from_prefix("[0#/]]1,").prefix();
334+
assert_eq!(got, want);
335+
336+
// or too short
337+
let got = Upid::from_prefix("[0").prefix();
338+
assert_eq!(got, want);
339+
}
320340
}

upid_rs/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ fn main() {
88
Some(value) => value,
99
None => &"".to_string(),
1010
};
11-
println!("{}", Upid::from_prefix(prefix).unwrap().to_string());
11+
println!("{}", Upid::from_prefix(prefix).to_string());
1212
}

0 commit comments

Comments
 (0)