sqlfuncs 0.6.0

Scalar functions for use in SQLite through rusqlite.
Documentation
//! A collection of functions intended to work with text strings.

use rusqlite::{Connection, Error, functions::FunctionFlags};

use orphanage::strx::{RndStr, validate_objname};

use unicode_width::UnicodeWidthStr;

use crate::err;


/// Add a `randomstr()` SQL function to the connection object.
///
/// The SQL function `randomstr()` takes a single argument:
/// 1. The length of the random string, in characters.
///
/// Its output will be a random string of the requested length where each
/// character is a ASCII alphabet or numeric character.
///
/// ```
/// use rusqlite::Connection;
/// use sqlfuncs::text::rndstr_alphanum;
///
/// let conn = Connection::open_in_memory().unwrap();
/// rndstr_alphanum(&conn).unwrap();
///
/// conn.execute(r#"
/// CREATE TABLE IF NOT EXISTS stuff (
///   id   INTEGER PRIMARY KEY,
///   salt TEXT NOT NULL DEFAULT (randomstr(8))
/// );
/// "#, []);
/// ```
///
/// ## SQLite function properties
/// - Innocuous
/// - UTF8
#[allow(clippy::missing_errors_doc)]
pub fn rndstr_alphanum(db: &Connection) -> Result<(), Error> {
  db.create_scalar_function(
    "randomstr",
    1,
    FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_INNOCUOUS,
    move |ctx| {
      //assert_eq!(ctx.len(), 1, "called with unexpected number of
      // arguments");
      let len = ctx.get::<usize>(0)?;

      Ok(String::rnd_alphanum(len))
    }
  )
}

/// Add a `randomstr()` SQL function to the connection object.
///
/// The SQL function `randomstr()` takes a single argument:
/// 1. The length of the random string, in characters.
/// 2. Character set used to generate random string.
///
/// Its output will be a random string of the requested length where each
/// character is a ASCII alphabet or numeric character.
///
/// ```
/// use rusqlite::Connection;
/// use sqlfuncs::text::rndstr;
///
/// let conn = Connection::open_in_memory().unwrap();
/// rndstr(&conn).unwrap();
///
/// conn.execute(r#"
/// CREATE TABLE IF NOT EXISTS stuff (
///   id   INTEGER PRIMARY KEY,
///   salt TEXT NOT NULL DEFAULT (randomstr(8, "abcdefABCD123456"))
/// );
/// "#, []);
/// ```
///
/// ## SQLite function properties
/// - Innocuous
/// - UTF8
#[allow(clippy::missing_errors_doc)]
pub fn rndstr(db: &Connection) -> Result<(), Error> {
  db.create_scalar_function(
    "randomstr",
    2,
    FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_INNOCUOUS,
    move |ctx| {
      //assert_eq!(ctx.len(), 2, "called with unexpected number of
      // arguments");
      let len = ctx.get::<usize>(0)?;
      let alphabet = ctx.get::<String>(1)?;
      let charset = alphabet.as_bytes();

      Ok(String::rnd_from_alphabet(len, charset))
    }
  )
}


/// Add a `isobjname()` SQL function to the connection object.
///
/// The SQL function `isobjname()` takes a signle argument:
/// 1. The input string to check whether it conforms to an object name.
///
/// ```
/// use rusqlite::Connection;
/// use sqlfuncs::text::isobjname;
///
/// let conn = Connection::open_in_memory().unwrap();
/// isobjname(&conn).unwrap();
///
/// conn.execute(r#"
/// CREATE TABLE IF NOT EXISTS stuff (
///   id   INTEGER PRIMARY KEY,
///   name TEXT UNIQUE NOT NULL,
///   CHECK (isobjname(name) == 1)
/// );
/// "#, []);
/// ```
///
/// ## SQLite function properties
/// - Deterministic
/// - Innocuous
/// - UTF8
///
/// # Panics
/// The number of input parameters must be exactly 1.
#[allow(clippy::missing_errors_doc)]
pub fn isobjname(conn: &Connection) -> Result<(), Error> {
  conn.create_scalar_function(
    "isobjname",
    1,
    FunctionFlags::SQLITE_UTF8
      | FunctionFlags::SQLITE_INNOCUOUS
      | FunctionFlags::SQLITE_DETERMINISTIC,
    move |ctx| {
      assert_eq!(ctx.len(), 1, "called with unexpected number of arguments");
      let name = ctx.get::<String>(0)?;
      validate_objname(&name).map_err(|e| {
        Error::UserFunctionError(Box::new(err::Error::ValidationFailed(e)))
      })?;
      Ok(1)
    }
  )
}

/// Add a `uwidth()` SQL function to the connection object.
///
/// The `uwidth()` function takes a (utf-8) string input and returns the number
/// of text cells the "character" occupies.  This can be useful to calculate
/// how wide a string will be when printed to a unicode-aware console.
///
/// ```
/// use rusqlite::Connection;
/// use sqlfuncs::text::uwidth;
///
/// let conn = Connection::open_in_memory().unwrap();
/// uwidth(&conn).unwrap();
///
/// conn.execute(r#"
/// CREATE TABLE IF NOT EXISTS stuff (
///   id   INTEGER PRIMARY KEY,
///   name TEXT NOT NULL
/// );
/// "#, []);
///
/// conn.execute(r#"INSERT INTO stuff (id, name) VALUES (1, "a");"#, [])
///   .unwrap();
/// conn.execute(r#"INSERT INTO stuff (id, name) VALUES (2, "รค");"#, [])
///   .unwrap();
/// conn.execute(r#"INSERT INTO stuff (id, name) VALUES (3, "๐Ÿ”’");"#, [])
///   .unwrap();
///
/// let len: usize = conn.query_one(
///   "SELECT uwidth(name) FROM stuff WHERE id=1;",
///   [],
///   |row| row.get(0)
/// ).unwrap();
/// assert_eq!(len, 1);
///
/// let len: usize = conn.query_one(
///   "SELECT uwidth(name) FROM stuff WHERE id=2;",
///   [],
///   |row| row.get(0)
/// ).unwrap();
/// assert_eq!(len, 1);
///
/// let len: usize = conn.query_one(
///   "SELECT uwidth(name) FROM stuff WHERE id=3;",
///   [],
///   |row| row.get(0)
/// ).unwrap();
/// assert_eq!(len, 2);
/// ```
///
/// ## SQLite function properties
/// - Deterministic
/// - Innocuous
/// - UTF8
///
/// # Panics
/// The number of input parameters must be exactly 1.
#[allow(clippy::missing_errors_doc)]
pub fn uwidth(conn: &Connection) -> Result<(), Error> {
  conn.create_scalar_function(
    "uwidth",
    1,
    FunctionFlags::SQLITE_UTF8
      | FunctionFlags::SQLITE_INNOCUOUS
      | FunctionFlags::SQLITE_DETERMINISTIC,
    move |ctx| {
      assert_eq!(ctx.len(), 1, "called with unexpected number of arguments");
      let name = ctx.get::<String>(0)?;

      let width = name.width();

      Ok(width)
    }
  )
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :