julid-rs 1.6.1803398874989

A crate and loadable extension for SQLite that provides Joe's ULIDs.
Documentation
#[cfg(feature = "plugin")]
use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines};

mod base32;

/// Contains the [`Julid`] type, which is publicly exported at the top level.
pub mod julid;

/// Serialization into bytes, and deserialization from a variety of formats,
/// with Serde (feature `serde` (default))
#[cfg(feature = "serde")]
pub mod serde;

/// Traits from the SQLx crate for getting Julids into and out of SQLite
/// databases from normal Rust applications. (feature `sqlx` (default))
#[cfg(feature = "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)]
pub use base32::DecodeError;
#[doc(inline)]
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
/// 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")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sqlite3_julid_init(
    db: *mut sqlite3,
    _pz_err_msg: *mut *mut c_char,
    p_api: *mut sqlite3_api_routines,
) -> c_uint {
    unsafe { sqlite_loadable::ext::faux_sqlite_extension_init2(p_api) }
    match sqlite_plugin::init_rs(db) {
        Ok(()) => 256, // SQLITE_OK_LOAD_PERMANENTLY
        Err(err) => err.code_extended(),
    }
}

/// The code for the SQLite plugin is kept in this module, and exposed via the
/// [`sqlite3_julid_init`] function (feature `plugin` (non-default))
#[cfg(feature = "plugin")]
pub mod sqlite_plugin {
    use sqlite_loadable::{
        api, define_scalar_function,
        prelude::{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;
        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,
        )?;

        Ok(())
    }

    //-************************************************************************
    // impls
    //-************************************************************************

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

    /// 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.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")
            })?;
            let id: Julid = bytes.into();
            api::result_text(context, id.as_string())?;
        } else {
            api::result_text(context, Julid::new().as_string())?;
        }

        Ok(())
    }

    /// Returns the timestamp portion as fractional seconds (`f64`), for
    /// convenient use in SQLite's `datetime()` function.
    ///
    /// ```text
    /// sqlite> select julid_seconds(julid_new());
    /// 1690480066.208
    /// sqlite> select datetime(julid_seconds(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.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")
            })?;
            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",
            ));
        }

        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,535.
    ///
    /// ```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.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")
            })?;
            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",
            ));
        }

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