Compare commits

..

No commits in common. "main" and "1.61803" have entirely different histories.

13 changed files with 169 additions and 357 deletions

View file

@ -1,30 +1,22 @@
[package]
name = "julid-rs"
# 1.61803398874989484
#----------------^
version = "1.6.1803398874989"
version = "1.6.1803"
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
edition = "2024"
keywords = ["ulid", "sqlite", "julid", "uuid", "guid"]
edition = "2021"
keywords = ["ulid", "library", "sqlite", "extension", "julid"]
description = "A crate and loadable extension for SQLite that provides Joe's ULIDs."
readme = "README.md"
license-file = "LICENSE.md"
repository = "https://git.kittencollective.com/nebkor/julid-rs"
[package.metadata.docs.rs]
all-features = true
repository = "https://gitlab.com/nebkor/julid"
[features]
default = ["serde", "sqlx", "cli", "std", "chrono"] # no uuid or sqlite plugin
default = ["chrono", "serde", "sqlx"] # just the regular crate
chrono = ["dep:chrono"]
cli = ["dep:clap", "chrono"]
serde = ["dep:serde"]
sqlx = ["dep:sqlx"]
std = ["chrono/std", "serde?/alloc"]
uuid = ["dep:uuid"]
# WARNING! don't enable this feature in your project's Cargo.toml if using julid-rs as a Rust dependency;
# WARNING! don't enable this feature in your project's Cargo.toml if using julid-rs as a dependency;
# see https://gitlab.com/nebkor/julid/-/issues/1
plugin = ["dep:sqlite-loadable"] # builds libjulid.* for loading into sqlite
@ -35,25 +27,18 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
rand = "0.8"
# all other deps are optional
chrono = { version = "0.4", default-features = false, features = ["std"], optional = true }
# for the CLI
clap = { version = "4", default-features = false, features = ["help", "usage", "std", "derive"], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
sqlx = { version = "0.8", features = ["sqlite"], default-features = false, optional = true }
clap = { version = "4.3", default-features = false, features = ["help", "usage", "std", "derive"] }
# all other deps are optional
chrono = { version = "0.4", optional = true, default-features = false, features = ["std", "time"] }
serde = { version = "1.0", features = ["derive"], optional = true }
sqlx = { version = "0.7", features = ["sqlite"], default-features = false, optional = true }
sqlite-loadable = { version = "0.0.5", optional = true }
uuid = { version = "1.17", default-features = false, optional = true }
[dev-dependencies]
divan = "0.1"
uuid = { version = "1", default-features = false, features = ["v4"] }
julid-rs = { path = ".", features = ["uuid"] }
[[bench]]
name = "simple"
harness = false
[[bin]]
name = "julid-gen"
path = "src/bin/gen.rs"
required-features = ["chrono", "cli"]
[package.metadata.docs.rs]
all-features = true

View file

@ -1,13 +1,5 @@
# Dual Licensed (combined terms are binding)
This software is governed under the combined terms of the following two licenses:
## The Chaos License (GLP)
# The Chaos License (GLP)
This software is released under the terms of the Chaos License. In cases where the terms of the
license are unclear, refer to the [Fuck Around and Find Out
License](https://git.sr.ht/~boringcactus/fafol/tree/master/LICENSE-v0.2.md).
## The Butlerian Jihad License (DUN)
If you feed this code into an LLM, I will fuck you up.

113
README.md
View file

@ -20,8 +20,6 @@ sqlite> select datetime(julid_timestamp(julid_new()), 'auto');
2023-07-27 17:47:50
sqlite> select julid_counter(julid_new());
0
sqlite> select julid_string();
01HM4WJ7T90001P8SN9898FBTN
```
Crates.io: <https://crates.io/crates/julid-rs>
@ -30,9 +28,6 @@ Docs.rs: <https://docs.rs/julid-rs/latest/julid/>
Blog post: <https://proclamations.nebcorp-hias.com/sundries/presenting-julids/>
As of June of 2025, they can also be converted to and from [version 7
UUIDs](https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7), though some precision in the
intra-millisecond counter is lost when going to a UUID, via the `uuid` optional feature.
## A slightly deeper look
@ -76,8 +71,9 @@ are always fresh, it's not possible to easily guess a valid Julid if you already
# How to use
The Julid crate can be used in two different ways: as a regular Rust library, declared in your Rust
project's `Cargo.toml` file (say, by running `cargo add julid-rs`), and used as shown in the sample
commandline program (see below). But the primary use case for me was as a loadable
project's `Cargo.toml` file (say, by running `cargo add julid-rs`), and used as shown above. There's
a rudimentary [benchmark](https://gitlab.com/nebkor/julid/-/blob/main/examples/benchmark.rs) example
in the repo that shows off most of the Rust API. But the primary use case for me was as a loadable
SQLite extension. Both are covered in the [documentation](https://docs.rs/julid-rs/latest/julid/),
but let's go over them here, starting with the extension.
@ -87,8 +83,6 @@ The extension, when loaded into SQLite, provides the following functions:
* `julid_new()`: create a new Julid and return it as a 16-byte
[blob](https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes)
* `julid_string()`: create a new Julid and return it as a 26-character [base-32
Crockford](https://en.wikipedia.org/wiki/Base32)-encoded string
* `julid_seconds(julid)`: get the number seconds (as a 64-bit float) since the UNIX epoch that this
julid was created (convenient for passing to the builtin `datetime()` function)
* `julid_counter(julid)`: show the value of this julid's monotonic counter
@ -125,11 +119,11 @@ For a table created like:
create table if not exists watches (
id blob not null primary key default (julid_new()),
kind int not null, -- enum for movie or tv show or whatev
title text not null,
title text not null, -- this has a secondary index
length int,
release_date date,
release_date int,
added_by blob not null,
last_updated date not null default CURRENT_TIMESTAMP,
last_updated int not null default (unixepoch()),
foreign key (added_by) references users (id)
);
```
@ -155,53 +149,86 @@ the C interface, which is inherently unsafe. If you are not building the plugin,
## Inside a Rust program
Of course, you can also use it outside of a database; the `Julid` type is publicly exported. There's
a simple commandline program in `src/bin/gen.rs`, and can be run like `cargo run --bin julid-gen`
(or you can `cargo install julid-rs` to get the `julid-gen` program installed on your computer),
which will generate and print one Julid. If you want to see its component pieces, grab the Julid
printed from it, and then run it with the `-d` flag:
a simple benchmark in the examples folder of the repo, the important parts of which look like:
```
$ julid-gen 4
01HV2G2ATR000CJ2WESB7CVC19
01HV2G2ATR000K1AGQPKMX5H0M
01HV2G2ATR001CM27S59BHZ25G
01HV2G2ATR001WPJ8BS7PZHE6A
$ julid-gen -d 01HV2G2ATR001WPJ8BS7PZHE6A
Created at: 2024-04-09 22:36:11.992 UTC
Monotonic counter: 3
Random: 14648252224908081354
``` rust
use julid::Julid;
fn main() {
/* snip some stuff */
let start = Instant::now();
for _ in 0..num {
v.push(Julid::new());
}
let end = Instant::now();
let dur = (end - start).as_micros();
for id in v.iter() {
eprintln!(
"{id}: created_at {}; counter: {}; sortable: {}",
id.created_at(),
id.counter(),
id.sortable()
);
}
println!("{num} Julids generated in {dur}us");
```
The help is useful:
If you were to run it on a computer like mine (AMD Ryzen 9 3900X, 12-core, 2.2-4.6 GHz), you might
see something like this:
```
$ julid-gen -h
Generate, print, and parse Julids
Usage: julid-gen [OPTIONS] [NUM]
Arguments:
[NUM] Number of Julids to generate [default: 1]
Options:
-d, --decode <INPUT> Print the components of the given Julid
-a, --answer The answer to the meaning of Julid
-h, --help Print help
-V, --version Print version
``` text
$ cargo run --example=benchmark --release -- -n 30000 2> /dev/null
30000 Julids generated in 1240us
```
The whole program is just 34 lines, so check it out.
That's about 24,000 IDs/millisecond; 24 *MILLION* per second!
The default optional Cargo features include implementations of traits for getting Julids into and
out of SQLite with [SQLx](https://github.com/launchbadge/sqlx), and for generally
serializing/deserializing with [Serde](https://serde.rs/), via the `sqlx` and `serde` features,
respectively.
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`.
Something to note: don't enable the `plugin` feature in your Cargo.toml if you're using this crate
inside your Rust application, especially if you're *also* loading it as an extension in SQLite in
your application. You'll get a long and confusing runtime panic due to there being multiple
entrypoints defined with the same name.
## On the command line
An even simpler program than the benchmark called `julid-gen` is available to install via cargo:
`cargo install julid-rs --no-default-features`
And then using it is as simple as,
``` text
$ julid-gen -h
Generate and print Julids
Usage: julid-gen [NUM]
Arguments:
[NUM] Number of Julids to generate [default: 1]
Options:
-h, --help Print help
-V, --version Print version
$ julid-gen
01H9DYRVDX0001X0RE5Y7XFGBC
$ julid-gen 5
01H9DYT48E000EK2EH7P67N8GG
01H9DYT48E000ZBKXVZ91HEZX4
01H9DYT48E0012VX89PYX4HDKP
01H9DYT48E001GE29AWCH1RDCM
01H9DYT48E0028CDHNVC59KKHQ
```
# Thanks
This project wouldn't have happened without a lot of inspiration (and a little shameless stealing)

View file

@ -1 +1 @@
1.61803398874989
1.61803

View file

@ -1,16 +0,0 @@
use divan::black_box;
use julid::Julid;
fn main() {
divan::main();
}
#[divan::bench]
fn jbench() {
for _ in 0..1_000_000 {
let x = black_box(Julid::new());
if x < 1u128.into() {
println!("that's weird");
}
}
}

37
examples/benchmark.rs Normal file
View file

@ -0,0 +1,37 @@
use std::time::Instant;
use clap::Parser;
use julid::Julid;
#[derive(Debug, Parser)]
struct Cli {
#[clap(
long,
short,
help = "Number of Julids to generate",
default_value_t = 2_000
)]
pub num: usize,
}
fn main() {
let cli = Cli::parse();
let num = cli.num;
let mut v = Vec::with_capacity(num);
let start = Instant::now();
for _ in 0..num {
v.push(Julid::new());
}
let end = Instant::now();
let dur = (end - start).as_micros();
for id in v.iter() {
eprintln!(
"{id}: created_at {}; counter: {}; sortable: {}",
id.created_at(),
id.counter(),
id.sortable()
);
}
println!("{num} Julids generated in {dur}us");
}

View file

@ -65,7 +65,7 @@ impl fmt::Display for DecodeError {
DecodeError::InvalidLength(len) => format!("invalid length: {len}"),
DecodeError::InvalidChar(c) => format!("invalid character: {c}"),
};
write!(f, "{text}")
write!(f, "{}", text)
}
}

View file

@ -2,50 +2,17 @@ use clap::Parser;
use julid::Julid;
#[derive(Debug, Parser)]
#[command(
author,
version = "1.618033988",
about = "Generate, print, and parse Julids"
)]
#[command(author, version = "1.61803", about = "Generate and print Julids")]
struct Cli {
#[clap(
help = "Print the components of the given Julid",
short = 'd',
long = "decode"
)]
pub input: Option<String>,
#[clap(help = "Number of Julids to generate", default_value_t = 1)]
pub num: usize,
#[clap(
help = "The answer to the meaning of Julid",
default_value_t = false,
short,
long
)]
pub answer: bool,
}
fn main() {
let cli = Cli::parse();
if let Some(julid_string_input) = cli.input {
if let Ok(ts) = Julid::from_str(&julid_string_input) {
println!("Created at:\t\t{}", ts.created_at());
println!("Monotonic counter:\t{}", ts.counter());
println!("Random:\t\t\t{}", ts.random());
} else {
eprintln!("Could not parse input '{julid_string_input}' as a Julid");
std::process::exit(1);
}
} else {
// Just print some Julids
let num = cli.num;
for _ in 0..num {
let j = if cli.answer {
42u128.into()
} else {
Julid::new()
};
println!("{j}");
}
let num = cli.num;
for _ in 0..num {
println!("{}", Julid::new());
}
}

View file

@ -6,11 +6,20 @@ use std::{
use rand::{random, thread_rng, Rng};
use crate::{base32, DecodeError, COUNTER_BITS, RANDOM_BITS, TIME_BITS, UNIQUE_BITS};
use crate::{base32, DecodeError};
/// This is used to ensure monotonicity for new IDs.
static LAST_MSB: AtomicU64 = AtomicU64::new(0);
/// The number of bits in a Julid's time portion
const TIME_BITS: u8 = 48;
/// The number of bits in the monotonic counter for intra-millisecond IDs
const COUNTER_BITS: u8 = 16;
/// The number of random bits + bits in the monotonic counter
const UNIQUE_BITS: u8 = 80;
/// The number of fully random bits
const RANDOM_BITS: u8 = 64;
macro_rules! bitmask {
($len:expr) => {
((1 << $len) - 1)
@ -65,14 +74,6 @@ impl Julid {
}
}
/// Return a new Julid with the given timestamp (in milliseconds), no
/// counter bits set, and 64 random lower bits.
pub fn at(ts_ms: u64) -> Self {
let hi = ts_ms << COUNTER_BITS;
let lo = random();
(hi, lo).into()
}
/// The 'Alpha Julid'.
///
/// The Alpha Julid is special form of Julid that is specified to have
@ -139,7 +140,7 @@ impl Julid {
/// ```rust
/// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let id = Julid::from_str(text).unwrap();
/// let id = Julid::from_string(text).unwrap();
///
/// assert_eq!(&id.to_string(), text);
/// ```
@ -156,12 +157,12 @@ impl Julid {
/// ```rust
/// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let result = Julid::from_str(text);
/// let result = Julid::from_string(text);
///
/// assert!(result.is_ok());
/// assert_eq!(&result.unwrap().to_string(), text);
/// ```
pub const fn from_str(encoded: &str) -> Result<Self, DecodeError> {
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),
@ -174,7 +175,7 @@ impl Julid {
}
/// Creates a Julid using the provided bytes array, assumed big-endian.
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
pub const fn from_bytes(bytes: [u8; 16]) -> Julid {
Self(u128::from_be_bytes(bytes))
}
}
@ -231,7 +232,7 @@ impl FromStr for Julid {
type Err = DecodeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Julid::from_str(s)
Julid::from_string(s)
}
}
@ -248,7 +249,7 @@ mod tests {
#[test]
fn test_static() {
let s = Julid(0x41414141414141414141414141414141).as_string();
let u = Julid::from_str(&s).unwrap();
let u = Julid::from_string(&s).unwrap();
assert_eq!(&s, "21850M2GA1850M2GA1850M2GA1");
assert_eq!(u.0, 0x41414141414141414141414141414141);
}

View file

@ -2,52 +2,26 @@
use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines};
mod base32;
/// Contains the [`Julid`] type, which is publicly exported at the top level.
pub mod julid;
#[cfg(feature = "serde")]
/// Serialization into bytes, and deserialization from a variety of formats,
/// with Serde (feature `serde` (default))
#[cfg(feature = "serde")]
pub mod serde;
#[cfg(feature = "sqlx")]
/// Traits from the SQLx crate for getting Julids into and out of SQLite
/// databases from normal Rust applications. (feature `sqlx` (default))
#[cfg(feature = "sqlx")]
pub mod sqlx;
/// UUIDv7s are almost as good as Julids, and can be interconverted almost
/// perfectly. (feature `uuid` (non-default))
///
/// See the [`Julid::as_uuid`] and [`Julid::from_uuid`] methods for
/// converting a Julid to a UUID and constructing a Julid from a UUID
/// respectively.
#[cfg(feature = "uuid")]
pub mod uuid;
#[doc(inline)]
pub use base32::DecodeError;
#[doc(inline)]
pub use julid::Julid;
/// The number of bits in a Julid's millisecond timestamp (48)
pub const TIME_BITS: u8 = 48;
/// The number of bits in the monotonic counter for intra-millisecond IDs (16)
pub const COUNTER_BITS: u8 = 16;
/// The number of random bits + bits in the monotonic counter (80)
pub const UNIQUE_BITS: u8 = 80;
/// The number of fully random bits (64)
pub const RANDOM_BITS: u8 = 64;
/// This `unsafe extern "C"` function is the main entry point into the loadable
/// SQLite extension. By default, it and the `plugin` module it depends on will
/// not be built. Build with `cargo build --features plugin`
///
/// # Safety
/// This is FFI; it's inherently unsafe. But this function is called by
/// sqlite, not by a user, so it should be OK.
#[cfg(feature = "plugin")]
#[unsafe(no_mangle)]
#[no_mangle]
pub unsafe extern "C" fn sqlite3_julid_init(
db: *mut sqlite3,
_pz_err_msg: *mut *mut c_char,
@ -61,30 +35,24 @@ pub unsafe extern "C" fn sqlite3_julid_init(
}
/// The code for the SQLite plugin is kept in this module, and exposed via the
/// [`sqlite3_julid_init`] function (feature `plugin` (non-default))
/// `sqlite3_julid_init` function (feature `plugin`)
#[cfg(feature = "plugin")]
pub mod sqlite_plugin {
use sqlite_loadable::{
api, define_scalar_function,
prelude::{sqlite3_context, sqlite3_value, FunctionFlags},
prelude::{sqlite3, sqlite3_context, sqlite3_value, FunctionFlags},
Result,
};
use super::*;
pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> {
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC | FunctionFlags::INNOCUOUS;
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC;
define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?;
define_scalar_function(db, "julid_seconds", 1, julid_seconds, flags)?;
define_scalar_function(db, "julid_counter", 1, julid_counter, flags)?;
define_scalar_function(db, "julid_sortable", 1, julid_sortable, flags)?;
define_scalar_function(
db,
"julid_string",
-1,
julid_string,
FunctionFlags::UTF8 | FunctionFlags::INNOCUOUS,
)?;
define_scalar_function(db, "julid_string", 1, julid_string, flags)?;
Ok(())
}
@ -108,17 +76,13 @@ pub mod sqlite_plugin {
Ok(())
}
/// Return the human-readable base32 Crockford encoding of the given Julid,
/// or create a new one if no arguments.
///
/// Return the human-readable base32 Crockford encoding of this Julid.
/// ```text
/// sqlite> select julid_string(julid_new());
/// 01H6C7D9CT00009TF3EXXJHX4Y
/// sqlite> select julid_string();
/// 01HJSHZ0PN000EKP3H94R6TPWH
/// ```
pub fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")
@ -126,7 +90,9 @@ pub mod sqlite_plugin {
let id: Julid = bytes.into();
api::result_text(context, id.as_string())?;
} else {
api::result_text(context, Julid::new().as_string())?;
return Err(sqlite_loadable::Error::new_message(
"Could not convert empty Julid to string",
));
}
Ok(())
@ -142,7 +108,7 @@ pub mod sqlite_plugin {
/// 2023-07-27 17:47:50
/// ```
pub fn julid_seconds(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")
@ -169,7 +135,7 @@ pub mod sqlite_plugin {
/// 0
/// ```
pub fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")
@ -193,7 +159,7 @@ pub mod sqlite_plugin {
/// 110787724287475712
/// ```
pub fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")

View file

@ -34,7 +34,6 @@ impl<'de> Visitor<'de> for JulidVisitor {
}
}
#[cfg(feature = "std")]
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
@ -98,13 +97,10 @@ impl<'de> Deserialize<'de> for Julid {
/// }
/// ```
pub mod julid_as_str {
#[cfg(feature = "std")]
use serde::{Deserialize, Deserializer};
use serde::{Serialize, Serializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::Julid;
/// Serialize a Julid into a String
pub fn serialize<S>(value: &Julid, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@ -113,13 +109,11 @@ pub mod julid_as_str {
text.serialize(serializer)
}
#[cfg(feature = "std")]
/// Deserialize a String into a Julid (feature `std` only)
pub fn deserialize<'de, D>(deserializer: D) -> Result<Julid, D::Error>
where
D: Deserializer<'de>,
{
let deserialized_str = String::deserialize(deserializer)?;
Julid::from_str(&deserialized_str).map_err(serde::de::Error::custom)
Julid::from_string(&deserialized_str).map_err(serde::de::Error::custom)
}
}

View file

@ -3,7 +3,7 @@ use std::borrow::Cow;
use sqlx::{
encode::IsNull,
sqlite::{SqliteArgumentValue, SqliteValueRef},
Decode, Encode, Sqlite, Value, ValueRef,
Decode, Encode, Sqlite,
};
use crate::Julid;
@ -15,33 +15,18 @@ impl sqlx::Type<sqlx::Sqlite> for Julid {
}
impl<'q> Encode<'q, Sqlite> for Julid {
fn encode_by_ref(
&self,
args: &mut Vec<SqliteArgumentValue<'q>>,
) -> std::result::Result<
sqlx::encode::IsNull,
std::boxed::Box<(dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static)>,
> {
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
args.push(SqliteArgumentValue::Blob(Cow::Owned(
self.as_bytes().to_vec(),
)));
Ok(IsNull::No)
IsNull::No
}
}
impl Decode<'_, Sqlite> for Julid {
fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
let julid = match <&[u8] as Decode<Sqlite>>::decode(value.to_owned().as_ref()) {
Ok(bytes) => {
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
Julid::from_bytes(bytes)
}
_ => {
let string = <&str as Decode<Sqlite>>::decode(value)?;
Julid::from_str(string)?
}
};
Ok(julid)
let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?;
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
Ok(bytes.into())
}
}

View file

@ -1,126 +0,0 @@
use std::fmt;
use uuid::{Uuid, Variant};
use crate::{Julid, COUNTER_BITS};
impl Julid {
/// Convert to UUIDv7, possibly losing counter bits and altering the top
/// two bits from the lower 64.
///
/// UUIDv7s are very similar to Julids, but use 12 bits for a monotonic
/// counter instead of 16, and only 62 bits of entropy vs Julids' 64. This
/// means that some bits in the original Julid are overwritten with
/// UUID-specific values, but only six bits in total are potentially
/// altered.
pub const fn as_uuid(&self) -> Uuid {
let counter_mask = (1 << 12) - 1;
let entropy_mask = (1 << 62) - 1;
let timestamp = self.timestamp();
// https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7 "ver" is 0b0111
let counter = (self.counter() & counter_mask) | (0b0111 << 12);
// https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7 "var" is 0b10
let entropy = (self.random() & entropy_mask) | (0b10 << 62);
let top = (timestamp << 16) | counter as u64;
Uuid::from_u64_pair(top, entropy)
}
/// Create from a UUIDv7; will fail if the UUID is not a valid v7 UUID.
///
/// UUIDv7s are very similar to Julids, but use 12 bits for a monotonic
/// counter instead of 16, and only 62 bits of entropy vs Julids' 64.
/// Therefore, no bits technically need to be altered when converting to a
/// Julid, but we zero out the high bits of the counter where the UUID
/// version was stored.
pub fn from_uuid(id: Uuid) -> Result<Self, UuidError> {
let ver = id.get_version_num();
if ver != 7 {
return Err(UuidError::UnsupportedVersion(ver));
}
let var = id.get_variant();
if var != Variant::RFC4122 {
return Err(UuidError::UnsupportedVariant(var));
}
let (hi, lo) = id.as_u64_pair();
// zero out the high bits of the counter, which are "7" (0b0111) from the uuid
let mask = (1 << 12) - 1;
let counter = hi & mask;
let ts = hi >> COUNTER_BITS;
let hi = (ts << COUNTER_BITS) | counter;
Ok((hi, lo).into())
}
}
impl From<Julid> for Uuid {
fn from(value: Julid) -> Self {
value.as_uuid()
}
}
impl TryFrom<Uuid> for Julid {
type Error = UuidError;
fn try_from(value: Uuid) -> Result<Self, Self::Error> {
Julid::from_uuid(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UuidError {
UnsupportedVersion(usize),
UnsupportedVariant(uuid::Variant),
}
impl std::error::Error for UuidError {}
impl fmt::Display for UuidError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let text = match *self {
UuidError::UnsupportedVersion(v) => format!("unsupported version {v}"),
UuidError::UnsupportedVariant(v) => format!("unsupported variant: {v:?}"),
};
write!(f, "{text}")
}
}
#[cfg(test)]
mod test {
use uuid::Uuid;
use crate::{uuid::UuidError, Julid};
#[test]
fn into_uuid() {
// see example from https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html#method.new_v7
let ts = 1497624119 * 1000;
let j = Julid::at(ts);
let u = j.as_uuid().hyphenated().to_string();
assert!(u.starts_with("015cb15a-86d8-7"));
}
#[test]
fn from_uuid() {
let j1 = Julid::new();
let u1: Uuid = j1.into();
let ju1: Julid = u1.try_into().unwrap();
assert_eq!(j1.timestamp(), ju1.timestamp());
assert_eq!(j1.counter(), ju1.counter());
assert_eq!(j1.random() << 2, ju1.random() << 2);
assert_eq!(ju1.random() >> 62, 2);
// once we've converted to uuid and then back to julid, we've reached the fixed
// point
let u2 = ju1.as_uuid();
let ju2 = u2.try_into().unwrap();
assert_eq!(ju1, ju2);
}
#[test]
fn cant_even_from_uuid_non_v7() {
let u = uuid::Uuid::new_v4();
let jr: Result<Julid, UuidError> = u.try_into();
assert_eq!(jr, Err(UuidError::UnsupportedVersion(4)));
}
}