This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new b25114f7c6 [CALCITE-7529] Casts between literals and TIME/TIMESTAMP
can lose precision beyond milliseconds
b25114f7c6 is described below
commit b25114f7c616b6e3fb4007071406f79e765ff66f
Author: zzwqqq <[email protected]>
AuthorDate: Tue Jun 2 14:14:51 2026 +0800
[CALCITE-7529] Casts between literals and TIME/TIMESTAMP can lose precision
beyond milliseconds
---
.../adapter/enumerable/RexToLixTranslator.java | 6 +-
.../java/org/apache/calcite/rex/RexBuilder.java | 127 +++++++++++++
.../org/apache/calcite/rex/RexExecutorImpl.java | 27 ++-
.../org/apache/calcite/util/BuiltInMethod.java | 4 +
.../org/apache/calcite/rex/RexExecutorTest.java | 207 +++++++++++++++++++++
.../org/apache/calcite/test/SqlOperatorTest.java | 2 +-
6 files changed, 367 insertions(+), 6 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index 61fa2b0d71..7d2cb6b732 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
@@ -491,7 +491,8 @@ private Expression getConvertExpression(
case TIME:
return RexImpTable.optimize2(operand,
Expressions.isConstantNull(format)
- ? Expressions.call(BuiltInMethod.UNIX_TIME_TO_STRING.method,
operand)
+ ?
Expressions.call(BuiltInMethod.UNIX_TIME_TO_STRING_WITH_PRECISION.method,
+ operand, Expressions.constant(sourceType.getPrecision()))
: Expressions.call(
Expressions.new_(
BuiltInMethod.FORMAT_TIME.method.getDeclaringClass()),
@@ -508,7 +509,8 @@ private Expression getConvertExpression(
case TIMESTAMP:
return RexImpTable.optimize2(operand,
Expressions.isConstantNull(format)
- ? Expressions.call(BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method,
operand)
+ ?
Expressions.call(BuiltInMethod.UNIX_TIMESTAMP_TO_STRING_WITH_PRECISION.method,
+ operand, Expressions.constant(sourceType.getPrecision()))
: Expressions.call(
Expressions.new_(
BuiltInMethod.FORMAT_TIMESTAMP.method.getDeclaringClass()),
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index b77fe02830..95030b5b90 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -29,17 +29,21 @@
import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
import org.apache.calcite.runtime.FlatLists;
import org.apache.calcite.runtime.SqlFunctions;
+import org.apache.calcite.sql.SqlAbstractDateTimeLiteral;
import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlIntervalQualifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlSpecialOperator;
+import org.apache.calcite.sql.SqlTimeLiteral;
+import org.apache.calcite.sql.SqlTimestampLiteral;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.fun.SqlCountAggFunction;
import org.apache.calcite.sql.fun.SqlLibraryOperators;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.parser.SqlParserUtil;
import org.apache.calcite.sql.type.ArraySqlType;
import org.apache.calcite.sql.type.IntervalSqlType;
import org.apache.calcite.sql.type.MapSqlType;
@@ -805,6 +809,12 @@ public RexNode makeCast(
&& SqlTypeUtil.isExactNumeric(type)) {
return makeCastBooleanToExact(type, exp);
}
+ final RexNode literalCast =
+ makeCastForTemporalLiteral(
+ pos, type, literal, matchNullability, safe, format);
+ if (literalCast != null) {
+ return literalCast;
+ }
if (canRemoveCastFromLiteral(type, value, typeName)) {
switch (typeName) {
case INTERVAL_YEAR:
@@ -877,6 +887,123 @@ public RexNode makeCast(
return makeAbstractCast(pos, type, exp, safe, format);
}
+ private @Nullable RexNode makeCastForTemporalLiteral(
+ SqlParserPos pos,
+ RelDataType type,
+ RexLiteral literal,
+ boolean matchNullability,
+ boolean safe,
+ RexLiteral format) {
+ if (!format.isNull()) {
+ return null;
+ }
+ if (SqlTypeUtil.isCharacter(literal.getType())) {
+ return makeCastFromCharacterLiteralToTemporal(
+ pos, type, literal, matchNullability, safe, format);
+ }
+ if (SqlTypeUtil.isCharacter(type)) {
+ return makeCharacterLiteralFromTemporalLiteral(type, literal);
+ }
+ return null;
+ }
+
+ private @Nullable RexNode makeCastFromCharacterLiteralToTemporal(
+ SqlParserPos pos,
+ RelDataType type,
+ RexLiteral literal,
+ boolean matchNullability,
+ boolean safe,
+ RexLiteral format) {
+ final NlsString nlsString = literal.getValueAs(NlsString.class);
+ if (nlsString == null) {
+ return null;
+ }
+ final String value = nlsString.getValue().trim();
+ final RexNode temporalLiteral;
+ try {
+ switch (type.getSqlTypeName()) {
+ case TIME:
+ final SqlTimeLiteral timeLiteral =
+ SqlParserUtil.parseTimeLiteral(value, pos);
+ if (!isExactFractionalSecondLiteral(timeLiteral, value)) {
+ return null;
+ }
+ final TimeString time =
+ requireNonNull(timeLiteral.getValueAs(TimeString.class),
+ "timeLiteral.getValueAs(TimeString.class)");
+ temporalLiteral = makeTimeLiteral(time, type.getPrecision());
+ break;
+ case TIMESTAMP:
+ final SqlTimestampLiteral timestampLiteral =
+ SqlParserUtil.parseTimestampLiteral(value, pos);
+ if (!isExactFractionalSecondLiteral(timestampLiteral, value)) {
+ return null;
+ }
+ final TimestampString timestamp =
+ requireNonNull(timestampLiteral.getValueAs(TimestampString.class),
+ "timestampLiteral.getValueAs(TimestampString.class)");
+ temporalLiteral = makeTimestampLiteral(timestamp, type.getPrecision());
+ break;
+ default:
+ return null;
+ }
+ } catch (RuntimeException e) {
+ return safe ? makeNullLiteral(type) : null;
+ }
+ if (type.isNullable()
+ && !temporalLiteral.getType().isNullable()
+ && matchNullability) {
+ return makeAbstractCast(pos, type, temporalLiteral, safe, format);
+ }
+ return temporalLiteral;
+ }
+
+ /** Converts a TIME or TIMESTAMP literal to a character literal.
+ *
+ * <p>Returns null if the literal is not a TIME or TIMESTAMP, the literal
+ * cannot be read as the corresponding temporal string value, or the
formatted
+ * temporal value does not fit in the target character type. */
+ private @Nullable RexNode makeCharacterLiteralFromTemporalLiteral(
+ RelDataType type,
+ RexLiteral literal) {
+ // Format temporal literals directly so they do not go through generated
+ // code, whose TIME/TIMESTAMP runtime values have millisecond precision.
+ final String value;
+ final int precision = literal.getType().getPrecision();
+ switch (literal.getType().getSqlTypeName()) {
+ case TIME:
+ final TimeString time = literal.getValueAs(TimeString.class);
+ if (time == null) {
+ return null;
+ }
+ value = time.toString(precision);
+ break;
+ case TIMESTAMP:
+ final TimestampString timestamp =
literal.getValueAs(TimestampString.class);
+ if (timestamp == null) {
+ return null;
+ }
+ value = timestamp.toString(precision);
+ break;
+ default:
+ return null;
+ }
+ // Only create a character literal if the formatted temporal value fits in
+ // the target type.
+ return SqlTypeUtil.comparePrecision(type.getPrecision(), value.length())
>= 0
+ ? makeLiteral(value, type, true)
+ : null;
+ }
+
+ /** Returns whether a parsed TIME or TIMESTAMP literal has fractional-second
+ * precision high enough to preserve the original character value exactly. */
+ private static boolean isExactFractionalSecondLiteral(
+ SqlAbstractDateTimeLiteral literal, String value) {
+ return literal.getPrec() != 0
+ && literal.getPrec() != RelDataType.PRECISION_NOT_SPECIFIED
+ && literal.toFormattedString().equals(value);
+ }
+
/** Returns the lowest granularity unit for the given unit.
* YEAR and MONTH intervals are stored as months;
* HOUR, MINUTE, SECOND intervals are stored as milliseconds. */
diff --git a/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java
b/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java
index 3a894d0fba..acfbbdc56d 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexExecutorImpl.java
@@ -43,6 +43,7 @@
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -136,14 +137,34 @@ public static RexExecutable getExecutable(RexBuilder
rexBuilder, List<RexNode> e
@Override public void reduce(RexBuilder rexBuilder, List<RexNode> constExps,
List<RexNode> reducedValues) {
assert reducedValues.isEmpty();
+ final List<RexNode> exps = new ArrayList<>();
+ final List<Integer> ordinals = new ArrayList<>();
+ for (int i = 0; i < constExps.size(); i++) {
+ final RexNode constExp = constExps.get(i);
+ // Literals are already reduced. Keep them as Rex values instead of
+ // round-tripping through generated code.
+ if (!(constExp instanceof RexLiteral)) {
+ ordinals.add(i);
+ exps.add(constExp);
+ }
+ }
+ if (exps.isEmpty()) {
+ reducedValues.addAll(constExps);
+ return;
+ }
try {
- String code = compile(rexBuilder, constExps, (list, index, storageType)
-> {
+ String code = compile(rexBuilder, exps, (list, index, storageType) -> {
throw new UnsupportedOperationException();
});
- final RexExecutable executable = new RexExecutable(code, constExps);
+ final RexExecutable executable = new RexExecutable(code, exps);
executable.setDataContext(dataContext);
- executable.reduce(rexBuilder, constExps, reducedValues);
+ final List<RexNode> values = new ArrayList<>(exps.size());
+ executable.reduce(rexBuilder, exps, values);
+ reducedValues.addAll(constExps);
+ for (int i = 0; i < ordinals.size(); i++) {
+ reducedValues.set(ordinals.get(i), values.get(i));
+ }
} catch (RuntimeException ex) {
// Something went wrong during constant reduction (for example,
// we may have attempted a division by zero).
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 98b67a02bb..81fc9d6f04 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -784,8 +784,12 @@ public enum BuiltInMethod {
String.class, int.class),
UNIX_DATE_TO_STRING(DateTimeUtils.class, "unixDateToString", int.class),
UNIX_TIME_TO_STRING(DateTimeUtils.class, "unixTimeToString", int.class),
+ UNIX_TIME_TO_STRING_WITH_PRECISION(DateTimeUtils.class, "unixTimeToString",
+ int.class, int.class),
UNIX_TIMESTAMP_TO_STRING(DateTimeUtils.class, "unixTimestampToString",
long.class),
+ UNIX_TIMESTAMP_TO_STRING_WITH_PRECISION(DateTimeUtils.class,
+ "unixTimestampToString", long.class, int.class),
INTERVAL_YEAR_MONTH_TO_STRING(DateTimeUtils.class,
"intervalYearMonthToString", int.class, TimeUnitRange.class),
INTERVAL_DAY_TIME_TO_STRING(DateTimeUtils.class, "intervalDayTimeToString",
diff --git a/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java
b/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java
index a98cffc000..16d2978355 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexExecutorTest.java
@@ -15,11 +15,14 @@
* limitations under the License.
*/
package org.apache.calcite.rex;
+
import org.apache.calcite.DataContext;
import org.apache.calcite.DataContexts;
import org.apache.calcite.avatica.util.ByteString;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
import org.apache.calcite.sql.SqlBinaryOperator;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperator;
@@ -35,6 +38,7 @@
import org.apache.calcite.util.DateString;
import org.apache.calcite.util.NlsString;
import org.apache.calcite.util.TestUtil;
+import org.apache.calcite.util.TimeString;
import org.apache.calcite.util.TimestampString;
import org.apache.calcite.util.Util;
@@ -160,6 +164,209 @@ protected void check(final Action action) {
});
}
+ @Test void testReduceTimeCastWithFractionalSeconds() {
+ checkHighPrecision((rexBuilder, executor) -> {
+ for (int precision = 1; precision <= 9; precision++) {
+ final RexNode cast =
+ rexBuilder.makeCast(
+ rexBuilder.getTypeFactory().createSqlType(SqlTypeName.TIME,
+ precision),
+ rexBuilder.makeLiteral("12:34:56.123456789"));
+
+ final RexNode reduced = reduce(rexBuilder, executor, cast).get(0);
+
+ assertThat(reduced, instanceOf(RexLiteral.class));
+ assertThat(
+ ((RexLiteral) reduced).getValueAs(TimeString.class)
+ .toString(precision),
+ equalTo(new TimeString("12:34:56.123456789").round(precision)
+ .toString(precision)));
+ }
+ });
+ }
+
+ @Test void testReduceTimestampCastWithFractionalSeconds() {
+ checkHighPrecision((rexBuilder, executor) -> {
+ for (int precision = 1; precision <= 9; precision++) {
+ final RexNode cast =
+ rexBuilder.makeCast(
+
rexBuilder.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP,
+ precision),
+ rexBuilder.makeLiteral("2020-01-01 12:34:56.123456789"));
+
+ final RexNode reduced = reduce(rexBuilder, executor, cast).get(0);
+
+ assertThat(reduced, instanceOf(RexLiteral.class));
+ assertThat(
+ ((RexLiteral) reduced).getValueAs(TimestampString.class)
+ .toString(precision),
+ equalTo(new TimestampString("2020-01-01 12:34:56.123456789")
+ .round(precision).toString(precision)));
+ }
+ });
+ }
+
+ @Test void testReduceLiteralWithMicroseconds() {
+ checkHighPrecision((rexBuilder, executor) -> {
+ final RexLiteral timeLiteral =
+ rexBuilder.makeTimeLiteral(new TimeString("12:34:56.123456"), 6);
+ final RexLiteral timestampLiteral =
+ rexBuilder.makeTimestampLiteral(
+ new TimestampString("2020-01-01 12:34:56.123456"), 6);
+ final RexNode expression =
+ rexBuilder.makeCall(SqlStdOperatorTable.PLUS,
+ rexBuilder.makeExactLiteral(BigDecimal.TEN),
+ rexBuilder.makeExactLiteral(BigDecimal.ONE));
+
+ final List<RexNode> reducedValues =
+ reduce(rexBuilder, executor, timeLiteral, expression,
timestampLiteral);
+
+ assertThat(reducedValues, hasSize(3));
+ assertThat(
+ ((RexLiteral)
reducedValues.get(0)).getValueAs(TimeString.class).toString(6),
+ equalTo("12:34:56.123456"));
+ assertThat(((RexLiteral) reducedValues.get(1)).getValue2(),
equalTo(11L));
+ assertThat(
+ ((RexLiteral) reducedValues.get(2)).getValueAs(TimestampString.class)
+ .toString(6),
+ equalTo("2020-01-01 12:34:56.123456"));
+ });
+ }
+
+ @Test void testReduceTimeToVarcharWithFractionalSeconds() {
+ checkHighPrecision((rexBuilder, executor) -> {
+ final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory();
+ for (int precision = 1; precision <= 9; precision++) {
+ final RexNode castToTime =
+ rexBuilder.makeCast(
+ typeFactory.createSqlType(SqlTypeName.TIME,
+ precision),
+ rexBuilder.makeLiteral("12:34:56.123456789"));
+ final RexNode castToVarchar =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR,
30),
+ castToTime);
+
+ final RexNode reduced = reduce(rexBuilder, executor,
castToVarchar).get(0);
+
+ assertThat(reduced, instanceOf(RexLiteral.class));
+ assertThat(((RexLiteral) reduced).getValueAs(String.class),
+ equalTo(new TimeString("12:34:56.123456789")
+ .toString(precision)));
+ }
+ });
+ }
+
+ @Test void testReduceTimeToVarcharWithMilliseconds() {
+ check((rexBuilder, executor) -> {
+ final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory();
+ final RexNode castToTime =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.TIME, 3),
+ rexBuilder.makeLiteral("12:34:56.987654"));
+ final RexNode castToVarchar =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR,
30),
+ castToTime);
+
+ final RexNode reduced = reduce(rexBuilder, executor,
castToVarchar).get(0);
+
+ assertThat(reduced, instanceOf(RexLiteral.class));
+ assertThat(((RexLiteral) reduced).getValueAs(String.class),
+ equalTo("12:34:56.987"));
+ });
+ }
+
+ @Test void testReduceTimeToVarcharWithZeroMilliseconds() {
+ check((rexBuilder, executor) -> {
+ final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory();
+ final RexNode castToTime =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.TIME, 3),
+ rexBuilder.makeLiteral("12:34:56.000456"));
+ final RexNode castToVarchar =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR,
30),
+ castToTime);
+
+ final RexNode reduced = reduce(rexBuilder, executor,
castToVarchar).get(0);
+
+ assertThat(reduced, instanceOf(RexLiteral.class));
+ assertThat(((RexLiteral) reduced).getValueAs(String.class),
+ equalTo("12:34:56.000"));
+ });
+ }
+
+ @Test void testReduceTimestampToVarcharWithFractionalSeconds() {
+ checkHighPrecision((rexBuilder, executor) -> {
+ final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory();
+ for (int precision = 1; precision <= 9; precision++) {
+ final RexNode castToTimestamp =
+ rexBuilder.makeCast(
+ typeFactory.createSqlType(SqlTypeName.TIMESTAMP,
+ precision),
+ rexBuilder.makeLiteral("2020-01-01 12:34:56.123456789"));
+ final RexNode castToVarchar =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR,
30),
+ castToTimestamp);
+
+ final RexNode reduced = reduce(rexBuilder, executor,
castToVarchar).get(0);
+
+ assertThat(reduced, instanceOf(RexLiteral.class));
+ assertThat(((RexLiteral) reduced).getValueAs(String.class),
+ equalTo(new TimestampString("2020-01-01 12:34:56.123456789")
+ .toString(precision)));
+ }
+ });
+ }
+
+ @Test void testReduceTimestampToVarcharWithZeroMilliseconds() {
+ check((rexBuilder, executor) -> {
+ final RelDataTypeFactory typeFactory = rexBuilder.getTypeFactory();
+ final RexNode castToTimestamp =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.TIMESTAMP,
3),
+ rexBuilder.makeLiteral("2020-01-01 12:34:56.000456"));
+ final RexNode castToVarchar =
+ rexBuilder.makeCast(typeFactory.createSqlType(SqlTypeName.VARCHAR,
30),
+ castToTimestamp);
+
+ final RexNode reduced = reduce(rexBuilder, executor,
castToVarchar).get(0);
+
+ assertThat(reduced, instanceOf(RexLiteral.class));
+ assertThat(((RexLiteral) reduced).getValueAs(String.class),
+ equalTo("2020-01-01 12:34:56.000"));
+ });
+ }
+
+ private static void checkHighPrecision(Action action) {
+ action.check(new RexBuilder(highPrecisionTemporalTypeFactory()),
executor());
+ }
+
+ private static List<RexNode> reduce(RexBuilder rexBuilder,
+ RexExecutorImpl executor, RexNode... nodes) {
+ final List<RexNode> reducedValues = new ArrayList<>();
+ executor.reduce(rexBuilder, ImmutableList.copyOf(nodes), reducedValues);
+ return reducedValues;
+ }
+
+ private static RelDataTypeFactory highPrecisionTemporalTypeFactory() {
+ return new JavaTypeFactoryImpl(
+ new RelDataTypeSystemImpl() {
+ @Override public int getMaxPrecision(SqlTypeName typeName) {
+ switch (typeName) {
+ case TIME:
+ case TIMESTAMP:
+ return 9;
+ default:
+ return super.getMaxPrecision(typeName);
+ }
+ }
+ });
+ }
+
+ private static RexExecutorImpl executor() {
+ return new RexExecutorImpl(
+ DataContexts.of(
+ ImmutableMap.of(
+ DataContext.Variable.TIME_ZONE.camelName,
TimeZone.getTimeZone("GMT"),
+ DataContext.Variable.LOCALE.camelName, Locale.US)));
+ }
+
private void checkConstant(final Object operand,
final Function<RexBuilder, RexNode> function) {
check((rexBuilder, executor) -> {
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 cf2792c5a6..c40f077d8d 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -2706,7 +2706,7 @@ private static void
checkConcatWithSeparatorInPostgres(SqlOperatorFixture f) {
f.checkString("concat_ws(',', 'a', array['b', 'c'], DATE '1945-02-24')",
"a,[b, c],1945-02-24", "VARCHAR NOT NULL");
f.checkString("concat_ws(',', timestamp '2024-07-06 12:15:48.678')",
- "2024-07-06 12:15:48", "VARCHAR NOT NULL");
+ "2024-07-06 12:15:48.678", "VARCHAR NOT NULL");
f.checkString("concat_ws(',', time '12:34:56', time '13:00:00', 2, 'abc')",
"12:34:56,13:00:00,2,abc", "VARCHAR NOT NULL");
f.checkString("concat_ws(',', null, null)", "", "VARCHAR NOT NULL");