Skip to content

Commit

Permalink
MDEV-20017 Implement TO_CHAR() Oracle compatible function
Browse files Browse the repository at this point in the history
TO_CHAR(expr, fmt)
- expr: required parameter, data/time/timestamp type expression
- fmt: optional parameter, format string, supports
  YYYY/YYY/YY/RRRR/RR/MM/MON/MONTH/MI/DD/DY/HH/HH12/HH24/SS and special
  characters. The default value is "YYYY-MM-DD HH24:MI:SS"

In Oracle, TO_CHAR() can also be used to convert numbers to strings, but
this is not supported. This will gave an error in this patch.

Other things:
- If format strings is a constant, it's evaluated only once and if there
  is any errors in it, they are given at once and the statement will abort.

Original author: woqutech
Lots of optimizations and cleanups done as part of review
  • Loading branch information
montywi authored and vuvova committed May 19, 2021
1 parent cf93209 commit 81d9bed
Show file tree
Hide file tree
Showing 9 changed files with 1,587 additions and 7 deletions.
7 changes: 7 additions & 0 deletions mysql-test/suite/compat/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
To run a test suite under this directory, you should use the format:

mysql-test-run --suite=compat/oracle

or to run one test:

mysql-test-run compat/oracle.test_name
441 changes: 441 additions & 0 deletions mysql-test/suite/compat/oracle/r/func_to_char.result

Large diffs are not rendered by default.

226 changes: 226 additions & 0 deletions mysql-test/suite/compat/oracle/t/func_to_char.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
##############################################################
# testcase for TO_CHAR() function for oracle
# Part of MDEV-20017 Implement TO_CHAR() Oracle compatible function
##############################################################

# Save sql_mode
set @save_sql_mode=@@sql_mode;

--echo #
--echo # test for datetime
--echo #

CREATE TABLE t_to_char1(c0 int, c1 date, c2 time, c3 datetime);

INSERT INTO t_to_char1 VALUES (1, '1000-1-1', '00:00:00', '1000-1-1 00:00:00');
INSERT INTO t_to_char1 VALUES (2, '9999-12-31', '23:59:59', '9999-12-31 23:59:59');
INSERT INTO t_to_char1 VALUES (3, '2021-01-03', '08:30:00', '2021-01-03 08:30:00');
INSERT INTO t_to_char1 VALUES (4, '2021-07-03', '18:30:00', '2021-07-03 18:30:00');

CREATE TABLE t_to_char2(c1 timestamp);
INSERT INTO t_to_char2 VALUES ('1980-01-11 04:50:39');
INSERT INTO t_to_char2 VALUES ('2000-11-11 12:50:00');
INSERT INTO t_to_char2 VALUES ('2030-11-11 18:20:10');

# test for timestamp
SELECT TO_CHAR(c1, 'YYYY-MM-DD') FROM t_to_char2;
SELECT TO_CHAR(c1, 'HH24-MI-SS') FROM t_to_char2;

# test full output format
--echo #
--echo # test YYYY/YY/MM/DD/HH/HH24/MI/SS
--echo #
SELECT TO_CHAR(c1, 'YYYY-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'YY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'yyyy-mm-dd') AS C1, TO_CHAR(c2, 'hh:mi:ss') AS C2, TO_CHAR(c3, 'yy-mm-dd hh24:mi:ss') AS C3 FROM t_to_char1;

--echo #
--echo # test YYY/Y/MON/DD/DY/HH/HH12/MI/SS
--echo #
SELECT TO_CHAR(c1, 'YYY-MON-DD') AS C1, TO_CHAR(c2, 'HH12:MI:SS') AS C2, TO_CHAR(c3, 'Y-MONTH-DY HH:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'yyy-Mon-Dd') AS C1, TO_CHAR(c2, 'Hh12:mi:Ss') AS C2, TO_CHAR(c3, 'y-Month-Dy Hh:Mi:Ss') AS C3 FROM t_to_char1;

--echo #
--echo # test RRRR/RR/DAY
--echo #
SELECT TO_CHAR(c1, 'RRRR-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'RRRR-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'RR-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'YY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'Rrrr-Mm-Dd') AS C1, TO_CHAR(c2, 'hh:mi:ss') AS C2, TO_CHAR(c3, 'Rrrr-mm-dd Hh24:mi:ss') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'rr-mm-dd') AS C1, TO_CHAR(c2, 'hh:mi:ss') AS C2, TO_CHAR(c3, 'yy-mm-dd hh24:Mi:ss') AS C3 FROM t_to_char1;

