diff --git a/COMPAT.md b/COMPAT.md index 213841cb1..38d475680 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -160,6 +160,19 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | upper(X) | Yes | | | zeroblob(N) | Yes | | + +|-------------------------------------------------| +| LibSql / sqlean Scalar | | | +| ---------------------------- | ------ | ------- | +| uuid4() | Yes | uuid version 4 **uuid's are `blob` by default** | +| uuid4_str() | Yes | uuid v4 string alias `gen_random_uuid()` for PG compatibility| +| uuid7(X?) | Yes | uuid version 7, Optional arg for seconds since epoch| +| uuid7_timestamp_ms(X) | Yes | Convert a uuid v7 to milliseconds since epoch| +| uuid_str(X) | Yes | Convert a valid uuid to string| +| uuid_blob(X) | Yes | Convert a valid uuid to blob| + + + ### Mathematical functions | Function | Status | Comment | diff --git a/core/function.rs b/core/function.rs index a158c1dce..b17d388bb 100644 --- a/core/function.rs +++ b/core/function.rs @@ -96,7 +96,7 @@ pub enum ScalarFunc { UuidStr, UuidBlob, Uuid7, - Uuid7Str, + Uuid7TS, } impl Display for ScalarFunc { @@ -146,8 +146,8 @@ impl Display for ScalarFunc { ScalarFunc::UuidStr => "uuid_str".to_string(), ScalarFunc::UuidBlob => "uuid_blob".to_string(), ScalarFunc::Uuid7 => "uuid7".to_string(), - ScalarFunc::Uuid7Str => "uuid7_str".to_string(), ScalarFunc::Uuid4Str => "uuid4_str".to_string(), + ScalarFunc::Uuid7TS => "uuid7_timestamp_ms".to_string(), }; write!(f, "{}", str) } @@ -337,12 +337,14 @@ impl Func { "typeof" => Ok(Func::Scalar(ScalarFunc::Typeof)), "last_insert_rowid" => Ok(Func::Scalar(ScalarFunc::LastInsertRowid)), "unicode" => Ok(Func::Scalar(ScalarFunc::Unicode)), + "uuid4_str" => Ok(Func::Scalar(ScalarFunc::Uuid4Str)), "uuid4" => Ok(Func::Scalar(ScalarFunc::Uuid4)), "uuid7" => Ok(Func::Scalar(ScalarFunc::Uuid7)), - "uuid4_str" => Ok(Func::Scalar(ScalarFunc::Uuid4Str)), - "uuid7_str" => Ok(Func::Scalar(ScalarFunc::Uuid7Str)), "uuid_str" => Ok(Func::Scalar(ScalarFunc::UuidStr)), "uuid_blob" => Ok(Func::Scalar(ScalarFunc::UuidBlob)), + "uuid7_timestamp_ms" => Ok(Func::Scalar(ScalarFunc::Uuid7TS)), + // postgres_compatability + "gen_random_uuid" => Ok(Func::Scalar(ScalarFunc::Uuid4Str)), "quote" => Ok(Func::Scalar(ScalarFunc::Quote)), "sqlite_version" => Ok(Func::Scalar(ScalarFunc::SqliteVersion)), "replace" => Ok(Func::Scalar(ScalarFunc::Replace)), diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 9bdd297ac..25c986739 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1196,7 +1196,8 @@ pub fn translate_expr( | ScalarFunc::Soundex | ScalarFunc::ZeroBlob | ScalarFunc::UuidStr - | ScalarFunc::UuidBlob => { + | ScalarFunc::UuidBlob + | ScalarFunc::Uuid7TS => { let args = if let Some(args) = args { if args.len() != 1 { crate::bail_parse_error!( @@ -1228,30 +1229,30 @@ pub fn translate_expr( }); Ok(target_register) } - ScalarFunc::Uuid7 | ScalarFunc::Uuid7Str => { - if let Some(args) = args { - // can take optional time arg - if args.len() > 1 { - crate::bail_parse_error!( - "{} function with more than 1 argument", - srf.to_string() - ); - } - if let Some(arg) = args.first() { - let regs = program.alloc_register(); - translate_expr( - program, - referenced_tables, - arg, - regs, - precomputed_exprs_to_registers, - )?; - } + ScalarFunc::Uuid7 => { + let args = match args { + Some(args) if args.len() > 3 => crate::bail_parse_error!( + "{} function with more than 2 arguments", + srf.to_string() + ), + Some(args) => args, + None => &vec![], + }; + let mut start_reg = None; + for arg in args.iter() { + let reg = program.alloc_register(); + start_reg = Some(start_reg.unwrap_or(reg)); + translate_expr( + program, + referenced_tables, + arg, + reg, + precomputed_exprs_to_registers, + )?; } - let regs = program.alloc_register(); program.emit_insn(Insn::Function { constant_mask: 0, - start_reg: regs, + start_reg: start_reg.unwrap_or(target_register), dest: target_register, func: func_ctx, }); diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 517b839ed..68cfa264f 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -43,7 +43,7 @@ use datetime::{exec_date, exec_time, exec_unixepoch}; use rand::distributions::{Distribution, Uniform}; use rand::{thread_rng, Rng}; use regex::Regex; -use std::borrow::BorrowMut; +use std::borrow::{Borrow, BorrowMut}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; @@ -2399,7 +2399,8 @@ impl Program { | ScalarFunc::Soundex | ScalarFunc::ZeroBlob | ScalarFunc::UuidStr - | ScalarFunc::UuidBlob => { + | ScalarFunc::UuidBlob + | ScalarFunc::Uuid7TS => { let reg_value = state.registers[*start_reg].borrow_mut(); let result = match scalar_func { ScalarFunc::Sign => exec_sign(reg_value), @@ -2416,27 +2417,23 @@ impl Program { ScalarFunc::Soundex => Some(exec_soundex(reg_value)), ScalarFunc::UuidStr => Some(exec_uuidstr(reg_value)?), ScalarFunc::UuidBlob => Some(exec_uuidblob(reg_value)?), + ScalarFunc::Uuid7TS => Some(exec_ts_from_uuid7(reg_value)), _ => unreachable!(), }; state.registers[*dest] = result.unwrap_or(OwnedValue::Null); } - ScalarFunc::Uuid7Str | ScalarFunc::Uuid7 => match arg_count { + ScalarFunc::Uuid7 => match arg_count { 0 => { state.registers[*dest] = exec_uuid(scalar_func, None).unwrap_or(OwnedValue::Null); } 1 => { - let reg_value = state.registers[*start_reg].borrow_mut(); + let reg_value = state.registers[*start_reg].borrow(); state.registers[*dest] = exec_uuid(scalar_func, Some(reg_value)) .unwrap_or(OwnedValue::Null); } - _ => { - return Err(LimboError::ParseError(format!( - "Invalid number of arguments for Uuid7 function: {}", - arg_count - ))); - } + _ => unreachable!(), }, ScalarFunc::Hex => { let reg_value = state.registers[*start_reg].borrow_mut(); @@ -3120,35 +3117,25 @@ fn exec_random() -> OwnedValue { OwnedValue::Integer(random_number) } -enum UuidType { - V4Blob, - V4Str, - V7Blob, - V7Str, -} - -fn exec_uuid(var: &ScalarFunc, time: Option<&OwnedValue>) -> Result { +fn exec_uuid(var: &ScalarFunc, sec: Option<&OwnedValue>) -> Result { match var { - ScalarFunc::Uuid4Str => Ok(OwnedValue::Text(Rc::new(Uuid::new_v4().to_string()))), ScalarFunc::Uuid4 => Ok(OwnedValue::Blob(Rc::new( Uuid::new_v4().into_bytes().to_vec(), ))), - ScalarFunc::Uuid7 | ScalarFunc::Uuid7Str => { - let uuid = if let Some(OwnedValue::Integer(ref i)) = time { - let ctx = ContextV7::new(); - if *i < 0 { - // not valid unix timestamp, error or null? - return Ok(OwnedValue::Null); + ScalarFunc::Uuid4Str => Ok(OwnedValue::Text(Rc::new(Uuid::new_v4().to_string()))), + ScalarFunc::Uuid7 => { + let uuid = match sec { + Some(OwnedValue::Integer(ref seconds)) => { + let ctx = ContextV7::new(); + if *seconds < 0 { + // not valid unix timestamp, error or null? + return Ok(OwnedValue::Null); + } + Uuid::new_v7(Timestamp::from_unix(ctx, *seconds as u64, 0)) } - Uuid::new_v7(Timestamp::from_unix(ctx, *i as u64, 0)) - } else { - Uuid::now_v7() - }; - return match var { - ScalarFunc::Uuid7Str => Ok(OwnedValue::Text(Rc::new(uuid.to_string()))), - ScalarFunc::Uuid7 => Ok(OwnedValue::Blob(Rc::new(uuid.into_bytes().to_vec()))), - _ => unreachable!(), + _ => Uuid::now_v7(), }; + Ok(OwnedValue::Blob(Rc::new(uuid.into_bytes().to_vec()))) } _ => unreachable!(), } @@ -3188,6 +3175,35 @@ fn exec_uuidblob(reg: &OwnedValue) -> Result { } } +fn exec_ts_from_uuid7(reg: &OwnedValue) -> OwnedValue { + let uuid = match reg { + OwnedValue::Blob(blob) => { + Uuid::from_slice(blob).map_err(|e| LimboError::ParseError(e.to_string())) + } + OwnedValue::Text(val) => { + Uuid::parse_str(val).map_err(|e| LimboError::ParseError(e.to_string())) + } + _ => Err(LimboError::ParseError( + "Invalid argument type for UUID function".to_string(), + )), + }; + match uuid { + Ok(uuid) => OwnedValue::Integer(uuid_to_unix(uuid.as_bytes()) as i64), + // display error? sqlean seems to set value to null + Err(_) => OwnedValue::Null, + } +} + +#[inline(always)] +fn uuid_to_unix(uuid: &[u8; 16]) -> u64 { + ((uuid[0] as u64) << 40) + | ((uuid[1] as u64) << 32) + | ((uuid[2] as u64) << 24) + | ((uuid[3] as u64) << 16) + | ((uuid[4] as u64) << 8) + | (uuid[5] as u64) +} + fn exec_randomblob(reg: &OwnedValue) -> OwnedValue { let length = match reg { OwnedValue::Integer(i) => *i, @@ -4954,4 +4970,202 @@ mod tests { expected_str ); } + + #[test] + fn test_exec_uuid_v4blob() { + use super::{exec_uuid, ScalarFunc}; + use uuid::Uuid; + let func = ScalarFunc::Uuid4; + let owned_val = exec_uuid(&func, None); + match owned_val { + Ok(OwnedValue::Blob(blob)) => { + assert_eq!(blob.len(), 16); + let uuid = Uuid::from_slice(&blob); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 4); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } + + #[test] + fn test_exec_uuid_v4str() { + use super::{exec_uuid, ScalarFunc}; + use uuid::Uuid; + let func = ScalarFunc::Uuid4Str; + let owned_val = exec_uuid(&func, None); + match owned_val { + Ok(OwnedValue::Text(v4str)) => { + assert_eq!(v4str.len(), 36); + let uuid = Uuid::parse_str(&v4str); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 4); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } + + #[test] + fn test_exec_uuid_v7_now() { + use super::{exec_uuid, ScalarFunc}; + use uuid::Uuid; + let func = ScalarFunc::Uuid7; + let owned_val = exec_uuid(&func, None); + match owned_val { + Ok(OwnedValue::Blob(blob)) => { + assert_eq!(blob.len(), 16); + let uuid = Uuid::from_slice(&blob); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 7); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } + + #[test] + fn test_exec_uuid_v7_with_input() { + use super::{exec_uuid, ScalarFunc}; + use uuid::Uuid; + let func = ScalarFunc::Uuid7; + let owned_val = exec_uuid(&func, Some(&OwnedValue::Integer(946702800))); + match owned_val { + Ok(OwnedValue::Blob(blob)) => { + assert_eq!(blob.len(), 16); + let uuid = Uuid::from_slice(&blob); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 7); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } + + #[test] + fn test_exec_uuid_v7_now_to_timestamp() { + use super::{exec_ts_from_uuid7, exec_uuid, ScalarFunc}; + use uuid::Uuid; + let func = ScalarFunc::Uuid7; + let owned_val = exec_uuid(&func, None); + match owned_val { + Ok(OwnedValue::Blob(ref blob)) => { + assert_eq!(blob.len(), 16); + let uuid = Uuid::from_slice(blob); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 7); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + let result = exec_ts_from_uuid7(&owned_val.expect("uuid7")); + if let OwnedValue::Integer(ref ts) = result { + let unixnow = (std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + * 1000) as i64; + assert!(*ts >= unixnow - 1000); + } + } + + #[test] + fn test_exec_uuid_v7_to_timestamp() { + use super::{exec_ts_from_uuid7, exec_uuid, ScalarFunc}; + use uuid::Uuid; + let func = ScalarFunc::Uuid7; + let owned_val = exec_uuid(&func, Some(&OwnedValue::Integer(946702800))); + match owned_val { + Ok(OwnedValue::Blob(ref blob)) => { + assert_eq!(blob.len(), 16); + let uuid = Uuid::from_slice(blob); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 7); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + let result = exec_ts_from_uuid7(&owned_val.expect("uuid7")); + assert_eq!(result, OwnedValue::Integer(946702800 * 1000)); + if let OwnedValue::Integer(ts) = result { + let time = chrono::DateTime::from_timestamp(ts / 1000, 0); + assert_eq!( + time.unwrap(), + "2000-01-01T05:00:00Z" + .parse::>() + .unwrap() + ); + } + } + + #[test] + fn test_exec_uuid_v4_str_to_blob() { + use super::{exec_uuid, exec_uuidblob, ScalarFunc}; + use uuid::Uuid; + let owned_val = exec_uuidblob( + &exec_uuid(&ScalarFunc::Uuid4Str, None).expect("uuid v4 string to generate"), + ); + match owned_val { + Ok(OwnedValue::Blob(blob)) => { + assert_eq!(blob.len(), 16); + let uuid = Uuid::from_slice(&blob); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 4); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } + + #[test] + fn test_exec_uuid_v7_str_to_blob() { + use super::{exec_uuid, exec_uuidblob, exec_uuidstr, ScalarFunc}; + use uuid::Uuid; + // convert a v7 blob to a string then back to a blob + let owned_val = exec_uuidblob( + &exec_uuidstr(&exec_uuid(&ScalarFunc::Uuid7, None).expect("uuid v7 blob to generate")) + .expect("uuid v7 string to generate"), + ); + match owned_val { + Ok(OwnedValue::Blob(blob)) => { + assert_eq!(blob.len(), 16); + let uuid = Uuid::from_slice(&blob); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 7); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } + + #[test] + fn test_exec_uuid_v4_blob_to_str() { + use super::{exec_uuid, exec_uuidstr, ScalarFunc}; + use uuid::Uuid; + // convert a v4 blob to a string + let owned_val = + exec_uuidstr(&exec_uuid(&ScalarFunc::Uuid4, None).expect("uuid v7 blob to generate")); + match owned_val { + Ok(OwnedValue::Text(v4str)) => { + assert_eq!(v4str.len(), 36); + let uuid = Uuid::parse_str(&v4str); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 4); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } + + #[test] + fn test_exec_uuid_v7_blob_to_str() { + use super::{exec_uuid, exec_uuidstr, ScalarFunc}; + use uuid::Uuid; + // convert a v7 blob to a string + let owned_val = exec_uuidstr( + &exec_uuid(&ScalarFunc::Uuid7, Some(&OwnedValue::Integer(123456789))) + .expect("uuid v7 blob to generate"), + ); + match owned_val { + Ok(OwnedValue::Text(v7str)) => { + assert_eq!(v7str.len(), 36); + let uuid = Uuid::parse_str(&v7str); + assert!(uuid.is_ok()); + assert_eq!(uuid.unwrap().get_version_num(), 7); + } + _ => panic!("exec_uuid did not return a Blob variant"), + } + } }