Prepare for new release.
Adds copious docs and reorganizes the code a little bit. Ready for final release, hopefully.
This commit is contained in:
parent
d12811dffd
commit
b799a9955f
8 changed files with 273 additions and 192 deletions
13
Cargo.toml
13
Cargo.toml
|
@ -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"] }
|
||||
|
|
69
README.md
69
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.
|
||||
|
|
|
@ -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()
|
||||
);
|
|
@ -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!(
|
||||
|
|
97
src/julid.rs
97
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 <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 {
|
||||
|
|
233
src/lib.rs
233
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(())
|
||||
}
|
||||
}
|
||||
|
|
13
src/serde.rs
13
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue