Compare commits

..

No commits in common. "7dfe1ffb9485980ce648db2de547cee5b250502d" and "7067721f725b98830d8d530fff68b67cf268e6c6" have entirely different histories.

10 changed files with 123 additions and 190 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "julid-rs"
version = "1.6.18033988"
version = "1.6.180"
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
edition = "2021"
keywords = ["ulid", "library", "sqlite", "extension", "julid"]
@ -11,7 +11,8 @@ license-file = "LICENSE.md"
repository = "https://gitlab.com/nebkor/julid"
[features]
default = ["serde", "sqlx"] # just the regular crate
default = ["chrono", "serde", "sqlx"] # just the regular crate
chrono = ["dep:chrono"]
serde = ["dep:serde"]
sqlx = ["dep:sqlx"]
@ -25,26 +26,14 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
rand = "0.8"
# for the CLI
clap = { version = "4.3", default-features = false, features = ["help", "usage", "std", "derive"] }
chrono = { version = "0.4", default-features = false, features = ["std", "time"] }
# all other deps are optional
chrono = { version = "0.4", optional = true, default-features = false, features = ["std", "time"] }
serde = { version = "1.0", features = ["derive"], optional = true }
sqlx = { version = "0.7", features = ["sqlite"], default-features = false, optional = true }
sqlite-loadable = { version = "0.0.5", optional = true }
[dev-dependencies]
divan = "0.1"
[[bench]]
name = "simple"
harness = false
[[bin]]
name = "julid-gen"
path = "src/bin/gen.rs"
clap = { version = "4.3", default-features = false, features = ["help", "usage", "std", "derive"] }
[package.metadata.docs.rs]
all-features = true

View file

@ -20,8 +20,6 @@ sqlite> select datetime(julid_timestamp(julid_new()), 'auto');
2023-07-27 17:47:50
sqlite> select julid_counter(julid_new());
0
sqlite> select julid_string();
01HM4WJ7T90001P8SN9898FBTN
```
Crates.io: <https://crates.io/crates/julid-rs>
@ -73,8 +71,9 @@ are always fresh, it's not possible to easily guess a valid Julid if you already
# How to use
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 in the sample
commandline program (see below). But the primary use case for me was as a loadable
project's `Cargo.toml` file (say, by running `cargo add julid-rs`), and used as shown above. There's
a rudimentary [benchmark](https://gitlab.com/nebkor/julid/-/blob/main/examples/benchmark.rs) example
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/),
but let's go over them here, starting with the extension.
@ -84,8 +83,6 @@ The extension, when loaded into SQLite, provides the following functions:
* `julid_new()`: create a new Julid and return it as a 16-byte
[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 was created (convenient for passing to the builtin `datetime()` function)
* `julid_counter(julid)`: show the value of this julid's monotonic counter
@ -122,11 +119,11 @@ For a table created like:
create table if not exists watches (
id blob not null primary key default (julid_new()),
kind int not null, -- enum for movie or tv show or whatev
title text not null,
title text not null, -- this has a secondary index
length int,
release_date date,
release_date int,
added_by blob not null,
last_updated date not null default CURRENT_TIMESTAMP,
last_updated int not null default (unixepoch()),
foreign key (added_by) references users (id)
);
```
@ -143,62 +140,63 @@ where the wildcards get bound in a loop with unique values and the Julid `id` fi
generated by the extension for each row, I get over 100,000 insertions/second when using a
file-backed DB in WAL mode and `NORMAL` durability settings.
### Safety
There is one `unsafe fn` in this project, `sqlite_julid_init()`, and it is only built for the
`plugin` feature. The reason for it is that it's interacting with foreign code (SQLite itself) via
the C interface, which is inherently unsafe. If you are not building the plugin, there is no
`unsafe` code.
## Inside a Rust program
Of course, you can also use it outside of a database; the `Julid` type is publicly exported. There's
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:
a simple benchmark in the examples folder of the repo, the important parts of which look like:
```
$ julid-gen 4
01HV2G2ATR000CJ2WESB7CVC19
01HV2G2ATR000K1AGQPKMX5H0M
01HV2G2ATR001CM27S59BHZ25G
01HV2G2ATR001WPJ8BS7PZHE6A
$ julid-gen -d 01HV2G2ATR001WPJ8BS7PZHE6A
Created at: 2024-04-09 22:36:11.992 UTC
Monotonic counter: 3
Random: 14648252224908081354
``` rust
use julid::Julid;
fn main() {
/* snip some stuff */
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");
```
The help is useful:
If you were to run it on a computer like mine (AMD Ryzen 9 3900X, 12-core, 2.2-4.6 GHz), you might
see something like this:
```
$ julid-gen -h
Generate, print, and parse Julids
Usage: julid-gen [OPTIONS] [NUM]
Arguments:
[NUM] Number of Julids to generate [default: 1]
Options:
-d, --decode <INPUT> Print the components of the given Julid
-a, --answer The answer to the meaning of Julid
-h, --help Print help
-V, --version Print version
``` text
$ cargo run --example=benchmark --release -- -n 30000 2> /dev/null
30000 Julids generated in 1240us
```
The whole program is just 34 lines, so check it out.
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.
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.
### Safety
There is one `unsafe fn` in this project, `sqlite_julid_init()`, and it is only built for the
`plugin` feature. The reason for it is that it's interacting with foreign code (SQLite itself) via
the C interface, which is inherently unsafe. If you are not building the plugin, there is no
`unsafe` code.
# Thanks
This project wouldn't have happened without a lot of inspiration (and a little shameless stealing)

View file

@ -1 +1 @@
1.618033988
1.6180

View file

@ -1,16 +0,0 @@
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");
}
}
}