--echo #
--echo # test AD/A.D./BC/B.C./AM/A.M./PM/P.M.
--echo #
SELECT TO_CHAR(c1, 'ADYYYY-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'AD.YYYY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'A.D.YYYY-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'A.D..YYYY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'ADYYYY-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'AD.YYYY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'A.D.YYYY-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'A.D..YYYY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'BCYYYY-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'BCYYYY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'B.C.YYYY-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'B.C.YYYY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'bcyyyy-mm-dd') AS C1, TO_CHAR(c2, 'hh:mi:ss') AS C2, TO_CHAR(c3, 'BcYYyy-MM-DD Hh24:mi:sS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'b.c.yyyy-mm-dd') AS C1, TO_CHAR(c2, 'hh:mI:Ss') AS C2, TO_CHAR(c3, 'b.C.Yyyy-Mm-dd hH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'A.D.YYYY-MM-DD') AS C1, TO_CHAR(c2, 'PMHH:MI:SS') AS C2, TO_CHAR(c3, 'A.D..YYYY-MM-DD P.M.HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'A.D.YYYY-MM-DD') AS C1, TO_CHAR(c2, 'pmHH:MI:SS') AS C2, TO_CHAR(c3, 'A.D..YYYY-MM-DD p.m.HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'A.D.YYYY-MM-DD') AS C1, TO_CHAR(c2, 'AMHH:MI:SS') AS C2, TO_CHAR(c3, 'A.D..YYYY-MM-DD A.m.HH24:MI:SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'A.D.YYYY-MM-DD') AS C1, TO_CHAR(c2, 'amHH:MI:SS') AS C2, TO_CHAR(c3, 'A.D..YYYY-MM-DD a.M.HH24:MI:SS') AS C3 FROM t_to_char1;

--echo #
--echo # test format without order
--echo #
SELECT TO_CHAR(c1, 'MM-YYYY-DD') AS C1, TO_CHAR(c2, 'HH:SS:MI') AS C2, TO_CHAR(c3, 'DD-YY-MM MI:SS:HH24') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'yyy-Dd-Mon') AS C1, TO_CHAR(c2, 'mi:Hh12:Ss') AS C2, TO_CHAR(c3, 'Ss:Hh:Mi Dy-y-Month') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'Dd-Mm-Rrrr') AS C1, TO_CHAR(c2, 'ss:hh:mi') AS C2, TO_CHAR(c3, 'ss:Rrrr-hh24-dd mon:mi') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'YYYYA.D.-MM-DD') AS C1, TO_CHAR(c2, 'HH:MI:SS') AS C2, TO_CHAR(c3, 'A.D..YYYY-MM-DD HH24:MI:SS') AS C3 FROM t_to_char1;

--echo #
--echo # test for special characters
--echo #
SELECT TO_CHAR(c1, 'YYYYMMDD') AS C1, TO_CHAR(c2, 'HHMISS') AS C2, TO_CHAR(c3, 'YYMMDDHH24MISS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'YYYY!!MM@DD') AS C1, TO_CHAR(c2, 'HH#MI$SS') AS C2, TO_CHAR(c3, 'YY%MM^DD*HH24(MI)SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'YYYY_MM+DD') AS C1, TO_CHAR(c2, 'HH=MI{SS') AS C2, TO_CHAR(c3, 'YY}MMDDHH24MISS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'YYYY,MM.DD') AS C1, TO_CHAR(c2, 'HH/MI;SS') AS C2, TO_CHAR(c3, 'YY>MM<DD]HH24[MI\SS') AS C3 FROM t_to_char1;
SELECT TO_CHAR(c1, 'YYYY||||MM|DD') AS C1, TO_CHAR(c2, 'HH&|MI|&|SS') AS C2, TO_CHAR(c3, 'YY&&&\\MM|&&|DD HH24|| MI&||"abx"|SS') AS C3 FROM t_to_char1;
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c1, 'YYYY&MM-DD') FROM t_to_char1 where c0=1;
SELECT TO_CHAR(c1, 'YYYY"abx"MM"bsz"DD') AS C1 FROM t_to_char1;

