use rand::{Rng, RngExt};
use vibesql_types::SqlValue;
use crate::errors::ExecutorError;
pub(crate) fn hex(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 1 {
return Err(ExecutorError::UnsupportedFeature(format!(
"HEX requires exactly 1 argument, got {}",
args.len()
)));
}
let hex_string: String = match &args[0] {
SqlValue::Null => return Ok(SqlValue::Null),
SqlValue::Vector(floats) => {
floats.iter().flat_map(|f| f.to_le_bytes()).map(|b| format!("{:02X}", b)).collect()
}
SqlValue::Varchar(s) | SqlValue::Character(s) => {
let has_latin1_high_bytes = s.chars().any(|c| {
let code = c as u32;
(0x80..=0xFF).contains(&code)
});
if has_latin1_high_bytes {
s.chars()
.map(|c| {
let byte = (c as u32) as u8;
format!("{:02X}", byte)
})
.collect()
} else {
s.as_bytes().iter().map(|b| format!("{:02X}", b)).collect()
}
}
SqlValue::Blob(bytes) => {
bytes.iter().map(|b| format!("{:02X}", b)).collect()
}
_ => {
let s = args[0].to_string();
s.as_bytes().iter().map(|b| format!("{:02X}", b)).collect()
}
};
Ok(SqlValue::Varchar(hex_string.into()))
}
pub(crate) fn unhex(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 1 {
return Err(ExecutorError::UnsupportedFeature(format!(
"UNHEX requires exactly 1 argument, got {}",
args.len()
)));
}
let hex_str = match &args[0] {
SqlValue::Null => return Ok(SqlValue::Null),
SqlValue::Varchar(s) | SqlValue::Character(s) => s.as_str(),
_ => {
return Err(ExecutorError::UnsupportedFeature(
"UNHEX argument must be a string".to_string(),
));
}
};
if hex_str.len() % 2 != 0 {
return Ok(SqlValue::Null);
}
let mut bytes = Vec::with_capacity(hex_str.len() / 2);
let chars: Vec<char> = hex_str.chars().collect();
for chunk in chars.chunks(2) {
let high = match chunk[0].to_digit(16) {
Some(d) => d as u8,
None => return Ok(SqlValue::Null), };
let low = match chunk[1].to_digit(16) {
Some(d) => d as u8,
None => return Ok(SqlValue::Null), };
bytes.push((high << 4) | low);
}
Ok(SqlValue::Blob(bytes))
}
pub(crate) fn zeroblob(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 1 {
return Err(ExecutorError::UnsupportedFeature(format!(
"ZEROBLOB requires exactly 1 argument, got {}",
args.len()
)));
}
let n = match &args[0] {
SqlValue::Null => return Ok(SqlValue::Null),
SqlValue::Integer(i) => *i as usize,
SqlValue::Bigint(i) => *i as usize,
SqlValue::Smallint(i) => *i as usize,
SqlValue::Unsigned(u) => *u as usize,
SqlValue::Numeric(n) => *n as usize,
SqlValue::Real(r) => *r as usize,
SqlValue::Double(d) => *d as usize,
SqlValue::Float(f) => *f as usize,
SqlValue::Varchar(s) | SqlValue::Character(s) => {
s.trim().parse::<f64>().map(|v| v as usize).unwrap_or(0)
}
SqlValue::Blob(_) => {
0
}
SqlValue::Boolean(b) => {
if *b {
1
} else {
0
}
}
_ => {
return Err(ExecutorError::UnsupportedFeature(
"ZEROBLOB argument must be numeric".to_string(),
));
}
};
const MAX_BLOB_SIZE: usize = 1_000_000_000; if n > MAX_BLOB_SIZE {
return Err(ExecutorError::UnsupportedFeature(format!(
"ZEROBLOB size {} exceeds maximum {}",
n, MAX_BLOB_SIZE
)));
}
Ok(SqlValue::Blob(vec![0u8; n]))
}
pub(crate) fn randomblob(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 1 {
return Err(ExecutorError::UnsupportedFeature(format!(
"RANDOMBLOB requires exactly 1 argument, got {}",
args.len()
)));
}
let n = match &args[0] {
SqlValue::Null => return Ok(SqlValue::Null),
SqlValue::Integer(i) => {
if *i < 0 {
1usize } else {
*i as usize
}
}
SqlValue::Bigint(i) => {
if *i < 0 {
1usize
} else {
*i as usize
}
}
SqlValue::Smallint(i) => {
if *i < 0 {
1usize
} else {
*i as usize
}
}
SqlValue::Unsigned(u) => *u as usize,
SqlValue::Numeric(n) => {
if *n < 0.0 {
1usize
} else {
*n as usize
}
}
SqlValue::Real(r) => {
if *r < 0.0 {
1usize
} else {
*r as usize
}
}
SqlValue::Double(d) => {
if *d < 0.0 {
1usize
} else {
*d as usize
}
}
SqlValue::Float(f) => {
if *f < 0.0 {
1usize
} else {
*f as usize
}
}
_ => {
return Err(ExecutorError::UnsupportedFeature(
"RANDOMBLOB argument must be numeric".to_string(),
));
}
};
const MAX_BLOB_SIZE: usize = 1_000_000_000; if n > MAX_BLOB_SIZE {
return Err(ExecutorError::UnsupportedFeature(format!(
"RANDOMBLOB size {} exceeds maximum {}",
n, MAX_BLOB_SIZE
)));
}
let mut rng = rand::rng();
let bytes: Vec<u8> = (0..n).map(|_| rng.random()).collect();
Ok(SqlValue::Blob(bytes))
}
fn format_float_for_quote(n: f64) -> String {
if n.is_nan() {
"NaN".to_string()
} else if n.is_infinite() {
if n > 0.0 {
"9.0e+999".to_string()
} else {
"-9.0e+999".to_string()
}
} else {
n.to_string()
}
}
pub(crate) fn quote(args: &[SqlValue]) -> Result<SqlValue, ExecutorError> {
if args.len() != 1 {
return Err(ExecutorError::UnsupportedFeature(format!(
"QUOTE requires exactly 1 argument, got {}",
args.len()
)));
}
match &args[0] {
SqlValue::Null => Ok(SqlValue::Varchar("NULL".into())),
SqlValue::Integer(i) => Ok(SqlValue::Varchar(i.to_string().into())),
SqlValue::Bigint(i) => Ok(SqlValue::Varchar(i.to_string().into())),
SqlValue::Smallint(i) => Ok(SqlValue::Varchar(i.to_string().into())),
SqlValue::Unsigned(u) => Ok(SqlValue::Varchar(u.to_string().into())),
SqlValue::Real(r) => Ok(SqlValue::Varchar(format_float_for_quote(*r).into())),
SqlValue::Double(d) => Ok(SqlValue::Varchar(format_float_for_quote(*d).into())),
SqlValue::Numeric(n) => Ok(SqlValue::Varchar(format_float_for_quote(*n).into())),
SqlValue::Float(f) => Ok(SqlValue::Varchar(format_float_for_quote(*f as f64).into())),
SqlValue::Boolean(b) => Ok(SqlValue::Varchar(if *b { "1" } else { "0" }.into())),
SqlValue::Varchar(s) | SqlValue::Character(s) => {
let escaped = s.replace('\'', "''");
Ok(SqlValue::Varchar(format!("'{}'", escaped).into()))
}
SqlValue::Vector(floats) => {
let hex: String =
floats.iter().flat_map(|f| f.to_le_bytes()).map(|b| format!("{:02X}", b)).collect();
Ok(SqlValue::Varchar(format!("X'{}'", hex).into()))
}
SqlValue::Date(d) => Ok(SqlValue::Varchar(format!("'{}'", d).into())),
SqlValue::Time(t) => Ok(SqlValue::Varchar(format!("'{}'", t).into())),
SqlValue::Timestamp(ts) => Ok(SqlValue::Varchar(format!("'{}'", ts).into())),
SqlValue::Interval(i) => Ok(SqlValue::Varchar(format!("'{}'", i).into())),
SqlValue::Blob(b) => {
let hex: String = b.iter().map(|byte| format!("{:02X}", byte)).collect();
Ok(SqlValue::Varchar(format!("X'{}'", hex).into()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex() {
assert_eq!(
hex(&[SqlValue::Varchar("abc".into())]).unwrap(),
SqlValue::Varchar("616263".into())
);
assert_eq!(hex(&[SqlValue::Null]).unwrap(), SqlValue::Null);
assert_eq!(hex(&[SqlValue::Integer(255)]).unwrap(), SqlValue::Varchar("323535".into()));
assert_eq!(
hex(&[SqlValue::Varchar("ABC".into())]).unwrap(),
SqlValue::Varchar("414243".into())
);
}
#[test]
fn test_unhex() {
let result = unhex(&[SqlValue::Varchar("616263".into())]).unwrap();
match result {
SqlValue::Blob(bytes) => assert_eq!(bytes, vec![0x61, 0x62, 0x63]),
_ => panic!("Expected Blob"),
}
assert_eq!(unhex(&[SqlValue::Varchar("abc".into())]).unwrap(), SqlValue::Null);
assert_eq!(unhex(&[SqlValue::Varchar("zz".into())]).unwrap(), SqlValue::Null);
}
#[test]
fn test_zeroblob() {
let result = zeroblob(&[SqlValue::Integer(4)]).unwrap();
match result {
SqlValue::Blob(bytes) => assert_eq!(bytes, vec![0, 0, 0, 0]),
_ => panic!("Expected Blob"),
}
let result = zeroblob(&[SqlValue::Integer(0)]).unwrap();
match result {
SqlValue::Blob(bytes) => assert_eq!(bytes.len(), 0),
_ => panic!("Expected Blob"),
}
assert_eq!(zeroblob(&[SqlValue::Null]).unwrap(), SqlValue::Null);
}
#[test]
fn test_randomblob() {
let result = randomblob(&[SqlValue::Integer(10)]).unwrap();
match result {
SqlValue::Blob(bytes) => {
assert_eq!(bytes.len(), 10);
}
_ => panic!("Expected Blob"),
}
let result = randomblob(&[SqlValue::Integer(0)]).unwrap();
match result {
SqlValue::Blob(bytes) => assert_eq!(bytes.len(), 0),
_ => panic!("Expected Blob"),
}
let result = randomblob(&[SqlValue::Integer(-5)]).unwrap();
match result {
SqlValue::Blob(bytes) => assert_eq!(bytes.len(), 1),
_ => panic!("Expected Blob"),
}
assert_eq!(randomblob(&[SqlValue::Null]).unwrap(), SqlValue::Null);
assert!(randomblob(&[]).is_err());
}
#[test]
fn test_quote() {
assert_eq!(quote(&[SqlValue::Null]).unwrap(), SqlValue::Varchar("NULL".into()));
assert_eq!(quote(&[SqlValue::Integer(123)]).unwrap(), SqlValue::Varchar("123".into()));
assert_eq!(
quote(&[SqlValue::Varchar("hello".into())]).unwrap(),
SqlValue::Varchar("'hello'".into())
);
assert_eq!(
quote(&[SqlValue::Varchar("it's".into())]).unwrap(),
SqlValue::Varchar("'it''s'".into())
);
assert_eq!(quote(&[SqlValue::Numeric(2.5)]).unwrap(), SqlValue::Varchar("2.5".into()));
assert_eq!(quote(&[SqlValue::Boolean(true)]).unwrap(), SqlValue::Varchar("1".into()));
assert_eq!(quote(&[SqlValue::Boolean(false)]).unwrap(), SqlValue::Varchar("0".into()));
assert_eq!(quote(&[SqlValue::Varchar("".into())]).unwrap(), SqlValue::Varchar("''".into()));
}
}