From 77c3d130f37d86c406b3293a76eddc7ad173b10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=A0=EC=9A=B0?= Date: Fri, 16 Aug 2024 06:26:06 +0900 Subject: [PATCH] Add char function support --- COMPAT.md | 2 +- core/function.rs | 3 +++ core/translate/expr.rs | 22 ++++++++++++++++++ core/vdbe/mod.rs | 44 +++++++++++++++++++++++++++++++++-- testing/scalar-functions.test | 16 +++++++++++++ 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index 1e0def3aa..d36b38991 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -63,7 +63,7 @@ This document describes the SQLite compatibility status of Limbo: |------------------------------|--------|---------| | abs(X) | Yes | | | changes() | No | | -| char(X1,X2,...,XN) | No | | +| char(X1,X2,...,XN) | Yes | | | coalesce(X,Y,...) | Yes | | | concat(X,...) | No | | | concat_ws(SEP,X,...) | No | | diff --git a/core/function.rs b/core/function.rs index d6cc28947..f12e22c48 100644 --- a/core/function.rs +++ b/core/function.rs @@ -40,6 +40,7 @@ impl AggFunc { #[derive(Debug, Clone, PartialEq)] pub enum ScalarFunc { + Char, Coalesce, Like, Abs, @@ -62,6 +63,7 @@ pub enum ScalarFunc { impl ToString for ScalarFunc { fn to_string(&self) -> String { match self { + ScalarFunc::Char => "char".to_string(), ScalarFunc::Coalesce => "coalesce".to_string(), ScalarFunc::Like => "like(2)".to_string(), ScalarFunc::Abs => "abs".to_string(), @@ -103,6 +105,7 @@ impl Func { "string_agg" => Ok(Func::Agg(AggFunc::StringAgg)), "sum" => Ok(Func::Agg(AggFunc::Sum)), "total" => Ok(Func::Agg(AggFunc::Total)), + "char" => Ok(Func::Scalar(ScalarFunc::Char)), "coalesce" => Ok(Func::Scalar(ScalarFunc::Coalesce)), "like" => Ok(Func::Scalar(ScalarFunc::Like)), "abs" => Ok(Func::Scalar(ScalarFunc::Abs)), diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 882f64c3b..2b20ca04f 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -164,6 +164,28 @@ pub fn translate_expr( }, Some(Func::Scalar(srf)) => { match srf { + ScalarFunc::Char => { + let args = if let Some(args) = args { + args + } else { + crate::bail_parse_error!( + "{} function with no arguments", + srf.to_string() + ); + }; + + for arg in args.iter() { + let reg = program.alloc_register(); + translate_expr(program, select, arg, reg, cursor_hint)?; + } + + program.emit_insn(Insn::Function { + start_reg: target_register + 1, + dest: target_register, + func: crate::vdbe::Func::Scalar(srf), + }); + Ok(target_register) + } ScalarFunc::Coalesce => { let args = if let Some(args) = args { if args.len() < 2 { diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index bdcf51a7b..45af1e055 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -1185,6 +1185,12 @@ impl Program { } state.pc += 1; } + Func::Scalar(ScalarFunc::Char) => { + let start_reg = *start_reg; + let reg_values = state.registers[start_reg..state.registers.len()].to_vec(); + state.registers[*dest] = exec_char(reg_values); + state.pc += 1; + } Func::Scalar(ScalarFunc::Coalesce) => {} Func::Scalar(ScalarFunc::Like) => { let start_reg = *start_reg; @@ -1636,6 +1642,20 @@ fn exec_random() -> OwnedValue { OwnedValue::Integer(random_number) } +fn exec_char(values: Vec) -> OwnedValue { + let result: String = values + .iter() + .filter_map(|x| { + if let OwnedValue::Integer(i) = x { + Some(*i as u8 as char) + } else { + None + } + }) + .collect(); + OwnedValue::Text(Rc::new(result)) +} + // Implements LIKE pattern matching. fn exec_like(pattern: &str, text: &str) -> bool { let re = Regex::new(&pattern.replace('%', ".*").replace('_', ".").to_string()).unwrap(); @@ -1798,12 +1818,14 @@ fn exec_if(reg: &OwnedValue, null_reg: &OwnedValue, not: bool) -> bool { #[cfg(test)] mod tests { use super::{ - exec_abs, exec_if, exec_length, exec_like, exec_lower, exec_ltrim, exec_minmax, + exec_abs, exec_char, exec_if, exec_length, exec_like, exec_lower, exec_ltrim, exec_minmax, exec_random, exec_round, exec_rtrim, exec_substring, exec_trim, exec_unicode, exec_upper, get_new_rowid, Cursor, CursorResult, LimboError, OwnedRecord, OwnedValue, Result, }; - use mockall::{mock, predicate, predicate::*}; + use mockall::{mock, predicate}; use rand::{rngs::mock::StepRng, thread_rng}; + use rusqlite::types::ToSqlOutput::Owned; + use rustix::path::Arg; use std::{cell::Ref, rc::Rc}; mock! { @@ -2118,6 +2140,24 @@ mod tests { ); assert_eq!(exec_abs(&OwnedValue::Null).unwrap(), OwnedValue::Null); } + + #[test] + fn test_char() { + assert_eq!( + exec_char(vec![OwnedValue::Integer(108), OwnedValue::Integer(105)]), + OwnedValue::Text(Rc::new("li".to_string())) + ); + assert_eq!(exec_char(vec![]), OwnedValue::Text(Rc::new("".to_string()))); + assert_eq!( + exec_char(vec![OwnedValue::Null]), + OwnedValue::Text(Rc::new("".to_string())) + ); + assert_eq!( + exec_char(vec![OwnedValue::Text(Rc::new("a".to_string()))]), + OwnedValue::Text(Rc::new("".to_string())) + ); + } + #[test] fn test_like() { assert!(exec_like("a%", "aaaa")); diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index 41eed2fcc..a82d069db 100755 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -3,6 +3,22 @@ set testdir [file dirname $argv0] source $testdir/tester.tcl +do_execsql_test char { + select char(108, 105) +} {li} + +do_execsql_test char-empty { + select char() +} {} + +do_execsql_test char-null { + select char(null) +} {} + +do_execsql_test char-non-integer { + select char('a') +} {} + do_execsql_test abs { select abs(1); } {1}