Compare commits

...

6 commits

Author SHA1 Message Date
Joe Ardent
e333ea5263 tighten up and modularize deps 2025-07-03 17:48:06 -07:00
Joe Ardent
8bbad764c3 Update docs, tests, and improve conversion to UUIDs. 2025-06-26 17:46:09 -07:00
Joe Ardent
02b99ba010 even betterer uuid stuff 2025-06-25 23:24:34 -07:00
Joe Ardent
66621bb8c7 add tryfrom impls from uuid 2025-06-24 20:35:58 -07:00
Joe Ardent
dd6b28a039 get docs building 2025-06-22 18:29:28 -07:00
Joe Ardent
acb32e56a6 Bump the version of sqlx, update the readme. 2025-06-22 16:13:03 -07:00
9 changed files with 104 additions and 44 deletions

View file

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

View file

@ -30,6 +30,9 @@ Docs.rs: <https://docs.rs/julid-rs/latest/julid/>
Blog post: <https://proclamations.nebcorp-hias.com/sundries/presenting-julids/> 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 ## A slightly deeper look

View file

@ -1 +1 @@
1.6180339887 1.61803398874989

View file

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

View file

@ -116,6 +116,7 @@ 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 /// Returns the timestamp as a `chrono::DateTime<chrono::Utc>` (feature
/// `chrono` (default)) /// `chrono` (default))
pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> { pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {

View file

@ -2,18 +2,27 @@
use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines}; use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines};
mod base32; mod base32;
/// Contains the [`Julid`] type, which is publicly exported at the top level.
pub mod julid; pub mod julid;
#[cfg(feature = "serde")]
/// Serialization into bytes, and deserialization from a variety of formats, /// Serialization into bytes, and deserialization from a variety of formats,
/// with Serde (feature `serde` (default)) /// with Serde (feature `serde` (default))
#[cfg(feature = "serde")]
pub mod serde; pub mod serde;
#[cfg(feature = "sqlx")]
/// Traits from the SQLx crate for getting Julids into and out of SQLite /// Traits from the SQLx crate for getting Julids into and out of SQLite
/// databases from normal Rust applications. (feature `sqlx` (default)) /// databases from normal Rust applications. (feature `sqlx` (default))
#[cfg(feature = "sqlx")]
pub mod sqlx; pub mod sqlx;
#[cfg(feature = "uuid")]
/// UUIDv7s are almost as good as Julids, and can be interconverted almost /// UUIDv7s are almost as good as Julids, and can be interconverted almost
/// perfectly. /// 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; pub mod uuid;
#[doc(inline)] #[doc(inline)]
@ -21,13 +30,13 @@ pub use base32::DecodeError;
#[doc(inline)] #[doc(inline)]
pub use julid::Julid; pub use julid::Julid;
/// The number of bits in a Julid's time portion /// The number of bits in a Julid's millisecond timestamp (48)
pub const TIME_BITS: u8 = 48; pub 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 (16)
pub const COUNTER_BITS: u8 = 16; pub 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 (80)
pub const UNIQUE_BITS: u8 = 80; pub const UNIQUE_BITS: u8 = 80;
/// The number of fully random bits /// The number of fully random bits (64)
pub const RANDOM_BITS: u8 = 64; pub const RANDOM_BITS: u8 = 64;
/// This `unsafe extern "C"` function is the main entry point into the loadable /// This `unsafe extern "C"` function is the main entry point into the loadable
@ -38,7 +47,7 @@ pub const RANDOM_BITS: u8 = 64;
/// This is FFI; it's inherently unsafe. But this function is called by /// This is FFI; it's inherently unsafe. But this function is called by
/// sqlite, not by a user, so it should be OK. /// sqlite, not by a user, so it should be OK.
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
#[no_mangle] #[unsafe(no_mangle)]
pub unsafe extern "C" fn sqlite3_julid_init( pub unsafe extern "C" fn sqlite3_julid_init(
db: *mut sqlite3, db: *mut sqlite3,
_pz_err_msg: *mut *mut c_char, _pz_err_msg: *mut *mut c_char,
@ -52,7 +61,7 @@ pub unsafe extern "C" fn sqlite3_julid_init(
} }
/// The code for the SQLite plugin is kept in this module, and exposed via the /// The code for the SQLite plugin is kept in this module, and exposed via the
/// `sqlite3_julid_init` function (feature `plugin`) /// [`sqlite3_julid_init`] function (feature `plugin` (non-default))
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub mod sqlite_plugin { pub mod sqlite_plugin {
use sqlite_loadable::{ use sqlite_loadable::{

View file

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

View file

@ -15,11 +15,17 @@ 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>>,
) -> std::result::Result<
sqlx::encode::IsNull,
std::boxed::Box<(dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static)>,
> {
args.push(SqliteArgumentValue::Blob(Cow::Owned( args.push(SqliteArgumentValue::Blob(Cow::Owned(
self.as_bytes().to_vec(), self.as_bytes().to_vec(),
))); )));
IsNull::No Ok(IsNull::No)
} }
} }

View file

@ -2,7 +2,7 @@ use std::fmt;
use uuid::{Uuid, Variant}; use uuid::{Uuid, Variant};
use crate::Julid; use crate::{Julid, COUNTER_BITS};
impl Julid { impl Julid {
/// Convert to UUIDv7, possibly losing counter bits and altering the top /// Convert to UUIDv7, possibly losing counter bits and altering the top
@ -13,7 +13,7 @@ impl Julid {
/// means that some bits in the original Julid are overwritten with /// means that some bits in the original Julid are overwritten with
/// UUID-specific values, but only six bits in total are potentially /// UUID-specific values, but only six bits in total are potentially
/// altered. /// altered.
pub fn as_uuid(&self) -> Uuid { pub const fn as_uuid(&self) -> Uuid {
let counter_mask = (1 << 12) - 1; let counter_mask = (1 << 12) - 1;
let entropy_mask = (1 << 62) - 1; let entropy_mask = (1 << 62) - 1;
let timestamp = self.timestamp(); let timestamp = self.timestamp();
@ -29,7 +29,9 @@ impl Julid {
/// ///
/// UUIDv7s are very similar to Julids, but use 12 bits for a monotonic /// 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. /// counter instead of 16, and only 62 bits of entropy vs Julids' 64.
/// Therefore, no bits need to be altered when converting to a Julid. /// 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> { pub fn from_uuid(id: Uuid) -> Result<Self, UuidError> {
let ver = id.get_version_num(); let ver = id.get_version_num();
if ver != 7 { if ver != 7 {
@ -40,11 +42,32 @@ impl Julid {
return Err(UuidError::UnsupportedVariant(var)); return Err(UuidError::UnsupportedVariant(var));
} }
Ok(id.as_u64_pair().into()) 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())
} }
} }
#[derive(Debug)] 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 { pub enum UuidError {
UnsupportedVersion(usize), UnsupportedVersion(usize),
UnsupportedVariant(uuid::Variant), UnsupportedVariant(uuid::Variant),
@ -64,7 +87,9 @@ impl fmt::Display for UuidError {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::Julid; use uuid::Uuid;
use crate::{uuid::UuidError, Julid};
#[test] #[test]
fn into_uuid() { fn into_uuid() {
@ -78,17 +103,24 @@ mod test {
#[test] #[test]
fn from_uuid() { fn from_uuid() {
let j1 = Julid::new(); let j1 = Julid::new();
let u1 = j1.as_uuid();
let ju1 = Julid::from_uuid(u1).unwrap();
// casting a julid to a uuid alters the counter and entropy bits slightly, so
// the original julid and one derived from a uuid made from it won't be the
// same.
assert_ne!(j1, ju1);
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 u2 = ju1.as_uuid();
let ju2 = Julid::from_uuid(u2).unwrap(); let ju2 = u2.try_into().unwrap();
// but once we've made that alteration, we've reached the fixed point
assert_eq!(ju1, ju2); assert_eq!(ju1, ju2);
assert_eq!(u1, u2); }
#[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)));
} }
} }