37
examples/benchmark.rs Normal file
View file

@ -0,0 +1,37 @@
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

@ -1,51 +0,0 @@
use clap::Parser;
use julid::Julid;
#[derive(Debug, Parser)]
#[command(
author,
version = "1.618033988",
about = "Generate, print, and parse Julids"
)]
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)]
pub num: usize,
#[clap(
help = "The answer to the meaning of Julid",
default_value_t = false,
short,
long
)]
pub answer: bool,
}
fn main() {
let cli = Cli::parse();
if let Some(ts) = cli.input {
if let Ok(ts) = Julid::from_str(&ts) {
println!("Created at:\t\t{}", ts.created_at());
println!("Monotonic counter:\t{}", ts.counter());
println!("Random:\t\t\t{}", ts.random());
} else {
eprintln!("Could not parse input '{}' as a Julid", ts);
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

@ -54,20 +54,24 @@ impl Julid {
.as_millis() as u64;
let last = LAST_MSB.load(Ordering::SeqCst);
let ots = last >> COUNTER_BITS;
let msb = if ots < ts {
ts << COUNTER_BITS
if ots < ts {
let msb = ts << COUNTER_BITS;
if LAST_MSB
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
{
break (msb, lsb).into();
}
} else {
let counter = ((last & bitmask!(COUNTER_BITS) as u64) as u16).saturating_add(1);
(ots << COUNTER_BITS) + counter as u64
};
if LAST_MSB
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
{
break (msb, lsb).into();
let msb = (ots << COUNTER_BITS) + counter as u64;
if LAST_MSB
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
.is_ok()
{
break (msb, lsb).into();
}
}
// we didn't update the global counter, try again
let micros = thread_rng().gen_range(10..50);
std::thread::sleep(Duration::from_micros(micros));
@ -117,6 +121,7 @@ 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> {
@ -139,7 +144,7 @@ impl Julid {
/// ```rust
/// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let id = Julid::from_str(text).unwrap();
/// let id = Julid::from_string(text).unwrap();
///
/// assert_eq!(&id.to_string(), text);
/// ```
@ -156,19 +161,11 @@ impl Julid {
/// ```rust
/// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let result = Julid::from_str(text);
/// let result = Julid::from_string(text);
///
/// assert!(result.is_ok());
/// assert_eq!(&result.unwrap().to_string(), text);
/// ```
pub const fn from_str(encoded: &str) -> Result<Julid, DecodeError> {
match base32::decode(encoded) {
Ok(int_val) => Ok(Julid(int_val)),
Err(err) => Err(err),
}
}
#[deprecated(since = "1.6.1803398", note = "use `from_str` instead")]
pub const fn from_string(encoded: &str) -> Result<Julid, DecodeError> {
match base32::decode(encoded) {
Ok(int_val) => Ok(Julid(int_val)),
@ -239,7 +236,7 @@ impl FromStr for Julid {
type Err = DecodeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Julid::from_str(s)
Julid::from_string(s)
}
}
@ -256,7 +253,7 @@ mod tests {
#[test]
fn test_static() {
let s = Julid(0x41414141414141414141414141414141).as_string();
let u = Julid::from_str(&s).unwrap();
let u = Julid::from_string(&s).unwrap();
assert_eq!(&s, "21850M2GA1850M2GA1850M2GA1");
assert_eq!(u.0, 0x41414141414141414141414141414141);
}

View file

@ -20,10 +20,6 @@ pub use julid::Julid;
/// 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`
///
/// # 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")]
#[no_mangle]
pub unsafe extern "C" fn sqlite3_julid_init(
@ -44,25 +40,19 @@ pub unsafe extern "C" fn sqlite3_julid_init(
pub mod sqlite_plugin {
use sqlite_loadable::{
api, define_scalar_function,
prelude::{sqlite3_context, sqlite3_value, FunctionFlags},
prelude::{sqlite3, sqlite3_context, sqlite3_value, FunctionFlags},
Result,
};
use super::*;
pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> {
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC | FunctionFlags::INNOCUOUS;
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,
FunctionFlags::UTF8 | FunctionFlags::INNOCUOUS,
)?;
define_scalar_function(db, "julid_string", 1, julid_string, flags)?;
Ok(())
}
@ -86,17 +76,13 @@ pub mod sqlite_plugin {
Ok(())
}
/// Return the human-readable base32 Crockford encoding of the given Julid,
/// or create a new one if no arguments.
///
/// Return the human-readable base32 Crockford encoding of this Julid.
/// ```text
/// sqlite> select julid_string(julid_new());
/// 01H6C7D9CT00009TF3EXXJHX4Y
/// sqlite> select julid_string();
/// 01HJSHZ0PN000EKP3H94R6TPWH
/// ```
pub fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")
@ -104,7 +90,9 @@ pub mod sqlite_plugin {
let id: Julid = bytes.into();
api::result_text(context, id.as_string())?;
} else {
api::result_text(context, Julid::new().as_string())?;
return Err(sqlite_loadable::Error::new_message(
"Could not convert empty Julid to string",
));
}
Ok(())
@ -120,7 +108,7 @@ pub mod sqlite_plugin {
/// 2023-07-27 17:47:50
/// ```
pub fn julid_seconds(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")
@ -147,7 +135,7 @@ pub mod sqlite_plugin {
/// 0
/// ```
pub fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")
@ -171,7 +159,7 @@ pub mod sqlite_plugin {
/// 110787724287475712
/// ```
pub fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
if let Some(value) = id.first() {
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")

View file

@ -114,6 +114,6 @@ pub mod julid_as_str {
D: Deserializer<'de>,
{
let deserialized_str = String::deserialize(deserializer)?;
Julid::from_str(&deserialized_str).map_err(serde::de::Error::custom)
Julid::from_string(&deserialized_str).map_err(serde::de::Error::custom)
}
}

View file

@ -3,7 +3,7 @@ use std::borrow::Cow;
use sqlx::{
encode::IsNull,
sqlite::{SqliteArgumentValue, SqliteValueRef},
Decode, Encode, Sqlite, Value, ValueRef,
Decode, Encode, Sqlite,
};
use crate::Julid;
@ -25,17 +25,8 @@ impl<'q> Encode<'q, Sqlite> for Julid {
impl Decode<'_, Sqlite> for Julid {
fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
let julid = match <&[u8] as Decode<Sqlite>>::decode(value.to_owned().as_ref()) {
Ok(bytes) => {
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)
let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?;
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
Ok(bytes.into())
}
}