Compare commits

...

10 commits

Author SHA1 Message Date
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
Joe Ardent
f4ac603ac8 add cli util, bump version to publish to crates 2023-09-03 09:44:35 -07:00
Joe Ardent
50a59e1898 clean up new() logic 2023-08-05 12:16:23 -07:00
10 changed files with 203 additions and 136 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "julid-rs"
version = "1.6.180"
version = "1.6.18033988"
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
edition = "2021"
keywords = ["ulid", "library", "sqlite", "extension", "julid"]
@ -11,8 +11,7 @@ license-file = "LICENSE.md"
repository = "https://gitlab.com/nebkor/julid"
[features]
default = ["chrono", "serde", "sqlx"] # just the regular crate
chrono = ["dep:chrono"]
default = ["serde", "sqlx"] # just the regular crate
serde = ["dep:serde"]
sqlx = ["dep:sqlx"]
@ -26,14 +25,26 @@ 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]
clap = { version = "4.3", default-features = false, features = ["help", "usage", "std", "derive"] }
divan = "0.1"
[[bench]]
name = "simple"
harness = false
[[bin]]
name = "julid-gen"
path = "src/bin/gen.rs"
[package.metadata.docs.rs]
all-features = true

116
README.md
View file

@ -20,6 +20,8 @@ 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>
@ -71,9 +73,8 @@ 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 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
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
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.
@ -83,6 +84,8 @@ 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
@ -119,11 +122,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, -- this has a secondary index
title text not null,
length int,
release_date int,
release_date date,
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)
);
```
@ -140,63 +143,62 @@ 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.
## 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 benchmark in the examples folder of the repo, the important parts of which look like:
``` 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");
```
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:
``` 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.
### 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:
```
$ 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
```
The help is useful:
```
$ 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
```
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
This project wouldn't have happened without a lot of inspiration (and a little shameless stealing)

View file

@ -1 +1 @@
1.6180
1.618033988

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");
}

51
src/bin/gen.rs Normal file
View file

@ -0,0 +1,51 @@
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,24 +54,20 @@ impl Julid {
.as_millis() as u64;
let last = LAST_MSB.load(Ordering::SeqCst);
let ots = last >> 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();
}
let msb = if ots < ts {
ts << COUNTER_BITS
} else {
let counter = ((last & bitmask!(COUNTER_BITS) as u64) as u16).saturating_add(1);
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();
}
(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));
@ -121,7 +117,6 @@ 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> {
@ -144,7 +139,7 @@ impl Julid {
/// ```rust
/// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let id = Julid::from_string(text).unwrap();
/// let id = Julid::from_str(text).unwrap();
///
/// assert_eq!(&id.to_string(), text);
/// ```
@ -161,11 +156,19 @@ impl Julid {
/// ```rust
/// use julid::julid::Julid;
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
/// let result = Julid::from_string(text);
/// let result = Julid::from_str(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)),
@ -236,7 +239,7 @@ impl FromStr for Julid {
type Err = DecodeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Julid::from_string(s)
Julid::from_str(s)
}
}
@ -253,7 +256,7 @@ mod tests {
#[test]
fn test_static() {
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!(u.0, 0x41414141414141414141414141414141);
}

View file

@ -20,6 +20,10 @@ 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(
@ -40,19 +44,25 @@ pub unsafe extern "C" fn sqlite3_julid_init(
pub mod sqlite_plugin {
use sqlite_loadable::{
api, define_scalar_function,
prelude::{sqlite3, sqlite3_context, sqlite3_value, FunctionFlags},
prelude::{sqlite3_context, sqlite3_value, FunctionFlags},
Result,
};
use super::*;
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_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)?;
define_scalar_function(
db,
"julid_string",
-1,
julid_string,
FunctionFlags::UTF8 | FunctionFlags::INNOCUOUS,
)?;
Ok(())
}
@ -76,13 +86,17 @@ pub mod sqlite_plugin {
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
/// 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.get(0) {
if let Some(value) = id.first() {
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")
@ -90,9 +104,7 @@ pub mod sqlite_plugin {
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",
));
api::result_text(context, Julid::new().as_string())?;
}
Ok(())
@ -108,7 +120,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.get(0) {
if let Some(value) = id.first() {
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")
@ -135,7 +147,7 @@ pub mod sqlite_plugin {
/// 0
/// ```
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 bytes: [u8; 16] = id.try_into().map_err(|_| {
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
@ -159,7 +171,7 @@ pub mod sqlite_plugin {
/// 110787724287475712
/// ```
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 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_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::{
encode::IsNull,
sqlite::{SqliteArgumentValue, SqliteValueRef},
Decode, Encode, Sqlite,
Decode, Encode, Sqlite, Value, ValueRef,
};
use crate::Julid;
@ -25,8 +25,17 @@ impl<'q> Encode<'q, Sqlite> for Julid {
impl Decode<'_, Sqlite> for Julid {
fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?;
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
Ok(bytes.into())
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)
}
}