diff --git a/mysql-test/suite/compat/oracle/r/func_concat.result b/mysql-test/suite/compat/oracle/r/func_concat.result
new file mode 100644
index 0000000..0367e96
--- /dev/null
+++ b/mysql-test/suite/compat/oracle/r/func_concat.result
@@ -0,0 +1,41 @@
+SET sql_mode=ORACLE;
+EXPLAIN EXTENDED SELECT 'a'||'b'||'c';
+id	select_type	table	type	possible_keys	key	key_len	ref	rows	filtered	Extra
+1	SIMPLE	NULL	NULL	NULL	NULL	NULL	NULL	NULL	NULL	No tables used
+Warnings:
+Note	1003	select 'a' || 'b' || 'c' AS "'a'||'b'||'c'"
+SELECT 'a'||'b';
+'a'||'b'
+ab
+SELECT 'a'||'b'||NULL;
+'a'||'b'||NULL
+ab
+SELECT NULL||'b'||NULL;
+NULL||'b'||NULL
+b
+SELECT NULL||NULL||'c';
+NULL||NULL||'c'
+c
+SELECT NULL||NULL;
+NULL||NULL
+NULL
+CREATE TABLE t1 (a VARCHAR(10), b VARCHAR(10), c VARCHAR(10), ord integer);
+INSERT INTO t1 VALUES (NULL,NULL,NULL,0);
+INSERT INTO t1 VALUES ('a',NULL,NULL,1);
+INSERT INTO t1 VALUES (NULL,'b',NULL,2);
+INSERT INTO t1 VALUES (NULL,NULL,'c',3);
+INSERT INTO t1 VALUES ('a','b',NULL,4);
+INSERT INTO t1 VALUES (NULL,'b','c',5);
+INSERT INTO t1 VALUES ('a',NULL,'c',6);
+INSERT INTO t1 VALUES ('a','b','c',6);
+SELECT a||b||c FROM t1 order by ord asc;
+a||b||c 
+NULL
+a
+b
+c
+ab
+bc
+ac
+abc
+DROP TABLE t1;
diff --git a/mysql-test/suite/compat/oracle/r/ps.result b/mysql-test/suite/compat/oracle/r/ps.result
index 7117467..4ecbe3c 100644
--- a/mysql-test/suite/compat/oracle/r/ps.result
+++ b/mysql-test/suite/compat/oracle/r/ps.result
@@ -178,9 +178,9 @@ EXECUTE IMMEDIATE 'SELECT :1 FROM DUAL' USING 10;
 # Testing erroneous and diallowed prepare source
 #
 EXECUTE IMMEDIATE _latin1'SELECT 1 AS c FROM ' || _latin2 'DUAL';
-ERROR HY000: Illegal mix of collations (latin1_swedish_ci,COERCIBLE) and (latin2_general_ci,COERCIBLE) for operation 'concat'
+ERROR HY000: Illegal mix of collations (latin1_swedish_ci,COERCIBLE) and (latin2_general_ci,COERCIBLE) for operation '||'
 PREPARE stmt FROM _latin1'SELECT 1 AS c FROM ' || _latin2 'DUAL';
-ERROR HY000: Illegal mix of collations (latin1_swedish_ci,COERCIBLE) and (latin2_general_ci,COERCIBLE) for operation 'concat'
+ERROR HY000: Illegal mix of collations (latin1_swedish_ci,COERCIBLE) and (latin2_general_ci,COERCIBLE) for operation '||'
 EXECUTE IMMEDIATE (SELECT 'SELECT 1');
 ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'SELECT 'SELECT 1')' at line 1
 PREPARE stmt FROM (SELECT 'SELECT 1');
