Repository: gluesql/gluesql
Pull request #206 (2021-04-18 15:22:51)

Implement text concat operator(||) support,
e.g. `"abc" || "def" = "abcdef"`

related to https://github.com/gluesql/gluesql/issues/201

This follows ANSI SQL which uses text concat operator as `||`.

This PR handles concat of `literal || literal`, `value || value` and `literal || value`.
In any cases, `NULL || value` is `NULL`.


Patch:
diff --git a/src/data/literal.rs b/src/data/literal.rs
index c816f372..12374433 100644
--- a/src/data/literal.rs
+++ b/src/data/literal.rs
@@ -164,8 +164,52 @@ impl<'a> Literal<'a> {
         }
     }
 
+    pub fn concat(self, other: Literal<'_>) -> Self {
+        let convert = |literal| match literal {
+            Boolean(v) => Some(if v {
+                "TRUE".to_owned()
+            } else {
+                "FALSE".to_owned()
+            }),
+            Number(v) => Some(v.into_owned()),
+            Text(v) => Some(v.into_owned()),
+            Null => None,
+        };
+
+        match (convert(self), convert(other)) {
+            (Some(l), Some(r)) => Literal::Text(Cow::Owned(l + &r)),
+            _ => Literal::Null,
+        }
+    }
+
     binary_op!(add, +);
     binary_op!(subtract, -);
     binary_op!(multiply, *);
     binary_op!(divide, /);
 }
+
+#[cfg(test)]
+mod tests {
+    use super::Literal::*;
+    use std::borrow::Cow;
+
+    #[test]
+    fn concat() {
+        macro_rules! text {
+            ($text: expr) => {
+                Text(Cow::Owned($text.to_owned()))
+            };
+        }
+
+        let num = || Number(Cow::Owned("123".to_owned()));
+        let text = || text!("Foo");
+
+        assert_eq!(Boolean(true).concat(num()), text!("TRUE123"));
+        assert_eq!(Boolean(false).concat(text()), text!("FALSEFoo"));
+        assert_eq!(num().concat(num()), text!("123123"));
+        assert_eq!(text().concat(num()), text!("Foo123"));
+        matches!(text().concat(Null), Null);
+        matches!(Null.concat(Boolean(true)), Null);
+        matches!(Null.concat(Null), Null);
+    }
+}
diff --git a/src/data/value/into.rs b/src/data/value/into.rs
index 913fafef..45b04163 100644
--- a/src/data/value/into.rs
+++ b/src/data/value/into.rs
@@ -16,6 +16,15 @@ impl From<&Value> for String {
     }
 }
 