--echo #
--echo # test for other locale
--echo #
SET character_set_client='utf8';
SET character_set_connection='utf8';
SET character_set_results='utf8';
SET lc_time_names='zh_TW';
SELECT TO_CHAR(c1, 'YYYY-MON-DAY') FROM t_to_char1;
SET lc_time_names='de_DE';
SELECT TO_CHAR(c1, 'YYYY-MON-DAY') FROM t_to_char1;
SET lc_time_names='en_US';
SELECT TO_CHAR(c1, 'YYYY-MON-DAY') FROM t_to_char1;
SET lc_time_names='zh_CN';
SELECT TO_CHAR(c1, 'YYYY-MON-DAY') FROM t_to_char1;

--echo #
--echo # test for invalid format
--echo #

--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c1, 'YYYYaxMON-DAY') FROM t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c1, 'YYYY\nMON-DAY') FROM t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c1, 'YYYY\rMON-DAY') FROM t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c1, 'YYYY分隔MON-DAY') FROM t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c1, 'YYYY-分隔MON-DAY') FROM t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'YYYYxDDD') from t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'YYYY&DDD') from t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'xxYYYY-DD') from t_to_char1 where c0 = 1;

SET character_set_client='latin1';
SET character_set_connection='latin1';
SET character_set_results='latin1';
--echo #
--echo # test for unusual format
--echo #
select to_char(c3, 'YYYYYYYYYYYYYYY') from t_to_char1;
select to_char(c3, 'YYYYYYYYYYYYYYYDDDDDD') from t_to_char1;

--echo #
--echo # oracle max length is 144
--echo #

--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY') from t_to_char1 where c0 = 1;
CREATE TABLE t_f(c1 varchar(150));
insert into t_f values('YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY-DD');
select to_char('2000-11-11', c1) from t_f;
DROP TABLE t_f;
select to_char(c3, 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY-DD-MM') from t_to_char1 where c0 = 1;

--echo #
--echo # now only support two parameter.
--echo #
select to_char(c3) from t_to_char1 where c0 =1;
select to_char(c3, "YYYY-MM-DD HH:MI:SS") from t_to_char1 where c0 =1;
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
select to_char(c3, "YYYY-MM-DD HH:MI:SS", "zh_CN") from t_to_char1 where c0 = 1;
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
select to_char(c3, "YYYY-MM-DD HH:MI:SS", "NLS_DATE_LANGUAGE = zh_CN") from t_to_char1 where c0 = 1;

--echo #
--echo # oracle support format but mariadb does not support
--echo #
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'DDD') from t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'D') from t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'DS') from t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'IY') from t_to_char1 where c0 = 1;
--error ER_STD_INVALID_ARGUMENT
select to_char(c3, 'IYYY') from t_to_char1 where c0 = 1;

--echo #
--echo # test for first argument data type
--echo #
--error ER_STD_INVALID_ARGUMENT
select to_char(1, 'yyyy');
--error ER_STD_INVALID_ARGUMENT
select to_char(1.1, 'yyyy');
CREATE TABLE t_a(c1 int, c2 float, c3 decimal, c4 char(20), c5 varchar(20), c6 nchar(20), c7 nvarchar(20));
insert into t_a VALUES (1, 3.2, 2002.02, '2000-11-11', '2000-11-11', '2000-11-11', '2000-11-11');
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c1, 'YYYY') from t_a;
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c2, 'YYYY') from t_a;
--error ER_STD_INVALID_ARGUMENT
SELECT TO_CHAR(c3, 'YYYY') from t_a;
SELECT TO_CHAR(c4, 'YYYY') from t_a;
SELECT TO_CHAR(c5, 'YYYY') from t_a;
SELECT TO_CHAR(c6, 'YYYY') from t_a;
SELECT TO_CHAR(c7, 'YYYY') from t_a;
DROP TABLE t_a;

CREATE TABLE t_b(c0 int, c1 char(20), c2 varchar(20), c3 nchar(20), c4 nvarchar(20));
INSERT INTO t_b VALUES (1111, 'YYYY-MM-DD', 'YYYY-MM-DD', 'YYYY-MM-DD', 'YYYY-MM-DD');
SELECT TO_CHAR('2000-11-11', c0) FROM t_b;
SELECT TO_CHAR('2000-11-11', c1) FROM t_b;
SELECT TO_CHAR('2000-11-11', c2) FROM t_b;
SELECT TO_CHAR('2000-11-11', c3) FROM t_b;
SELECT TO_CHAR('2000-11-11', c4) FROM t_b;
DROP TABLE t_b;

