From b799a9955f886c0df8370838fc5a8f40edb3efea Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Thu, 27 Jul 2023 14:00:26 -0700 Subject: [PATCH] Prepare for new release. Adds copious docs and reorganizes the code a little bit. Ready for final release, hopefully. --- Cargo.toml | 13 +- README.md | 69 ++++---- src/main.rs => examples/benchmark.rs | 4 +- src/base32.rs | 34 ++-- src/julid.rs | 97 +++++------ src/lib.rs | 233 ++++++++++++++++----------- src/serde.rs | 13 +- src/sqlx.rs | 2 +- 8 files changed, 273 insertions(+), 192 deletions(-) rename src/main.rs => examples/benchmark.rs (82%) diff --git a/Cargo.toml b/Cargo.toml index fe85318..62407df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "julid-rs" -version = "0.1.6" +version = "1.6.1" authors = ["Joe Ardent "] edition = "2021" keywords = ["ulid", "library", "sqlite", "extension", "julid"] @@ -11,8 +11,12 @@ license-file = "LICENSE.md" repository = "https://gitlab.com/nebkor/julid" [features] -default = ["serde", "sqlx"] -clib = [] +default = ["chrono", "serde", "sqlx"] # just the regular crate + +chrono = ["dep:chrono"] +plugin = ["sqlite-loadable"] # builds libjulid.* for loading into sqlite +serde = ["dep:serde"] +sqlx = ["dep:sqlx"] [lib] name = "julid" @@ -22,4 +26,5 @@ crate-type = ["cdylib", "rlib"] rand = "0.8" serde = { version = "1.0", features = ["derive"], 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"] } diff --git a/README.md b/README.md index 499fbd9..be20691 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# quick take -Globally unique sortable identifiers for SQLite! +# 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 @@ -9,67 +11,71 @@ Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database. sqlite> .load ./libjulid sqlite> select hex(julid_new()); -01898F1332B90000D6F0F0FE1066A6BF +018998768ACF000060B31DB175E0C5F9 sqlite> select julid_string(julid_new()); -01H67GV14N000BBHJD6FARVB7Q -sqlite> select datetime(julid_timestamp(julid_new()) / 1000, 'auto'); -- sqlite wants seconds, not milliseconds -2023-07-25 21:58:56 +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 ``` -## 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, -but not all ULIDs are Julids) identifiers with the following properties: +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 + * 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 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) 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 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 big-endian bit structure: +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 -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 is not possible to guess a valid Julid if you already know one. +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. -# 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_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 * `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 -# how to use +## Building and loading If you want to use it as a SQLite extension: - * clone the repo - * build it with `cargo build --features clib` (this builds the 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 @@ -78,14 +84,14 @@ 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(), + id blob not null primary key default (julid_new()), ... ); ``` 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 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. diff --git a/src/main.rs b/examples/benchmark.rs similarity index 82% rename from src/main.rs rename to examples/benchmark.rs index 00f00f9..cc44a03 100644 --- a/src/main.rs +++ b/examples/benchmark.rs @@ -13,8 +13,8 @@ fn main() { for id in v.iter() { println!( - "{id}: {}ms and {} incs; sortable: {}", - id.timestamp(), + "{id}: created_at {}; counter: {}; sortable: {}", + id.created_at(), id.counter(), id.sortable() ); diff --git a/src/base32.rs b/src/base32.rs index a875efb..a239c6a 100644 --- a/src/base32.rs +++ b/src/base32.rs @@ -1,7 +1,7 @@ use core::fmt; /// 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"; @@ -37,17 +37,18 @@ lookup[(c + 32) as usize] = i as u8; */ /// Encode the given 128-bit little-endian number as a base32 string. -pub fn encode(mut value: u128) -> String { - let mut buffer: [u8; JULID_LEN] = [0; JULID_LEN]; - for i in 0..JULID_LEN { - buffer[JULID_LEN - 1 - i] = ALPHABET[(value & 0x1f) as usize]; +pub(crate) fn encode(mut value: u128) -> String { + let mut buffer: [u8; JULID_STR_LEN] = [0; JULID_STR_LEN]; + for i in 0..JULID_STR_LEN { + buffer[JULID_STR_LEN - 1 - i] = ALPHABET[(value & 0x1f) as usize]; value >>= 5; } 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)] pub enum DecodeError { /// 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 { +pub(crate) const fn decode(encoded: &str) -> Result { let len = encoded.len(); - if len != JULID_LEN { + if len != JULID_STR_LEN { return Err(DecodeError::InvalidLength(len)); } @@ -80,7 +81,7 @@ pub const fn decode(encoded: &str) -> Result { // Manual for loop because Range::iter() isn't const let mut i = 0; - while i < JULID_LEN { + while i < JULID_STR_LEN { let val = LOOKUP[bytes[i] as usize]; if val != NO_VALUE { value = (value << 5) | val as u128; @@ -113,9 +114,18 @@ mod tests { #[test] fn test_length() { - assert_eq!(encode(0xffffffffffffffffffffffffffffffff).len(), JULID_LEN); - assert_eq!(encode(0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f).len(), JULID_LEN); - assert_eq!(encode(0x00000000000000000000000000000000).len(), JULID_LEN); + assert_eq!( + encode(0xffffffffffffffffffffffffffffffff).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!( diff --git a/src/julid.rs b/src/julid.rs index 66255eb..79df358 100644 --- a/src/julid.rs +++ b/src/julid.rs @@ -1,24 +1,24 @@ use core::{fmt, str::FromStr}; use std::{ sync::atomic::{AtomicU64, Ordering}, - time::Duration, + time::{Duration, SystemTime}, }; use rand::{random, thread_rng, Rng}; -use crate::base32::{self, DecodeError}; +use crate::{base32, DecodeError}; /// 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 -pub const TIME_BITS: u8 = 48; +const TIME_BITS: u8 = 48; /// 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 -pub const UNIQUE_BITS: u8 = 80; +const UNIQUE_BITS: u8 = 80; /// The number of fully random bits -pub const RANDOM_BITS: u8 = 64; +const RANDOM_BITS: u8 = 64; macro_rules! bitmask { ($len:expr) => { @@ -37,26 +37,26 @@ macro_rules! bitmask { /// the same millisecond. The remaining 64 least-significant bits are fully /// random. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy)] -pub struct Julid(pub u128); +pub struct Julid(pub(crate) u128); impl Julid { /// Return a new Julid. If a previous ID was generated in the same /// millisecond, increment the monotonic counter, up to u16::MAX. The random /// bits are always fresh, so once the monotonic counter is saturated, /// 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 pub fn new() -> Self { let lsb: u64 = random(); loop { - let ts = std::time::SystemTime::now() + let ts = SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or(Duration::ZERO) .as_millis() as u64; - let last = LAST_SORTABLE.load(Ordering::SeqCst); + let last = LAST_MSB.load(Ordering::SeqCst); let ots = last >> COUNTER_BITS; if ots < ts { let msb = ts << COUNTER_BITS; - if LAST_SORTABLE + if LAST_MSB .compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed) .is_ok() { @@ -65,7 +65,7 @@ impl Julid { } else { let counter = ((last & bitmask!(COUNTER_BITS) as u64) as u16).saturating_add(1); let msb = (ots << COUNTER_BITS) + counter as u64; - if LAST_SORTABLE + if LAST_MSB .compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed) .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 { - match base32::decode(encoded) { - Ok(int_val) => Ok(Julid(int_val)), - Err(err) => Err(err), - } - } - /// The 'Alpha Julid'. /// /// 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 } + #[cfg(feature = "chrono")] + /// Returns the timestamp as a `chrono::DateTime` (feature + /// `chrono` (default)) + pub fn created_at(&self) -> chrono::DateTime { + (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 /// /// # Example @@ -156,25 +152,36 @@ impl Julid { base32::encode(self.0) } - /// Test if the Julid is Alpha - pub const fn is_alpha(&self) -> bool { - self.0 == 0u128 + /// Creates a Julid from a Crockford Base32 encoded string + /// + /// 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 { + match base32::decode(encoded) { + Ok(int_val) => Ok(Julid(int_val)), + Err(err) => Err(err), + } } - /// Test if the Julid is Omega - pub const fn is_omega(&self) -> bool { - self.0 == u128::MAX + /// Returns the bytes of the Julid in big-endian order. + pub const fn as_bytes(self) -> [u8; 16] { + self.0.to_be_bytes() } /// Creates a Julid using the provided bytes array, assumed big-endian. pub const fn from_bytes(bytes: [u8; 16]) -> Julid { 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 { diff --git a/src/lib.rs b/src/lib.rs index 939ad18..cfcf79f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,28 +1,26 @@ -#[cfg(feature = "clib")] -use sqlite_loadable::{ - api, define_scalar_function, - prelude::{ - c_char, c_uint, sqlite3, sqlite3_api_routines, sqlite3_context, sqlite3_value, - FunctionFlags, - }, - Result, -}; +#[cfg(feature = "plugin")] +use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines}; mod base32; pub mod julid; #[cfg(feature = "serde")] +/// Serialization into bytes, and deserialization from a variety of formats, +/// with Serde (feature `serde` (default)) 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)) pub mod sqlx; -pub use base32::JULID_LEN; +#[doc(inline)] +pub use base32::DecodeError; +#[doc(inline)] pub use julid::Julid; -//-************************************************************************ -// Entrypoint into the loadable extension -//-************************************************************************ -#[cfg(feature = "clib")] +/// 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` +#[cfg(feature = "plugin")] #[no_mangle] pub unsafe extern "C" fn sqlite3_julid_init( db: *mut sqlite3, @@ -30,101 +28,150 @@ pub unsafe extern "C" fn sqlite3_julid_init( p_api: *mut sqlite3_api_routines, ) -> c_uint { 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 Err(err) => err.code_extended(), } } -#[cfg(feature = "clib")] -fn init_rs(db: *mut sqlite3) -> Result<()> { - let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC; - define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?; - define_scalar_function(db, "julid_timestamp", 1, julid_timestamp, 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, flags)?; +/// The code for the SQLite plugin is kept in this module, and exposed via the +/// `sqlite3_julid_init` function (feature `plugin`) +#[cfg(feature = "plugin")] +pub mod sqlite_plugin { + use sqlite_loadable::{ + api, define_scalar_function, + prelude::{sqlite3, sqlite3_context, sqlite3_value, FunctionFlags}, + Result, + }; - Ok(()) -} + use super::*; -//-************************************************************************ -// impls -//-************************************************************************ -#[cfg(feature = "clib")] -fn julid_new(context: *mut sqlite3_context, _vals: &[*mut sqlite3_value]) -> Result<()> { - api::result_blob(context, Julid::new().to_bytes().as_slice()); - Ok(()) -} + pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> { + 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, flags)?; -#[cfg(feature = "clib")] -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(()) } - Ok(()) -} + //-************************************************************************ + // impls + //-************************************************************************ -#[cfg(feature = "clib")] -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", - )); + /// Create a new `Julid` and return it as a `blob`. Because the bytes inside + /// a `Julid` are not valid UTF8, if you wish to see a human-readable + /// representation, use the built-in `hex()` function, or `julid_string()`. + /// + /// ```text + /// sqlite> select hex(julid_new()); + /// 018998768ACF000060B31DB175E0C5F9 + /// sqlite> select julid_string(julid_new()); + /// 01H6C7D9CT00009TF3EXXJHX4Y + /// ``` + pub fn julid_new(context: *mut sqlite3_context, _vals: &[*mut sqlite3_value]) -> Result<()> { + 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")] -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(()) } - 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")] -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(()) } - 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(()) + } } diff --git a/src/serde.rs b/src/serde.rs index dd0c5ac..ebf7cc7 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -1,8 +1,7 @@ -//! Serialization and deserialization. -//! -//! By default, serialization and deserialization go through Julid's big-endian -//! bytes representation. - +/// Serialization and deserialization. +/// +/// By default, serialization and deserialization go through Julid's big-endian +/// bytes representation. use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; use crate::Julid; @@ -12,7 +11,7 @@ impl Serialize for Julid { where 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 /// ``` /// # use julid::Julid; -/// # use julid::serde::ulid_as_str; +/// # use julid::serde::julid_as_str; /// # use serde::{Serialize, Deserialize}; /// #[derive(Serialize, Deserialize)] /// struct StrExample { diff --git a/src/sqlx.rs b/src/sqlx.rs index 524ee89..3e70ce4 100644 --- a/src/sqlx.rs +++ b/src/sqlx.rs @@ -17,7 +17,7 @@ impl sqlx::Type for Julid { impl<'q> Encode<'q, Sqlite> for Julid { fn encode_by_ref(&self, args: &mut Vec>) -> IsNull { args.push(SqliteArgumentValue::Blob(Cow::Owned( - self.to_bytes().to_vec(), + self.as_bytes().to_vec(), ))); IsNull::No }