use alloc::borrow::Cow;
use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::Ordering;
#[derive(Clone, Default, Eq)]
pub struct Text(Vec<u8>);
impl Text {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn byte_len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn as_str(&self) -> &str {
core::str::from_utf8(&self.0).unwrap_or("")
}
pub fn to_str_lossy(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.0)
}
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Text(bytes)
}
}
impl core::ops::Deref for Text {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl core::fmt::Debug for Text {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Debug::fmt(&self.to_str_lossy(), f)
}
}
impl core::fmt::Display for Text {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.to_str_lossy())
}
}
impl From<String> for Text {
fn from(s: String) -> Self {
Text(s.into_bytes())
}
}
impl From<&str> for Text {
fn from(s: &str) -> Self {
Text(s.as_bytes().to_vec())
}
}
impl From<Cow<'_, str>> for Text {
fn from(s: Cow<'_, str>) -> Self {
Text(s.into_owned().into_bytes())
}
}
impl PartialEq for Text {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl PartialEq<str> for Text {
fn eq(&self, other: &str) -> bool {
self.0 == other.as_bytes()
}
}
impl PartialEq<&str> for Text {
fn eq(&self, other: &&str) -> bool {
self.0 == other.as_bytes()
}
}
impl PartialEq<String> for Text {
fn eq(&self, other: &String) -> bool {
self.0 == other.as_bytes()
}
}
impl PartialEq<Text> for String {
fn eq(&self, other: &Text) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl PartialEq<Text> for str {
fn eq(&self, other: &Text) -> bool {
self.as_bytes() == other.as_bytes()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Integer(i64),
Real(f64),
Text(Text),
Blob(Vec<u8>),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ValueRef<'a> {
Null,
Integer(i64),
Real(f64),
Text(&'a str),
Blob(&'a [u8]),
}
impl ValueRef<'_> {
pub fn to_owned(&self) -> Value {
match *self {
ValueRef::Null => Value::Null,
ValueRef::Integer(i) => Value::Integer(i),
ValueRef::Real(r) => Value::Real(r),
ValueRef::Text(s) => Value::Text(String::from(s).into()),
ValueRef::Blob(b) => Value::Blob(Vec::from(b)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Collation {
#[default]
Binary,
NoCase,
RTrim,
Custom(u32),
}
impl Collation {
pub fn parse(name: &str) -> Option<Collation> {
match name.to_ascii_lowercase().as_str() {
"binary" => Some(Collation::Binary),
"nocase" => Some(Collation::NoCase),
"rtrim" => Some(Collation::RTrim),
_ => None,
}
}
}
pub fn resolve_collation_name(name: &str) -> Option<Collation> {
if let Some(c) = Collation::parse(name) {
return Some(c);
}
#[cfg(feature = "std")]
{
registry::resolve_name(name).map(Collation::Custom)
}
#[cfg(not(feature = "std"))]
{
None
}
}
pub fn collation_name(coll: Collation) -> alloc::string::String {
use alloc::string::ToString;
match coll {
Collation::Binary => "BINARY".to_string(),
Collation::NoCase => "NOCASE".to_string(),
Collation::RTrim => "RTRIM".to_string(),
Collation::Custom(id) => {
#[cfg(feature = "std")]
{
registry::name_of(id).unwrap_or_else(|| "BINARY".to_string())
}
#[cfg(not(feature = "std"))]
{
let _ = id;
"BINARY".to_string()
}
}
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn register_collation<F>(name: &str, cmp: F) -> u32
where
F: Fn(&str, &str) -> Ordering + Send + 'static,
{
registry::register(name, alloc::boxed::Box::new(cmp))
}
#[cfg(feature = "std")]
mod registry {
use super::Ordering;
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::sync::{Mutex, OnceLock};
type CollFn = Box<dyn Fn(&str, &str) -> Ordering + Send>;
struct Registry {
by_name: BTreeMap<String, u32>,
names: Vec<String>,
fns: Vec<CollFn>,
}
fn registry() -> &'static Mutex<Registry> {
static REG: OnceLock<Mutex<Registry>> = OnceLock::new();
REG.get_or_init(|| {
Mutex::new(Registry {
by_name: BTreeMap::new(),
names: Vec::new(),
fns: Vec::new(),
})
})
}
pub(super) fn register(name: &str, f: CollFn) -> u32 {
let mut reg = registry().lock().unwrap();
let key = name.to_ascii_lowercase();
if let Some(&id) = reg.by_name.get(&key) {
reg.fns[id as usize] = f;
reg.names[id as usize] = name.to_string();
return id;
}
let id = reg.fns.len() as u32;
reg.fns.push(f);
reg.names.push(name.to_string());
reg.by_name.insert(key, id);
id
}
pub(super) fn resolve_name(name: &str) -> Option<u32> {
registry()
.lock()
.unwrap()
.by_name
.get(&name.to_ascii_lowercase())
.copied()
}
pub(super) fn name_of(id: u32) -> Option<String> {
registry().lock().unwrap().names.get(id as usize).cloned()
}
pub(super) fn compare(id: u32, x: &str, y: &str) -> Ordering {
let reg = registry().lock().unwrap();
match reg.fns.get(id as usize) {
Some(f) => f(x, y),
None => x.as_bytes().cmp(y.as_bytes()),
}
}
}
pub fn cmp_text(x: &str, y: &str, coll: Collation) -> Ordering {
match coll {
Collation::Binary => x.as_bytes().cmp(y.as_bytes()),
Collation::NoCase => x
.bytes()
.map(|b| b.to_ascii_lowercase())
.cmp(y.bytes().map(|b| b.to_ascii_lowercase())),
Collation::RTrim => x
.trim_end_matches(' ')
.as_bytes()
.cmp(y.trim_end_matches(' ').as_bytes()),
Collation::Custom(_id) => {
#[cfg(feature = "std")]
{
registry::compare(_id, x, y)
}
#[cfg(not(feature = "std"))]
{
x.as_bytes().cmp(y.as_bytes())
}
}
}
}
pub fn cmp_values_coll(a: &Value, b: &Value, coll: Collation) -> Ordering {
match (a, b) {
(Value::Text(x), Value::Text(y)) => cmp_text(x, y, coll),
_ => cmp_values(a, b),
}
}
pub fn cmp_values(a: &Value, b: &Value) -> Ordering {
fn class(v: &Value) -> u8 {
match v {
Value::Null => 0,
Value::Integer(_) | Value::Real(_) => 1,
Value::Text(_) => 2,
Value::Blob(_) => 3,
}
}
match (a, b) {
(Value::Null, Value::Null) => Ordering::Equal,
(Value::Integer(x), Value::Integer(y)) => x.cmp(y),
(Value::Real(x), Value::Real(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
(Value::Integer(i), Value::Real(r)) => int_float_cmp(*i, *r),
(Value::Real(r), Value::Integer(i)) => int_float_cmp(*i, *r).reverse(),
(Value::Text(x), Value::Text(y)) => x.as_bytes().cmp(y.as_bytes()),
(Value::Blob(x), Value::Blob(y)) => x.cmp(y),
_ => class(a).cmp(&class(b)),
}
}
fn int_float_cmp(i: i64, r: f64) -> Ordering {
if r.is_nan() {
return Ordering::Equal;
}
if r < -9_223_372_036_854_775_808.0 {
return Ordering::Greater;
}
if r >= 9_223_372_036_854_775_808.0 {
return Ordering::Less;
}
let y = r as i64; match i.cmp(&y) {
Ordering::Equal => (i as f64).partial_cmp(&r).unwrap_or(Ordering::Equal),
other => other,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SerialType(pub u64);
impl SerialType {
pub fn content_len(self) -> Option<usize> {
Some(match self.0 {
0 | 8 | 9 => 0,
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 6,
6 | 7 => 8,
10 | 11 => return None,
n if n % 2 == 0 => ((n - 12) / 2) as usize,
n => ((n - 13) / 2) as usize,
})
}
pub fn for_value(value: &Value) -> SerialType {
SerialType(match value {
Value::Null => 0,
Value::Integer(0) => 8,
Value::Integer(1) => 9,
Value::Integer(i) => {
let i = *i;
if (-0x80..=0x7f).contains(&i) {
1
} else if (-0x8000..=0x7fff).contains(&i) {
2
} else if (-0x80_0000..=0x7f_ffff).contains(&i) {
3
} else if (-0x8000_0000..=0x7fff_ffff).contains(&i) {
4
} else if (-0x8000_0000_0000..=0x7fff_ffff_ffff).contains(&i) {
5
} else {
6
}
}
Value::Real(_) => 7,
Value::Blob(b) => 12 + 2 * b.len() as u64,
Value::Text(s) => 13 + 2 * s.byte_len() as u64,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn content_lengths() {
assert_eq!(SerialType(0).content_len(), Some(0));
assert_eq!(SerialType(1).content_len(), Some(1));
assert_eq!(SerialType(5).content_len(), Some(6));
assert_eq!(SerialType(6).content_len(), Some(8));
assert_eq!(SerialType(7).content_len(), Some(8));
assert_eq!(SerialType(8).content_len(), Some(0));
assert_eq!(SerialType(9).content_len(), Some(0));
assert_eq!(SerialType(10).content_len(), None);
assert_eq!(SerialType(11).content_len(), None);
assert_eq!(SerialType(20).content_len(), Some(4));
assert_eq!(SerialType(23).content_len(), Some(5));
}
#[test]
fn serial_type_selection_matches_sqlite() {
assert_eq!(SerialType::for_value(&Value::Null), SerialType(0));
assert_eq!(SerialType::for_value(&Value::Integer(0)), SerialType(8));
assert_eq!(SerialType::for_value(&Value::Integer(1)), SerialType(9));
assert_eq!(SerialType::for_value(&Value::Integer(2)), SerialType(1));
assert_eq!(SerialType::for_value(&Value::Integer(127)), SerialType(1));
assert_eq!(SerialType::for_value(&Value::Integer(128)), SerialType(2));
assert_eq!(SerialType::for_value(&Value::Integer(-1)), SerialType(1));
assert_eq!(
SerialType::for_value(&Value::Integer(i64::MAX)),
SerialType(6)
);
assert_eq!(SerialType::for_value(&Value::Real(1.5)), SerialType(7));
assert_eq!(
SerialType::for_value(&Value::Text("abc".to_string().into())),
SerialType(19) );
assert_eq!(
SerialType::for_value(&Value::Blob(vec![0u8; 4])),
SerialType(20) );
}
#[test]
fn nocase_folds_to_lowercase_like_sqlite() {
use core::cmp::Ordering;
assert_eq!(cmp_text("A", "[", Collation::NoCase), Ordering::Greater);
assert_eq!(cmp_text("A", "_", Collation::NoCase), Ordering::Greater);
assert_eq!(cmp_text("A", "`", Collation::NoCase), Ordering::Greater);
assert_eq!(cmp_text("9", "[", Collation::NoCase), Ordering::Less);
assert_eq!(
cmp_text("Apple", "apple", Collation::NoCase),
Ordering::Equal
);
assert_eq!(cmp_text("Z", "z", Collation::NoCase), Ordering::Equal);
assert_eq!(cmp_text("[", "[", Collation::NoCase), Ordering::Equal);
}
#[test]
fn value_ref_round_trips() {
assert_eq!(ValueRef::Integer(5).to_owned(), Value::Integer(5));
assert_eq!(
ValueRef::Text("x").to_owned(),
Value::Text("x".to_string().into())
);
}
}