Display errors for invalid timezones in TIME_FORMAT (#11423)

Users sometimes make typos when picking timezones - like `America/Los Angeles`
instead of `America/Los_Angeles` instead of defaulting to UTC, this change
makes it so that an error is thrown instead notifying the user of their mistake.
diff --git a/core/src/main/java/org/apache/druid/java/util/common/DateTimes.java b/core/src/main/java/org/apache/druid/java/util/common/DateTimes.java
index c1718bd..09ac522 100644
--- a/core/src/main/java/org/apache/druid/java/util/common/DateTimes.java
+++ b/core/src/main/java/org/apache/druid/java/util/common/DateTimes.java
@@ -19,6 +19,7 @@
 
 package org.apache.druid.java.util.common;
 
+import com.google.common.collect.ImmutableSet;
 import io.netty.util.SuppressForbidden;
 import org.joda.time.Chronology;
 import org.joda.time.DateTime;
@@ -28,6 +29,7 @@
 import org.joda.time.format.DateTimeFormatter;
 import org.joda.time.format.ISODateTimeFormat;
 
+import java.util.Set;
 import java.util.TimeZone;
 import java.util.regex.Pattern;
 
@@ -43,6 +45,8 @@
       ISODateTimeFormat.dateTimeParser().withOffsetParsed()
   );
 
+  private static final Set<String> AVAILABLE_TIMEZONE_IDS = ImmutableSet.copyOf(TimeZone.getAvailableIDs());
+
   /**
    * This pattern aims to match strings, produced by {@link DateTime#toString()}. It's not rigorous: it could accept
    * some strings that couldn't be obtained by calling toString() on any {@link DateTime} object, and also it could
@@ -53,14 +57,28 @@
       "[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\\.[0-9]{3}(Z|[+\\-][0-9]{2}(:[0-9]{2}))"
   );
 
-  @SuppressForbidden(reason = "DateTimeZone#forID")
   public static DateTimeZone inferTzFromString(String tzId)
   {
+    return inferTzFromString(tzId, true);
+  }
+
+  /**
+   * @return The dateTimezone for the provided {@param tzId}. If {@param fallback} is true, the default timezone
+   * will be returned if the tzId does not match a supported timezone.
+   * @throws IllegalArgumentException if {@param fallback} is false and the provided tzId doesn't match a supported
+   * timezone.
+   */
+  @SuppressForbidden(reason = "DateTimeZone#forID")
+  public static DateTimeZone inferTzFromString(String tzId, boolean fallback) throws IllegalArgumentException
+  {
     try {
       return DateTimeZone.forID(tzId);
     }
     catch (IllegalArgumentException e) {
       // also support Java timezone strings
+      if (!fallback && !AVAILABLE_TIMEZONE_IDS.contains(tzId)) {
+        throw e;
+      }
       return DateTimeZone.forTimeZone(TimeZone.getTimeZone(tzId));
     }
   }
