This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit 89c940cc8b6e5ded4c0fa4fa044bab212a7f8599
Author: xiejiajun <jiajunbernou...@foxmail.com>
AuthorDate: Sun Aug 21 14:47:58 2022 +0800

    [CALCITE-5241] Implement CHAR function for MySQL and Spark, also JDBC '{fn 
CHAR(n)}'
    
    Close apache/calcite#2878
---
 core/src/main/codegen/templates/Parser.jj          |  1 +
 .../calcite/adapter/enumerable/RexImpTable.java    |  5 ++++-
 .../org/apache/calcite/runtime/SqlFunctions.java   | 19 ++++++++++++++++---
 .../apache/calcite/sql/SqlJdbcFunctionCall.java    |  1 +
 .../calcite/sql/fun/SqlLibraryOperators.java       | 15 ++++++++++++++-
 .../org/apache/calcite/sql/type/ReturnTypes.java   |  7 +++++++
 .../org/apache/calcite/util/BuiltInMethod.java     |  2 ++
 .../apache/calcite/sql/test/SqlAdvisorTest.java    |  1 +
 core/src/test/resources/sql/functions.iq           | 15 +++++++++++++--
 site/_docs/reference.md                            | 10 ++++------
 .../org/apache/calcite/test/SqlOperatorTest.java   | 22 +++++++++++++++++++---
 11 files changed, 82 insertions(+), 16 deletions(-)

diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index 506661ed19..9ad6582315 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -6949,6 +6949,7 @@ SqlIdentifier ReservedFunctionName() :
     |   <AVG>
     |   <CARDINALITY>
     |   <CEILING>
+    |   <CHAR>
     |   <CHAR_LENGTH>
     |   <CHARACTER_LENGTH>
     |   <COALESCE>
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 9738523f2c..1864b1e459 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -115,6 +115,7 @@ import static 
org.apache.calcite.sql.fun.SqlLibraryOperators.ARRAY_LENGTH;
 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;
@@ -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,
@@ -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);
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index 636b27bfd8..61914293ef 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -462,9 +462,22 @@ public class SqlFunctions {
     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. */
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlJdbcFunctionCall.java 
b/core/src/main/java/org/apache/calcite/sql/SqlJdbcFunctionCall.java
index ccb804912d..4ef5e9d4ed 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlJdbcFunctionCall.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlJdbcFunctionCall.java
@@ -708,6 +708,7 @@ public class SqlJdbcFunctionCall extends SqlFunction {
       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",
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index acec95ff1e..794270682c 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -650,7 +650,20 @@ public abstract class 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,
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
index 3512ba3a3e..99750c535b 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
@@ -347,6 +347,13 @@ public abstract class ReturnTypes {
   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.
    */
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index a3e11dba27..2c3c2d9ba1 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -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),
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java 
b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
index 82075d2ae1..54032f3db1 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
@@ -138,6 +138,7 @@ class SqlAdvisorTest extends SqlValidatorTestCase {
           "KEYWORD(CAST)",
           "KEYWORD(CEIL)",
           "KEYWORD(CEILING)",
+          "KEYWORD(CHAR)",
           "KEYWORD(CHARACTER_LENGTH)",
           "KEYWORD(CHAR_LENGTH)",
           "KEYWORD(CLASSIFIER)",
diff --git a/core/src/test/resources/sql/functions.iq 
b/core/src/test/resources/sql/functions.iq
index dbba1a895d..9b9ab67089 100644
--- a/core/src/test/resources/sql/functions.iq
+++ b/core/src/test/resources/sql/functions.iq
@@ -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 |
@@ -115,7 +126,7 @@ select sinh(1);
 
 !ok
 
-#CONCAT
+# CONCAT
 select concat('a', 'b');
 +--------+
 | EXPR$0 |
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 1f4f08efeb..54befa1199 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -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
@@ -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
@@ -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
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 5bac48f953..8c94c1ed4b 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -1492,9 +1492,8 @@ public class SqlOperatorTest {
     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",
@@ -1630,6 +1629,23 @@ public class SqlOperatorTest {
 
   }
 
+  @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);

Reply via email to