Repository: gluesql/gluesql
Pull request #157 (2021-03-02 13:27:21)

LEFT and RIGHT functions
Relates to #150

KyGost: @panarch I know realise the issue with the way I was doing it and why it was being mapped!-- `None`

Patch:
diff --git a/src/executor/evaluate/error.rs b/src/executor/evaluate/error.rs
index c56783a3..74b4f7ea 100644
--- a/src/executor/evaluate/error.rs
+++ b/src/executor/evaluate/error.rs
@@ -19,6 +19,12 @@ pub enum EvaluateError {
     #[error("function requires string value: {0}")]
     FunctionRequiresStringValue(String),
 
+    #[error("function requires string value: {0}")]
+    FunctionRequiresIntegerValue(String),
+
+    #[error("function requires usize value: {0}")]
+    FunctionRequiresUSizeValue(String),
+
     #[error(
         "number of function parameters not matching (expected: {expected:?}, found: {found:?})"
     )]
diff --git a/src/executor/evaluate/mod.rs b/src/executor/evaluate/mod.rs
index 03381e93..6694c727 100644
--- a/src/executor/evaluate/mod.rs
+++ b/src/executor/evaluate/mod.rs
@@ -14,7 +14,11 @@ use {
     sqlparser::ast::{
         BinaryOperator, Expr, Function, FunctionArg, UnaryOperator, Value as AstValue,
     },
-    std::{convert::TryInto, fmt::Debug, rc::Rc},
+    std::{
+        convert::{TryFrom, TryInto},
+        fmt::Debug,
+        rc::Rc,
+    },
 };
 
 pub use {error::EvaluateError, evaluated::Evaluated};
@@ -175,6 +179,51 @@ async fn evaluate_function<'a, T: 'static + Debug>(
 
             Ok(Evaluated::Value(value))
         }
+        name @ "LEFT" | name @ "RIGHT" => {
+            let return_wrap = |value| Ok(Evaluated::Value(Value::OptStr(value)));
+
+            let convert = |string: String, number: usize| {
+                if name == "LEFT" {
+                    string.get(..number)
+                } else {
+                    let start_pos = if number > string.len() {
+                        0
+                    } else {
+                        string.len() - number
+                    };
+                    string.get(start_pos..)
+                }
+                .or(Some(&string))
+                .map(|value| value.to_string())
+            };
+
+            match args.len() {
+                2 => (),
+                found => {
+                    return Err(EvaluateError::NumberOfFunctionParamsNotMatching {
+                        expected: 2,
+                        found,
+                    }
+                    .into())
+                }
+            }
+
+            let string = match eval(get_arg_expr(&args[0])?).await?.try_into()? {
+                Value::Str(string) | Value::OptStr(Some(string)) => Ok(string),
+                Value::OptStr(None) => return return_wrap(None),
+                _ => Err(EvaluateError::FunctionRequiresStringValue(name.to_string())),
+            }?;
+            let number = match eval(get_arg_expr(&args[1])?).await?.try_into()? {
+                Value::I64(number) | Value::OptI64(Some(number)) => usize::try_from(number)
+                    .map_err(|_| EvaluateError::FunctionRequiresUSizeValue(name.to_string())), // Unlikely to occur hence the imperfect error
+                Value::OptI64(None) => return return_wrap(None),
+                _ => Err(EvaluateError::FunctionRequiresIntegerValue(
+                    name.to_string(),
+                )),
+            }?;
+
+            return_wrap(convert(string, number))
+        }
         name => Err(EvaluateError::FunctionNotSupported(name.to_owned()).into()),
     }
 }


