Prepare for new release.
Adds copious docs and reorganizes the code a little bit. Ready for final release, hopefully.
This commit is contained in:
parent
d12811dffd
commit
b799a9955f
8 changed files with 273 additions and 192 deletions
13
Cargo.toml
13
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "julid-rs"
|
name = "julid-rs"
|
||||||
version = "0.1.6"
|
version = "1.6.1"
|
||||||
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
keywords = ["ulid", "library", "sqlite", "extension", "julid"]
|
keywords = ["ulid", "library", "sqlite", "extension", "julid"]
|
||||||
|
@ -11,8 +11,12 @@ license-file = "LICENSE.md"
|
||||||
repository = "https://gitlab.com/nebkor/julid"
|
repository = "https://gitlab.com/nebkor/julid"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serde", "sqlx"]
|
default = ["chrono", "serde", "sqlx"] # just the regular crate
|
||||||
clib = []
|
|
||||||
|
chrono = ["dep:chrono"]
|
||||||
|
plugin = ["sqlite-loadable"] # builds libjulid.* for loading into sqlite
|
||||||
|
serde = ["dep:serde"]
|
||||||
|
sqlx = ["dep:sqlx"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "julid"
|
name = "julid"
|
||||||
|
@ -22,4 +26,5 @@ crate-type = ["cdylib", "rlib"]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
sqlx = { version = "0.7", features = ["sqlite"], default-features = false, optional = true }
|
sqlx = { version = "0.7", features = ["sqlite"], default-features = false, optional = true }
|
||||||
sqlite-loadable = "0.0.5"
|
sqlite-loadable = { version = "0.0.5", optional = true }
|
||||||
|
chrono = { version = "0.4.26", optional = true, default-features = false, features = ["std", "time"] }
|
||||||
|
|
69
README.md
69
README.md
|
@ -1,5 +1,7 @@
|
||||||
# quick take
|
# Bottom line up front
|
||||||
Globally unique sortable identifiers for SQLite!
|
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
|
``` text
|
||||||
$ sqlite3
|
$ sqlite3
|
||||||
|
@ -9,67 +11,71 @@ Connected to a transient in-memory database.
|
||||||
Use ".open FILENAME" to reopen on a persistent database.
|
Use ".open FILENAME" to reopen on a persistent database.
|
||||||
sqlite> .load ./libjulid
|
sqlite> .load ./libjulid
|
||||||
sqlite> select hex(julid_new());
|
sqlite> select hex(julid_new());
|
||||||
01898F1332B90000D6F0F0FE1066A6BF
|
018998768ACF000060B31DB175E0C5F9
|
||||||
sqlite> select julid_string(julid_new());
|
sqlite> select julid_string(julid_new());
|
||||||
01H67GV14N000BBHJD6FARVB7Q
|
01H6C7D9CT00009TF3EXXJHX4Y
|
||||||
sqlite> select datetime(julid_timestamp(julid_new()) / 1000, 'auto'); -- sqlite wants seconds, not milliseconds
|
sqlite> select julid_seconds(julid_new());
|
||||||
2023-07-25 21:58:56
|
1690480066.208
|
||||||
|
sqlite> select datetime(julid_timestamp(julid_new()), 'auto');
|
||||||
|
2023-07-27 17:47:50
|
||||||
sqlite> select julid_counter(julid_new());
|
sqlite> select julid_counter(julid_new());
|
||||||
0
|
0
|
||||||
```
|
```
|
||||||
|
|
||||||
## a little more in depth
|
## A slightly deeper look
|
||||||
|
|
||||||
Julids are [ULID](https://github.com/ulid/spec)-backwards-compatible (that is, all Julids are valid ULIDs,
|
Julids are ULID-backwards-compatible (that is, all Julids are valid ULIDs, but not all ULIDs are
|
||||||
but not all ULIDs are Julids) identifiers with the following properties:
|
Julids) identifiers with the following properties:
|
||||||
|
|
||||||
* they are 128-bits long
|
* they are 128-bits long
|
||||||
* they are lexicographically sortable
|
* they are lexicographically sortable
|
||||||
* they encode their creation time as the number of milliseconds since the UNIX epoch
|
* 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
|
* 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
|
presence of a 16-bit monotonic counter, placed immediately after the creation time bits
|
||||||
|
|
||||||
It's that last bit that makes them distinctive. ULIDs have the following big-endian bit structure:
|
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)
|
![ULID bit structure](./ulid.svg)
|
||||||
|
|
||||||
According to the ULID spec, for ULIDs created in the same millisecond, the least-significant bit
|
According to the ULID spec, for ULIDs created in the same millisecond, the least-significant bit
|
||||||
should be incremented for each new one. Since that portion of the ULID is random, that means you may
|
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
|
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
|
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
|
that sorting will need to read all the way to the end of the ULID for IDs created in the same
|
||||||
millisecond.
|
millisecond.
|
||||||
|
|
||||||
To address these shortcomings, Julids (Joe's ULIDs) have the following big-endian bit structure:
|
To address these shortcomings, Julids (Joe's ULIDs) have the following structure:
|
||||||
|
|
||||||
![Julid bit structure](./julid.svg)
|
![Julid bit structure](./julid.svg)
|
||||||
|
|
||||||
As with ULIDs, the 48 most-significant bits encode the time of creation. Unlike ULIDs, the next 16
|
As with ULIDs, the 48 most-significant bits encode the time of creation. Unlike ULIDs, the next 16
|
||||||
bits are not random, they're a monotonic counter for IDs created within the same millisecond. Since
|
most-significant bits are not random, they're a monotonic counter for IDs created within the same
|
||||||
it's only 16 bits, it will saturate after 65,536 IDs intra-millisecond creations, after which, IDs
|
millisecond. Since it's only 16 bits, it will saturate after 65,536 IDs intra-millisecond creations,
|
||||||
in that same millisecond will not have an intrinsic total order (the random bits will still be
|
after which, IDs in that same millisecond will not have an intrinsic total order (the random bits
|
||||||
different, so you shouldn't have collisions). My PC, which is no slouch, can only generate about
|
will still be different, so you shouldn't have collisions). My PC, which is no slouch, can only
|
||||||
20,000 per millisecond, so hopefully this is not an issue! Because the random bits are always fresh,
|
generate about 20,000 per millisecond, so hopefully this is not an issue! Because the random bits
|
||||||
it is not possible to guess a valid Julid if you already know one.
|
are always fresh, it's not possible to easily guess a valid Julid if you already know one.
|
||||||
|
|
||||||
# functions overview
|
# SQLite extension
|
||||||
|
|
||||||
This extension provides the following functions:
|
The extension, when loaded into SQLite, provides the following functions:
|
||||||
|
|
||||||
* `julid_new()`: create a new Julid and return it as a `blob`
|
* `julid_new()`: create a new Julid and return it as a `blob`
|
||||||
* `julid_timestamp(julid)`: get the number milliseconds since the UNIX epoch that this julid was
|
* `julid_seconds(julid)`: get the number milliseconds since the UNIX epoch that this julid was
|
||||||
created
|
created
|
||||||
* `julid_counter(julid)`: show the value of this julid's monotonic counter
|
* `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_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)
|
* `julid_string(julid)`: show the [base-32 Crockford](https://en.wikipedia.org/wiki/Base32)
|
||||||
encoding of this julid
|
encoding of this julid
|
||||||
|
|
||||||
# how to use
|
## Building and loading
|
||||||
|
|
||||||
If you want to use it as a SQLite extension:
|
If you want to use it as a SQLite extension:
|
||||||
|
|
||||||
* clone the repo
|
* clone the [repo](https://gitlab.com/nebkor/julid)
|
||||||
* build it with `cargo build --features clib` (this builds the SQLite extension)
|
* 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...
|
* 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
|
* load it into SQLite with `.load /path/to/libjulid` as shown at the top
|
||||||
* party
|
* party
|
||||||
|
@ -78,14 +84,14 @@ If you, like me, wish to use Julids as primary keys, just create your table like
|
||||||
|
|
||||||
``` sql
|
``` sql
|
||||||
create table users (
|
create table users (
|
||||||
id blob not null primary key default julid_new(),
|
id blob not null primary key default (julid_new()),
|
||||||
...
|
...
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
and you've got a first-class ticket straight to Julid City, baby!
|
and you've got a first-class ticket straight to Julid City, baby!
|
||||||
|
|
||||||
## using it as a library in a Rust application
|
# Rust crate
|
||||||
|
|
||||||
Of course, you can also use it outside of a database; the `Julid` type is publicly exported, and
|
Of course, you can also use it outside of a database; the `Julid` type is publicly exported, and
|
||||||
you can do like such as:
|
you can do like such as:
|
||||||
|
@ -99,4 +105,11 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
after adding it to your project's dependencies, like `cargo add julid-rs`.
|
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
|
||||||
|
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()`
|
||||||
|
method to `Julid` instances.
|
||||||
|
|
|
@ -13,8 +13,8 @@ fn main() {
|
||||||
|
|
||||||
for id in v.iter() {
|
for id in v.iter() {
|
||||||
println!(
|
println!(
|
||||||
"{id}: {}ms and {} incs; sortable: {}",
|
"{id}: created_at {}; counter: {}; sortable: {}",
|
||||||
id.timestamp(),
|
id.created_at(),
|
||||||
id.counter(),
|
id.counter(),
|
||||||
id.sortable()
|
id.sortable()
|
||||||
);
|
);
|
|
@ -1,7 +1,7 @@
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
/// Length of a string-encoded Julid
|
/// Length of a string-encoded Julid
|
||||||
pub const JULID_LEN: usize = 26;
|
pub(crate) const JULID_STR_LEN: usize = 26;
|
||||||
|
|
||||||
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||||
|
|
||||||
|
@ -37,17 +37,18 @@ lookup[(c + 32) as usize] = i as u8;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// Encode the given 128-bit little-endian number as a base32 string.
|
/// Encode the given 128-bit little-endian number as a base32 string.
|
||||||
pub fn encode(mut value: u128) -> String {
|
pub(crate) fn encode(mut value: u128) -> String {
|
||||||
let mut buffer: [u8; JULID_LEN] = [0; JULID_LEN];
|
let mut buffer: [u8; JULID_STR_LEN] = [0; JULID_STR_LEN];
|
||||||
for i in 0..JULID_LEN {
|
for i in 0..JULID_STR_LEN {
|
||||||
buffer[JULID_LEN - 1 - i] = ALPHABET[(value & 0x1f) as usize];
|
buffer[JULID_STR_LEN - 1 - i] = ALPHABET[(value & 0x1f) as usize];
|
||||||
value >>= 5;
|
value >>= 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
String::from_utf8(buffer.to_vec()).expect("unexpected failure in base32 encode for ulid")
|
String::from_utf8(buffer.to_vec()).expect("unexpected failure in base32 encode for ulid")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that can occur when decoding a base32 string
|
/// An error that can occur when decoding a base32 string into a Julid (invalid
|
||||||
|
/// length, or invalid character)
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||||
pub enum DecodeError {
|
pub enum DecodeError {
|
||||||
/// The length of the string does not match the expected length
|
/// The length of the string does not match the expected length
|
||||||
|
@ -68,9 +69,9 @@ impl fmt::Display for DecodeError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn decode(encoded: &str) -> Result<u128, DecodeError> {
|
pub(crate) const fn decode(encoded: &str) -> Result<u128, DecodeError> {
|
||||||
let len = encoded.len();
|
let len = encoded.len();
|
||||||
if len != JULID_LEN {
|
if len != JULID_STR_LEN {
|
||||||
return Err(DecodeError::InvalidLength(len));
|
return Err(DecodeError::InvalidLength(len));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +81,7 @@ pub const fn decode(encoded: &str) -> Result<u128, DecodeError> {
|
||||||
|
|
||||||
// Manual for loop because Range::iter() isn't const
|
// Manual for loop because Range::iter() isn't const
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < JULID_LEN {
|
while i < JULID_STR_LEN {
|
||||||
let val = LOOKUP[bytes[i] as usize];
|
let val = LOOKUP[bytes[i] as usize];
|
||||||
if val != NO_VALUE {
|
if val != NO_VALUE {
|
||||||
value = (value << 5) | val as u128;
|
value = (value << 5) | val as u128;
|
||||||
|
@ -113,9 +114,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_length() {
|
fn test_length() {
|
||||||
assert_eq!(encode(0xffffffffffffffffffffffffffffffff).len(), JULID_LEN);
|
assert_eq!(
|
||||||
assert_eq!(encode(0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f).len(), JULID_LEN);
|
encode(0xffffffffffffffffffffffffffffffff).len(),
|
||||||
assert_eq!(encode(0x00000000000000000000000000000000).len(), JULID_LEN);
|
JULID_STR_LEN
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
encode(0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f).len(),
|
||||||
|
JULID_STR_LEN
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
encode(0x00000000000000000000000000000000).len(),
|
||||||
|
JULID_STR_LEN
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(decode(""), Err(DecodeError::InvalidLength(0)));
|
assert_eq!(decode(""), Err(DecodeError::InvalidLength(0)));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
97
src/julid.rs
97
src/julid.rs
|
@ -1,24 +1,24 @@
|
||||||
use core::{fmt, str::FromStr};
|
use core::{fmt, str::FromStr};
|
||||||
use std::{
|
use std::{
|
||||||
sync::atomic::{AtomicU64, Ordering},
|
sync::atomic::{AtomicU64, Ordering},
|
||||||
time::Duration,
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use rand::{random, thread_rng, Rng};
|
use rand::{random, thread_rng, Rng};
|
||||||
|
|
||||||
use crate::base32::{self, DecodeError};
|
use crate::{base32, DecodeError};
|
||||||
|
|
||||||
/// This is used to ensure monotonicity for new IDs.
|
/// This is used to ensure monotonicity for new IDs.
|
||||||
static LAST_SORTABLE: AtomicU64 = AtomicU64::new(0);
|
static LAST_MSB: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
/// The number of bits in a Julid's time portion
|
/// The number of bits in a Julid's time portion
|
||||||
pub const TIME_BITS: u8 = 48;
|
const TIME_BITS: u8 = 48;
|
||||||
/// The number of bits in the monotonic counter for intra-millisecond IDs
|
/// The number of bits in the monotonic counter for intra-millisecond IDs
|
||||||
pub const COUNTER_BITS: u8 = 16;
|
const COUNTER_BITS: u8 = 16;
|
||||||
/// The number of random bits + bits in the monotonic counter
|
/// The number of random bits + bits in the monotonic counter
|
||||||
pub const UNIQUE_BITS: u8 = 80;
|
const UNIQUE_BITS: u8 = 80;
|
||||||
/// The number of fully random bits
|
/// The number of fully random bits
|
||||||
pub const RANDOM_BITS: u8 = 64;
|
const RANDOM_BITS: u8 = 64;
|
||||||
|
|
||||||
macro_rules! bitmask {
|
macro_rules! bitmask {
|
||||||
($len:expr) => {
|
($len:expr) => {
|
||||||
|
@ -37,26 +37,26 @@ macro_rules! bitmask {
|
||||||
/// the same millisecond. The remaining 64 least-significant bits are fully
|
/// the same millisecond. The remaining 64 least-significant bits are fully
|
||||||
/// random.
|
/// random.
|
||||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy)]
|
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy)]
|
||||||
pub struct Julid(pub u128);
|
pub struct Julid(pub(crate) u128);
|
||||||
|
|
||||||
impl Julid {
|
impl Julid {
|
||||||
/// Return a new Julid. If a previous ID was generated in the same
|
/// Return a new Julid. If a previous ID was generated in the same
|
||||||
/// millisecond, increment the monotonic counter, up to u16::MAX. The random
|
/// millisecond, increment the monotonic counter, up to u16::MAX. The random
|
||||||
/// bits are always fresh, so once the monotonic counter is saturated,
|
/// bits are always fresh, so once the monotonic counter is saturated,
|
||||||
/// subsequent IDs from the current millisecond will not have an
|
/// subsequent IDs from the current millisecond will not have an
|
||||||
/// inherent ordering. See discussion at https://github.com/ahawker/ulid/issues/306#issuecomment-451850395
|
/// inherent ordering. See discussion at <https://github.com/ahawker/ulid/issues/306#issuecomment-451850395>
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let lsb: u64 = random();
|
let lsb: u64 = random();
|
||||||
loop {
|
loop {
|
||||||
let ts = std::time::SystemTime::now()
|
let ts = SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or(Duration::ZERO)
|
.unwrap_or(Duration::ZERO)
|
||||||
.as_millis() as u64;
|
.as_millis() as u64;
|
||||||
let last = LAST_SORTABLE.load(Ordering::SeqCst);
|
let last = LAST_MSB.load(Ordering::SeqCst);
|
||||||
let ots = last >> COUNTER_BITS;
|
let ots = last >> COUNTER_BITS;
|
||||||
if ots < ts {
|
if ots < ts {
|
||||||
let msb = ts << COUNTER_BITS;
|
let msb = ts << COUNTER_BITS;
|
||||||
if LAST_SORTABLE
|
if LAST_MSB
|
||||||
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
|
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
|
@ -65,7 +65,7 @@ impl Julid {
|
||||||
} else {
|
} else {
|
||||||
let counter = ((last & bitmask!(COUNTER_BITS) as u64) as u16).saturating_add(1);
|
let counter = ((last & bitmask!(COUNTER_BITS) as u64) as u16).saturating_add(1);
|
||||||
let msb = (ots << COUNTER_BITS) + counter as u64;
|
let msb = (ots << COUNTER_BITS) + counter as u64;
|
||||||
if LAST_SORTABLE
|
if LAST_MSB
|
||||||
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
|
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
|
@ -78,27 +78,6 @@ impl Julid {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Julid from a Crockford Base32 encoded string
|
|
||||||
///
|
|
||||||
/// An DecodeError will be returned when the given string is not formated
|
|
||||||
/// properly.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use julid::julid::Julid;
|
|
||||||
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
|
||||||
/// let result = Julid::from_string(text);
|
|
||||||
///
|
|
||||||
/// assert!(result.is_ok());
|
|
||||||
/// assert_eq!(&result.unwrap().to_string(), text);
|
|
||||||
/// ```
|
|
||||||
pub const fn from_string(encoded: &str) -> Result<Julid, DecodeError> {
|
|
||||||
match base32::decode(encoded) {
|
|
||||||
Ok(int_val) => Ok(Julid(int_val)),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The 'Alpha Julid'.
|
/// The 'Alpha Julid'.
|
||||||
///
|
///
|
||||||
/// The Alpha Julid is special form of Julid that is specified to have
|
/// The Alpha Julid is special form of Julid that is specified to have
|
||||||
|
@ -142,6 +121,23 @@ impl Julid {
|
||||||
(self.0 & bitmask!(UNIQUE_BITS)) as u64
|
(self.0 & bitmask!(UNIQUE_BITS)) as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
/// Returns the timestamp as a `chrono::DateTime<chrono::Utc>` (feature
|
||||||
|
/// `chrono` (default))
|
||||||
|
pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
|
||||||
|
(SystemTime::UNIX_EPOCH + Duration::from_millis(self.timestamp())).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test if the Julid is Alpha
|
||||||
|
pub const fn is_alpha(&self) -> bool {
|
||||||
|
self.0 == 0u128
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test if the Julid is Omega
|
||||||
|
pub const fn is_omega(&self) -> bool {
|
||||||
|
self.0 == u128::MAX
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a Crockford Base32 encoded string that represents this Julid
|
/// Creates a Crockford Base32 encoded string that represents this Julid
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -156,25 +152,36 @@ impl Julid {
|
||||||
base32::encode(self.0)
|
base32::encode(self.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test if the Julid is Alpha
|
/// Creates a Julid from a Crockford Base32 encoded string
|
||||||
pub const fn is_alpha(&self) -> bool {
|
///
|
||||||
self.0 == 0u128
|
/// A [`DecodeError`] will be returned if the given string is not formated
|
||||||
|
/// properly.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use julid::julid::Julid;
|
||||||
|
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
||||||
|
/// let result = Julid::from_string(text);
|
||||||
|
///
|
||||||
|
/// assert!(result.is_ok());
|
||||||
|
/// assert_eq!(&result.unwrap().to_string(), text);
|
||||||
|
/// ```
|
||||||
|
pub const fn from_string(encoded: &str) -> Result<Julid, DecodeError> {
|
||||||
|
match base32::decode(encoded) {
|
||||||
|
Ok(int_val) => Ok(Julid(int_val)),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test if the Julid is Omega
|
/// Returns the bytes of the Julid in big-endian order.
|
||||||
pub const fn is_omega(&self) -> bool {
|
pub const fn as_bytes(self) -> [u8; 16] {
|
||||||
self.0 == u128::MAX
|
self.0.to_be_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Julid using the provided bytes array, assumed big-endian.
|
/// Creates a Julid using the provided bytes array, assumed big-endian.
|
||||||
pub const fn from_bytes(bytes: [u8; 16]) -> Julid {
|
pub const fn from_bytes(bytes: [u8; 16]) -> Julid {
|
||||||
Self(u128::from_be_bytes(bytes))
|
Self(u128::from_be_bytes(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the bytes of the Julid in big-endian order.
|
|
||||||
pub const fn to_bytes(self) -> [u8; 16] {
|
|
||||||
self.0.to_be_bytes()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Julid {
|
impl Default for Julid {
|
||||||
|
|
233
src/lib.rs
233
src/lib.rs
|
@ -1,28 +1,26 @@
|
||||||
#[cfg(feature = "clib")]
|
#[cfg(feature = "plugin")]
|
||||||
use sqlite_loadable::{
|
use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines};
|
||||||
api, define_scalar_function,
|
|
||||||
prelude::{
|
|
||||||
c_char, c_uint, sqlite3, sqlite3_api_routines, sqlite3_context, sqlite3_value,
|
|
||||||
FunctionFlags,
|
|
||||||
},
|
|
||||||
Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod base32;
|
mod base32;
|
||||||
pub mod julid;
|
pub mod julid;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
|
/// Serialization into bytes, and deserialization from a variety of formats,
|
||||||
|
/// with Serde (feature `serde` (default))
|
||||||
pub mod serde;
|
pub mod serde;
|
||||||
|
|
||||||
#[cfg(feature = "sqlx")]
|
#[cfg(feature = "sqlx")]
|
||||||
|
/// Traits from the SQLx crate for getting Julids into and out of SQLite
|
||||||
|
/// databases from normal Rust applications. (feature `sqlx` (default))
|
||||||
pub mod sqlx;
|
pub mod sqlx;
|
||||||
|
|
||||||
pub use base32::JULID_LEN;
|
#[doc(inline)]
|
||||||
|
pub use base32::DecodeError;
|
||||||
|
#[doc(inline)]
|
||||||
pub use julid::Julid;
|
pub use julid::Julid;
|
||||||
|
|
||||||
//-************************************************************************
|
/// This `unsafe extern "C"` function is the main entry point into the loadable
|
||||||
// Entrypoint into the loadable extension
|
/// SQLite extension. By default, it and the `plugin` module it depends on will
|
||||||
//-************************************************************************
|
/// not be built. Build with `cargo build --features plugin`
|
||||||
#[cfg(feature = "clib")]
|
#[cfg(feature = "plugin")]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn sqlite3_julid_init(
|
pub unsafe extern "C" fn sqlite3_julid_init(
|
||||||
db: *mut sqlite3,
|
db: *mut sqlite3,
|
||||||
|
@ -30,101 +28,150 @@ pub unsafe extern "C" fn sqlite3_julid_init(
|
||||||
p_api: *mut sqlite3_api_routines,
|
p_api: *mut sqlite3_api_routines,
|
||||||
) -> c_uint {
|
) -> c_uint {
|
||||||
unsafe { sqlite_loadable::ext::faux_sqlite_extension_init2(p_api) }
|
unsafe { sqlite_loadable::ext::faux_sqlite_extension_init2(p_api) }
|
||||||
match init_rs(db) {
|
match sqlite_plugin::init_rs(db) {
|
||||||
Ok(()) => 256, // SQLITE_OK_LOAD_PERMANENTLY
|
Ok(()) => 256, // SQLITE_OK_LOAD_PERMANENTLY
|
||||||
Err(err) => err.code_extended(),
|
Err(err) => err.code_extended(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "clib")]
|
/// The code for the SQLite plugin is kept in this module, and exposed via the
|
||||||
fn init_rs(db: *mut sqlite3) -> Result<()> {
|
/// `sqlite3_julid_init` function (feature `plugin`)
|
||||||
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC;
|
#[cfg(feature = "plugin")]
|
||||||
define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?;
|
pub mod sqlite_plugin {
|
||||||
define_scalar_function(db, "julid_timestamp", 1, julid_timestamp, flags)?;
|
use sqlite_loadable::{
|
||||||
define_scalar_function(db, "julid_counter", 1, julid_counter, flags)?;
|
api, define_scalar_function,
|
||||||
define_scalar_function(db, "julid_sortable", 1, julid_sortable, flags)?;
|
prelude::{sqlite3, sqlite3_context, sqlite3_value, FunctionFlags},
|
||||||
define_scalar_function(db, "julid_string", 1, julid_string, flags)?;
|
Result,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
use super::*;
|
||||||
}
|
|
||||||
|
|
||||||
//-************************************************************************
|
pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> {
|
||||||
// impls
|
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC;
|
||||||
//-************************************************************************
|
define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?;
|
||||||
#[cfg(feature = "clib")]
|
define_scalar_function(db, "julid_seconds", 1, julid_seconds, flags)?;
|
||||||
fn julid_new(context: *mut sqlite3_context, _vals: &[*mut sqlite3_value]) -> Result<()> {
|
define_scalar_function(db, "julid_counter", 1, julid_counter, flags)?;
|
||||||
api::result_blob(context, Julid::new().to_bytes().as_slice());
|
define_scalar_function(db, "julid_sortable", 1, julid_sortable, flags)?;
|
||||||
Ok(())
|
define_scalar_function(db, "julid_string", 1, julid_string, flags)?;
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "clib")]
|
Ok(())
|
||||||
fn julid_timestamp(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
|
||||||
if let Some(value) = id.get(0) {
|
|
||||||
let id = api::value_blob(value);
|
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
|
||||||
})?;
|
|
||||||
let id: Julid = bytes.into();
|
|
||||||
api::result_int64(context, id.timestamp() as i64);
|
|
||||||
} else {
|
|
||||||
return Err(sqlite_loadable::Error::new_message(
|
|
||||||
"Could not get timestamp for empty Julid",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
//-************************************************************************
|
||||||
}
|
// impls
|
||||||
|
//-************************************************************************
|
||||||
|
|
||||||
#[cfg(feature = "clib")]
|
/// Create a new `Julid` and return it as a `blob`. Because the bytes inside
|
||||||
fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
/// a `Julid` are not valid UTF8, if you wish to see a human-readable
|
||||||
if let Some(value) = id.get(0) {
|
/// representation, use the built-in `hex()` function, or `julid_string()`.
|
||||||
let id = api::value_blob(value);
|
///
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
/// ```text
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
/// sqlite> select hex(julid_new());
|
||||||
})?;
|
/// 018998768ACF000060B31DB175E0C5F9
|
||||||
let id: Julid = bytes.into();
|
/// sqlite> select julid_string(julid_new());
|
||||||
api::result_int64(context, id.counter() as i64);
|
/// 01H6C7D9CT00009TF3EXXJHX4Y
|
||||||
} else {
|
/// ```
|
||||||
return Err(sqlite_loadable::Error::new_message(
|
pub fn julid_new(context: *mut sqlite3_context, _vals: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
"Could not get counter value for empty Julid",
|
api::result_blob(context, Julid::new().as_bytes().as_slice());
|
||||||
));
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
/// Returns the timestamp portion as number of fractional seconds (`f64`),
|
||||||
}
|
/// for use in SQLite's `datetime()` function.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// sqlite> select julid_seconds(julid_new());
|
||||||
|
/// 1690480066.208
|
||||||
|
/// sqlite> select datetime(julid_timestamp(julid_new()), 'auto');
|
||||||
|
/// 2023-07-27 17:47:50
|
||||||
|
/// ```
|
||||||
|
pub fn julid_seconds(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
|
if let Some(value) = id.get(0) {
|
||||||
|
let id = api::value_blob(value);
|
||||||
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
})?;
|
||||||
|
let id: Julid = bytes.into();
|
||||||
|
let ts = id.timestamp() as f64 / 1000.0;
|
||||||
|
api::result_double(context, ts);
|
||||||
|
} else {
|
||||||
|
return Err(sqlite_loadable::Error::new_message(
|
||||||
|
"Could not get timestamp for empty Julid",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "clib")]
|
Ok(())
|
||||||
fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
|
||||||
if let Some(value) = id.get(0) {
|
|
||||||
let id = api::value_blob(value);
|
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
|
||||||
})?;
|
|
||||||
let id: Julid = bytes.into();
|
|
||||||
api::result_int64(context, id.sortable() as i64);
|
|
||||||
} else {
|
|
||||||
return Err(sqlite_loadable::Error::new_message(
|
|
||||||
"Could not get sortable bits for empty Julid",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
/// Return the value of the monotonic counter for this Julid. For the first
|
||||||
}
|
/// Julid created in a millisecond, its value will be 0. If you are
|
||||||
|
/// creating more than 65,536 Julids per millisecond, the counter will
|
||||||
|
/// saturate at 65,536.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// sqlite> select julid_counter(julid_new());
|
||||||
|
/// 0
|
||||||
|
/// ```
|
||||||
|
pub fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
|
if let Some(value) = id.get(0) {
|
||||||
|
let id = api::value_blob(value);
|
||||||
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
})?;
|
||||||
|
let id: Julid = bytes.into();
|
||||||
|
api::result_int64(context, id.counter() as i64);
|
||||||
|
} else {
|
||||||
|
return Err(sqlite_loadable::Error::new_message(
|
||||||
|
"Could not get counter value for empty Julid",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "clib")]
|
Ok(())
|
||||||
fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
|
||||||
if let Some(value) = id.get(0) {
|
|
||||||
let id = api::value_blob(value);
|
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
|
||||||
})?;
|
|
||||||
let id: Julid = bytes.into();
|
|
||||||
api::result_text(context, id.as_string())?;
|
|
||||||
} else {
|
|
||||||
return Err(sqlite_loadable::Error::new_message(
|
|
||||||
"Could not convert empty Julid to string",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
/// Return the 64-bit concatenation of the Julid's timestamp and monotonic
|
||||||
|
/// counter.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// sqlite> select julid_sortable(julid_new());
|
||||||
|
/// 110787724287475712
|
||||||
|
/// ```
|
||||||
|
pub fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
|
if let Some(value) = id.get(0) {
|
||||||
|
let id = api::value_blob(value);
|
||||||
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
})?;
|
||||||
|
let id: Julid = bytes.into();
|
||||||
|
api::result_int64(context, id.sortable() as i64);
|
||||||
|
} else {
|
||||||
|
return Err(sqlite_loadable::Error::new_message(
|
||||||
|
"Could not get sortable bits for empty Julid",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the human-readable base32 Crockford encoding of this Julid.
|
||||||
|
/// ```text
|
||||||
|
/// sqlite> select julid_string(julid_new());
|
||||||
|
/// 01H6C7D9CT00009TF3EXXJHX4Y
|
||||||
|
/// ```
|
||||||
|
pub fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
|
if let Some(value) = id.get(0) {
|
||||||
|
let id = api::value_blob(value);
|
||||||
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
})?;
|
||||||
|
let id: Julid = bytes.into();
|
||||||
|
api::result_text(context, id.as_string())?;
|
||||||
|
} else {
|
||||||
|
return Err(sqlite_loadable::Error::new_message(
|
||||||
|
"Could not convert empty Julid to string",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
13
src/serde.rs
13
src/serde.rs
|
@ -1,8 +1,7 @@
|
||||||
//! Serialization and deserialization.
|
/// Serialization and deserialization.
|
||||||
//!
|
///
|
||||||
//! By default, serialization and deserialization go through Julid's big-endian
|
/// By default, serialization and deserialization go through Julid's big-endian
|
||||||
//! bytes representation.
|
/// bytes representation.
|
||||||
|
|
||||||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
use crate::Julid;
|
use crate::Julid;
|
||||||
|
@ -12,7 +11,7 @@ impl Serialize for Julid {
|
||||||
where
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
serializer.serialize_bytes(&self.to_bytes())
|
serializer.serialize_bytes(&self.as_bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +88,7 @@ impl<'de> Deserialize<'de> for Julid {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use julid::Julid;
|
/// # use julid::Julid;
|
||||||
/// # use julid::serde::ulid_as_str;
|
/// # use julid::serde::julid_as_str;
|
||||||
/// # use serde::{Serialize, Deserialize};
|
/// # use serde::{Serialize, Deserialize};
|
||||||
/// #[derive(Serialize, Deserialize)]
|
/// #[derive(Serialize, Deserialize)]
|
||||||
/// struct StrExample {
|
/// struct StrExample {
|
||||||
|
|
|
@ -17,7 +17,7 @@ impl sqlx::Type<sqlx::Sqlite> for Julid {
|
||||||
impl<'q> Encode<'q, Sqlite> for Julid {
|
impl<'q> Encode<'q, Sqlite> for Julid {
|
||||||
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
|
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
|
||||||
args.push(SqliteArgumentValue::Blob(Cow::Owned(
|
args.push(SqliteArgumentValue::Blob(Cow::Owned(
|
||||||
self.to_bytes().to_vec(),
|
self.as_bytes().to_vec(),
|
||||||
)));
|
)));
|
||||||
IsNull::No
|
IsNull::No
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue