Skip to content

Commit

Permalink
[CALCITE-5241] Implement CHAR function for MySQL and Spark, also JDBC…
Browse files Browse the repository at this point in the history
… '{fn CHAR(n)}'

Close #2878
  • Loading branch information
JiajunBernoulli authored and julianhyde committed Sep 9, 2022
1 parent d20fd09 commit 89c940c
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 16 deletions.
1 change: 1 addition & 0 deletions core/src/main/codegen/templates/Parser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -6949,6 +6949,7 @@ SqlIdentifier ReservedFunctionName() :
| <AVG>
| <CARDINALITY>
| <CEILING>
| <CHAR>
| <CHAR_LENGTH>
| <CHARACTER_LENGTH>
| <COALESCE>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_REVERSE;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.BOOL_AND;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.BOOL_OR;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.CHAR;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.CHR;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.COMPRESS;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.CONCAT2;
Expand Down Expand Up @@ -366,7 +367,7 @@ public class RexImpTable {
defineMethod(RIGHT, BuiltInMethod.RIGHT.method, NullPolicy.ANY);
defineMethod(REPLACE, BuiltInMethod.REPLACE.method, NullPolicy.STRICT);
defineMethod(TRANSLATE3, BuiltInMethod.TRANSLATE3.method, NullPolicy.STRICT);
defineMethod(CHR, "chr", NullPolicy.STRICT);
defineMethod(CHR, BuiltInMethod.CHAR_FROM_UTF8.method, NullPolicy.STRICT);
defineMethod(CHARACTER_LENGTH, BuiltInMethod.CHAR_LENGTH.method,
NullPolicy.STRICT);
defineMethod(CHAR_LENGTH, BuiltInMethod.CHAR_LENGTH.method,
Expand All @@ -380,6 +381,8 @@ public class RexImpTable {
defineMethod(OVERLAY, BuiltInMethod.OVERLAY.method, NullPolicy.STRICT);
defineMethod(POSITION, BuiltInMethod.POSITION.method, NullPolicy.STRICT);
defineMethod(ASCII, BuiltInMethod.ASCII.method, NullPolicy.STRICT);
defineMethod(CHAR, BuiltInMethod.CHAR_FROM_ASCII.method,
NullPolicy.SEMI_STRICT);
defineMethod(REPEAT, BuiltInMethod.REPEAT.method, NullPolicy.STRICT);
defineMethod(SPACE, BuiltInMethod.SPACE.method, NullPolicy.STRICT);
defineMethod(STRCMP, BuiltInMethod.STRCMP.method, NullPolicy.STRICT);
Expand Down
19 changes: 16 additions & 3 deletions core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,22 @@ public static ByteString right(ByteString s, int n) {
return s.substring(len - n);
}

/** SQL CHR(long) function. */
public static String chr(long n) {
return String.valueOf(Character.toChars((int) n));
/** SQL CHAR(integer) function, as in MySQL and Spark.
*
* <p>Returns the ASCII character of {@code n} modulo 256,
* or null if {@code n} &lt; 0. */
public static @Nullable String charFromAscii(int n) {
if (n < 0) {
return null;
}
return String.valueOf(Character.toChars(n % 256));
}

/** SQL CHR(integer) function, as in Oracle and Postgres.
*
* <p>Returns the UTF-8 character whose code is {@code n}. */
public static String charFromUtf8(int n) {
return String.valueOf(Character.toChars(n));
}

/** SQL OCTET_LENGTH(binary) function. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,7 @@ private JdbcToInternalLookupTable() {
map.put("TRUNCATE", simple(SqlStdOperatorTable.TRUNCATE));

map.put("ASCII", simple(SqlStdOperatorTable.ASCII));
map.put("CHAR", simple(SqlLibraryOperators.CHAR));
map.put("CONCAT", simple(SqlStdOperatorTable.CONCAT));
map.put("DIFFERENCE", simple(SqlLibraryOperators.DIFFERENCE));
map.put("INSERT",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,20 @@ private SqlLibraryOperators() {
ReturnTypes.BIGINT_NULLABLE, null, OperandTypes.TIMESTAMP,
SqlFunctionCategory.TIMEDATE);

@LibraryOperator(libraries = {ORACLE})
/** The "CHAR(n)" function; returns the character whose ASCII code is
* {@code n} % 256, or null if {@code n} &lt; 0. */
@LibraryOperator(libraries = {MYSQL, SPARK})
public static final SqlFunction CHAR =
new SqlFunction("CHAR",
SqlKind.OTHER_FUNCTION,
ReturnTypes.CHAR_FORCE_NULLABLE,
null,
OperandTypes.INTEGER,
SqlFunctionCategory.STRING);

/** The "CHR(n)" function; returns the character whose UTF-8 code is
* {@code n}. */
@LibraryOperator(libraries = {ORACLE, POSTGRESQL})
public static final SqlFunction CHR =
new SqlFunction("CHR",
SqlKind.OTHER_FUNCTION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ public static SqlCall stripSeparator(SqlCall call) {
public static final SqlReturnTypeInference CHAR =
explicit(SqlTypeName.CHAR);

/**
* Type-inference strategy whereby the result type of a call is a nullable
* CHAR(1).
*/
public static final SqlReturnTypeInference CHAR_FORCE_NULLABLE =
CHAR.andThen(SqlTypeTransforms.FORCE_NULLABLE);

/**
* Type-inference strategy whereby the result type of a call is an Integer.
*/
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ public enum BuiltInMethod {
UPPER(SqlFunctions.class, "upper", String.class),
LOWER(SqlFunctions.class, "lower", String.class),
ASCII(SqlFunctions.class, "ascii", String.class),
CHAR_FROM_ASCII(SqlFunctions.class, "charFromAscii", int.class),
CHAR_FROM_UTF8(SqlFunctions.class, "charFromUtf8", int.class),
REPEAT(SqlFunctions.class, "repeat", String.class, int.class),
SPACE(SqlFunctions.class, "space", int.class),
SOUNDEX(SqlFunctions.class, "soundex", String.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ class SqlAdvisorTest extends SqlValidatorTestCase {
"KEYWORD(CAST)",
"KEYWORD(CEIL)",
"KEYWORD(CEILING)",
"KEYWORD(CHAR)",
"KEYWORD(CHARACTER_LENGTH)",
"KEYWORD(CHAR_LENGTH)",
"KEYWORD(CLASSIFIER)",
Expand Down
15 changes: 13 additions & 2 deletions core/src/test/resources/sql/functions.iq
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ SELECT ExtractValue('<a>c</a>', '//a');

# STRING Functions

#CONCAT
# CHAR
SELECT char(null), char(-1), char(65), char(233), char(256+66);
+--------+--------+--------+--------+--------+
| EXPR$0 | EXPR$1 | EXPR$2 | EXPR$3 | EXPR$4 |
+--------+--------+--------+--------+--------+
| | | A | é | B |
+--------+--------+--------+--------+--------+
(1 row)

!ok

# CONCAT
SELECT CONCAT('c', 'h', 'a', 'r');
+--------+
| EXPR$0 |
Expand Down Expand Up @@ -115,7 +126,7 @@ select sinh(1);

!ok

#CONCAT
# CONCAT
select concat('a', 'b');
+--------+
| EXPR$0 |
Expand Down
10 changes: 4 additions & 6 deletions site/_docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,7 @@ period:
| Operator syntax | Description
|:--------------- |:-----------
| {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
| {fn CHAR(integer)} | Returns the character whose ASCII code is *integer* % 256, or null if *integer* &lt; 0
| {fn CONCAT(character, character)} | Returns the concatenation of character strings
| {fn INSERT(string1, start, length, string2)} | Inserts *string2* into a slot in *string1*
| {fn LCASE(string)} | Returns a string in which all alphabetic characters in *string* have been converted to lower case
Expand All @@ -1758,15 +1759,11 @@ period:
| {fn LTRIM(string)} | Returns *string* with leading space characters removed
| {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
| {fn REVERSE(string)} | Returns *string* with the order of the characters reversed
| {fn RIGHT(string, integer)} | Returns the rightmost *length* characters from *string*
| {fn RIGHT(string, length)} | Returns the rightmost *length* characters from *string*
| {fn RTRIM(string)} | Returns *string* with trailing space characters removed
| {fn SUBSTRING(string, offset, length)} | Returns a character string that consists of *length* characters from *string* starting at the *offset* position
| {fn UCASE(string)} | Returns a string in which all alphabetic characters in *string* have been converted to upper case

Not implemented:

* {fn CHAR(string)}

#### Date/time

| Operator syntax | Description
Expand Down Expand Up @@ -2564,7 +2561,8 @@ semantics.
| b | ARRAY_CONCAT(array [, array ]*) | Concatenates one or more arrays. If any input argument is `NULL` the function returns `NULL`
| b | ARRAY_LENGTH(array) | Synonym for `CARDINALITY`
| b | ARRAY_REVERSE(array) | Reverses elements of *array*
| o | CHR(integer) | Returns the character having the binary equivalent to *integer* as a CHAR value
| m s | CHAR(integer) | Returns the character whose ASCII code is *integer* % 256, or null if *integer* &lt; 0
| o p | CHR(integer) | Returns the character whose UTF-8 code is *integer*
| o | COSH(numeric) | Returns the hyperbolic cosine of *numeric*
| o | CONCAT(string, string) | Concatenates two strings
| m p | CONCAT(string [, string ]*) | Concatenates two or more strings
Expand Down
22 changes: 19 additions & 3 deletions testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1492,9 +1492,8 @@ protected static Calendar getCalendarNotTooNear(int timeUnit) {
f.checkScalar("{fn ASCII('ABC')}", "65", "INTEGER NOT NULL");
f.checkNull("{fn ASCII(cast(null as varchar(1)))}");

if (false) {
f.checkScalar("{fn CHAR(code)}", null, "");
}
f.checkScalar("{fn CHAR(97)}", "a", "CHAR(1)");

f.checkScalar("{fn CONCAT('foo', 'bar')}", "foobar", "CHAR(6) NOT NULL");

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

}

@Test void testChar() {
final SqlOperatorFixture f0 = fixture()
.setFor(SqlLibraryOperators.CHR, VM_FENNEL, VM_JAVA);
f0.checkFails("^char(97)^",
"No match found for function signature CHAR\\(<NUMERIC>\\)", false);
final SqlOperatorFixture f = f0.withLibrary(SqlLibrary.MYSQL);
f.checkScalar("char(null)", isNullValue(), "CHAR(1)");
f.checkScalar("char(-1)", isNullValue(), "CHAR(1)");
f.checkScalar("char(97)", "a", "CHAR(1)");
f.checkScalar("char(48)", "0", "CHAR(1)");
f.checkScalar("char(0)", String.valueOf('\u0000'), "CHAR(1)");
f.checkFails("^char(97.1)^",
"Cannot apply 'CHAR' to arguments of type 'CHAR\\(<DECIMAL\\(3, 1\\)>\\)'\\. "
+ "Supported form\\(s\\): 'CHAR\\(<INTEGER>\\)'",
false);
}

@Test void testChr() {
final SqlOperatorFixture f0 = fixture()
.setFor(SqlLibraryOperators.CHR, VM_FENNEL, VM_JAVA);
Expand Down

0 comments on commit 89c940c

Please sign in to comment.