Test patch:
diff --git a/src/tests/function/left_right.rs b/src/tests/function/left_right.rs
new file mode 100644
index 00000000..5e74608a
--- /dev/null
+++ b/src/tests/function/left_right.rs
@@ -0,0 +1,179 @@
+use crate::*;
+
+test_case!(left_right, async move {
+    use Value::OptStr;
+    let test_cases = vec![
+        ("CREATE TABLE Item (name TEXT)", Ok(Payload::Create)),
+        (
+            r#"INSERT INTO Item VALUES ("Blop mc blee"), ("B"), ("Steven the &long named$ folken!")"#,
+            Ok(Payload::Insert(3)),
+        ),
+        (
+            "CREATE TABLE SingleItem (id INTEGER PRIMARY KEY)",
+            Ok(Payload::Create),
+        ),
+        (
+            r#"INSERT INTO SingleItem VALUES (0)"#,
+            Ok(Payload::Insert(1)),
+        ),
+        (
+            "CREATE TABLE NullName (name TEXT NULL)",
+            Ok(Payload::Create),
+        ),
+        (
+            r#"INSERT INTO NullName VALUES (NULL)"#,
+            Ok(Payload::Insert(1)),
+        ),
+        (
+            "CREATE TABLE NullNumber (number INTEGER NULL)",
+            Ok(Payload::Create),
+        ),
+        (
+            r#"INSERT INTO NullNumber VALUES (NULL)"#,
+            Ok(Payload::Insert(1)),
+        ),
+        (
+            "CREATE TABLE NullableName (name TEXT NULL)",
+            Ok(Payload::Create),
+        ),
+        (
+            r#"INSERT INTO NullableName VALUES ('name')"#,
+            Ok(Payload::Insert(1)),
+        ),
+        (
+            r#"SELECT LEFT(name, 3) AS test FROM Item"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                Some("Blo".to_owned());
+                Some("B".to_owned());
+                Some("Ste".to_owned())
+            )),
+        ),
+        (
+            r#"SELECT RIGHT(name, 10) AS test FROM Item"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                Some("op mc blee".to_owned());
+                Some("B".to_owned());
+                Some("d$ folken!".to_owned())
+            )),
+        ),
+        // TODO Concatenation
+        /*(
+            r#"SELECT LEFT((name + 'bobbert'), 10) AS test FROM Item"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                "Blop mc blee".to_owned();
+                "Bbobbert".to_owned();
+                "Steven the".to_owned()
+            )),
+        ),*/
+        (
+            r#"SELECT LEFT('blue', 10) AS test FROM SingleItem"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                Some("blue".to_owned())
+            )),
+        ),
+        (
+            r#"SELECT LEFT("blunder", 3) AS test FROM SingleItem"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                Some("blu".to_owned())
+            )),
+        ),
+        (
+            r#"SELECT LEFT(name, 3) AS test FROM NullName"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                None
+            )),
+        ),
+        (
+            r#"SELECT LEFT('Words', number) AS test FROM NullNumber"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                None
+            )),
+        ),
+        (
+            r#"SELECT LEFT(name, number) AS test FROM NullNumber INNER JOIN NullName ON 1 = 1"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                None
+            )),
+        ),
+        (
+            r#"SELECT LEFT(name, 1) AS test FROM NullableName"#,
+            Ok(select!(
+                "test"
+                OptStr;
+                Some("n".to_owned())
+            )),
+        ),
+        // TODO: Cast cannot handle
+        /*(
+            r#"SELECT LEFT('Words', CAST(NULL AS INTEGER)) AS test FROM SingleItem"#,
+            Ok(select!(
+                "Words"
+                OptStr;
+                Some("blu".to_owned())
+            )),
+        ),
+        (
+            r#"SELECT LEFT(CAST(NULL AS TEXT), 10) AS test FROM SingleItem"#,
+            Ok(select!(
+                ""
+                OptStr;
+                Some("blu".to_owned())
+            )),
+        ),*/
+        (
+            r#"SELECT RIGHT(name, 10, 10) AS test FROM SingleItem"#,
+            Err(EvaluateError::NumberOfFunctionParamsNotMatching {
+                expected: 2,
+                found: 3,
+            }
+            .into()),
+        ),
+        (
+            r#"SELECT RIGHT(name) AS test FROM SingleItem"#,
+            Err(EvaluateError::NumberOfFunctionParamsNotMatching {
+                expected: 2,
+                found: 1,
+            }
+            .into()),
+        ),
+        (
+            r#"SELECT RIGHT() AS test FROM SingleItem"#,
+            Err(EvaluateError::NumberOfFunctionParamsNotMatching {
+                expected: 2,
+                found: 0,
+            }
+            .into()),
+        ),
+        (
+            r#"SELECT RIGHT(1, 1) AS test FROM SingleItem"#,
+            Err(EvaluateError::FunctionRequiresStringValue("RIGHT".to_string()).into()),
+        ),
+        (
+            r#"SELECT RIGHT('Words', 1.1) AS test FROM SingleItem"#,
+            Err(EvaluateError::FunctionRequiresIntegerValue("RIGHT".to_string()).into()),
+        ),
+        (
+            r#"SELECT RIGHT('Words', -4) AS test FROM SingleItem"#,
+            Err(EvaluateError::FunctionRequiresUSizeValue("RIGHT".to_string()).into()),
+        ),
+    ];
+    for (sql, expected) in test_cases.into_iter() {
+        test!(expected, sql);
+    }
+});
diff --git a/src/tests/function/mod.rs b/src/tests/function/mod.rs
index dc8d6da9..9aabde71 100644
--- a/src/tests/function/mod.rs
+++ b/src/tests/function/mod.rs
@@ -1,67 +1,3 @@
 pub mod cast;