EXPLAIN EXTENDED SELECT TO_CHAR(c1, 'YYYY-MM-DD') FROM t_to_char1;

--echo #
--echo # test for time type with date format string
--echo #
SELECT TO_CHAR(c2, 'YYYY-MM-DD HH:MI:SS') from t_to_char1;
SELECT TO_CHAR(c2, 'YYYY-MON-DY HH:MI:SS') from t_to_char1;
SELECT TO_CHAR(c2, 'MON-YYYY-DY HH:MI:SS') from t_to_char1;
SELECT TO_CHAR(c2, 'YYYY-MONTH-DAY HH:MI:SS') from t_to_char1;

DROP TABLE t_to_char1;
DROP TABLE t_to_char2;


--echo #
--echo # Test strict mode
--echo #

create table t1 (a datetime, b int, f varchar(30)) engine=myisam;
insert into t1 values ("2021-01-24 19:22:10", 2014, "YYYY-MM-DD");
insert into t1 values ("2021-01-24 19:22:10", 2014, "YYYY-MQ-DD");
create table t2 (a varchar(30)) engine=myisam;
insert into t2 select to_char(a,f) from t1;
set @@sql_mode="STRICT_ALL_TABLES";
--error ER_STD_INVALID_ARGUMENT
insert into t2 select to_char(a,f) from t1;
select * from t2;
drop table t1,t2;
set @local.sql_mode=@sql_mode;

52 changes: 52 additions & 0 deletions sql/item_create.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2132,6 +2132,19 @@ class Create_func_to_base64 : public Create_func_arg1
};


class Create_func_to_char : public Create_native_func
{
public:
virtual Item *create_native(THD *thd, LEX_CSTRING *name, List<Item> *item_list);

static Create_func_to_char s_singleton;

protected:
Create_func_to_char() {}
virtual ~Create_func_to_char() {}
};


class Create_func_to_days : public Create_func_arg1
{
public:
Expand Down Expand Up @@ -5142,6 +5155,44 @@ Create_func_to_base64::create_1_arg(THD *thd, Item *arg1)
}


Create_func_to_char Create_func_to_char::s_singleton;

Item*
Create_func_to_char::create_native(THD *thd, LEX_CSTRING *name,
List<Item> *item_list)
{
Item *func= NULL;
int arg_count= 0;

if (item_list != NULL)
arg_count= item_list->elements;

switch (arg_count) {
case 1:
{
Item *param_1= item_list->pop();
Item *i0= new (thd->mem_root) Item_string_sys(thd, "YYYY-MM-DD HH24:MI:SS", 21);
func= new (thd->mem_root) Item_func_tochar(thd, param_1, i0);
break;
}
case 2:
{
Item *param_1= item_list->pop();
Item *param_2= item_list->pop();
func= new (thd->mem_root) Item_func_tochar(thd, param_1, param_2);
break;
}
default:
{
my_error(ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT, MYF(0), name->str);
break;
}
}

return func;
}


Create_func_to_days Create_func_to_days::s_singleton;

Item*
Expand Down Expand Up @@ -5601,6 +5652,7 @@ static Native_func_registry func_array[] =
{ { STRING_WITH_LEN("TIME_FORMAT") }, BUILDER(Create_func_time_format)},
{ { STRING_WITH_LEN("TIME_TO_SEC") }, BUILDER(Create_func_time_to_sec)},
{ { STRING_WITH_LEN("TO_BASE64") }, BUILDER(Create_func_to_base64)},
{ { STRING_WITH_LEN("TO_CHAR") }, BUILDER(Create_func_to_char)},
{ { STRING_WITH_LEN("TO_DAYS") }, BUILDER(Create_func_to_days)},
{ { STRING_WITH_LEN("TO_SECONDS") }, BUILDER(Create_func_to_seconds)},
{ { STRING_WITH_LEN("UCASE") }, BUILDER(Create_func_ucase)},
Expand Down
Loading

0 comments on commit 81d9bed

Please sign in to comment.