+impl From<Value> for String {
+    fn from(v: Value) -> String {
+        match v {
+            Value::Str(value) => value,
+            _ => String::from(&v),
+        }
+    }
+}
+
 impl TryInto<bool> for &Value {
     type Error = Error;
     fn try_into(self) -> Result<bool> {
diff --git a/src/data/value/mod.rs b/src/data/value/mod.rs
index 1feda632..5e865c2b 100644
--- a/src/data/value/mod.rs
+++ b/src/data/value/mod.rs
@@ -127,6 +127,13 @@ impl Value {
         }
     }
 
+    pub fn concat(&self, other: &Value) -> Value {
+        match (self, other) {
+            (Value::Null, _) | (_, Value::Null) => Value::Null,
+            _ => Value::Str(String::from(self) + &String::from(other)),
+        }
+    }
+
     pub fn add(&self, other: &Value) -> Result<Value> {
         use Value::*;
 
@@ -265,4 +272,16 @@ mod tests {
         cast!(F64(1.0)      => Text, Str("1".to_owned()));
         cast!(Null          => Text, Null);
     }
+
+    #[test]
+    fn concat() {
+        let a = Str("A".to_owned());
+
+        assert_eq!(a.concat(&Str("B".to_owned())), Str("AB".to_owned()));
+        assert_eq!(a.concat(&Bool(true)), Str("ATRUE".to_owned()));
+        assert_eq!(a.concat(&I64(1)), Str("A1".to_owned()));
+        assert_eq!(a.concat(&F64(1.0)), Str("A1".to_owned()));
+        assert_eq!(I64(2).concat(&I64(1)), Str("21".to_owned()));
+        matches!(a.concat(&Null), Null);
+    }
 }
diff --git a/src/executor/evaluate/evaluated.rs b/src/executor/evaluate/evaluated.rs
index b8cb140f..eb3fdedd 100644
--- a/src/executor/evaluate/evaluated.rs
+++ b/src/executor/evaluate/evaluated.rs
@@ -119,6 +119,23 @@ impl<'a> Evaluated<'a> {
         .map(Evaluated::from)
     }
 
+    pub fn concat(self, other: Evaluated) -> Result<Evaluated<'a>> {
+        let evaluated = match (self, other) {
+            (Evaluated::Literal(l), Evaluated::Literal(r)) => Evaluated::Literal(l.concat(r)),
+            (Evaluated::Literal(l), Evaluated::Value(r)) => {
+                Evaluated::from((&Value::try_from(l)?).concat(r.as_ref()))
+            }
+            (Evaluated::Value(l), Evaluated::Literal(r)) => {
+                Evaluated::from(l.as_ref().concat(&Value::try_from(r)?))
+            }
+            (Evaluated::Value(l), Evaluated::Value(r)) => {
+                Evaluated::from(l.as_ref().concat(r.as_ref()))
+            }
+        };
+
+        Ok(evaluated)
+    }
+
     pub fn is_some(&self) -> bool {
         match self {
             Evaluated::Value(v) => v.is_some(),
diff --git a/src/executor/evaluate/mod.rs b/src/executor/evaluate/mod.rs
index b530b3b1..cd1c0b6d 100644
--- a/src/executor/evaluate/mod.rs
+++ b/src/executor/evaluate/mod.rs
@@ -89,6 +89,7 @@ pub async fn evaluate<'a, T: 'static + Debug>(
                 BinaryOperator::Minus => l.subtract(&r),
                 BinaryOperator::Multiply => l.multiply(&r),
                 BinaryOperator::Divide => l.divide(&r),
+                BinaryOperator::StringConcat => l.concat(r),
                 _ => Err(EvaluateError::Unimplemented.into()),
             }
         }
diff --git a/src/glue/value/mod.rs b/src/glue/value/mod.rs
index 0d56580c..8218b29c 100644
--- a/src/glue/value/mod.rs
+++ b/src/glue/value/mod.rs
@@ -6,12 +6,6 @@ use {
     std::convert::TryInto,
 };
 
-impl From<Value> for String {
-    fn from(v: Value) -> String {
-        (&v).into()
-    }
-}
-
 impl TryInto<bool> for Value {
     type Error = Error;
     fn try_into(self) -> Result<bool> {


Test patch:
diff --git a/src/tests/concat.rs b/src/tests/concat.rs
new file mode 100644
index 00000000..825eed46
--- /dev/null
+++ b/src/tests/concat.rs
@@ -0,0 +1,93 @@
+use crate::*;
+
+test_case!(concat, async move {
+    use Value::{Null, Str};
+
+    run!(
+        "
+        CREATE TABLE Concat (
+            id INTEGER,
+            rate FLOAT,
+            flag BOOLEAN,
+            text TEXT,
+            null_value TEXT NULL,
+        );
+    "
+    );
+    run!(r#"INSERT INTO Concat VALUES (1, 2.3, TRUE, "Foo", NULL);"#);
+
+    test!(
+        Ok(select!(
+            value_value         | value_literal       | literal_value       | literal_literal
+            Str                 | Str                 | Str                 | Str;
+            "FooFoo".to_owned()   "FooBar".to_owned()   "BarFoo".to_owned()   "FooBar".to_owned()
+        )),
+        r#"
+        SELECT
+            text || text AS value_value,
+            text || "Bar" AS value_literal,
+            "Bar" || text AS literal_value,
+            "Foo" || "Bar" AS literal_literal
+        FROM Concat;
+        "#
+    );
+
+    test!(
+        Ok(select_with_null!(
+            id_n | rate_n | flag_n | text_n | n_id | n_text;
+            Null   Null     Null     Null     Null   Null
+        )),
+        "SELECT
+            id || null_value AS id_n,
+            rate || null_value AS rate_n,
+            flag || null_value AS flag_n,
+            text || null_value AS text_n,
+            null_value || id AS n_id,
+            null_value || text AS n_text
+        FROM
+            Concat;"
+    );
+
+    test!(
+        Ok(select!(
+            "id || rate"      | "rate || flag"       | "flag || text"       | "id || text"
+            Str               | Str                  | Str                  | Str;
+            "12.3".to_owned()   "2.3TRUE".to_owned()   "TRUEFoo".to_owned()   "1Foo".to_owned()
+        )),
+        "SELECT
+            id || rate,
+            rate || flag,
+            flag || text,
+            id || text
+        FROM
+            Concat;"
+    );
+
+    test!(
+        Ok(select!(
+            int_float         | float_bool           | bool_text             | int_text
+            Str               | Str                  | Str                   | Str;
+            "12.3".to_owned()   "2.3TRUE".to_owned()   "FALSEFoo".to_owned()   "1Bar".to_owned()
+        )),
+        r#"SELECT
+            1 || 2.3 AS int_float,
+            2.3 || TRUE AS float_bool,
+            FALSE || "Foo" AS bool_text,
+            1 || "Bar" AS int_text
+        FROM
+            Concat;"#
+    );
+
+    test!(
+        Ok(select_with_null!(
+            Case1                      | Case2                         | Case3;
+            Str("112.3Bar".to_owned())   Str("1TRUE3.5Foo".to_owned())   Null
+        )),
+        r#"SELECT
+            1 || id || rate || "Bar" AS Case1,
+            id || flag || 3.5 || text AS Case2,
+            flag || "wow" || null_value AS Case3
+        FROM
+            Concat;"#
+    );
+});
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 7307962e..b58d47ae 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -4,6 +4,7 @@ pub mod alter_table;
 pub mod arithmetic;
 pub mod basic;
 pub mod blend;
+pub mod concat;
 pub mod create_table;
 pub mod default;
 pub mod drop_table;
@@ -60,6 +61,7 @@ macro_rules! generate_tests {
         glue!(aggregate_group_by, aggregate::group_by);
         glue!(arithmetic, arithmetic::arithmetic);
         glue!(arithmetic_blend, arithmetic::blend);
+        glue!(concat, concat::concat);
         glue!(blend, blend::blend);
         glue!(create_table, create_table::create_table);
         glue!(default, default::default);