-
-use crate::*;
-
-test_case!(function, async move {
-    use Value::Str;
-
-    let test_cases = vec![
-        ("CREATE TABLE Item (name TEXT)", Ok(Payload::Create)),
-        (
-            r#"INSERT INTO Item VALUES ("abcd"), ("Abcd"), ("ABCD")"#,
-            Ok(Payload::Insert(3)),
-        ),
-        (
-            r#"SELECT name FROM Item WHERE LOWER(name) = "abcd";"#,
-            Ok(select!(
-                name Str;
-                "abcd".to_owned();
-                "Abcd".to_owned();
-                "ABCD".to_owned()
-            )),
-        ),
-        (
-            "SELECT LOWER(name), UPPER(name) FROM Item;",
-            Ok(select!(
-                "LOWER(name)"      | "UPPER(name)"
-                Str                | Str;
-                "abcd".to_owned()    "ABCD".to_owned();
-                "abcd".to_owned()    "ABCD".to_owned();
-                "abcd".to_owned()    "ABCD".to_owned()
-            )),
-        ),
-        (
-            r#"
-            SELECT
-                LOWER("Abcd") as lower,
-                UPPER("abCd") as upper
-            FROM Item LIMIT 1;
-            "#,
-            Ok(select!(
-                lower             | upper
-                Str               | Str;
-                "abcd".to_owned()   "ABCD".to_owned()
-            )),
-        ),
-        (
-            "SELECT LOWER() FROM Item",
-            Err(EvaluateError::NumberOfFunctionParamsNotMatching {
-                expected: 1,
-                found: 0,
-            }
-            .into()),
-        ),
-        (
-            "SELECT LOWER(1) FROM Item",
-            Err(EvaluateError::FunctionRequiresStringValue("LOWER".to_owned()).into()),
-        ),
-        (
-            "SELECT WHATEVER(1) FROM Item",
-            Err(EvaluateError::FunctionNotSupported("WHATEVER".to_owned()).into()),
-        ),
-    ];
-
-    for (sql, expected) in test_cases.into_iter() {
-        test!(expected, sql);
-    }
-});
+pub mod left_right;
+pub mod upper_lower;
diff --git a/src/tests/function/upper_lower.rs b/src/tests/function/upper_lower.rs
new file mode 100644
index 00000000..2946d118
--- /dev/null
+++ b/src/tests/function/upper_lower.rs
@@ -0,0 +1,65 @@
+use crate::*;
+
+test_case!(upper_lower, async move {
+    use Value::Str;
+
+    let test_cases = vec![
+        ("CREATE TABLE Item (name TEXT)", Ok(Payload::Create)),
+        (
+            r#"INSERT INTO Item VALUES ("abcd"), ("Abcd"), ("ABCD")"#,
+            Ok(Payload::Insert(3)),
+        ),
+        (
+            r#"SELECT name FROM Item WHERE LOWER(name) = "abcd";"#,
+            Ok(select!(
+                name Str;
+                "abcd".to_owned();
+                "Abcd".to_owned();
+                "ABCD".to_owned()
+            )),
+        ),
+        (
+            "SELECT LOWER(name), UPPER(name) FROM Item;",
+            Ok(select!(
+                "LOWER(name)"      | "UPPER(name)"
+                Str                | Str;
+                "abcd".to_owned()    "ABCD".to_owned();
+                "abcd".to_owned()    "ABCD".to_owned();
+                "abcd".to_owned()    "ABCD".to_owned()
+            )),
+        ),
+        (
+            r#"
+            SELECT
+                LOWER("Abcd") as lower,
+                UPPER("abCd") as upper
+            FROM Item LIMIT 1;
+            "#,
+            Ok(select!(
+                lower             | upper
+                Str               | Str;
+                "abcd".to_owned()   "ABCD".to_owned()
+            )),
+        ),
+        (
+            "SELECT LOWER() FROM Item",
+            Err(EvaluateError::NumberOfFunctionParamsNotMatching {
+                expected: 1,
+                found: 0,
+            }
+            .into()),
+        ),
+        (
+            "SELECT LOWER(1) FROM Item",
+            Err(EvaluateError::FunctionRequiresStringValue("LOWER".to_owned()).into()),
+        ),
+        (
+            "SELECT WHATEVER(1) FROM Item",
+            Err(EvaluateError::FunctionNotSupported("WHATEVER".to_owned()).into()),
+        ),
+    ];
+
+    for (sql, expected) in test_cases.into_iter() {
+        test!(expected, sql);
+    }
+});
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 383c6c26..7307962e 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -66,7 +66,8 @@ macro_rules! generate_tests {
         glue!(drop_table, drop_table::drop_table);
         glue!(error, error::error);
         glue!(filter, filter::filter);
-        glue!(function, function::function);
+        glue!(function_upper_lower, function::upper_lower::upper_lower);
+        glue!(function_left_right, function::left_right::left_right);
         glue!(function_cast_literal, function::cast::cast_literal);
         glue!(function_cast_value, function::cast::cast_value);
         glue!(join, join::join);