diff --git a/ksql-cli/src/test/java/io/confluent/ksql/CliTest.java b/ksql-cli/src/test/java/io/confluent/ksql/CliTest.java index 9c76d2e41473..41588d887f6f 100644 --- a/ksql-cli/src/test/java/io/confluent/ksql/CliTest.java +++ b/ksql-cli/src/test/java/io/confluent/ksql/CliTest.java @@ -666,16 +666,23 @@ public void shouldListFunctions() { public void shouldDescribeScalarFunction() throws Exception { final String expectedOutput = "Name : TIMESTAMPTOSTRING\n" + - "Author : confluent\n" + + "Author : Confluent\n" + + "Overview : Converts a BIGINT millisecond timestamp value into the string " + + "representation of the \n" + + " timestamp in the given format.\n" + "Type : scalar\n" + "Jar : internal\n" + - "Variations : \n" + - "\n" + - "\tVariation : TIMESTAMPTOSTRING(BIGINT, VARCHAR)\n" + - "\tReturns : VARCHAR\n"; + "Variations : \n"; localCli.handleLine("describe function timestamptostring;"); - assertThat(terminal.getOutputString(), containsString(expectedOutput)); + final String outputString = terminal.getOutputString(); + assertThat(outputString, containsString(expectedOutput)); + + // variations for Udfs are loaded non-deterministically. Don't assume which variation is first + final String expectedVariation = + "\tVariation : TIMESTAMPTOSTRING(BIGINT, VARCHAR)\n" + + "\tReturns : VARCHAR\n"; + assertThat(outputString, containsString(expectedVariation)); } @Test diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java b/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java index bf5382dccff6..7aa999423855 100644 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/InternalFunctionRegistry.java @@ -24,8 +24,6 @@ import io.confluent.ksql.function.udaf.topk.TopKAggregateFunctionFactory; import io.confluent.ksql.function.udaf.topkdistinct.TopkDistinctAggFunctionFactory; import io.confluent.ksql.function.udf.UdfMetadata; -import io.confluent.ksql.function.udf.datetime.StringToTimestamp; -import io.confluent.ksql.function.udf.datetime.TimestampToString; import io.confluent.ksql.function.udf.geo.GeoDistanceKudf; import io.confluent.ksql.function.udf.json.ArrayContainsKudf; import io.confluent.ksql.function.udf.json.JsonExtractStringKudf; @@ -75,7 +73,6 @@ private InternalFunctionRegistry( private void init() { addStringFunctions(); addMathFunctions(); - addDateTimeFunctions(); addGeoFunctions(); addJsonFunctions(); addStructFieldFetcher(); @@ -249,25 +246,6 @@ private void addMathFunctions() { } - - private void addDateTimeFunctions() { - - final KsqlFunction timestampToString = new KsqlFunction( - Schema.OPTIONAL_STRING_SCHEMA, - Arrays.asList(Schema.OPTIONAL_INT64_SCHEMA, Schema.OPTIONAL_STRING_SCHEMA), - "TIMESTAMPTOSTRING", - TimestampToString.class); - addFunction(timestampToString); - - final KsqlFunction stringToTimestamp = new KsqlFunction( - Schema.OPTIONAL_INT64_SCHEMA, - Arrays.asList(Schema.OPTIONAL_STRING_SCHEMA, Schema.OPTIONAL_STRING_SCHEMA), - "STRINGTOTIMESTAMP", - StringToTimestamp.class); - addFunction(stringToTimestamp); - - } - private void addGeoFunctions() { final KsqlFunction geoDistance = new KsqlFunction( Schema.OPTIONAL_FLOAT64_SCHEMA, diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/DateToString.java b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/DateToString.java index 98dc30f05a73..4f5e131c0d52 100644 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/DateToString.java +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/DateToString.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Confluent Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.confluent.ksql.function.udf.datetime; import com.google.common.cache.CacheBuilder; @@ -9,18 +25,20 @@ import java.time.format.DateTimeFormatter; @UdfDescription(name = "datetostring", author = "Confluent", - description = "Converts an integer representing days since epoch to a date string using the given format pattern." - + " Note this is the format Kafka Connect uses to represent dates with no time component." - + " The format pattern should be in the format expected by java.time.format.DateTimeFormatter") + description = "Converts an integer representing days since epoch to a date string" + + " using the given format pattern. Note this is the format Kafka Connect uses" + + " to represent dates with no time component. The format pattern should be" + + " in the format expected by java.time.format.DateTimeFormatter") public class DateToString { private final LoadingCache formatters = CacheBuilder.newBuilder() - .maximumSize(1000) - .build(CacheLoader.from(DateTimeFormatter::ofPattern)); + .maximumSize(1000) + .build(CacheLoader.from(DateTimeFormatter::ofPattern)); - @Udf(description = "Converts an integer representing days since epoch to a string using the given format pattern." - + " The format pattern should be in the format expected by java.time.format.DateTimeFormatter") + @Udf(description = "Converts an integer representing days since epoch to a string" + + " using the given format pattern. The format pattern should be in the format" + + " expected by java.time.format.DateTimeFormatter") public String dateToString(final Integer daysSinceEpoch, final String formatPattern) { final DateTimeFormatter formatter = formatters.getUnchecked(formatPattern); return LocalDate.ofEpochDay(daysSinceEpoch).format(formatter); diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToDate.java b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToDate.java index c676b8e9598f..d73a92b693af 100644 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToDate.java +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToDate.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 Confluent Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.confluent.ksql.function.udf.datetime; import com.google.common.cache.CacheBuilder; @@ -12,7 +28,8 @@ description = "Converts a string representation of a date into an integer representing" + " days since epoch using the given format pattern." + " Note this is the format Kafka Connect uses to represent dates with no time component." - + " The format pattern should be in the format expected by java.time.format.DateTimeFormatter") + + " The format pattern should be in the format expected by" + + " java.time.format.DateTimeFormatter") public class StringToDate { private final LoadingCache formatters = @@ -22,9 +39,10 @@ public class StringToDate { @Udf(description = "Converts a string representation of a date into an integer representing" + " days since epoch using the given format pattern." - + " The format pattern should be in the format expected by java.time.format.DateTimeFormatter") + + " The format pattern should be in the format expected by" + + " java.time.format.DateTimeFormatter") public int stringToDate(final String formattedDate, final String formatPattern) { - DateTimeFormatter formatter = formatters.getUnchecked(formatPattern); + final DateTimeFormatter formatter = formatters.getUnchecked(formatPattern); return ((int)LocalDate.parse(formattedDate, formatter).toEpochDay()); } diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToTimestamp.java b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToTimestamp.java index 7f613223bb57..0bf9f43276c0 100644 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToTimestamp.java +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/StringToTimestamp.java @@ -16,33 +16,30 @@ package io.confluent.ksql.function.udf.datetime; -import io.confluent.ksql.function.KsqlFunctionException; -import io.confluent.ksql.function.udf.Kudf; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import io.confluent.ksql.function.udf.Udf; +import io.confluent.ksql.function.udf.UdfDescription; import io.confluent.ksql.util.timestamp.StringToTimestampParser; -public class StringToTimestamp implements Kudf { +@UdfDescription(name = "stringtotimestamp", author = "Confluent", + description = "Converts a string value in the given format into the BIGINT value" + + " that represents the millisecond timestamp.") +public class StringToTimestamp { - private StringToTimestampParser timestampParser; + private final LoadingCache parsers = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(StringToTimestampParser::new)); - @Override - public Object evaluate(final Object... args) { - if (args.length != 2) { - throw new KsqlFunctionException("StringToTimestamp udf should have two input argument:" - + " date value and format."); - } - try { - ensureInitialized(args); - return timestampParser.parse(args[0].toString()); - } catch (final Exception e) { - throw new KsqlFunctionException("Exception running StringToTimestamp(" + args[0] + ", " - + args[1] + ") : " + e.getMessage(), e); - } - } - - private void ensureInitialized(final Object[] args) { - if (timestampParser == null) { - timestampParser = new StringToTimestampParser(args[1].toString()); - } + @Udf(description = "Converts a string value in the given format into the BIGINT value" + + " that represents the millisecond timestamp." + + " Single quotes in the timestamp format can be escaped with ''," + + " for example: 'yyyy-MM-dd''T''HH:mm:ssX'.") + public long stringToTimestamp(final String formattedTimestamp, final String formatPattern) { + final StringToTimestampParser timestampParser = parsers.getUnchecked(formatPattern); + return timestampParser.parse(formattedTimestamp); } } diff --git a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/TimestampToString.java b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/TimestampToString.java index 10ee055d50b0..2a11c6c74c64 100644 --- a/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/TimestampToString.java +++ b/ksql-engine/src/main/java/io/confluent/ksql/function/udf/datetime/TimestampToString.java @@ -16,45 +16,52 @@ package io.confluent.ksql.function.udf.datetime; -import io.confluent.ksql.function.KsqlFunctionException; -import io.confluent.ksql.function.udf.Kudf; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import io.confluent.ksql.function.udf.Udf; +import io.confluent.ksql.function.udf.UdfDescription; import java.sql.Timestamp; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -public class TimestampToString implements Kudf { +@UdfDescription(name = "timestamptostring", author = "Confluent", + description = "Converts a BIGINT millisecond timestamp value into" + + " the string representation of the timestamp in the given format.") +public class TimestampToString { - private DateTimeFormatter threadSafeFormatter; + private final LoadingCache formatters = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(DateTimeFormatter::ofPattern)); - @Override - public Object evaluate(final Object... args) { - if (args.length < 2) { - throw new KsqlFunctionException("TimestampToString udf should have at least " - + "two input arguments: date value and format."); - } - - if (args.length > 3) { - throw new KsqlFunctionException("TimestampToString udf should have at most " - + "three input arguments: date value, format and zone."); - } - - try { - ensureInitialized(args); - final Timestamp timestamp = new Timestamp((Long) args[0]); - final ZoneId zoneId = - (args.length == 3) ? ZoneId.of(args[2].toString()) : ZoneId.systemDefault(); - return timestamp.toInstant() - .atZone(zoneId) - .format(threadSafeFormatter); - } catch (final Exception e) { - throw new KsqlFunctionException("Exception running TimestampToString(" + args[0] + " , " - + args[1] + ((args.length == 3) ? (" , " + args[2]) : "") + ") : " + e.getMessage(), e); - } + @Udf(description = "Converts a BIGINT millisecond timestamp value into the" + + " string representation of the timestamp in the given format. Single quotes in the" + + " timestamp format can be escaped with '', for example: 'yyyy-MM-dd''T''HH:mm:ssX'" + + " The format pattern should be in the format expected" + + " by java.time.format.DateTimeFormatter") + public String timestampToString(final long millisSinceEpoch, final String formatPattern) { + final Timestamp timestamp = new Timestamp(millisSinceEpoch); + final DateTimeFormatter formatter = formatters.getUnchecked(formatPattern); + return timestamp.toInstant() + .atZone(ZoneId.systemDefault()) + .format(formatter); } - private void ensureInitialized(final Object[] args) { - if (threadSafeFormatter == null) { - threadSafeFormatter = DateTimeFormatter.ofPattern(args[1].toString()); - } + @Udf(description = "Converts a BIGINT millisecond timestamp value into the" + + " string representation of the timestamp in the given format. Single quotes in the" + + " timestamp format can be escaped with '', for example: 'yyyy-MM-dd''T''HH:mm:ssX'" + + " The format pattern should be in the format expected by java.time.format.DateTimeFormatter" + + " TIMEZONE is a java.util.TimeZone ID format, for example: \"UTC\"," + + " \"America/Los_Angeles\", \"PDT\", \"Europe/London\"") + public String timestampToString(final long millisSinceEpoch, final String formatPattern, + final String timeZone) { + final Timestamp timestamp = new Timestamp(millisSinceEpoch); + final DateTimeFormatter formatter = formatters.getUnchecked(formatPattern); + final ZoneId zoneId = ZoneId.of(timeZone); + return timestamp.toInstant() + .atZone(zoneId) + .format(formatter); } + } diff --git a/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/StringToTimestampTest.java b/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/StringToTimestampTest.java index 7cef3428d6aa..f0be5e24b457 100644 --- a/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/StringToTimestampTest.java +++ b/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/StringToTimestampTest.java @@ -19,18 +19,24 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import io.confluent.ksql.function.KsqlFunctionException; +import com.google.common.util.concurrent.UncheckedExecutionException; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.format.DateTimeParseException; import java.util.stream.IntStream; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; public class StringToTimestampTest { private StringToTimestamp udf; + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + @Before public void setUp(){ udf = new StringToTimestamp(); @@ -39,7 +45,7 @@ public void setUp(){ @Test public void shouldConvertStringToTimestamp() throws ParseException { // When: - final Object result = udf.evaluate("2021-12-01 12:10:11.123", "yyyy-MM-dd HH:mm:ss.SSS"); + final Object result = udf.stringToTimestamp("2021-12-01 12:10:11.123", "yyyy-MM-dd HH:mm:ss.SSS"); // Then: final long expectedResult = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS") @@ -50,7 +56,7 @@ public void shouldConvertStringToTimestamp() throws ParseException { @Test public void shouldSupportEmbeddedChars() throws ParseException { // When: - final Object result = udf.evaluate("2021-12-01T12:10:11.123Fred", "yyyy-MM-dd'T'HH:mm:ss.SSS'Fred'"); + final Object result = udf.stringToTimestamp("2021-12-01T12:10:11.123Fred", "yyyy-MM-dd'T'HH:mm:ss.SSS'Fred'"); // Then: final long expectedResult = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Fred'") @@ -58,29 +64,25 @@ public void shouldSupportEmbeddedChars() throws ParseException { assertThat(result, is(expectedResult)); } - @Test(expected = KsqlFunctionException.class) - public void shouldThrowIfTooFewParameters() { - udf.evaluate("2021-12-01 12:10:11.123"); - } - - @Test(expected = KsqlFunctionException.class) - public void shouldThrowIfTooManyParameters() { - udf.evaluate("2021-12-01 12:10:11.123", "yyyy-MM-dd HH:mm:ss.SSS", "extra"); - } - - @Test(expected = KsqlFunctionException.class) + @Test public void shouldThrowIfFormatInvalid() { - udf.evaluate("2021-12-01 12:10:11.123", "invalid"); + expectedException.expect(UncheckedExecutionException.class); + expectedException.expectMessage("Unknown pattern letter: i"); + udf.stringToTimestamp("2021-12-01 12:10:11.123", "invalid"); } - @Test(expected = KsqlFunctionException.class) + @Test public void shouldThrowIfParseFails() { - udf.evaluate("invalid", "yyyy-MM-dd'T'HH:mm:ss.SSS"); + expectedException.expect(DateTimeParseException.class); + expectedException.expectMessage("Text 'invalid' could not be parsed at index 0"); + udf.stringToTimestamp("invalid", "yyyy-MM-dd'T'HH:mm:ss.SSS"); } - @Test(expected = KsqlFunctionException.class) + @Test public void shouldThrowOnEmptyString() { - udf.evaluate("", "yyyy-MM-dd'T'HH:mm:ss.SSS"); + expectedException.expect(DateTimeParseException.class); + expectedException.expectMessage("Text '' could not be parsed at index 0"); + udf.stringToTimestamp("", "yyyy-MM-dd'T'HH:mm:ss.SSS"); } @Test @@ -93,7 +95,24 @@ public void shouldBeThreadSafe() { } catch (final ParseException e) { Assert.fail(e.getMessage()); } - udf.evaluate("1988-01-12 10:12:13.456", "yyyy-MM-dd HH:mm:ss.SSS"); + udf.stringToTimestamp("1988-01-12 10:12:13.456", "yyyy-MM-dd HH:mm:ss.SSS"); + }); + } + + @Test + public void shouldWorkWithManyDifferentFormatters() { + IntStream.range(0, 10_000) + .parallel() + .forEach(idx -> { + try { + final String sourceDate = "2018-12-01 10:12:13.456X" + idx; + final String pattern = "yyyy-MM-dd HH:mm:ss.SSS'X" + idx + "'"; + final long result = udf.stringToTimestamp(sourceDate, pattern); + final long expectedResult = new SimpleDateFormat(pattern).parse(sourceDate).getTime(); + assertThat(result, is(expectedResult)); + } catch (final Exception e) { + Assert.fail(e.getMessage()); + } }); } @@ -122,7 +141,7 @@ public void shouldBehaveLikeSimpleDateFormat() throws Exception { private void assertLikeSimpleDateFormat(final String value, final String format) throws Exception { final long expected = new SimpleDateFormat(format).parse(value).getTime(); - final Object result = new StringToTimestamp().evaluate(value, format); + final Object result = new StringToTimestamp().stringToTimestamp(value, format); assertThat(result, is(expected)); } } \ No newline at end of file diff --git a/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/TimestampToStringTest.java b/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/TimestampToStringTest.java index 0a0c575823fa..5c62c56ffc64 100644 --- a/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/TimestampToStringTest.java +++ b/ksql-engine/src/test/java/io/confluent/ksql/function/udf/datetime/TimestampToStringTest.java @@ -19,10 +19,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import io.confluent.ksql.function.KsqlFunctionException; +import com.google.common.util.concurrent.UncheckedExecutionException; import java.text.SimpleDateFormat; +import java.time.zone.ZoneRulesException; import java.util.Date; import java.util.stream.IntStream; +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -43,7 +45,7 @@ public void setUp(){ @Test public void shouldConvertTimestampToString() { // When: - final Object result = udf.evaluate(1638360611123L, "yyyy-MM-dd HH:mm:ss.SSS"); + final Object result = udf.timestampToString(1638360611123L, "yyyy-MM-dd HH:mm:ss.SSS"); // Then: final String expectedResult = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS") @@ -54,7 +56,7 @@ public void shouldConvertTimestampToString() { @Test public void testUTCTimeZone() { // When: - final Object result = udf.evaluate(1534353043000L, "yyyy-MM-dd HH:mm:ss", "UTC"); + final Object result = udf.timestampToString(1534353043000L, "yyyy-MM-dd HH:mm:ss", "UTC"); // Then: assertThat(result, is("2018-08-15 17:10:43")); @@ -63,7 +65,7 @@ public void testUTCTimeZone() { @Test public void testPSTTimeZone() { // When: - final Object result = udf.evaluate(1534353043000L, + final Object result = udf.timestampToString(1534353043000L, "yyyy-MM-dd HH:mm:ss", "America/Los_Angeles"); // Then: @@ -74,11 +76,11 @@ public void testPSTTimeZone() { public void testTimeZoneInFormat() { // When: final long timestamp = 1534353043000L; - final Object localTime = udf.evaluate(timestamp, + final Object localTime = udf.timestampToString(timestamp, "yyyy-MM-dd HH:mm:ss zz"); - final Object pacificTime = udf.evaluate(timestamp, + final Object pacificTime = udf.timestampToString(timestamp, "yyyy-MM-dd HH:mm:ss zz", "America/Los_Angeles"); - final Object universalTime = udf.evaluate(timestamp, + final Object universalTime = udf.timestampToString(timestamp, "yyyy-MM-dd HH:mm:ss zz", "UTC"); // Then: @@ -92,15 +94,15 @@ public void testTimeZoneInFormat() { @Test public void shouldThrowIfInvalidTimeZone() { - expectedException.expect(KsqlFunctionException.class); + expectedException.expect(ZoneRulesException.class); expectedException.expectMessage("Unknown time-zone ID: PST"); - udf.evaluate(1638360611123L, "yyyy-MM-dd HH:mm:ss.SSS", "PST"); + udf.timestampToString(1638360611123L, "yyyy-MM-dd HH:mm:ss.SSS", "PST"); } @Test public void shouldSupportEmbeddedChars() { // When: - final Object result = udf.evaluate(1638360611123L, "yyyy-MM-dd'T'HH:mm:ss.SSS'Fred'"); + final Object result = udf.timestampToString(1638360611123L, "yyyy-MM-dd'T'HH:mm:ss.SSS'Fred'"); // Then: final String expectedResult = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Fred'") @@ -108,43 +110,54 @@ public void shouldSupportEmbeddedChars() { assertThat(result, is(expectedResult)); } - @Test - public void shouldThrowIfTooFewParameters() { - expectedException.expect(KsqlFunctionException.class); - expectedException.expectMessage( - "should have at least two input arguments: date value and format."); - udf.evaluate(1638360611123L); - } - - @Test - public void shouldThrowIfTooManyParameters() { - expectedException.expect(KsqlFunctionException.class); - expectedException.expectMessage( - "should have at most three input arguments: date value, format and zone."); - udf.evaluate(1638360611123L, "yyyy-MM-dd HH:mm:ss.SSS", "UTC", "extra"); - } - @Test public void shouldThrowIfFormatInvalid() { - expectedException.expect(KsqlFunctionException.class); + expectedException.expect(UncheckedExecutionException.class); expectedException.expectMessage("Unknown pattern letter: i"); - udf.evaluate(1638360611123L, "invalid"); + udf.timestampToString(1638360611123L, "invalid"); } @Test - public void shouldThrowIfNotTimestamp() { - expectedException.expect(KsqlFunctionException.class); - expectedException.expectMessage("java.lang.String cannot be cast to java.lang.Long"); - udf.evaluate("invalid", "yyyy-MM-dd HH:mm:ss.SSS"); + public void shouldBeThreadSafe() { + IntStream.range(0, 10_000) + .parallel() + .forEach(idx -> { + shouldConvertTimestampToString(); + udf.timestampToString(1538361611123L, "yyyy-MM-dd HH:mm:ss.SSS"); + }); } @Test - public void shouldBeThreadSafe() { + public void shouldWorkWithManyDifferentFormatters() { IntStream.range(0, 10_000) .parallel() .forEach(idx -> { - shouldConvertTimestampToString(); - udf.evaluate(1538361611123L, "yyyy-MM-dd HH:mm:ss.SSS"); + try { + final String pattern = "yyyy-MM-dd HH:mm:ss.SSS'X" + idx + "'"; + final long millis = 1538361611123L + idx; + final String result = udf.timestampToString(millis, pattern); + final String expectedResult = new SimpleDateFormat(pattern).format(new Date(millis)); + assertThat(result, is(expectedResult)); + } catch (final Exception e) { + Assert.fail(e.getMessage()); + } + }); + } + + @Test + public void shouldRoundTripWithStringToTimestamp() { + final String pattern = "yyyy-MM-dd HH:mm:ss.SSS'Freya'"; + final StringToTimestamp stringToTimestamp = new StringToTimestamp(); + IntStream.range(-10_000, 20_000) + .parallel() + .forEach(idx -> { + final long millis = 1538361611123L + idx; + final String result = udf.timestampToString(millis, pattern); + final String expectedResult = new SimpleDateFormat(pattern).format(new Date(millis)); + assertThat(result, is(expectedResult)); + + final long roundtripMillis = stringToTimestamp.stringToTimestamp(result, pattern); + assertThat(roundtripMillis, is(millis)); }); } @@ -179,7 +192,7 @@ public void shouldBehaveLikeSimpleDateFormat() { private void assertLikeSimpleDateFormat(final String format) { final String expected = new SimpleDateFormat(format).format(1538361611123L); - final Object result = new TimestampToString().evaluate(1538361611123L, format); + final Object result = new TimestampToString().timestampToString(1538361611123L, format); assertThat(result, is(expected)); } } diff --git a/ksql-rest-app/src/test/java/io/confluent/ksql/rest/server/resources/KsqlResourceTest.java b/ksql-rest-app/src/test/java/io/confluent/ksql/rest/server/resources/KsqlResourceTest.java index 7af7709469af..dfcbbc3d241b 100644 --- a/ksql-rest-app/src/test/java/io/confluent/ksql/rest/server/resources/KsqlResourceTest.java +++ b/ksql-rest-app/src/test/java/io/confluent/ksql/rest/server/resources/KsqlResourceTest.java @@ -375,7 +375,7 @@ public void shouldListFunctions() { // not going to check every function assertThat(functionList.getFunctions(), hasItems( - new SimpleFunctionInfo("TIMESTAMPTOSTRING", FunctionType.scalar), + new SimpleFunctionInfo("EXTRACTJSONFIELD", FunctionType.scalar), new SimpleFunctionInfo("ARRAYCONTAINS", FunctionType.scalar), new SimpleFunctionInfo("CONCAT", FunctionType.scalar), new SimpleFunctionInfo("TOPK", FunctionType.aggregate),