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]
name = "julid-rs"
version = "0.1.6"
version = "1.6.1"
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
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"] }

View file

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

View file

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

View file

@ -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<u128, DecodeError> {
pub(crate) const fn decode(encoded: &str) -> Result<u128, DecodeError> {
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<u128, DecodeError> {
// 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!(

View file

@ -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 <https://github.com/ahawker/ulid/issues/306#issuecomment-451850395>
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<Julid, DecodeError> {
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<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
///
/// # 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<Julid, DecodeError> {
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 {

View file

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

View file

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

View file

@ -17,7 +17,7 @@ impl sqlx::Type<sqlx::Sqlite> for Julid {
impl<'q> Encode<'q, Sqlite> for Julid {
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
args.push(SqliteArgumentValue::Blob(Cow::Owned(
self.to_bytes().to_vec(),
self.as_bytes().to_vec(),
)));
IsNull::No
}