Repository: databricks/koalas

Issue:
String concatenation does not work correctly
When concatenating columns and assigning to another column works for numeric columns, it does not work for strings:

Works for numeric columns:
```python
df["a_b"] = df["a_b"] + df["a_b"]
df.toPandas()

# Out[29]: 
#    id  a.b  a_b
# 0   0    1    4
```

Does not work for strings:
```python
from pyspark.sql.functions import lit
spark.range(1).withColumn("a", lit("abc")).withColumn("b", lit("def")).withColumn("c", lit("+")).coalesce(1).write.mode("overwrite").parquet("/mnt/ivan/sample2.parquet")

df = ks.read_parquet("/mnt/ivan/sample2.parquet")
df["c"] = df["a"] + df["b"]
df.toPandas()

# Out[36]: 
#    id    a    b     c
# 0   0  abc  def  None
```

However, this works in Pandas:
```python
df = ks.read_parquet("/mnt/ivan/sample2.parquet").toPandas()
df["c"] = df["a"] + df["b"]
df

# Out[38]: 
#    id    a    b       c
# 0   0  abc  def  abcdef
```


Discussion:
Want to submit a PR?
Yes, I will do.

Resolving patch:
diff --git a/databricks/koalas/series.py b/databricks/koalas/series.py
--- a/databricks/koalas/series.py
+++ b/databricks/koalas/series.py
@@ -25,8 +25,8 @@
 
 from pyspark import sql as spark
 from pyspark.sql import functions as F
-from pyspark.sql.types import BooleanType, FloatType, DoubleType, LongType, StructType, \
-    TimestampType, to_arrow_type
+from pyspark.sql.types import BooleanType, FloatType, DoubleType, LongType, StringType, \
+    StructType, TimestampType, to_arrow_type
 
 from databricks import koalas as ks  # For running doctests and reference resolution in PyCharm.
 from databricks.koalas.dask.utils import derived_from
@@ -124,7 +124,19 @@ def _init_from_spark(self, scol, kdf, index_info, *args):
 
     # arithmetic operators
     __neg__ = _column_op(spark.Column.__neg__)
-    __add__ = _column_op(spark.Column.__add__)
+
+    def __add__(self, other):
+        if isinstance(self.spark_type, StringType):
+            # Concatenate string columns
+            if isinstance(other, Series) and isinstance(other.spark_type, StringType):
+                return _column_op(F.concat)(self, other)
+            # Handle df['col'] + 'literal'
+            elif isinstance(other, str):
+                return _column_op(F.concat)(self, F.lit(other))
+            else:
+                raise TypeError('string addition can only be applied to string series or literals.')
+        else:
+            return _column_op(spark.Column.__add__)(self, other)
 
     def __sub__(self, other):
         # Note that timestamp subtraction casts arguments to integer. This is to mimic Pandas's
@@ -140,7 +152,14 @@ def __sub__(self, other):
     __div__ = _numpy_column_op(spark.Column.__div__)
     __truediv__ = _numpy_column_op(spark.Column.__truediv__)
     __mod__ = _column_op(spark.Column.__mod__)
-    __radd__ = _column_op(spark.Column.__radd__)
+
+    def __radd__(self, other):
+        # Handle 'literal' + df['col']
+        if isinstance(self.spark_type, StringType) and isinstance(other, str):
+            return Series(F.concat(F.lit(other), self._scol), self._kdf, self._index_info)
+        else:
+            return _column_op(spark.Column.__radd__)(self, other)
+
     __rsub__ = _column_op(spark.Column.__rsub__)
     __rmul__ = _column_op(spark.Column.__rmul__)
     __rdiv__ = _numpy_column_op(spark.Column.__rdiv__)


Test patch:
diff --git a/databricks/koalas/tests/test_series_string.py b/databricks/koalas/tests/test_series_string.py
new file mode 100644
--- /dev/null
+++ b/databricks/koalas/tests/test_series_string.py
@@ -0,0 +1,48 @@
+#
+# Copyright (C) 2019 Databricks, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+import pandas as pd
+
+from databricks import koalas
+from databricks.koalas.testing.utils import ReusedSQLTestCase, SQLTestUtils
+
+
+class DataFrameTest(ReusedSQLTestCase, SQLTestUtils):
+    def test_string_add_str_num(self):
+        pdf = pd.DataFrame(dict(col1=['a'], col2=[1]))
+        ds = koalas.from_pandas(pdf)
+        with self.assertRaises(TypeError):
+            ds['col1'] + ds['col2']
+
+    def test_string_add_assign(self):
+        pdf = pd.DataFrame(dict(col1=['a', 'b', 'c'], col2=['1', '2', '3']))
+        ds = koalas.from_pandas(pdf)
+        ds['col1'] += ds['col2']
+        pdf['col1'] += pdf['col2']
+        self.assert_eq((ds['col1']).to_pandas(), pdf['col1'])
+
+    def test_string_add_str_str(self):
+        pdf = pd.DataFrame(dict(col1=['a', 'b', 'c'], col2=['1', '2', '3']))
+        ds = koalas.from_pandas(pdf)
+        self.assert_eq((ds['col1'] + ds['col2']).to_pandas(), pdf['col1'] + pdf['col2'])
+        self.assert_eq((ds['col2'] + ds['col1']).to_pandas(), pdf['col2'] + pdf['col1'])
+
+    def test_string_add_str_lit(self):
+        pdf = pd.DataFrame(dict(col1=['a', 'b', 'c']))
+        ds = koalas.from_pandas(pdf)
+        self.assert_eq((ds['col1'] + '_lit').to_pandas(), pdf['col1'] + '_lit')
+        self.assert_eq(('_lit' + ds['col1']).to_pandas(), '_lit' + pdf['col1'])