Prepare for new release.

Adds copious docs and reorganizes the code a little bit. Ready for final release, hopefully.
This commit is contained in:
Joe Ardent 2023-07-27 14:00:26 -07:00
parent d12811dffd
commit b799a9955f
8 changed files with 273 additions and 192 deletions

View file

@ -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"] }

View file

@ -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.

View file

@ -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()
); );

View file

@ -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!(

View file

@ -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 {

View file

@ -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(())
}
} }

View file

@ -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 {

View file

@ -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
} }