diff --git a/mysql-test/suite/compat/oracle/t/func_concat.test b/mysql-test/suite/compat/oracle/t/func_concat.test
new file mode 100644
index 0000000..629173a
--- /dev/null
+++ b/mysql-test/suite/compat/oracle/t/func_concat.test
@@ -0,0 +1,25 @@
+#
+# Testing CONCAT with null values
+#
+
+SET sql_mode=ORACLE;
+
+EXPLAIN EXTENDED SELECT 'a'||'b'||'c';
+
+SELECT 'a'||'b';
+SELECT 'a'||'b'||NULL;
+SELECT NULL||'b'||NULL;
+SELECT NULL||NULL||'c';
+SELECT NULL||NULL;
+
+CREATE TABLE t1 (a VARCHAR(10), b VARCHAR(10), c VARCHAR(10), ord integer);
+INSERT INTO t1 VALUES (NULL,NULL,NULL,0);
+INSERT INTO t1 VALUES ('a',NULL,NULL,1);
+INSERT INTO t1 VALUES (NULL,'b',NULL,2);
+INSERT INTO t1 VALUES (NULL,NULL,'c',3);
+INSERT INTO t1 VALUES ('a','b',NULL,4);
+INSERT INTO t1 VALUES (NULL,'b','c',5);
+INSERT INTO t1 VALUES ('a',NULL,'c',6);
+INSERT INTO t1 VALUES ('a','b','c',6);
+SELECT a||b||c FROM t1 order by ord asc;
+DROP TABLE t1;
diff --git a/sql/item_strfunc.cc b/sql/item_strfunc.cc
index fe1c470..1da5eef 100644
--- a/sql/item_strfunc.cc
+++ b/sql/item_strfunc.cc
@@ -617,125 +617,80 @@ String *Item_func_decode_histogram::val_str(String *str)
 String *Item_func_concat::val_str(String *str)
 {
   DBUG_ASSERT(fixed == 1);
-  String *res,*res2,*use_as_buff;
+  String *res,*use_as_buff;
   uint i;
   bool is_const= 0;
 
   null_value=0;
-  if (!(res=args[0]->val_str(str)))
+  if (!(res= arg_val_str(0, str, &is_const)))
     goto null;
   use_as_buff= &tmp_value;
-  is_const= args[0]->const_item();
   for (i=1 ; i < arg_count ; i++)
   {
     if (res->length() == 0)
     {
-      if (!(res=args[i]->val_str(str)))
-	goto null;
       /*
        CONCAT accumulates its result in the result of its the first
        non-empty argument. Because of this we need is_const to be 
        evaluated only for it.
       */
-      is_const= args[i]->const_item();
+      if (!(res= arg_val_str(i, str, &is_const)))
+        goto null;
     }
     else
     {
+      const String *res2;
       if (!(res2=args[i]->val_str(use_as_buff)))
-	goto null;
-      if (res2->length() == 0)
-	continue;
-      if (res->length()+res2->length() >
-	  current_thd->variables.max_allowed_packet)
-      {
-        THD *thd= current_thd;
-	push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN,
-			    ER_WARN_ALLOWED_PACKET_OVERFLOWED,
-			    ER_THD(thd, ER_WARN_ALLOWED_PACKET_OVERFLOWED),
-                            func_name(),
-			    thd->variables.max_allowed_packet);
-	goto null;
-      }
-      if (!is_const && res->alloced_length() >= res->length()+res2->length())
-      {						// Use old buffer
-	res->append(*res2);
-      }
-      else if (str->alloced_length() >= res->length()+res2->length())
-      {
-	if (str->ptr() == res2->ptr())
-	  str->replace(0,0,*res);
-	else
-	{
-	  str->copy(*res);
-	  str->append(*res2);
-	}
-        res= str;
-        use_as_buff= &tmp_value;
-      }
-      else if (res == &tmp_value)
-      {
-	if (res->append(*res2))			// Must be a blob
-	  goto null;
-      }
-      else if (res2 == &tmp_value)
-      {						// This can happend only 1 time
-	if (tmp_value.replace(0,0,*res))
-	  goto null;
-	res= &tmp_value;
-	use_as_buff=str;			// Put next arg here
-      }
-      else if (tmp_value.is_alloced() && res2->ptr() >= tmp_value.ptr() &&
-	       res2->ptr() <= tmp_value.ptr() + tmp_value.alloced_length())
-      {
-	/*
-	  This happens really seldom:
-	  In this case res2 is sub string of tmp_value.  We will
-	  now work in place in tmp_value to set it to res | res2
-	*/
-	/* Chop the last characters in tmp_value that isn't in res2 */
-	tmp_value.length((uint32) (res2->ptr() - tmp_value.ptr()) +
-			 res2->length());
-	/* Place res2 at start of tmp_value, remove chars before res2 */
-	if (tmp_value.replace(0,(uint32) (res2->ptr() - tmp_value.ptr()),
-			      *res))
-	  goto null;
-	res= &tmp_value;
-	use_as_buff=str;			// Put next arg here
-      }
-      else
-      {						// Two big const strings
-        /*
-          NOTE: We should be prudent in the initial allocation unit -- the
-          size of the arguments is a function of data distribution, which
-          can be any. Instead of overcommitting at the first row, we grow
-          the allocated amount by the factor of 2. This ensures that no
-          more than 25% of memory will be overcommitted on average.
-        */
+        goto null;
 
-        uint concat_len= res->length() + res2->length();
+      if (!(res= append_value(res, is_const, str, &use_as_buff, res2)))
+        goto null;
+      is_const= 0;
+    }
+  }
+  res->set_charset(collation.collation);
+  return res;
 
-        if (tmp_value.alloced_length() < concat_len)
-        {
-          if (tmp_value.alloced_length() == 0)
-          {
-            if (tmp_value.alloc(concat_len))
-              goto null;
-          }
-          else
-          {
-            uint new_len = MY_MAX(tmp_value.alloced_length() * 2, concat_len);
+null:
+  null_value=1;
+  return 0;
+}
 
-            if (tmp_value.realloc(new_len))
-              goto null;
-          }
-        }
 
