diff --git a/COMPAT.md b/COMPAT.md index 4d85a51cf..7d192760d 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -90,11 +90,11 @@ This document describes the SQLite compatibility status of Limbo: | octet_length(X) | No | | | printf(FORMAT,...) | No | | | quote(X) | No | | -| random() | Yes | | +| random() | Yes | | | randomblob(N) | No | | | replace(X,Y,Z) | No | | -| round(X) | Yes | | -| round(X,Y) | Yes | | +| round(X) | Yes | | +| round(X,Y) | Yes | | | rtrim(X) | No | | | rtrim(X,Y) | No | | | sign(X) | No | | @@ -109,12 +109,12 @@ This document describes the SQLite compatibility status of Limbo: | substring(X,Y,Z) | No | | | substring(X,Y) | No | | | total_changes() | No | | -| trim(X) | Yes | | -| trim(X,Y) | Yes | | +| trim(X) | Yes | | +| trim(X,Y) | Yes | | | typeof(X) | No | | | unhex(X) | No | | | unhex(X,Y) | No | | -| unicode(X) | No | | +| unicode(X) | Yes | | | unlikely(X) | No | | | upper(X) | Yes | | | zeroblob(N) | No | | diff --git a/core/function.rs b/core/function.rs index 7f7545f3d..e686c6721 100644 --- a/core/function.rs +++ b/core/function.rs @@ -39,6 +39,7 @@ pub enum ScalarFunc { Min, Max, Date, + Unicode, } impl ToString for ScalarFunc { @@ -56,6 +57,7 @@ impl ToString for ScalarFunc { ScalarFunc::Min => "min".to_string(), ScalarFunc::Max => "max".to_string(), ScalarFunc::Date => "date".to_string(), + ScalarFunc::Unicode => "unicode".to_string(), } } } @@ -89,6 +91,7 @@ impl Func { "round" => Ok(Func::Scalar(ScalarFunc::Round)), "length" => Ok(Func::Scalar(ScalarFunc::Length)), "date" => Ok(Func::Scalar(ScalarFunc::Date)), + "unicode" => Ok(Func::Scalar(ScalarFunc::Unicode)), _ => Err(()), } } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index f6e16f2ca..370db02f1 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -210,7 +210,8 @@ pub fn translate_expr( ScalarFunc::Abs | ScalarFunc::Lower | ScalarFunc::Upper - | ScalarFunc::Length => { + | ScalarFunc::Length + | ScalarFunc::Unicode => { let args = if let Some(args) = args { if args.len() != 1 { crate::bail_parse_error!( diff --git a/core/types.rs b/core/types.rs index 4276e7021..2562fc9de 100644 --- a/core/types.rs +++ b/core/types.rs @@ -43,7 +43,7 @@ impl Display for OwnedValue { OwnedValue::Integer(i) => write!(f, "{}", i), OwnedValue::Float(fl) => write!(f, "{:?}", fl), OwnedValue::Text(s) => write!(f, "{}", s), - OwnedValue::Blob(b) => write!(f, "{:?}", b), + OwnedValue::Blob(b) => write!(f, "{}", String::from_utf8_lossy(b)), OwnedValue::Agg(a) => match a.as_ref() { AggContext::Avg(acc, _count) => write!(f, "{}", acc), AggContext::Sum(acc) => write!(f, "{}", acc), diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 7aa52fd6d..f32d6cae8 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -1153,6 +1153,11 @@ impl Program { } state.pc += 1; } + ScalarFunc::Unicode => { + let reg_value = state.registers[*start_reg].borrow_mut(); + state.registers[*dest] = exec_unicode(reg_value); + state.pc += 1; + } }, } } @@ -1271,6 +1276,23 @@ fn exec_minmax<'a>( regs.into_iter().reduce(|a, b| op(a, b)).cloned() } +fn exec_unicode(reg: &OwnedValue) -> OwnedValue { + match reg { + OwnedValue::Text(_) + | OwnedValue::Integer(_) + | OwnedValue::Float(_) + | OwnedValue::Blob(_) => { + let text = reg.to_string(); + if let Some(first_char) = text.chars().next() { + OwnedValue::Integer(first_char as u32 as i64) + } else { + OwnedValue::Null + } + } + _ => OwnedValue::Null, + } +} + fn exec_round(reg: &OwnedValue, precision: Option) -> OwnedValue { let precision = match precision { Some(OwnedValue::Text(x)) => x.parse().unwrap_or(0.0), @@ -1327,7 +1349,7 @@ fn exec_if(reg: &OwnedValue, null_reg: &OwnedValue, not: bool) -> bool { mod tests { use super::{ exec_abs, exec_if, exec_length, exec_like, exec_lower, exec_minmax, exec_random, - exec_round, exec_trim, exec_upper, OwnedValue, + exec_round, exec_trim, exec_unicode, exec_upper, OwnedValue, }; use std::rc::Rc; @@ -1345,12 +1367,48 @@ mod tests { let expected_len = OwnedValue::Integer(7); assert_eq!(exec_length(&input_float), expected_len); - // Expected byte array for "example" - let expected_blob = OwnedValue::Blob(Rc::new(vec![101, 120, 97, 109, 112, 108, 101])); + let expected_blob = OwnedValue::Blob(Rc::new("example".as_bytes().to_vec())); let expected_len = OwnedValue::Integer(7); assert_eq!(exec_length(&expected_blob), expected_len); } + #[test] + fn test_unicode() { + assert_eq!( + exec_unicode(&OwnedValue::Text(Rc::new("a".to_string()))), + OwnedValue::Integer(97) + ); + assert_eq!( + exec_unicode(&OwnedValue::Text(Rc::new("😊".to_string()))), + OwnedValue::Integer(128522) + ); + assert_eq!( + exec_unicode(&OwnedValue::Text(Rc::new("".to_string()))), + OwnedValue::Null + ); + assert_eq!( + exec_unicode(&OwnedValue::Integer(23)), + OwnedValue::Integer(50) + ); + assert_eq!( + exec_unicode(&OwnedValue::Integer(0)), + OwnedValue::Integer(48) + ); + assert_eq!( + exec_unicode(&OwnedValue::Float(0.0)), + OwnedValue::Integer(48) + ); + assert_eq!( + exec_unicode(&OwnedValue::Float(23.45)), + OwnedValue::Integer(50) + ); + assert_eq!(exec_unicode(&OwnedValue::Null), OwnedValue::Null); + assert_eq!( + exec_unicode(&OwnedValue::Blob(Rc::new("example".as_bytes().to_vec()))), + OwnedValue::Integer(101) + ); + } + #[test] fn test_minmax() { let min_fn = |a, b| if a < b { a } else { b }; diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index 27014908e..248a69987 100644 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -243,3 +243,26 @@ do_execsql_test date-with-invalid-timezone { SELECT date('2023-05-18 15:30:45+25:00'); } {{}} +do_execsql_test unicode-a { + SELECT unicode('a'); +} {97} + +do_execsql_test unicode-emoji { + SELECT unicode('😊'); +} {128522} + +do_execsql_test unicode-empty { + SELECT unicode(''); +} {} + +do_execsql_test unicode-number { + SELECT unicode(23); +} {50} + +do_execsql_test unicode-float { + SELECT unicode(23.45); +} {50} + +do_execsql_test unicode-null { + SELECT unicode(NULL); +} {}