Compare commits

...

16 commits

Author SHA1 Message Date
Joe Ardent
e333ea5263 tighten up and modularize deps 2025-07-03 17:48:06 -07:00
Joe Ardent
8bbad764c3 Update docs, tests, and improve conversion to UUIDs. 2025-06-26 17:46:09 -07:00
Joe Ardent
02b99ba010 even betterer uuid stuff 2025-06-25 23:24:34 -07:00
Joe Ardent
66621bb8c7 add tryfrom impls from uuid 2025-06-24 20:35:58 -07:00
Joe Ardent
dd6b28a039 get docs building 2025-06-22 18:29:28 -07:00
Joe Ardent
acb32e56a6 Bump the version of sqlx, update the readme. 2025-06-22 16:13:03 -07:00
Joe Ardent
eabcc26276 add support for converting to and from v7 uuids
Update version, update license, add tests for UUIDv7.
2025-06-22 15:58:34 -07:00
Joe Ardent
1e93d0b1e4 update repo 2024-04-09 15:55:30 -07:00
Joe Ardent
7dfe1ffb94 spiff the readme 2024-04-09 15:50:26 -07:00
Joe Ardent
e0150a2161 silence lints 2024-04-09 15:11:28 -07:00
Joe Ardent
a54d704300 add innocuous flag to all sqlite functions 2024-04-09 14:54:11 -07:00
Joe Ardent
705afc19e9 Add --decode option, deprecate from_string(), bump version for new release. 2024-01-14 13:25:22 -08:00
Joe Ardent
6a9e43feee try to decode julids as strings in case bytes fails 2023-12-28 17:39:08 -08:00
Joe Ardent
f2ade6d85e Make julid_string() return a new stringy Julid. 2023-12-28 17:19:38 -08:00
Joe Ardent
34f85a95a0 Add Divan benchmarking. 2023-10-07 13:51:46 -07:00
Joe Ardent
d9e68f057c update version, add way to get timestamp from input to julid-gen, chrono now mandatory 2023-09-03 13:16:04 -07:00
13 changed files with 359 additions and 171 deletions

View file

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

View file