-	if (tmp_value.copy(*res) || tmp_value.append(*res2))
-	  goto null;
+String *Item_func_concat_operator_oracle::val_str(String *str)
+{
+  DBUG_ASSERT(fixed == 1);
+  String *res, *use_as_buff;
+  uint i;
+  bool is_const= false;
+  null_value= 0;
 
-	res= &tmp_value;
-	use_as_buff=str;
-      }
+  // Search first non null argument
+  for (i= 0; i < arg_count; i++)
+  {
+    if ((res= arg_val_str(i, str, &is_const)))
+      break;
+  }
+  if (i == arg_count)
+    goto null;
+
+  use_as_buff= &tmp_value;
+
+  for (i++ ; i < arg_count ; i++)
+  {
+    if (res->length() == 0)
+    {
+      // See comments in Item_func_concat::val_str()
+      if (!(res= arg_val_str(i, str, &is_const)))
+        continue;
+    }
+    else
+    {
+      const String *res2;
+      if (!(res2= args[i]->val_str(use_as_buff)))
+        continue;
+      if (!(res= append_value(res, is_const, str, &use_as_buff, res2)))
+        goto null;
       is_const= 0;
     }
   }
@@ -743,11 +698,120 @@ String *Item_func_concat::val_str(String *str)
   return res;
 
 null:
-  null_value=1;
+  null_value= true;
   return 0;
 }
 
 
