julid-rs/README.md

127 lines
5.3 KiB
Markdown

# Bottom line up front
Julids are globally unique, sortable identifiers, that are backwards-compatible with
[ULIDs](https://github.com/ulid/spec). This crate provides a Rust Julid datatype, as well as a
loadable extension for SQLite for creating and querying them:
``` text
$ sqlite3
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .load ./libjulid
sqlite> select hex(julid_new());
018998768ACF000060B31DB175E0C5F9
sqlite> select julid_string(julid_new());
01H6C7D9CT00009TF3EXXJHX4Y
sqlite> select julid_seconds(julid_new());
1690480066.208
sqlite> select datetime(julid_timestamp(julid_new()), 'auto');
2023-07-27 17:47:50
sqlite> select julid_counter(julid_new());
0
```
Crates.io: https://crates.io/crates/julid-rs
docs: https://docs.rs/julid-rs/latest/julid/
## A slightly deeper look
Julids are ULID-backwards-compatible (that is, all Julids are valid ULIDs, but not all ULIDs are
Julids) identifiers with the following properties:
* they are 128-bits long
* they are lexicographically sortable
* they encode their creation time as the number of milliseconds since the [UNIX
epoch](https://en.wikipedia.org/wiki/Unix_time)
* IDs created within the same millisecond will still sort in their order of creation, due to the
presence of a 16-bit monotonic counter, placed immediately after the creation time bits
It's that last thing that makes them distinctive. ULIDs have the following structure, from most to
least-significant bit:
![ULID bit structure](./ulid.svg)
According to the ULID spec, for ULIDs created in the same millisecond, the least-significant bit
should be incremented for each new ID. Since that portion of the ULID is random, that means you may
not be able to increment it without spilling into the timestamp portion. Likewise, it's easy to
guess a new possibly-valid ULID simply by incrementing an already-known one. And finally, this means
that sorting will need to read all the way to the end of the ULID for IDs created in the same
millisecond.
To address these shortcomings, Julids (Joe's ULIDs) have the following structure:
![Julid bit structure](./julid.svg)
As with ULIDs, the 48 most-significant bits encode the time of creation. Unlike ULIDs, the next 16
most-significant bits are not random, they're a monotonic counter for IDs created within the same
millisecond. Since it's only 16 bits, it will saturate after 65,536 IDs intra-millisecond creations,
after which, IDs in that same millisecond will not have an intrinsic total order (the random bits
will still be different, so you shouldn't have collisions). My PC, which is no slouch, can only
generate about 20,000 per millisecond, so hopefully this is not an issue! Because the random bits
are always fresh, it's not possible to easily guess a valid Julid if you already know one.
# SQLite extension
The extension, when loaded into SQLite, provides the following functions:
* `julid_new()`: create a new Julid and return it as a `blob`
* `julid_seconds(julid)`: get the number seconds (as a 64-bit float) since the UNIX epoch that this
julid was created
* `julid_counter(julid)`: show the value of this julid's monotonic counter
* `julid_sortable(julid)`: return the 64-bit concatenation of the timestamp and counter
* `julid_string(julid)`: show the [base-32 Crockford](https://en.wikipedia.org/wiki/Base32)
encoding of this julid
## Building and loading
If you want to use it as a SQLite extension:
* clone the [repo](https://gitlab.com/nebkor/julid)
* build it with `cargo build --features plugin` (this builds the SQLite extension)
* copy the resulting `libjulid.[so|dylib|whatevs]` to some place where you can...
* load it into SQLite with `.load /path/to/libjulid` as shown at the top
* party
If you, like me, wish to use Julids as primary keys, just create your table like:
``` sql
create table users (
id blob not null primary key default (julid_new()),
...
);
```
and you've got a first-class ticket straight to Julid City, baby!
# Rust crate
Of course, you can also use it outside of a database; the `Julid` type is publicly exported, and
you can do like such as:
``` rust
use julid::Julid;
fn main() {
let id = Julid::new();
dbg!(id.timestamp(), id.counter(), id.sortable(), id.as_string());
}
```
after adding it to your project's dependencies (eg, `cargo add julid-rs`; note the package name is
"julid-rs", but the library name as used in your `use` statements is just "julid"). By default, it
will also include trait implementations for using Julids with
[SQLx](https://github.com/launchbadge/sqlx), and serializing/deserializing with
[Serde](https://serde.rs/), via the `sqlx` and `serde` features, respectively. One final default
optional feature, `chrono`, uses the Chrono crate to return the timestamp as a
[`DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) by adding a `created_at(&self)`
method to `Julid`.
# Thanks
This crate wouldn't have been possible without a lot of inspiration (and a little shameless
stealing) from the [ulid-rs](https://github.com/dylanhart/ulid-rs) crate, as well as the
[sqlite-loadable-rs](https://github.com/asg017/sqlite-loadable-rs) crate, which made it *extremely*
easy to write the SQLite extension. Thank you, authors of those crates! Feel free to steal from this
project!