@ -1,5 +1,13 @@
# The Chaos License (GLP) # Dual Licensed (combined terms are binding)
This software is governed under the combined terms of the following two licenses:
## The Chaos License (GLP)
This software is released under the terms of the Chaos License. In cases where the terms of the This software is released under the terms of the Chaos License. In cases where the terms of the
license are unclear, refer to the [Fuck Around and Find Out license are unclear, refer to the [Fuck Around and Find Out
License](https://git.sr.ht/~boringcactus/fafol/tree/master/LICENSE-v0.2.md). License](https://git.sr.ht/~boringcactus/fafol/tree/master/LICENSE-v0.2.md).
## The Butlerian Jihad License (DUN)
If you feed this code into an LLM, I will fuck you up.

117
README.md
View file

@ -20,6 +20,8 @@ sqlite> select datetime(julid_timestamp(julid_new()), 'auto');
2023-07-27 17:47:50 2023-07-27 17:47:50
sqlite> select julid_counter(julid_new()); sqlite> select julid_counter(julid_new());
0 0
sqlite> select julid_string();
01HM4WJ7T90001P8SN9898FBTN
``` ```
Crates.io: <https://crates.io/crates/julid-rs> Crates.io: <https://crates.io/crates/julid-rs>
@ -28,6 +30,9 @@ Docs.rs: <https://docs.rs/julid-rs/latest/julid/>
Blog post: <https://proclamations.nebcorp-hias.com/sundries/presenting-julids/> Blog post: <https://proclamations.nebcorp-hias.com/sundries/presenting-julids/>
As of June of 2025, they can also be converted to and from [version 7
UUIDs](https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7), though some precision in the
intra-millisecond counter is lost when going to a UUID, via the `uuid` optional feature.
## A slightly deeper look ## A slightly deeper look
@ -71,9 +76,8 @@ are always fresh, it's not possible to easily guess a valid Julid if you already
# How to use # How to use
The Julid crate can be used in two different ways: as a regular Rust library, declared in your Rust The Julid crate can be used in two different ways: as a regular Rust library, declared in your Rust
project's `Cargo.toml` file (say, by running `cargo add julid-rs`), and used as shown above. There's project's `Cargo.toml` file (say, by running `cargo add julid-rs`), and used as shown in the sample
a rudimentary [benchmark](https://gitlab.com/nebkor/julid/-/blob/main/examples/benchmark.rs) example commandline program (see below). But the primary use case for me was as a loadable
in the repo that shows off most of the Rust API. But the primary use case for me was as a loadable
SQLite extension. Both are covered in the [documentation](https://docs.rs/julid-rs/latest/julid/), SQLite extension. Both are covered in the [documentation](https://docs.rs/julid-rs/latest/julid/),
but let's go over them here, starting with the extension. but let's go over them here, starting with the extension.
@ -83,6 +87,8 @@ The extension, when loaded into SQLite, provides the following functions:
* `julid_new()`: create a new Julid and return it as a 16-byte * `julid_new()`: create a new Julid and return it as a 16-byte
[blob](https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes) [blob](https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes)
* `julid_string()`: create a new Julid and return it as a 26-character [base-32
Crockford](https://en.wikipedia.org/wiki/Base32)-encoded string
* `julid_seconds(julid)`: get the number seconds (as a 64-bit float) since the UNIX epoch that this * `julid_seconds(julid)`: get the number seconds (as a 64-bit float) since the UNIX epoch that this
julid was created (convenient for passing to the builtin `datetime()` function) julid was created (convenient for passing to the builtin `datetime()` function)
* `julid_counter(julid)`: show the value of this julid's monotonic counter * `julid_counter(julid)`: show the value of this julid's monotonic counter
@ -119,11 +125,11 @@ For a table created like:
create table if not exists watches ( create table if not exists watches (
id blob not null primary key default (julid_new()), id blob not null primary key default (julid_new()),
kind int not null, -- enum for movie or tv show or whatev kind int not null, -- enum for movie or tv show or whatev
title text not null, -- this has a secondary index title text not null,
length int, length int,
release_date int, release_date date,
added_by blob not null, added_by blob not null,
last_updated int not null default (unixepoch()), last_updated date not null default CURRENT_TIMESTAMP,
foreign key (added_by) references users (id) foreign key (added_by) references users (id)
); );
``` ```
@ -149,86 +155,53 @@ the C interface, which is inherently unsafe. If you are not building the plugin,
## Inside a Rust program ## Inside a Rust program
Of course, you can also use it outside of a database; the `Julid` type is publicly exported. There's Of course, you can also use it outside of a database; the `Julid` type is publicly exported. There's
a simple benchmark in the examples folder of the repo, the important parts of which look like: a simple commandline program in `src/bin/gen.rs`, and can be run like `cargo run --bin julid-gen`
(or you can `cargo install julid-rs` to get the `julid-gen` program installed on your computer),
which will generate and print one Julid. If you want to see its component pieces, grab the Julid
printed from it, and then run it with the `-d` flag:
``` rust ```
use julid::Julid; $ julid-gen 4
01HV2G2ATR000CJ2WESB7CVC19
fn main() { 01HV2G2ATR000K1AGQPKMX5H0M
/* snip some stuff */ 01HV2G2ATR001CM27S59BHZ25G
01HV2G2ATR001WPJ8BS7PZHE6A
let start = Instant::now(); $ julid-gen -d 01HV2G2ATR001WPJ8BS7PZHE6A
for _ in 0..num { Created at: 2024-04-09 22:36:11.992 UTC
v.push(Julid::new()); Monotonic counter: 3
} Random: 14648252224908081354
let end = Instant::now();
let dur = (end - start).as_micros();
for id in v.iter() {
eprintln!(
"{id}: created_at {}; counter: {}; sortable: {}",
id.created_at(),
id.counter(),
id.sortable()
);
}
println!("{num} Julids generated in {dur}us");
``` ```
If you were to run it on a computer like mine (AMD Ryzen 9 3900X, 12-core, 2.2-4.6 GHz), you might The help is useful:
see something like this:
``` text
$ cargo run --example=benchmark --release -- -n 30000 2> /dev/null
30000 Julids generated in 1240us
``` ```
That's about 24,000 IDs/millisecond; 24 *MILLION* per second!
The default optional Cargo features include implementations of traits for getting Julids into and
out of SQLite with [SQLx](https://github.com/launchbadge/sqlx), and for generally
serializing/deserializing with [Serde](https://serde.rs/), via the `sqlx` and `serde` features,
respectively. One final default optional 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(&self)` method to `Julid`.
Something to note: don't enable the `plugin` feature in your Cargo.toml if you're using this crate
inside your Rust application, especially if you're *also* loading it as an extension in SQLite in
your application. You'll get a long and confusing runtime panic due to there being multiple
entrypoints defined with the same name.
## On the command line
An even simpler program than the benchmark called `julid-gen` is available to install via cargo:
`cargo install julid-rs --no-default-features`
And then using it is as simple as,
``` text
$ julid-gen -h $ julid-gen -h
Generate and print Julids Generate, print, and parse Julids
Usage: julid-gen [NUM] Usage: julid-gen [OPTIONS] [NUM]
Arguments: Arguments:
[NUM] Number of Julids to generate [default: 1] [NUM] Number of Julids to generate [default: 1]
Options: Options:
-h, --help Print help -d, --decode <INPUT> Print the components of the given Julid
-V, --version Print version -a, --answer The answer to the meaning of Julid
-h, --help Print help
$ julid-gen -V, --version Print version
01H9DYRVDX0001X0RE5Y7XFGBC
$ julid-gen 5
01H9DYT48E000EK2EH7P67N8GG
01H9DYT48E000ZBKXVZ91HEZX4
01H9DYT48E0012VX89PYX4HDKP
01H9DYT48E001GE29AWCH1RDCM
01H9DYT48E0028CDHNVC59KKHQ
``` ```
The whole program is just 34 lines, so check it out.
The default optional Cargo features include implementations of traits for getting Julids into and
out of SQLite with [SQLx](https://github.com/launchbadge/sqlx), and for generally
serializing/deserializing with [Serde](https://serde.rs/), via the `sqlx` and `serde` features,
respectively.
Something to note: don't enable the `plugin` feature in your Cargo.toml if you're using this crate
inside your Rust application, especially if you're *also* loading it as an extension in SQLite in
your application. You'll get a long and confusing runtime panic due to there being multiple
entrypoints defined with the same name.
# Thanks # Thanks
This project wouldn't have happened without a lot of inspiration (and a little shameless stealing) This project wouldn't have happened without a lot of inspiration (and a little shameless stealing)

View file

@ -1 +1 @@
1.61803 1.61803398874989

16
benches/simple.rs Normal file
View file

@ -0,0 +1,16 @@
use divan::black_box;
use julid::Julid;
fn main() {
divan::main();
}
#[divan::bench]
fn jbench() {
for _ in 0..1_000_000 {
let x = black_box(Julid::new());
if x < 1u128.into() {
println!("that's weird");
}
}
}

View file

@ -1,37 +0,0 @@
use std::time::Instant;
use clap::Parser;
use julid::Julid;
#[derive(Debug, Parser)]
struct Cli {
#[clap(
long,
short,
help = "Number of Julids to generate",
default_value_t = 2_000
)]
pub num: usize,
}
fn main() {
let cli = Cli::parse();
let num = cli.num;
let mut v = Vec::with_capacity(num);
let start = Instant::now();
for _ in 0..num {
v.push(Julid::new());
}
let end = Instant::now();
let dur = (end - start).as_micros();
for id in v.iter() {
eprintln!(
"{id}: created_at {}; counter: {}; sortable: {}",
id.created_at(),
id.counter(),
id.sortable()
);
}
println!("{num} Julids generated in {dur}us");
}

View file

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

View file

@ -2,17 +2,50 @@ use clap::Parser;
use julid::Julid; use julid::Julid;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(author, version = "1.61803", about = "Generate and print Julids")] #[command(
author,
version = "1.618033988",
about = "Generate, print, and parse Julids"
)]
struct Cli { struct Cli {
#[clap(
help = "Print the components of the given Julid",
short = 'd',
long = "decode"
)]
pub input: Option<String>,
#[clap(help = "Number of Julids to generate", default_value_t = 1)] #[clap(help = "Number of Julids to generate", default_value_t = 1)]
pub num: usize, pub num: usize,
#[clap(
help = "The answer to the meaning of Julid",
default_value_t = false,
short,
long
)]
pub answer: bool,
} }
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let num = cli.num; if let Some(julid_string_input) = cli.input {
if let Ok(ts) = Julid::from_str(&julid_string_input) {
for _ in 0..num { println!("Created at:\t\t{}", ts.created_at());
println!("{}", Julid::new()); println!("Monotonic counter:\t{}", ts.counter());
println!("Random:\t\t\t{}", ts.random());
} else {
eprintln!("Could not parse input '{julid_string_input}' as a Julid");
std::process::exit(1);
}
} else {
// Just print some Julids
let num = cli.num;
for _ in 0..num {
let j = if cli.answer {
42u128.into()
} else {
Julid::new()
};
println!("{j}");
}
} }
} }

View file

@ -6,20 +6,11 @@ use std::{
use rand::{random, thread_rng, Rng}; use rand::{random, thread_rng, Rng};
use crate::{base32, DecodeError}; use crate::{base32, DecodeError, COUNTER_BITS, RANDOM_BITS, TIME_BITS, UNIQUE_BITS};
/// This is used to ensure monotonicity for new IDs. /// This is used to ensure monotonicity for new IDs.
static LAST_MSB: AtomicU64 = AtomicU64::new(0); static LAST_MSB: AtomicU64 = AtomicU64::new(0);
/// The number of bits in a Julid's time portion
const TIME_BITS: u8 = 48;
/// The number of bits in the monotonic counter for intra-millisecond IDs
const COUNTER_BITS: u8 = 16;
/// The number of random bits + bits in the monotonic counter
const UNIQUE_BITS: u8 = 80;
/// The number of fully random bits
const RANDOM_BITS: u8 = 64;
macro_rules! bitmask { macro_rules! bitmask {
($len:expr) => { ($len:expr) => {
((1 << $len) - 1) ((1 << $len) - 1)
@ -74,6 +65,14 @@ impl Julid {
} }
} }
/// Return a new Julid with the given timestamp (in milliseconds), no
/// counter bits set, and 64 random lower bits.
pub fn at(ts_ms: u64) -> Self {
let hi = ts_ms << COUNTER_BITS;
let lo = random();
(hi, lo).into()
}
/// 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
@ -140,7 +139,7 @@ impl Julid {
/// ```rust /// ```rust
/// use julid::julid::Julid; /// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ"; /// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let id = Julid::from_string(text).unwrap(); /// let id = Julid::from_str(text).unwrap();
/// ///
/// assert_eq!(&id.to_string(), text); /// assert_eq!(&id.to_string(), text);
/// ``` /// ```
@ -157,12 +156,12 @@ impl Julid {
/// ```rust /// ```rust
/// use julid::julid::Julid; /// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ"; /// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let result = Julid::from_string(text); /// let result = Julid::from_str(text);
/// ///
/// assert!(result.is_ok()); /// assert!(result.is_ok());
/// assert_eq!(&result.unwrap().to_string(), text); /// assert_eq!(&result.unwrap().to_string(), text);
/// ``` /// ```
pub const fn from_string(encoded: &str) -> Result<Julid, DecodeError> { pub const fn from_str(encoded: &str) -> Result<Self, DecodeError> {
match base32::decode(encoded) { match base32::decode(encoded) {
Ok(int_val) => Ok(Julid(int_val)), Ok(int_val) => Ok(Julid(int_val)),
Err(err) => Err(err), Err(err) => Err(err),
@ -175,7 +174,7 @@ impl Julid {
} }
/// 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]) -> Self {
Self(u128::from_be_bytes(bytes)) Self(u128::from_be_bytes(bytes))
} }
} }
@ -232,7 +231,7 @@ impl FromStr for Julid {
type Err = DecodeError; type Err = DecodeError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
Julid::from_string(s) Julid::from_str(s)
} }
} }
@ -249,7 +248,7 @@ mod tests {
#[test] #[test]
fn test_static() { fn test_static() {
let s = Julid(0x41414141414141414141414141414141).as_string(); let s = Julid(0x41414141414141414141414141414141).as_string();
let u = Julid::from_string(&s).unwrap(); let u = Julid::from_str(&s).unwrap();
assert_eq!(&s, "21850M2GA1850M2GA1850M2GA1"); assert_eq!(&s, "21850M2GA1850M2GA1850M2GA1");
assert_eq!(u.0, 0x41414141414141414141414141414141); assert_eq!(u.0, 0x41414141414141414141414141414141);
} }

View file

@ -2,26 +2,52 @@
use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines}; use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines};
mod base32; mod base32;
/// Contains the [`Julid`] type, which is publicly exported at the top level.
pub mod julid; pub mod julid;
#[cfg(feature = "serde")]
/// Serialization into bytes, and deserialization from a variety of formats, /// Serialization into bytes, and deserialization from a variety of formats,
/// with Serde (feature `serde` (default)) /// with Serde (feature `serde` (default))
#[cfg(feature = "serde")]
pub mod serde; pub mod serde;
#[cfg(feature = "sqlx")]
/// Traits from the SQLx crate for getting Julids into and out of SQLite /// Traits from the SQLx crate for getting Julids into and out of SQLite
/// databases from normal Rust applications. (feature `sqlx` (default)) /// databases from normal Rust applications. (feature `sqlx` (default))
#[cfg(feature = "sqlx")]
pub mod sqlx; pub mod sqlx;
/// UUIDv7s are almost as good as Julids, and can be interconverted almost
/// perfectly. (feature `uuid` (non-default))
///
/// See the [`Julid::as_uuid`] and [`Julid::from_uuid`] methods for
/// converting a Julid to a UUID and constructing a Julid from a UUID
/// respectively.
#[cfg(feature = "uuid")]
pub mod uuid;
#[doc(inline)] #[doc(inline)]
pub use base32::DecodeError; pub use base32::DecodeError;
#[doc(inline)] #[doc(inline)]
pub use julid::Julid; pub use julid::Julid;
/// The number of bits in a Julid's millisecond timestamp (48)
pub const TIME_BITS: u8 = 48;
/// The number of bits in the monotonic counter for intra-millisecond IDs (16)
pub const COUNTER_BITS: u8 = 16;
/// The number of random bits + bits in the monotonic counter (80)
pub const UNIQUE_BITS: u8 = 80;
/// The number of fully random bits (64)
pub const RANDOM_BITS: u8 = 64;
/// This `unsafe extern "C"` function is the main entry point into the loadable /// This `unsafe extern "C"` function is the main entry point into the loadable
/// SQLite extension. By default, it and the `plugin` module it depends on will /// SQLite extension. By default, it and the `plugin` module it depends on will
/// not be built. Build with `cargo build --features plugin` /// not be built. Build with `cargo build --features plugin`
///
/// # Safety
/// This is FFI; it's inherently unsafe. But this function is called by
/// sqlite, not by a user, so it should be OK.
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
#[no_mangle] #[unsafe(no_mangle)]
pub unsafe extern "C" fn sqlite3_julid_init( pub unsafe extern "C" fn sqlite3_julid_init(
db: *mut sqlite3, db: *mut sqlite3,
_pz_err_msg: *mut *mut c_char, _pz_err_msg: *mut *mut c_char,
@ -35,24 +61,30 @@ pub unsafe extern "C" fn sqlite3_julid_init(
} }
/// The code for the SQLite plugin is kept in this module, and exposed via the /// The code for the SQLite plugin is kept in this module, and exposed via the
/// `sqlite3_julid_init` function (feature `plugin`) /// [`sqlite3_julid_init`] function (feature `plugin` (non-default))
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub mod sqlite_plugin { pub mod sqlite_plugin {
use sqlite_loadable::{ use sqlite_loadable::{
api, define_scalar_function, api, define_scalar_function,
prelude::{sqlite3, sqlite3_context, sqlite3_value, FunctionFlags}, prelude::{sqlite3_context, sqlite3_value, FunctionFlags},
Result, Result,
}; };
use super::*; use super::*;
pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> { pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> {
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC; let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC | FunctionFlags::INNOCUOUS;
define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?; 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_seconds", 1, julid_seconds, flags)?;
define_scalar_function(db, "julid_counter", 1, julid_counter, 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_sortable", 1, julid_sortable, flags)?;
define_scalar_function(db, "julid_string", 1, julid_string, flags)?; define_scalar_function(
db,
"julid_string",
-1,
julid_string,
FunctionFlags::UTF8 | FunctionFlags::INNOCUOUS,
)?;
Ok(()) Ok(())
} }
@ -76,13 +108,17 @@ pub mod sqlite_plugin {
Ok(()) Ok(())
} }
/// Return the human-readable base32 Crockford encoding of this Julid. /// Return the human-readable base32 Crockford encoding of the given Julid,
/// or create a new one if no arguments.
///
/// ```text /// ```text
/// sqlite> select julid_string(julid_new()); /// sqlite> select julid_string(julid_new());
/// 01H6C7D9CT00009TF3EXXJHX4Y /// 01H6C7D9CT00009TF3EXXJHX4Y
/// sqlite> select julid_string();
/// 01HJSHZ0PN000EKP3H94R6TPWH
/// ``` /// ```
pub fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { pub fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.get(0) { if let Some(value) = id.first() {
let id = api::value_blob(value); let id = api::value_blob(value);
let bytes: [u8; 16] = id.try_into().map_err(|_| { let bytes: [u8; 16] = id.try_into().map_err(|_| {
sqlite_loadable::Error::new_message("Could not convert given value to Julid") sqlite_loadable::Error::new_message("Could not convert given value to Julid")
@ -90,9 +126,7 @@ pub mod sqlite_plugin {
let id: Julid = bytes.into(); let id: Julid = bytes.into();
api::result_text(context, id.as_string())?; api::result_text(context, id.as_string())?;
} else { } else {
return Err(sqlite_loadable::Error::new_message( api::result_text(context, Julid::new().as_string())?;
"Could not convert empty Julid to string",
));
} }
Ok(()) Ok(())
@ -108,7 +142,7 @@ pub mod sqlite_plugin {
/// 2023-07-27 17:47:50 /// 2023-07-27 17:47:50
/// ``` /// ```
pub fn julid_seconds(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { pub fn julid_seconds(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.get(0) { if let Some(value) = id.first() {
let id = api::value_blob(value); let id = api::value_blob(value);
let bytes: [u8; 16] = id.try_into().map_err(|_| { let bytes: [u8; 16] = id.try_into().map_err(|_| {
sqlite_loadable::Error::new_message("Could not convert given value to Julid") sqlite_loadable::Error::new_message("Could not convert given value to Julid")
@ -135,7 +169,7 @@ pub mod sqlite_plugin {
/// 0 /// 0
/// ``` /// ```
pub fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { pub fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.get(0) { if let Some(value) = id.first() {
let id = api::value_blob(value); let id = api::value_blob(value);
let bytes: [u8; 16] = id.try_into().map_err(|_| { let bytes: [u8; 16] = id.try_into().map_err(|_| {
sqlite_loadable::Error::new_message("Could not convert given value to Julid") sqlite_loadable::Error::new_message("Could not convert given value to Julid")
@ -159,7 +193,7 @@ pub mod sqlite_plugin {
/// 110787724287475712 /// 110787724287475712
/// ``` /// ```
pub fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { pub fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.get(0) { if let Some(value) = id.first() {
let id = api::value_blob(value); let id = api::value_blob(value);
let bytes: [u8; 16] = id.try_into().map_err(|_| { let bytes: [u8; 16] = id.try_into().map_err(|_| {
sqlite_loadable::Error::new_message("Could not convert given value to Julid") sqlite_loadable::Error::new_message("Could not convert given value to Julid")

View file

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

View file

@ -3,7 +3,7 @@ use std::borrow::Cow;
use sqlx::{ use sqlx::{
encode::IsNull, encode::IsNull,
sqlite::{SqliteArgumentValue, SqliteValueRef}, sqlite::{SqliteArgumentValue, SqliteValueRef},
Decode, Encode, Sqlite, Decode, Encode, Sqlite, Value, ValueRef,
}; };
use crate::Julid; use crate::Julid;
@ -15,18 +15,33 @@ impl sqlx::Type<sqlx::Sqlite> for Julid {
} }
impl<'q> Encode<'q, Sqlite> for Julid { impl<'q> Encode<'q, Sqlite> for Julid {
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull { fn encode_by_ref(
&self,
args: &mut Vec<SqliteArgumentValue<'q>>,
) -> std::result::Result<
sqlx::encode::IsNull,
std::boxed::Box<(dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static)>,
> {
args.push(SqliteArgumentValue::Blob(Cow::Owned( args.push(SqliteArgumentValue::Blob(Cow::Owned(
self.as_bytes().to_vec(), self.as_bytes().to_vec(),
))); )));
IsNull::No Ok(IsNull::No)
} }
} }
impl Decode<'_, Sqlite> for Julid { impl Decode<'_, Sqlite> for Julid {
fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> { fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?; let julid = match <&[u8] as Decode<Sqlite>>::decode(value.to_owned().as_ref()) {
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default(); Ok(bytes) => {
Ok(bytes.into()) let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
Julid::from_bytes(bytes)
}
_ => {
let string = <&str as Decode<Sqlite>>::decode(value)?;
Julid::from_str(string)?
}
};
Ok(julid)
} }
} }

126
src/uuid.rs Normal file
View file

@ -0,0 +1,126 @@
use std::fmt;
use uuid::{Uuid, Variant};
use crate::{Julid, COUNTER_BITS};
impl Julid {
/// Convert to UUIDv7, possibly losing counter bits and altering the top
/// two bits from the lower 64.
///
/// UUIDv7s are very similar to Julids, but use 12 bits for a monotonic
/// counter instead of 16, and only 62 bits of entropy vs Julids' 64. This
/// means that some bits in the original Julid are overwritten with
/// UUID-specific values, but only six bits in total are potentially
/// altered.
pub const fn as_uuid(&self) -> Uuid {
let counter_mask = (1 << 12) - 1;
let entropy_mask = (1 << 62) - 1;
let timestamp = self.timestamp();
// https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7 "ver" is 0b0111
let counter = (self.counter() & counter_mask) | (0b0111 << 12);
// https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7 "var" is 0b10
let entropy = (self.random() & entropy_mask) | (0b10 << 62);
let top = (timestamp << 16) | counter as u64;
Uuid::from_u64_pair(top, entropy)
}
/// Create from a UUIDv7; will fail if the UUID is not a valid v7 UUID.
///
/// UUIDv7s are very similar to Julids, but use 12 bits for a monotonic
/// counter instead of 16, and only 62 bits of entropy vs Julids' 64.
/// Therefore, no bits technically need to be altered when converting to a
/// Julid, but we zero out the high bits of the counter where the UUID
/// version was stored.
pub fn from_uuid(id: Uuid) -> Result<Self, UuidError> {
let ver = id.get_version_num();
if ver != 7 {
return Err(UuidError::UnsupportedVersion(ver));
}
let var = id.get_variant();
if var != Variant::RFC4122 {
return Err(UuidError::UnsupportedVariant(var));
}
let (hi, lo) = id.as_u64_pair();
// zero out the high bits of the counter, which are "7" (0b0111) from the uuid
let mask = (1 << 12) - 1;
let counter = hi & mask;
let ts = hi >> COUNTER_BITS;
let hi = (ts << COUNTER_BITS) | counter;
Ok((hi, lo).into())
}
}
impl From<Julid> for Uuid {
fn from(value: Julid) -> Self {
value.as_uuid()
}
}
impl TryFrom<Uuid> for Julid {
type Error = UuidError;
fn try_from(value: Uuid) -> Result<Self, Self::Error> {
Julid::from_uuid(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UuidError {
UnsupportedVersion(usize),
UnsupportedVariant(uuid::Variant),
}
impl std::error::Error for UuidError {}
impl fmt::Display for UuidError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let text = match *self {
UuidError::UnsupportedVersion(v) => format!("unsupported version {v}"),
UuidError::UnsupportedVariant(v) => format!("unsupported variant: {v:?}"),
};
write!(f, "{text}")
}
}
#[cfg(test)]
mod test {
use uuid::Uuid;
use crate::{uuid::UuidError, Julid};
#[test]
fn into_uuid() {
// see example from https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html#method.new_v7
let ts = 1497624119 * 1000;
let j = Julid::at(ts);
let u = j.as_uuid().hyphenated().to_string();
assert!(u.starts_with("015cb15a-86d8-7"));
}
#[test]
fn from_uuid() {
let j1 = Julid::new();
let u1: Uuid = j1.into();
let ju1: Julid = u1.try_into().unwrap();
assert_eq!(j1.timestamp(), ju1.timestamp());
assert_eq!(j1.counter(), ju1.counter());
assert_eq!(j1.random() << 2, ju1.random() << 2);
assert_eq!(ju1.random() >> 62, 2);
// once we've converted to uuid and then back to julid, we've reached the fixed
// point
let u2 = ju1.as_uuid();
let ju2 = u2.try_into().unwrap();
assert_eq!(ju1, ju2);
}
#[test]
fn cant_even_from_uuid_non_v7() {
let u = uuid::Uuid::new_v4();
let jr: Result<Julid, UuidError> = u.try_into();
assert_eq!(jr, Err(UuidError::UnsupportedVersion(4)));
}
}