+String *Item_func_concat::append_value(String *res,
+                                       bool res_is_const,
+                                       String *str,
+                                       String **use_as_buff,
+                                       const String *res2)
+{
+  if (res2->length() == 0)
+    return res;
+
+  if ((ulong) res->length() + (ulong) res2->length() >
+      current_thd->variables.max_allowed_packet)
+  {
+    THD *thd= current_thd;
+    push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN,
+                        ER_WARN_ALLOWED_PACKET_OVERFLOWED,
+                        ER_THD(thd, ER_WARN_ALLOWED_PACKET_OVERFLOWED),
+                        func_name(),
+                        thd->variables.max_allowed_packet);
+    return NULL;
+  }
+
+  uint concat_len= res->length() + res2->length();
+
+  if (!res_is_const && res->alloced_length() >= concat_len)
+  {                                         // Use old buffer
+    res->append(*res2);
+    return res;
+  }
+
+  if (str->alloced_length() >= concat_len)
+  {
+    if (str->ptr() == res2->ptr())
+      str->replace(0, 0, *res);
+    else
+    {
+      str->copy(*res);
+      str->append(*res2);
+    }
+    *use_as_buff= &tmp_value;
+    return str;
+  }
+
+  if (res == &tmp_value)
+  {
+    if (res->append(*res2))                 // Must be a blob
+      return NULL;
+    return res;
+  }
+
+  if (res2 == &tmp_value)
+  {                                         // This can happend only 1 time
+    if (tmp_value.replace(0, 0, *res))
+      return NULL;
+    *use_as_buff= str;                      // Put next arg here
+    return &tmp_value;
+  }
+
+  if (tmp_value.is_alloced() && res2->ptr() >= tmp_value.ptr() &&
+      res2->ptr() <= tmp_value.ptr() + tmp_value.alloced_length())
+  {
+    /*
+      This happens really seldom:
+      In this case res2 is sub string of tmp_value.  We will
+      now work in place in tmp_value to set it to res | res2
+    */
+    /* Chop the last characters in tmp_value that isn't in res2 */
+    tmp_value.length((uint32) (res2->ptr() - tmp_value.ptr()) +
+                     res2->length());
+    /* Place res2 at start of tmp_value, remove chars before res2 */
+    if (tmp_value.replace(0,(uint32) (res2->ptr() - tmp_value.ptr()),
+                          *res))
+      return NULL;
+    *use_as_buff= str;                      // Put next arg here
+    return &tmp_value;
+  }
+
+  /*
+    Two big const strings
+    NOTE: We should be prudent in the initial allocation unit -- the
+    size of the arguments is a function of data distribution, which
+    can be any. Instead of overcommitting at the first row, we grow
+    the allocated amount by the factor of 2. This ensures that no
+    more than 25% of memory will be overcommitted on average.
+  */
+
+  if (tmp_value.alloced_length() < concat_len)
+  {
+    if (tmp_value.alloced_length() == 0)
+    {
+      if (tmp_value.alloc(concat_len))
+        return NULL;
+    }
+    else
+    {
+      uint new_len = MY_MAX(tmp_value.alloced_length() * 2, concat_len);
+
+      if (tmp_value.realloc(new_len))
+        return NULL;
+    }
+  }
+
+  if (tmp_value.copy(*res) || tmp_value.append(*res2))
+    return NULL;
+
+  *use_as_buff= str;
+  return &tmp_value;
+}
+
+
 void Item_func_concat::fix_length_and_dec()
 {
   ulonglong char_length= 0;
diff --git a/sql/item_strfunc.h b/sql/item_strfunc.h
index 6bfe88a..64ba342 100644
--- a/sql/item_strfunc.h
+++ b/sql/item_strfunc.h
@@ -195,10 +195,45 @@ class Item_func_aes_decrypt :public Item_aes_crypt
 
 class Item_func_concat :public Item_str_func
 {
+protected:
   String tmp_value;
+  /*
+     Get the i-th argument val_str() and its const_item()
+     @param i[IN]            - The argument number
+     @param str[IN]          - The buffer for val_str()
+     @param is_const[IN/OUT] - If args[i]->val_str() returned a non-null value,
+                               then args[i]->const_item() is returned here.
+                               Otherwise, the value of is_const is not touched.
+     @retval                 - the result of val_str().
+  */
+  String *arg_val_str(uint i, String *str, bool *is_const)
+  {
+    String *res= args[i]->val_str(str);
+    if (res)
+      *is_const= args[i]->const_item();
+    return res;
+  }
+  /*
+    Append a non-NULL value to the result.
+    @param [IN/OUT] res          - The current val_str() return value.
+    @param [IN]     res_is_const - If "false", then OK to append to "res"
+    @param [IN/OUT] str          - The val_str() argument.
+    @param [IN]     res2         - The value to be appended.
+    @param [IN/OUT] use_as_buff  - Which buffer to use for the next argument:
+                                     args[next_arg]->val_str(use_as_buff)
+  */
+  String *append_value(String *res,
+                       bool res_is_const,
+                       String *str,
+                       String **use_as_buff,
+                       const String *res2);
 public:
-  Item_func_concat(THD *thd, List<Item> &list): Item_str_func(thd, list) {}
-  Item_func_concat(THD *thd, Item *a, Item *b): Item_str_func(thd, a, b) {}
+  Item_func_concat(THD *thd, List<Item> &list):
+    Item_str_func(thd, list)
+  {}
+  Item_func_concat(THD *thd, Item *a, Item *b):
+    Item_str_func(thd, a, b)
+  {}
   String *val_str(String *);
   void fix_length_and_dec();
   const char *func_name() const { return "concat"; }
@@ -206,6 +241,30 @@ class Item_func_concat :public Item_str_func
   { return get_item_copy<Item_func_concat>(thd, mem_root, this); }
 };
 
+
+/*
+  This class handles the || operator in sql_mode=ORACLE.
+  Unlike the tradition MariaDB concat(), it treats NULL arguments as ''.
+*/
+class Item_func_concat_operator_oracle :public Item_func_concat
+{
+public:
+  Item_func_concat_operator_oracle(THD *thd, Item *a, Item *b)
+   :Item_func_concat(thd, a, b)
+  { }
+  void print(String *str, enum_query_type query_type)
+  {
+    print_op(str, query_type);
+  }
+  String *val_str(String *);
+  const char *func_name() const { return "||"; }
+  Item *get_copy(THD *thd, MEM_ROOT *mem_root)
+  {
+    return get_item_copy<Item_func_concat_operator_oracle>(thd, mem_root, this);
+  }
+};
+
+
 class Item_func_decode_histogram :public Item_str_func
 {
   String tmp_value;
diff --git a/sql/sql_yacc_ora.yy b/sql/sql_yacc_ora.yy
index 48449ed..89dc7d1 100644
--- a/sql/sql_yacc_ora.yy
+++ b/sql/sql_yacc_ora.yy
@@ -9072,7 +9072,8 @@ simple_expr:
           }
         | simple_expr OR_OR_SYM simple_expr
           {
-            $$= new (thd->mem_root) Item_func_concat(thd, $1, $3);
+            $$= new (thd->mem_root) Item_func_concat_operator_oracle(thd,
+                                                                     $1, $3);
             if ($$ == NULL)
               MYSQL_YYABORT;
           }
