Skip to content

Commit 89c940c

Browse files
JiajunBernoullijulianhyde
authored andcommitted
[CALCITE-5241] Implement CHAR function for MySQL and Spark, also JDBC '{fn CHAR(n)}'
Close #2878
1 parent d20fd09 commit 89c940c

File tree

11 files changed

+82
-16
lines changed

11 files changed

+82
-16
lines changed

core/src/main/codegen/templates/Parser.jj

+1
Original file line numberDiff line numberDiff line change
@@ -6949,6 +6949,7 @@ SqlIdentifier ReservedFunctionName() :
69496949
| <AVG>
69506950
| <CARDINALITY>
69516951
| <CEILING>
6952+
| <CHAR>
69526953
| <CHAR_LENGTH>
69536954
| <CHARACTER_LENGTH>
69546955
| <COALESCE>

core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_REVERSE;
116116
import static org.apache.calcite.sql.fun.SqlLibraryOperators.BOOL_AND;
117117
import static org.apache.calcite.sql.fun.SqlLibraryOperators.BOOL_OR;
118+
import static org.apache.calcite.sql.fun.SqlLibraryOperators.CHAR;
118119
import static org.apache.calcite.sql.fun.SqlLibraryOperators.CHR;
119120
import static org.apache.calcite.sql.fun.SqlLibraryOperators.COMPRESS;
120121
import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONCAT2;
@@ -366,7 +367,7 @@ public class RexImpTable {
366367
defineMethod(RIGHT, BuiltInMethod.RIGHT.method, NullPolicy.ANY);
367368
defineMethod(REPLACE, BuiltInMethod.REPLACE.method, NullPolicy.STRICT);
368369
defineMethod(TRANSLATE3, BuiltInMethod.TRANSLATE3.method, NullPolicy.STRICT);
369-
defineMethod(CHR, "chr", NullPolicy.STRICT);
370+
defineMethod(CHR, BuiltInMethod.CHAR_FROM_UTF8.method, NullPolicy.STRICT);
370371
defineMethod(CHARACTER_LENGTH, BuiltInMethod.CHAR_LENGTH.method,
371372
NullPolicy.STRICT);
372373
defineMethod(CHAR_LENGTH, BuiltInMethod.CHAR_LENGTH.method,
@@ -380,6 +381,8 @@ public class RexImpTable {
380381
defineMethod(OVERLAY, BuiltInMethod.OVERLAY.method, NullPolicy.STRICT);
381382
defineMethod(POSITION, BuiltInMethod.POSITION.method, NullPolicy.STRICT);
382383
defineMethod(ASCII, BuiltInMethod.ASCII.method, NullPolicy.STRICT);
384+
defineMethod(CHAR, BuiltInMethod.CHAR_FROM_ASCII.method,
385+
NullPolicy.SEMI_STRICT);
383386
defineMethod(REPEAT, BuiltInMethod.REPEAT.method, NullPolicy.STRICT);
384387
defineMethod(SPACE, BuiltInMethod.SPACE.method, NullPolicy.STRICT);
385388
defineMethod(STRCMP, BuiltInMethod.STRCMP.method, NullPolicy.STRICT);

core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -462,9 +462,22 @@ public static ByteString right(ByteString s, int n) {
462462
return s.substring(len - n);
463463
}
464464

465-
/** SQL CHR(long) function. */
466-
public static String chr(long n) {
467-
return String.valueOf(Character.toChars((int) n));
465+
/** SQL CHAR(integer) function, as in MySQL and Spark.
466+
*
467+
* <p>Returns the ASCII character of {@code n} modulo 256,
468+
* or null if {@code n} &lt; 0. */
469+
public static @Nullable String charFromAscii(int n) {
470+
if (n < 0) {
471+
return null;
472+
}
473+
return String.valueOf(Character.toChars(n % 256));
474+
}
475+
476+
/** SQL CHR(integer) function, as in Oracle and Postgres.
477+
*
478+
* <p>Returns the UTF-8 character whose code is {@code n}. */
479+
public static String charFromUtf8(int n) {
480+
return String.valueOf(Character.toChars(n));
468481
}
469482

470483
/** SQL OCTET_LENGTH(binary) function. */

core/src/main/java/org/apache/calcite/sql/SqlJdbcFunctionCall.java

+1
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ private JdbcToInternalLookupTable() {
708708
map.put("TRUNCATE", simple(SqlStdOperatorTable.TRUNCATE));
709709

710710
map.put("ASCII", simple(SqlStdOperatorTable.ASCII));
711+
map.put("CHAR", simple(SqlLibraryOperators.CHAR));
711712
map.put("CONCAT", simple(SqlStdOperatorTable.CONCAT));
712713
map.put("DIFFERENCE", simple(SqlLibraryOperators.DIFFERENCE));
713714
map.put("INSERT",

core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,20 @@ private SqlLibraryOperators() {
650650
ReturnTypes.BIGINT_NULLABLE, null, OperandTypes.TIMESTAMP,
651651
SqlFunctionCategory.TIMEDATE);
652652

653-
@LibraryOperator(libraries = {ORACLE})
653+
/** The "CHAR(n)" function; returns the character whose ASCII code is
654+
* {@code n} % 256, or null if {@code n} &lt; 0. */
655+
@LibraryOperator(libraries = {MYSQL, SPARK})
656+
public static final SqlFunction CHAR =
657+
new SqlFunction("CHAR",
658+
SqlKind.OTHER_FUNCTION,
659+
ReturnTypes.CHAR_FORCE_NULLABLE,
660+
null,
661+
OperandTypes.INTEGER,
662+
SqlFunctionCategory.STRING);
663+
664+
/** The "CHR(n)" function; returns the character whose UTF-8 code is
665+
* {@code n}. */
666+
@LibraryOperator(libraries = {ORACLE, POSTGRESQL})
654667
public static final SqlFunction CHR =
655668
new SqlFunction("CHR",
656669
SqlKind.OTHER_FUNCTION,

core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java

+7
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,13 @@ public static SqlCall stripSeparator(SqlCall call) {
347347
public static final SqlReturnTypeInference CHAR =
348348
explicit(SqlTypeName.CHAR);
349349

350+
/**
351+
* Type-inference strategy whereby the result type of a call is a nullable
352+
* CHAR(1).
353+
*/
354+
public static final SqlReturnTypeInference CHAR_FORCE_NULLABLE =
355+
CHAR.andThen(SqlTypeTransforms.FORCE_NULLABLE);
356+
350357
/**
351358
* Type-inference strategy whereby the result type of a call is an Integer.
352359
*/

core/src/main/java/org/apache/calcite/util/BuiltInMethod.java

+2
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ public enum BuiltInMethod {
339339
UPPER(SqlFunctions.class, "upper", String.class),
340340
LOWER(SqlFunctions.class, "lower", String.class),
341341
ASCII(SqlFunctions.class, "ascii", String.class),
342+
CHAR_FROM_ASCII(SqlFunctions.class, "charFromAscii", int.class),
343+
CHAR_FROM_UTF8(SqlFunctions.class, "charFromUtf8", int.class),
342344
REPEAT(SqlFunctions.class, "repeat", String.class, int.class),
343345
SPACE(SqlFunctions.class, "space", int.class),
344346
SOUNDEX(SqlFunctions.class, "soundex", String.class),

core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ class SqlAdvisorTest extends SqlValidatorTestCase {
138138
"KEYWORD(CAST)",
139139
"KEYWORD(CEIL)",
140140
"KEYWORD(CEILING)",
141+
"KEYWORD(CHAR)",
141142
"KEYWORD(CHARACTER_LENGTH)",
142143
"KEYWORD(CHAR_LENGTH)",
143144
"KEYWORD(CLASSIFIER)",

core/src/test/resources/sql/functions.iq

+13-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,18 @@ SELECT ExtractValue('<a>c</a>', '//a');
5656

5757
# STRING Functions
5858

59-
#CONCAT
59+
# CHAR
60+
SELECT char(null), char(-1), char(65), char(233), char(256+66);
61+
+--------+--------+--------+--------+--------+
62+
| EXPR$0 | EXPR$1 | EXPR$2 | EXPR$3 | EXPR$4 |
63+
+--------+--------+--------+--------+--------+
64+
| | | A | é | B |
65+
+--------+--------+--------+--------+--------+
66+
(1 row)
67+
68+
!ok
69+
70+
# CONCAT
6071
SELECT CONCAT('c', 'h', 'a', 'r');
6172
+--------+
6273
| EXPR$0 |
@@ -115,7 +126,7 @@ select sinh(1);
115126

116127
!ok
117128

118-
#CONCAT
129+
# CONCAT
119130
select concat('a', 'b');
120131
+--------+
121132
| EXPR$0 |

site/_docs/reference.md

+4-6
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,7 @@ period:
17491749
| Operator syntax | Description
17501750
|:--------------- |:-----------
17511751
| {fn ASCII(string)} | Returns the ASCII code of the first character of *string*; if the first character is a non-ASCII character, returns its Unicode code point; returns 0 if *string* is empty
1752+
| {fn CHAR(integer)} | Returns the character whose ASCII code is *integer* % 256, or null if *integer* &lt; 0
17521753
| {fn CONCAT(character, character)} | Returns the concatenation of character strings
17531754
| {fn INSERT(string1, start, length, string2)} | Inserts *string2* into a slot in *string1*
17541755
| {fn LCASE(string)} | Returns a string in which all alphabetic characters in *string* have been converted to lower case
@@ -1758,15 +1759,11 @@ period:
17581759
| {fn LTRIM(string)} | Returns *string* with leading space characters removed
17591760
| {fn REPLACE(string, search, replacement)} | Returns a string in which all the occurrences of *search* in *string* are replaced with *replacement*; if *replacement* is the empty string, the occurrences of *search* are removed
17601761
| {fn REVERSE(string)} | Returns *string* with the order of the characters reversed
1761-
| {fn RIGHT(string, integer)} | Returns the rightmost *length* characters from *string*
1762+
| {fn RIGHT(string, length)} | Returns the rightmost *length* characters from *string*
17621763
| {fn RTRIM(string)} | Returns *string* with trailing space characters removed
17631764
| {fn SUBSTRING(string, offset, length)} | Returns a character string that consists of *length* characters from *string* starting at the *offset* position
17641765
| {fn UCASE(string)} | Returns a string in which all alphabetic characters in *string* have been converted to upper case
17651766

1766-
Not implemented:
1767-
1768-
* {fn CHAR(string)}
1769-
17701767
#### Date/time
17711768

17721769
| Operator syntax | Description
@@ -2564,7 +2561,8 @@ semantics.
25642561
| b | ARRAY_CONCAT(array [, array ]*) | Concatenates one or more arrays. If any input argument is `NULL` the function returns `NULL`
25652562
| b | ARRAY_LENGTH(array) | Synonym for `CARDINALITY`
25662563
| b | ARRAY_REVERSE(array) | Reverses elements of *array*
2567-
| o | CHR(integer) | Returns the character having the binary equivalent to *integer* as a CHAR value
2564+
| m s | CHAR(integer) | Returns the character whose ASCII code is *integer* % 256, or null if *integer* &lt; 0
2565+
| o p | CHR(integer) | Returns the character whose UTF-8 code is *integer*
25682566
| o | COSH(numeric) | Returns the hyperbolic cosine of *numeric*
25692567
| o | CONCAT(string, string) | Concatenates two strings
25702568
| m p | CONCAT(string [, string ]*) | Concatenates two or more strings

testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java

+19-3
Original file line numberDiff line numberDiff line change
@@ -1492,9 +1492,8 @@ protected static Calendar getCalendarNotTooNear(int timeUnit) {
14921492
f.checkScalar("{fn ASCII('ABC')}", "65", "INTEGER NOT NULL");
14931493
f.checkNull("{fn ASCII(cast(null as varchar(1)))}");
14941494

1495-
if (false) {
1496-
f.checkScalar("{fn CHAR(code)}", null, "");
1497-
}
1495+
f.checkScalar("{fn CHAR(97)}", "a", "CHAR(1)");
1496+
14981497
f.checkScalar("{fn CONCAT('foo', 'bar')}", "foobar", "CHAR(6) NOT NULL");
14991498

15001499
f.checkScalar("{fn DIFFERENCE('Miller', 'miller')}", "4",
@@ -1630,6 +1629,23 @@ protected static Calendar getCalendarNotTooNear(int timeUnit) {
16301629

16311630
}
16321631

1632+
@Test void testChar() {
1633+
final SqlOperatorFixture f0 = fixture()
1634+
.setFor(SqlLibraryOperators.CHR, VM_FENNEL, VM_JAVA);
1635+
f0.checkFails("^char(97)^",
1636+
"No match found for function signature CHAR\\(<NUMERIC>\\)", false);
1637+
final SqlOperatorFixture f = f0.withLibrary(SqlLibrary.MYSQL);
1638+
f.checkScalar("char(null)", isNullValue(), "CHAR(1)");
1639+
f.checkScalar("char(-1)", isNullValue(), "CHAR(1)");
1640+
f.checkScalar("char(97)", "a", "CHAR(1)");
1641+
f.checkScalar("char(48)", "0", "CHAR(1)");
1642+
f.checkScalar("char(0)", String.valueOf('\u0000'), "CHAR(1)");
1643+
f.checkFails("^char(97.1)^",
1644+
"Cannot apply 'CHAR' to arguments of type 'CHAR\\(<DECIMAL\\(3, 1\\)>\\)'\\. "
1645+
+ "Supported form\\(s\\): 'CHAR\\(<INTEGER>\\)'",
1646+
false);
1647+
}
1648+
16331649
@Test void testChr() {
16341650
final SqlOperatorFixture f0 = fixture()
16351651
.setFor(SqlLibraryOperators.CHR, VM_FENNEL, VM_JAVA);

0 commit comments

Comments
 (0)