diff --git a/core/src/test/java/org/apache/druid/java/util/common/DateTimesTest.java b/core/src/test/java/org/apache/druid/java/util/common/DateTimesTest.java
index 15f3033..fd3d1ab 100644
--- a/core/src/test/java/org/apache/druid/java/util/common/DateTimesTest.java
+++ b/core/src/test/java/org/apache/druid/java/util/common/DateTimesTest.java
@@ -24,6 +24,8 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.TimeZone;
+
 public class DateTimesTest
 {
   @Test
@@ -33,12 +35,54 @@
     DateTime dt2 = new DateTime(System.currentTimeMillis(), DateTimes.inferTzFromString("IST"));
     DateTime dt3 = new DateTime(System.currentTimeMillis(), DateTimeZone.forOffsetHoursMinutes(1, 30));
 
-    for (DateTime dt : new DateTime[] {dt1, dt2, dt3}) {
+    for (DateTime dt : new DateTime[]{dt1, dt2, dt3}) {
       Assert.assertTrue(DateTimes.COMMON_DATE_TIME_PATTERN.matcher(dt.toString()).matches());
     }
   }
 
   @Test
+  public void testinferTzFromStringWithKnownTzId()
+  {
+    Assert.assertEquals(DateTimeZone.UTC, DateTimes.inferTzFromString("UTC"));
+  }
+
+  @Test
+  public void testinferTzFromStringWithOffset()
+  {
+    Assert.assertEquals(DateTimeZone.forOffsetHoursMinutes(10, 30), DateTimes.inferTzFromString("+1030"));
+  }
+
+  @Test
+  public void testinferTzFromStringWithJavaTimeZone()
+  {
+    Assert.assertEquals(
+        DateTimeZone.forTimeZone(TimeZone.getTimeZone("ACT")),
+        DateTimes.inferTzFromString("ACT")
+    );
+  }
+
+  @Test
+  public void testinferTzFromStringWithJavaTimeZoneAndNoFallback()
+  {
+    Assert.assertEquals(
+        DateTimeZone.forTimeZone(TimeZone.getTimeZone("ACT")),
+        DateTimes.inferTzFromString("ACT", false)
+    );
+  }
+
+  @Test
+  public void testinferTzFromStringWithUnknownTimeZoneShouldReturnUTC()
+  {
+    Assert.assertEquals(DateTimeZone.UTC, DateTimes.inferTzFromString("America/Unknown"));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testinferTzFromStringWithUnknownTimeZoneAndNoFallbackShouldThrowException()
+  {
+    Assert.assertEquals(DateTimeZone.getDefault(), DateTimes.inferTzFromString("America/Unknown", false));
+  }
+
+  @Test
   public void testStringToDateTimeConversion()
   {
     String seconds = "2018-01-30T06:00:00";
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFormatOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFormatOperatorConversion.java
index e44734f..8afeb80 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFormatOperatorConversion.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFormatOperatorConversion.java
@@ -29,6 +29,7 @@
 import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.segment.column.RowSignature;
 import org.apache.druid.sql.calcite.expression.DruidExpression;
 import org.apache.druid.sql.calcite.expression.Expressions;
@@ -81,7 +82,14 @@
     final DateTimeZone timeZone = OperatorConversions.getOperandWithDefault(
         call.getOperands(),
         2,
-        operand -> DateTimes.inferTzFromString(RexLiteral.stringValue(operand)),
+        operand -> {
+          try {
+            return DateTimes.inferTzFromString(RexLiteral.stringValue(operand), false);
+          }
+          catch (IllegalArgumentException e) {
+            throw new IAE(e.getMessage());
+          }
+        },
         plannerContext.getTimeZone()
     );
 
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/expression/TimeFormatOperatorConversionTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/expression/TimeFormatOperatorConversionTest.java
new file mode 100644
index 0000000..36a8bb0
--- /dev/null
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/expression/TimeFormatOperatorConversionTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.druid.sql.calcite.expression;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.calcite.rex.RexNode;
+import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.IAE;
+import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.segment.column.ValueType;
+import org.apache.druid.sql.calcite.expression.builtin.TimeFormatOperatorConversion;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.annotation.Nullable;
+import java.util.Map;
+
+/**
+ * Tests for TIME_FORMAT
+ */
+public class TimeFormatOperatorConversionTest extends ExpressionTestBase
+{
+  private static final RowSignature ROW_SIGNATURE = RowSignature
+      .builder()
+      .add("t", ValueType.LONG)
+      .build();
+  private static final Map<String, Object> BINDINGS = ImmutableMap
+      .<String, Object>builder()
+      .put("t", DateTimes.of("2000-02-03T04:05:06").getMillis())
+      .build();
+
+  private TimeFormatOperatorConversion target;
+  private ExpressionTestHelper testHelper;
+
+  @Before
+  public void setUp()
+  {
+    target = new TimeFormatOperatorConversion();
+    testHelper = new ExpressionTestHelper(ROW_SIGNATURE, BINDINGS);
+  }
+
+  @Test
+  public void testConversionToUTC()
+  {
+    testExpression(
+        "2000-02-03 04:05:06",
+        "timestamp_format(\"t\",'yyyy-MM-dd HH:mm:ss','UTC')",
+        "yyyy-MM-dd HH:mm:ss",
+        "UTC"
+    );
+  }
+
+  @Test
+  public void testConversionWithDefaultShouldUseUTC()
+  {
+    testExpression(
+        "2000-02-03 04:05:06",
+        "timestamp_format(\"t\",'yyyy-MM-dd HH:mm:ss','UTC')",
+        "yyyy-MM-dd HH:mm:ss",
+        null
+    );
+  }
+
+  @Test
+  public void testConversionToTimezone()
+  {
+    testExpression(
+        "2000-02-02 20:05:06",
+        "timestamp_format(\"t\",'yyyy-MM-dd HH:mm:ss','America/Los_Angeles')",
+        "yyyy-MM-dd HH:mm:ss",
+        "America/Los_Angeles"
+    );
+  }
+
+  @Test(expected = IAE.class)
+  public void testConversionToUnknownTimezoneShouldThrowException()
+  {
+    testExpression(
+        "2000-02-02 20:05:06",
+        "timestamp_format(\"t\",'yyyy-MM-dd HH:mm:ss','America/NO_TZ')",
+        "yyyy-MM-dd HH:mm:ss",
+        "America/NO_TZ"
+    );
+  }
+
+  private void testExpression(
+      String expectedResult,
+      String expectedExpression,
+      String format,
+      @Nullable String timezone
+  )
+  {
+    ImmutableList.Builder<RexNode> exprsBuilder = ImmutableList
+        .<RexNode>builder()
+        .add(testHelper.makeInputRef("t"))
+        .add(testHelper.makeLiteral(format));
+    if (timezone != null) {
+      exprsBuilder.add(testHelper.makeLiteral(timezone));
+    }
+
+    testHelper.testExpression(
+        target.calciteOperator(),
+        exprsBuilder.build(),
+        DruidExpression.fromExpression(expectedExpression),
+        expectedResult
+    );
+  }
+}