/*
 * 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.calcite.sql.parser;
import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.sql.SqlExplain;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlSelect;
import org.apache.calcite.sql.SqlSetOption;
import org.apache.calcite.sql.SqlUnknownLiteral;
import org.apache.calcite.sql.SqlWriterConfig;
import org.apache.calcite.sql.dialect.AnsiSqlDialect;
import org.apache.calcite.sql.dialect.SparkSqlDialect;
import org.apache.calcite.sql.parser.SqlParser.Config;
import org.apache.calcite.sql.pretty.SqlPrettyWriter;
import org.apache.calcite.sql.test.SqlTestFactory;
import org.apache.calcite.sql.test.SqlTests;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.util.SqlShuttle;
import org.apache.calcite.sql.validate.SqlConformance;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.test.IntervalTest;
import org.apache.calcite.tools.Hoist;
import org.apache.calcite.util.Bug;
import org.apache.calcite.util.ConversionUtil;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.TestUtil;
import org.apache.calcite.util.Util;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSortedSet;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.hamcrest.BaseMatcher;
import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.SortedSet;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import static org.apache.calcite.util.Static.RESOURCE;
import static org.apache.calcite.util.Util.toLinux;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.hasToString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assumptions.assumeFalse;

/**
 * A <code>SqlParserTest</code> is a unit-test for
 * {@link SqlParser the SQL parser}.
 *
 * <p>To reuse this test for an extension parser, override the
 * {@link #fixture()} method,
 * calling {@link SqlParserFixture#withConfig(UnaryOperator)}
 * and then {@link SqlParser.Config#withParserFactory(SqlParserImplFactory)}.
 *
 * @see SqlParserFixture
 * @see SqlParserListFixture
 */
public class SqlParserTest {
  /**
   * List of reserved keywords.
   *
   * <p>Each keyword is followed by tokens indicating whether it is reserved in
   * the SQL:92, SQL:99, SQL:2003, SQL:2011, SQL:2014 standards and in Calcite.
   *
   * <p>The standard keywords are derived from
   * <a href="https://developer.mimer.com/wp-content/uploads/standard-sql-reserved-words-summary.pdf">Mimer</a>
   * and from the specification.
   *
   * <p>If a new <b>reserved</b> keyword is added to the parser, include it in
   * this list, flagged "c". If the keyword is not intended to be a reserved
   * keyword, add it to the non-reserved keyword list in the parser.
   */
  private static final String[] RESERVED_KEYWORDS = {
      "ABS",                                               "2011", "2014", "c",
      "ABSOLUTE",                      "92", "99",
      "ACTION",                        "92", "99",
      "ADD",                           "92", "99", "2003",
      "AFTER",                               "99",
      "ALL",                           "92", "99", "2003", "2011", "2014", "c",
      "ALLOCATE",                      "92", "99", "2003", "2011", "2014", "c",
      "ALLOW",                                                             "c",
      "ALTER",                         "92", "99", "2003", "2011", "2014", "c",
      "AND",                           "92", "99", "2003", "2011", "2014", "c",
      "ANY",                           "92", "99", "2003", "2011", "2014", "c",
      "ARE",                           "92", "99", "2003", "2011", "2014", "c",
      "ARRAY",                               "99", "2003", "2011", "2014", "c",
      "ARRAY_AGG",                                         "2011",
      "ARRAY_MAX_CARDINALITY",                                     "2014", "c",
      "AS",                            "92", "99", "2003", "2011", "2014", "c",
      "ASC",                           "92", "99",
      "ASENSITIVE",                          "99", "2003", "2011", "2014", "c",
      "ASSERTION",                     "92", "99",
      "ASYMMETRIC",                          "99", "2003", "2011", "2014", "c",
      "AT",                            "92", "99", "2003", "2011", "2014", "c",
      "ATOMIC",                              "99", "2003", "2011", "2014", "c",
      "AUTHORIZATION",                 "92", "99", "2003", "2011", "2014", "c",
      "AVG",                           "92",               "2011", "2014", "c",
      "BEFORE",                              "99",
      "BEGIN",                         "92", "99", "2003", "2011", "2014", "c",
      "BEGIN_FRAME",                                               "2014", "c",
      "BEGIN_PARTITION",                                           "2014", "c",
      "BETWEEN",                       "92", "99", "2003", "2011", "2014", "c",
      "BIGINT",                                    "2003", "2011", "2014", "c",
      "BINARY",                              "99", "2003", "2011", "2014", "c",
      "BIT",                           "92", "99",                         "c",
      "BIT_LENGTH",                    "92",
      "BLOB",                                "99", "2003", "2011", "2014", "c",
      "BOOLEAN",                             "99", "2003", "2011", "2014", "c",
      "BOTH",                          "92", "99", "2003", "2011", "2014", "c",
      "BREADTH",                             "99",
      "BY",                            "92", "99", "2003", "2011", "2014", "c",
      "CALL",                          "92", "99", "2003", "2011", "2014", "c",
      "CALLED",                                    "2003", "2011", "2014", "c",
      "CARDINALITY",                                       "2011", "2014", "c",
      "CASCADE",                       "92", "99",
      "CASCADED",                      "92", "99", "2003", "2011", "2014", "c",
      "CASE",                          "92", "99", "2003", "2011", "2014", "c",
      "CAST",                          "92", "99", "2003", "2011", "2014", "c",
      "CATALOG",                       "92", "99",
      "CEIL",                                              "2011", "2014", "c",
      "CEILING",                                           "2011", "2014", "c",
      "CHAR",                          "92", "99", "2003", "2011", "2014", "c",
      "CHARACTER",                     "92", "99", "2003", "2011", "2014", "c",
      "CHARACTER_LENGTH",              "92",               "2011", "2014", "c",
      "CHAR_LENGTH",                   "92",               "2011", "2014", "c",
      "CHECK",                         "92", "99", "2003", "2011", "2014", "c",
      "CLASSIFIER",                                                "2014", "c",
      "CLOB",                                "99", "2003", "2011", "2014", "c",
      "CLOSE",                         "92", "99", "2003", "2011", "2014", "c",
      "COALESCE",                      "92",               "2011", "2014", "c",
      "COLLATE",                       "92", "99", "2003", "2011", "2014", "c",
      "COLLATION",                     "92", "99",
      "COLLECT",                                           "2011", "2014", "c",
      "COLUMN",                        "92", "99", "2003", "2011", "2014", "c",
      "COMMIT",                        "92", "99", "2003", "2011", "2014", "c",
      "CONDITION",                     "92", "99", "2003", "2011", "2014", "c",
      "CONNECT",                       "92", "99", "2003", "2011", "2014", "c",
      "CONNECTION",                    "92", "99",
      "CONSTRAINT",                    "92", "99", "2003", "2011", "2014", "c",
      "CONSTRAINTS",                   "92", "99",
      "CONSTRUCTOR",                         "99",
      "CONTAINS",                      "92",               "2011", "2014", "c",
      "CONTINUE",                      "92", "99", "2003",
      "CONVERT",                       "92",               "2011", "2014", "c",
      "CORR",                                              "2011", "2014", "c",
      "CORRESPONDING",                 "92", "99", "2003", "2011", "2014", "c",
      "COUNT",                         "92",               "2011", "2014", "c",
      "COVAR_POP",                                         "2011", "2014", "c",
      "COVAR_SAMP",                                        "2011", "2014", "c",
      "CREATE",                        "92", "99", "2003", "2011", "2014", "c",
      "CROSS",                         "92", "99", "2003", "2011", "2014", "c",
      "CUBE",                                "99", "2003", "2011", "2014", "c",
      "CUME_DIST",                                         "2011", "2014", "c",
      "CURRENT",                       "92", "99", "2003", "2011", "2014", "c",
      "CURRENT_CATALOG",                                   "2011", "2014", "c",
      "CURRENT_DATE",                  "92", "99", "2003", "2011", "2014", "c",
      "CURRENT_DEFAULT_TRANSFORM_GROUP",     "99", "2003", "2011", "2014", "c",
      "CURRENT_PATH",                  "92", "99", "2003", "2011", "2014", "c",
      "CURRENT_ROLE",                        "99", "2003", "2011", "2014", "c",
      "CURRENT_ROW",                                               "2014", "c",
      "CURRENT_SCHEMA",                                    "2011", "2014", "c",
      "CURRENT_TIME",                  "92", "99", "2003", "2011", "2014", "c",
      "CURRENT_TIMESTAMP",             "92", "99", "2003", "2011", "2014", "c",
      "CURRENT_TRANSFORM_GROUP_FOR_TYPE",    "99", "2003", "2011", "2014", "c",
      "CURRENT_USER",                  "92", "99", "2003", "2011", "2014", "c",
      "CURSOR",                        "92", "99", "2003", "2011", "2014", "c",
      "CYCLE",                               "99", "2003", "2011", "2014", "c",
      "DATA",                                "99",
      "DATE",                          "92", "99", "2003", "2011", "2014", "c",
      "DATETIME",                                                          "c",
      "DAY",                           "92", "99", "2003", "2011", "2014", "c",
      "DAYS",                                              "2011",
      "DEALLOCATE",                    "92", "99", "2003", "2011", "2014", "c",
      "DEC",                           "92", "99", "2003", "2011", "2014", "c",
      "DECIMAL",                       "92", "99", "2003", "2011", "2014", "c",
      "DECLARE",                       "92", "99", "2003", "2011", "2014", "c",
      "DEFAULT",                       "92", "99", "2003", "2011", "2014", "c",
      "DEFERRABLE",                    "92", "99",
      "DEFERRED",                      "92", "99",
      "DEFINE",                                                    "2014", "c",
      "DELETE",                        "92", "99", "2003", "2011", "2014", "c",
      "DENSE_RANK",                                        "2011", "2014", "c",
      "DEPTH",                               "99",
      "DEREF",                               "99", "2003", "2011", "2014", "c",
      "DESC",                          "92", "99",
      "DESCRIBE",                      "92", "99", "2003", "2011", "2014", "c",
      "DESCRIPTOR",                    "92", "99",
      "DETERMINISTIC",                 "92", "99", "2003", "2011", "2014", "c",
      "DIAGNOSTICS",                   "92", "99",
      "DISALLOW",                                                          "c",
      "DISCONNECT",                    "92", "99", "2003", "2011", "2014", "c",
      "DISTINCT",                      "92", "99", "2003", "2011", "2014", "c",
      "DO",                            "92", "99", "2003",
      "DOMAIN",                        "92", "99",
      "DOUBLE",                        "92", "99", "2003", "2011", "2014", "c",
      "DROP",                          "92", "99", "2003", "2011", "2014", "c",
      "DYNAMIC",                             "99", "2003", "2011", "2014", "c",
      "EACH",                                "99", "2003", "2011", "2014", "c",
      "ELEMENT",                                   "2003", "2011", "2014", "c",
      "ELSE",                          "92", "99", "2003", "2011", "2014", "c",
      "ELSEIF",                        "92", "99", "2003",
      "EMPTY",                                                     "2014", "c",
      "END",                           "92", "99", "2003", "2011", "2014", "c",
      "END-EXEC",                                          "2011", "2014", "c",
      "END_FRAME",                                                 "2014", "c",
      "END_PARTITION",                                             "2014", "c",
      "EQUALS",                              "99",                 "2014", "c",
      "ESCAPE",                        "92", "99", "2003", "2011", "2014", "c",
      "EVERY",                                             "2011", "2014", "c",
      "EXCEPT",                        "92", "99", "2003", "2011", "2014", "c",
      "EXCEPTION",                     "92", "99",
      "EXEC",                          "92", "99", "2003", "2011", "2014", "c",
      "EXECUTE",                       "92", "99", "2003", "2011", "2014", "c",
      "EXISTS",                        "92", "99", "2003", "2011", "2014", "c",
      "EXIT",                          "92", "99", "2003",
      "EXP",                                               "2011", "2014", "c",
      "EXPLAIN",                                                           "c",
      "EXTEND",                                                            "c",
      "EXTERNAL",                      "92", "99", "2003", "2011", "2014", "c",
      "EXTRACT",                       "92",               "2011", "2014", "c",
      "FALSE",                         "92", "99", "2003", "2011", "2014", "c",
      "FETCH",                         "92", "99", "2003", "2011", "2014", "c",
      "FILTER",                              "99", "2003", "2011", "2014", "c",
      "FIRST",                         "92", "99",
      "FIRST_VALUE",                                       "2011", "2014", "c",
      "FLOAT",                         "92", "99", "2003", "2011", "2014", "c",
      "FLOOR",                                             "2011", "2014", "c",
      "FOR",                           "92", "99", "2003", "2011", "2014", "c",
      "FOREIGN",                       "92", "99", "2003", "2011", "2014", "c",
      "FOREVER",                                           "2011",
      "FOUND",                         "92", "99",
      "FRAME_ROW",                                                 "2014", "c",
      "FREE",                                "99", "2003", "2011", "2014", "c",
      "FRIDAY",                                                            "c",
      "FROM",                          "92", "99", "2003", "2011", "2014", "c",
      "FULL",                          "92", "99", "2003", "2011", "2014", "c",
      "FUNCTION",                      "92", "99", "2003", "2011", "2014", "c",
      "FUSION",                                            "2011", "2014", "c",
      "GENERAL",                             "99",
      "GET",                           "92", "99", "2003", "2011", "2014", "c",
      "GLOBAL",                        "92", "99", "2003", "2011", "2014", "c",
      "GO",                            "92", "99",
      "GOTO",                          "92", "99",
      "GRANT",                         "92", "99", "2003", "2011", "2014", "c",
      "GROUP",                         "92", "99", "2003", "2011", "2014", "c",
      "GROUPING",                            "99", "2003", "2011", "2014", "c",
      "GROUPS",                                                    "2014", "c",
      "HANDLER",                       "92", "99", "2003",
      "HAVING",                        "92", "99", "2003", "2011", "2014", "c",
      "HOLD",                                "99", "2003", "2011", "2014", "c",
      "HOUR",                          "92", "99", "2003", "2011", "2014", "c",
      "HOURS",                                             "2011",
      "IDENTITY",                      "92", "99", "2003", "2011", "2014", "c",
      "IF",                            "92", "99", "2003",
      "ILIKE", // PostgreSQL
      "IMMEDIATE",                     "92", "99", "2003",
      "IMMEDIATELY",
      "IMPORT",                                                            "c",
      "IN",                            "92", "99", "2003", "2011", "2014", "c",
      "INDICATOR",                     "92", "99", "2003", "2011", "2014", "c",
      "INITIAL",                                                   "2014", "c",
      "INITIALLY",                     "92", "99",
      "INNER",                         "92", "99", "2003", "2011", "2014", "c",
      "INOUT",                         "92", "99", "2003", "2011", "2014", "c",
      "INPUT",                         "92", "99", "2003",
      "INSENSITIVE",                   "92", "99", "2003", "2011", "2014", "c",
      "INSERT",                        "92", "99", "2003", "2011", "2014", "c",
      "INT",                           "92", "99", "2003", "2011", "2014", "c",
      "INTEGER",                       "92", "99", "2003", "2011", "2014", "c",
      "INTERSECT",                     "92", "99", "2003", "2011", "2014", "c",
      "INTERSECTION",                                      "2011", "2014", "c",
      "INTERVAL",                      "92", "99", "2003", "2011", "2014", "c",
      "INTO",                          "92", "99", "2003", "2011", "2014", "c",
      "IS",                            "92", "99", "2003", "2011", "2014", "c",
      "ISOLATION",                     "92", "99",
      "ITERATE",                             "99", "2003",
      "JOIN",                          "92", "99", "2003", "2011", "2014", "c",
      "JSON_ARRAY",                                                        "c",
      "JSON_ARRAYAGG",                                                     "c",
      "JSON_EXISTS",                                                       "c",
      "JSON_OBJECT",                                                       "c",
      "JSON_OBJECTAGG",                                                    "c",
      "JSON_QUERY",                                                        "c",
      "JSON_SCOPE",                                                        "c",
      "JSON_VALUE",                                                        "c",
      "KEEP",                                              "2011",
      "KEY",                           "92", "99",
      "LAG",                                               "2011", "2014", "c",
      "LANGUAGE",                      "92", "99", "2003", "2011", "2014", "c",
      "LARGE",                               "99", "2003", "2011", "2014", "c",
      "LAST",                          "92", "99",
      "LAST_VALUE",                                        "2011", "2014", "c",
      "LATERAL",                             "99", "2003", "2011", "2014", "c",
      "LEAD",                                              "2011", "2014", "c",
      "LEADING",                       "92", "99", "2003", "2011", "2014", "c",
      "LEAVE",                         "92", "99", "2003",
      "LEFT",                          "92", "99", "2003", "2011", "2014", "c",
      "LEVEL",                         "92", "99",
      "LIKE",                          "92", "99", "2003", "2011", "2014", "c",
      "LIKE_REGEX",                                        "2011", "2014", "c",
      "LIMIT",                                                             "c",
      "LN",                                                "2011", "2014", "c",
      "LOCAL",                         "92", "99", "2003", "2011", "2014", "c",
      "LOCALTIME",                           "99", "2003", "2011", "2014", "c",
      "LOCALTIMESTAMP",                      "99", "2003", "2011", "2014", "c",
      "LOCATOR",                             "99",
      "LOOP",                          "92", "99", "2003",
      "LOWER",                         "92",               "2011", "2014", "c",
      "MAP",                                 "99",
      "MATCH",                         "92", "99", "2003", "2011", "2014", "c",
      "MATCHES",                                                   "2014", "c",
      "MATCH_NUMBER",                                              "2014", "c",
      "MATCH_RECOGNIZE",                                           "2014", "c",
      "MAX",                           "92",               "2011", "2014", "c",
      "MAX_CARDINALITY",                                   "2011",
      "MEASURES",                                                          "c",
      "MEMBER",                                    "2003", "2011", "2014", "c",
      "MERGE",                                     "2003", "2011", "2014", "c",
      "METHOD",                              "99", "2003", "2011", "2014", "c",
      "MIN",                           "92",               "2011", "2014", "c",
      "MINUS",                                                             "c",
      "MINUTE",                        "92", "99", "2003", "2011", "2014", "c",
      "MINUTES",                                           "2011",
      "MOD",                                               "2011", "2014", "c",
      "MODIFIES",                            "99", "2003", "2011", "2014", "c",
      "MODULE",                        "92", "99", "2003", "2011", "2014", "c",
      "MONDAY",                                                            "c",
      "MONTH",                         "92", "99", "2003", "2011", "2014", "c",
      "MULTISET",                                  "2003", "2011", "2014", "c",
      "NAMES",                         "92", "99",
      "NATIONAL",                      "92", "99", "2003", "2011", "2014", "c",
      "NATURAL",                       "92", "99", "2003", "2011", "2014", "c",
      "NCHAR",                         "92", "99", "2003", "2011", "2014", "c",
      "NCLOB",                               "99", "2003", "2011", "2014", "c",
      "NEW",                                 "99", "2003", "2011", "2014", "c",
      "NEXT",                          "92", "99",                         "c",
      "NO",                            "92", "99", "2003", "2011", "2014", "c",
      "NONE",                                "99", "2003", "2011", "2014", "c",
      "NORMALIZE",                                         "2011", "2014", "c",
      "NOT",                           "92", "99", "2003", "2011", "2014", "c",
      "NTH_VALUE",                                         "2011", "2014", "c",
      "NTILE",                                             "2011", "2014", "c",
      "NULL",                          "92", "99", "2003", "2011", "2014", "c",
      "NULLIF",                        "92",               "2011", "2014", "c",
      "NUMERIC",                       "92", "99", "2003", "2011", "2014", "c",
      "OBJECT",                              "99",
      "OCCURRENCES_REGEX",                                 "2011", "2014", "c",
      "OCTET_LENGTH",                  "92",               "2011", "2014", "c",
      "OF",                            "92", "99", "2003", "2011", "2014", "c",
      "OFFSET",                                            "2011", "2014", "c",
      "OLD",                                 "99", "2003", "2011", "2014", "c",
      "OMIT",                                                      "2014", "c",
      "ON",                            "92", "99", "2003", "2011", "2014", "c",
      "ONE",                                                       "2014", "c",
      "ONLY",                          "92", "99", "2003", "2011", "2014", "c",
      "OPEN",                          "92", "99", "2003", "2011", "2014", "c",
      "OPTION",                        "92", "99",
      "OR",                            "92", "99", "2003", "2011", "2014", "c",
      "ORDER",                         "92", "99", "2003", "2011", "2014", "c",
      "ORDINAL",                                                           "c",
      "ORDINALITY",                          "99",
      "OUT",                           "92", "99", "2003", "2011", "2014", "c",
      "OUTER",                         "92", "99", "2003", "2011", "2014", "c",
      "OUTPUT",                        "92", "99", "2003",
      "OVER",                                "99", "2003", "2011", "2014", "c",
      "OVERLAPS",                      "92", "99", "2003", "2011", "2014", "c",
      "OVERLAY",                                           "2011", "2014", "c",
      "PAD",                           "92", "99",
      "PARAMETER",                     "92", "99", "2003", "2011", "2014", "c",
      "PARTIAL",                       "92", "99",
      "PARTITION",                           "99", "2003", "2011", "2014", "c",
      "PATH",                          "92", "99",
      "PATTERN",                                                   "2014", "c",
      "PER",                                                       "2014", "c",
      "PERCENT",                                                   "2014", "c",
      "PERCENTILE_CONT",                                   "2011", "2014", "c",
      "PERCENTILE_DISC",                                   "2011", "2014", "c",
      "PERCENT_RANK",                                      "2011", "2014", "c",
      "PERIOD",                                                    "2014", "c",
      "PERMUTE",                                                           "c",
      "PORTION",                                                   "2014", "c",
      "POSITION",                      "92",               "2011", "2014", "c",
      "POSITION_REGEX",                                    "2011", "2014", "c",
      "POWER",                                             "2011", "2014", "c",
      "PRECEDES",                                                  "2014", "c",
      "PRECISION",                     "92", "99", "2003", "2011", "2014", "c",
      "PREPARE",                       "92", "99", "2003", "2011", "2014", "c",
      "PRESERVE",                      "92", "99",
      "PREV",                                                              "c",
      "PRIMARY",                       "92", "99", "2003", "2011", "2014", "c",
      "PRIOR",                         "92", "99",
      "PRIVILEGES",                    "92", "99",
      "PROCEDURE",                     "92", "99", "2003", "2011", "2014", "c",
      "PUBLIC",                        "92", "99",
      "QUALIFY",                                                           "c",
      "RANGE",                               "99", "2003", "2011", "2014", "c",
      "RANK",                                              "2011", "2014", "c",
      "READ",                          "92", "99",
      "READS",                               "99", "2003", "2011", "2014", "c",
      "REAL",                          "92", "99", "2003", "2011", "2014", "c",
      "RECURSIVE",                           "99", "2003", "2011", "2014", "c",
      "REF",                                 "99", "2003", "2011", "2014", "c",
      "REFERENCES",                    "92", "99", "2003", "2011", "2014", "c",
      "REFERENCING",                         "99", "2003", "2011", "2014", "c",
      "REGR_AVGX",                                         "2011", "2014", "c",
      "REGR_AVGY",                                         "2011", "2014", "c",
      "REGR_COUNT",                                        "2011", "2014", "c",
      "REGR_INTERCEPT",                                    "2011", "2014", "c",
      "REGR_R2",                                           "2011", "2014", "c",
      "REGR_SLOPE",                                        "2011", "2014", "c",
      "REGR_SXX",                                          "2011", "2014", "c",
      "REGR_SXY",                                          "2011", "2014", "c",
      "REGR_SYY",                                          "2011", "2014", "c",
      "RELATIVE",                      "92", "99",
      "RELEASE",                             "99", "2003", "2011", "2014", "c",
      "REPEAT",                        "92", "99", "2003",
      "RESET",                                                             "c",
      "RESIGNAL",                      "92", "99", "2003",
      "RESTRICT",                      "92", "99",
      "RESULT",                              "99", "2003", "2011", "2014", "c",
      "RETURN",                        "92", "99", "2003", "2011", "2014", "c",
      "RETURNS",                       "92", "99", "2003", "2011", "2014", "c",
      "REVOKE",                        "92", "99", "2003", "2011", "2014", "c",
      "RIGHT",                         "92", "99", "2003", "2011", "2014", "c",
      "RLIKE", // Hive and Spark
      "ROLE",                                "99",
      "ROLLBACK",                      "92", "99", "2003", "2011", "2014", "c",
      "ROLLUP",                              "99", "2003", "2011", "2014", "c",
      "ROUTINE",                       "92", "99",
      "ROW",                                 "99", "2003", "2011", "2014", "c",
      "ROWS",                          "92", "99", "2003", "2011", "2014", "c",
      "ROW_NUMBER",                                        "2011", "2014", "c",
      "RUNNING",                                                   "2014", "c",
      "SAFE_CAST",                                                         "c",
      "SAFE_OFFSET",                                                       "c",
      "SAFE_ORDINAL",                                                      "c",
      "SATURDAY",                                                          "c",
      "SAVEPOINT",                           "99", "2003", "2011", "2014", "c",
      "SCHEMA",                        "92", "99",
      "SCOPE",                               "99", "2003", "2011", "2014", "c",
      "SCROLL",                        "92", "99", "2003", "2011", "2014", "c",
      "SEARCH",                              "99", "2003", "2011", "2014", "c",
      "SECOND",                        "92", "99", "2003", "2011", "2014", "c",
      "SECONDS",                                           "2011",
      "SECTION",                       "92", "99",
      "SEEK",                                                      "2014", "c",
      "SELECT",                        "92", "99", "2003", "2011", "2014", "c",
      "SENSITIVE",                           "99", "2003", "2011", "2014", "c",
      "SESSION",                       "92", "99",
      "SESSION_USER",                  "92", "99", "2003", "2011", "2014", "c",
      "SET",                           "92", "99", "2003", "2011", "2014", "c",
      "SETS",                                "99",
      "SHOW",                                                      "2014", "c",
      "SIGNAL",                        "92", "99", "2003",
      "SIMILAR",                             "99", "2003", "2011", "2014", "c",
      "SIZE",                          "92", "99",
      "SKIP",                                                      "2014", "c",
      "SMALLINT",                      "92", "99", "2003", "2011", "2014", "c",
      "SOME",                          "92", "99", "2003", "2011", "2014", "c",
      "SPACE",                         "92", "99",
      "SPECIFIC",                      "92", "99", "2003", "2011", "2014", "c",
      "SPECIFICTYPE",                        "99", "2003", "2011", "2014", "c",
      "SQL",                           "92", "99", "2003", "2011", "2014", "c",
      "SQLCODE",                       "92",
      "SQLERROR",                      "92",
      "SQLEXCEPTION",                  "92", "99", "2003", "2011", "2014", "c",
      "SQLSTATE",                      "92", "99", "2003", "2011", "2014", "c",
      "SQLWARNING",                    "92", "99", "2003", "2011", "2014", "c",
      "SQRT",                                              "2011", "2014", "c",
      "START",                               "99", "2003", "2011", "2014", "c",
      "STATE",                               "99",
      "STATIC",                              "99", "2003", "2011", "2014", "c",
      "STDDEV_POP",                                        "2011", "2014", "c",
      "STDDEV_SAMP",                                       "2011", "2014", "c",
      "STREAM",                                                            "c",
      "SUBMULTISET",                               "2003", "2011", "2014", "c",
      "SUBSET",                                                    "2014", "c",
      "SUBSTRING",                     "92",               "2011", "2014", "c",
      "SUBSTRING_REGEX",                                   "2011", "2014", "c",
      "SUCCEEDS",                                                  "2014", "c",
      "SUM",                           "92",               "2011", "2014", "c",
      "SUNDAY",                                                            "c",
      "SYMMETRIC",                           "99", "2003", "2011", "2014", "c",
      "SYSTEM",                              "99", "2003", "2011", "2014", "c",
      "SYSTEM_TIME",                                               "2014", "c",
      "SYSTEM_USER",                   "92", "99", "2003", "2011", "2014", "c",
      "TABLE",                         "92", "99", "2003", "2011", "2014", "c",
      "TABLESAMPLE",                               "2003", "2011", "2014", "c",
      "TEMPORARY",                     "92", "99",
      "THEN",                          "92", "99", "2003", "2011", "2014", "c",
      "THURSDAY",                                                          "c",
      "TIME",                          "92", "99", "2003", "2011", "2014", "c",
      "TIMESTAMP",                     "92", "99", "2003", "2011", "2014", "c",
      "TIMEZONE_HOUR",                 "92", "99", "2003", "2011", "2014", "c",
      "TIMEZONE_MINUTE",               "92", "99", "2003", "2011", "2014", "c",
      "TINYINT",                                                           "c",
      "TO",                            "92", "99", "2003", "2011", "2014", "c",
      "TRAILING",                      "92", "99", "2003", "2011", "2014", "c",
      "TRANSACTION",                   "92", "99",
      "TRANSLATE",                     "92",               "2011", "2014", "c",
      "TRANSLATE_REGEX",                                   "2011", "2014", "c",
      "TRANSLATION",                   "92", "99", "2003", "2011", "2014", "c",
      "TREAT",                               "99", "2003", "2011", "2014", "c",
      "TRIGGER",                             "99", "2003", "2011", "2014", "c",
      "TRIM",                          "92",               "2011", "2014", "c",
      "TRIM_ARRAY",                                        "2011", "2014", "c",
      "TRUE",                          "92", "99", "2003", "2011", "2014", "c",
      "TRUNCATE",                                          "2011", "2014", "c",
      "TRY_CAST",                                                          "c",
      "TUESDAY",                                                           "c",
      "UESCAPE",                                           "2011", "2014", "c",
      "UNDER",                               "99",
      "UNDO",                          "92", "99", "2003",
      "UNION",                         "92", "99", "2003", "2011", "2014", "c",
      "UNIQUE",                        "92", "99", "2003", "2011", "2014", "c",
      "UNKNOWN",                       "92", "99", "2003", "2011", "2014", "c",
      "UNNEST",                              "99", "2003", "2011", "2014", "c",
      "UNTIL",                         "92", "99", "2003",
      "UPDATE",                        "92", "99", "2003", "2011", "2014", "c",
      "UPPER",                         "92",               "2011", "2014", "c",
      "UPSERT",                                                            "c",
      "USAGE",                         "92", "99",
      "USER",                          "92", "99", "2003", "2011", "2014", "c",
      "USING",                         "92", "99", "2003", "2011", "2014", "c",
      "VALUE",                         "92", "99", "2003", "2011", "2014", "c",
      "VALUES",                        "92", "99", "2003", "2011", "2014", "c",
      "VALUE_OF",                                                  "2014", "c",
      "VARBINARY",                                         "2011", "2014", "c",
      "VARCHAR",                       "92", "99", "2003", "2011", "2014", "c",
      "VARYING",                       "92", "99", "2003", "2011", "2014", "c",
      "VAR_POP",                                           "2011", "2014", "c",
      "VAR_SAMP",                                          "2011", "2014", "c",
      "VERSION",                                           "2011",
      "VERSIONING",                                        "2011", "2014", "c",
      "VERSIONS",                                          "2011",
      "VIEW",                          "92", "99",
      "WEDNESDAY",                                                         "c",
      "WHEN",                          "92", "99", "2003", "2011", "2014", "c",
      "WHENEVER",                      "92", "99", "2003", "2011", "2014", "c",
      "WHERE",                         "92", "99", "2003", "2011", "2014", "c",
      "WHILE",                         "92", "99", "2003",
      "WIDTH_BUCKET",                                      "2011", "2014", "c",
      "WINDOW",                              "99", "2003", "2011", "2014", "c",
      "WITH",                          "92", "99", "2003", "2011", "2014", "c",
      "WITHIN",                              "99", "2003", "2011", "2014", "c",
      "WITHOUT",                             "99", "2003", "2011", "2014", "c",
      "WORK",                          "92", "99",
      "WRITE",                         "92", "99",
      "YEAR",                          "92", "99", "2003", "2011", "2014", "c",
      "YEARS",                                             "2011",
      "ZONE",                          "92", "99",
  };

  private static final String ANY = "(?s).*";

  private static final SqlWriterConfig SQL_WRITER_CONFIG =
      SqlPrettyWriter.config()
          .withAlwaysUseParentheses(true)
          .withUpdateSetListNewline(false)
          .withFromFolding(SqlWriterConfig.LineFolding.TALL)
          .withIndentation(0);

  protected static final SqlDialect BIG_QUERY =
      SqlDialect.DatabaseProduct.BIG_QUERY.getDialect();
  private static final SqlDialect CALCITE =
      SqlDialect.DatabaseProduct.CALCITE.getDialect();
  private static final SqlDialect MSSQL =
      SqlDialect.DatabaseProduct.MSSQL.getDialect();
  private static final SqlDialect MYSQL =
      SqlDialect.DatabaseProduct.MYSQL.getDialect();
  private static final SqlDialect ORACLE =
      SqlDialect.DatabaseProduct.ORACLE.getDialect();
  private static final SqlDialect POSTGRESQL =
      SqlDialect.DatabaseProduct.POSTGRESQL.getDialect();
  private static final SqlDialect REDSHIFT =
      SqlDialect.DatabaseProduct.REDSHIFT.getDialect();

  /** Creates the test fixture that determines the behavior of tests.
   * Sub-classes that, say, test different parser implementations should
   * override. */
  public SqlParserFixture fixture() {
    return SqlParserFixture.DEFAULT;
  }

  protected SqlParserFixture sql(String sql) {
    return fixture().sql(sql);
  }

  protected SqlParserFixture expr(String sql) {
    return sql(sql).expression(true);
  }

  /** Converts a string to linux format (LF line endings rather than CR-LF),
   * except if disabled in {@link SqlParserFixture#convertToLinux}. */
  static UnaryOperator<String> linux(boolean convertToLinux) {
    return convertToLinux ? Util::toLinux : UnaryOperator.identity();
  }

  protected static SqlParser sqlParser(Reader source,
      UnaryOperator<SqlParser.Config> transform) {
    final SqlParser.Config config = transform.apply(SqlParser.Config.DEFAULT);
    return SqlParser.create(source, config);
  }

  /** Returns a {@link Matcher} that succeeds if the given {@link SqlNode} is a
   * DDL statement. */
  public static Matcher<SqlNode> isDdl() {
    return new BaseMatcher<SqlNode>() {
      @Override public boolean matches(Object item) {
        return item instanceof SqlNode
            && SqlKind.DDL.contains(((SqlNode) item).getKind());
      }

      @Override public void describeTo(Description description) {
        description.appendText("isDdl");
      }
    };
  }

  /** Returns a {@link Matcher} that succeeds if the given {@link SqlNode} is a
   * VALUES that contains a ROW that contains an identifier whose {@code i}th
   * element is quoted. */
  private static Matcher<SqlNode> isQuoted(final int i,
      final boolean quoted) {
    return new CustomTypeSafeMatcher<SqlNode>("quoting") {
      @Override protected boolean matchesSafely(SqlNode item) {
        final SqlCall valuesCall = (SqlCall) item;
        final SqlCall rowCall = valuesCall.operand(0);
        final SqlIdentifier id = rowCall.operand(0);
        return id.isComponentQuoted(i) == quoted;
      }
    };
  }

  protected SortedSet<String> getReservedKeywords() {
    return keywords("c");
  }

  /** Returns whether a word is reserved in this parser. This method can be
   * used to disable tests that behave differently with different collections
   * of reserved words. */
  protected boolean isReserved(String word) {
    SqlAbstractParserImpl.Metadata metadata = fixture().parser().getMetadata();
    return metadata.isReservedWord(word.toUpperCase(Locale.ROOT));
  }

  protected static SortedSet<String> keywords(@Nullable String dialect) {
    final ImmutableSortedSet.Builder<String> builder =
        ImmutableSortedSet.naturalOrder();
    String r = null;
    for (String w : RESERVED_KEYWORDS) {
      switch (w) {
      case "92":
      case "99":
      case "2003":
      case "2011":
      case "2014":
      case "c":
        assert r != null;
        if (dialect == null || dialect.equals(w)) {
          builder.add(r);
        }
        break;
      default:
        assert r == null || r.compareTo(w) < 0 : "table should be sorted: " + w;
        r = w;
      }
    }
    return builder.build();
  }

  /**
   * Tests that when there is an error, non-reserved keywords such as "A",
   * "ABSOLUTE" (which naturally arise whenever a production uses
   * "&lt;IDENTIFIER&gt;") are removed, but reserved words such as "AND"
   * remain.
   */
  @Test void testExceptionCleanup() {
    sql("select 0.5e1^.1^ from sales.emps")
        .fails("(?s).*Encountered \".1\" at line 1, column 13.\n"
            + "Was expecting one of:\n"
            + "    <EOF> \n"
            + "    \"AS\" \\.\\.\\.\n"
            + "    \"EXCEPT\" \\.\\.\\.\n"
            + ".*");
  }

  @Test void testInvalidToken() {
    // Causes problems to the test infrastructure because the token mgr
    // throws a java.lang.Error. The usual case is that the parser throws
    // an exception.
    sql("values (a^#^b)")
        .fails("Lexical error at line 1, column 10\\.  Encountered: \"#\" \\(35\\), after : \"\"");
  }

  // TODO: should fail in parser
  @Test void testStarAsFails() {
    sql("select * as x from emp")
        .ok("SELECT * AS `X`\n"
            + "FROM `EMP`");
  }

  @Test void testFromStarFails() {
    sql("select * from sales^.^*")
        .fails("(?s)Encountered \"\\. \\*\" at .*");
    sql("select emp.empno AS x from sales^.^*")
        .fails("(?s)Encountered \"\\. \\*\" at .*");
    sql("select * from emp^.^*")
        .fails("(?s)Encountered \"\\. \\*\" at .*");
    sql("select emp.empno AS x from emp^.^*")
        .fails("(?s)Encountered \"\\. \\*\" at .*");
    sql("select emp.empno AS x from ^*^")
        .fails("(?s)Encountered \"\\*\" at .*");
  }

  @Test void testPercentileCont() {
    sql("select percentile_cont(.5) within group (order by 3) from t")
        .ok("SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY 3)\n"
            + "FROM `T`");
  }

  @Test void testPercentileDisc() {
    sql("select percentile_disc(.5) within group (order by 3) from t")
        .ok("SELECT PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY 3)\n"
            + "FROM `T`");
  }

  /** Tests BigQuery's variant of PERCENTILE_CONT, which uses OVER rather than
   * WITHIN GROUP, and allows RESPECT/IGNORE NULLS inside the parentheses. */
  @Test void testPercentileContBigQuery() {
    sql("select percentile_cont(x, .5) over() from unnest(array[1,2,3,4]) as x")
        .withDialect(BIG_QUERY)
        .ok("SELECT (PERCENTILE_CONT(x, 0.5) OVER ())\n"
            + "FROM UNNEST((ARRAY[1, 2, 3, 4])) AS x");
    sql("select percentile_cont(x, .5 RESPECT NULLS) over() from unnest(array[1,2,3,4]) as x")
        .withDialect(BIG_QUERY)
        .ok("SELECT (PERCENTILE_CONT(x, 0.5) RESPECT NULLS OVER ())\n"
            + "FROM UNNEST((ARRAY[1, 2, 3, 4])) AS x");
    sql("select percentile_cont(x, .5 IGNORE NULLS) over() from unnest(array[1,null,3,4]) as x")
        .withDialect(BIG_QUERY)
        .ok("SELECT (PERCENTILE_CONT(x, 0.5) IGNORE NULLS OVER ())\n"
            + "FROM UNNEST((ARRAY[1, NULL, 3, 4])) AS x");
  }

  /** Tests BigQuery's variant of PERCENTILE_DISC, which uses OVER rather than
   * WITHIN GROUP, and allows RESPECT/IGNORE NULLS inside the parentheses. */
  @Test void testPercentileDiscBigQuery() {
    sql("select percentile_disc(x, .5) over() from unnest(array[1,2,3,4]) as x")
        .withDialect(BIG_QUERY)
        .ok("SELECT (PERCENTILE_DISC(x, 0.5) OVER ())\n"
            + "FROM UNNEST((ARRAY[1, 2, 3, 4])) AS x");
    sql("select percentile_disc(x, .5 RESPECT NULLS) over() from unnest(array[1,2,3,4]) as x")
        .withDialect(BIG_QUERY)
        .ok("SELECT (PERCENTILE_DISC(x, 0.5) RESPECT NULLS OVER ())\n"
            + "FROM UNNEST((ARRAY[1, 2, 3, 4])) AS x");
    sql("select percentile_disc(x, .5 IGNORE NULLS) over() from unnest(array[1,null,3,4]) as x")
        .withDialect(BIG_QUERY)
        .ok("SELECT (PERCENTILE_DISC(x, 0.5) IGNORE NULLS OVER ())\n"
            + "FROM UNNEST((ARRAY[1, NULL, 3, 4])) AS x");
  }

  @Test void testHyphenatedTableName() {
    sql("select * from bigquery^-^foo-bar.baz")
        .fails("(?s)Encountered \"-\" at .*")
        .withDialect(BIG_QUERY)
        .ok("SELECT *\n"
            + "FROM `bigquery-foo-bar`.baz");

    // Like BigQuery, MySQL allows back-ticks.
    sql("select `baz`.`buzz` from foo.`baz`")
        .withDialect(BIG_QUERY)
        .ok("SELECT baz.buzz\n"
            + "FROM foo.baz")
        .withDialect(MYSQL)
        .ok("SELECT `baz`.`buzz`\n"
            + "FROM `foo`.`baz`");

    // Unlike BigQuery, MySQL does not allow hyphenated identifiers.
    sql("select `baz`.`buzz` from foo^-^bar.`baz`")
        .withDialect(BIG_QUERY)
        .ok("SELECT baz.buzz\n"
            + "FROM `foo-bar`.baz")
        .withDialect(MYSQL)
        .fails("(?s)Encountered \"-\" at .*");

    // No hyphenated identifiers as table aliases.
    sql("select * from foo.baz as hyphenated^-^alias-not-allowed")
        .withDialect(BIG_QUERY)
        .fails("(?s)Encountered \"-\" at .*");

    sql("select * from foo.baz as `hyphenated-alias-allowed-if-quoted`")
        .withDialect(BIG_QUERY)
        .ok("SELECT *\n"
            + "FROM foo.baz AS `hyphenated-alias-allowed-if-quoted`");

    // No hyphenated identifiers as column names.
    sql("select * from foo-bar.baz cross join (select alpha-omega from t) as t")
        .withDialect(BIG_QUERY)
        .ok("SELECT *\n"
            + "FROM `foo-bar`.baz\n"
            + "CROSS JOIN (SELECT (alpha - omega)\n"
            + "FROM t) AS t");

    sql("select * from bigquery-foo-bar.baz as hyphenated^-^alias-not-allowed")
        .withDialect(BIG_QUERY)
        .fails("(?s)Encountered \"-\" at .*");

    sql("insert into bigquery^-^public-data.foo values (1)")
        .fails("Non-query expression encountered in illegal context")
        .withDialect(BIG_QUERY)
        .ok("INSERT INTO `bigquery-public-data`.foo\n"
            + "VALUES (1)");

    sql("update bigquery^-^public-data.foo set a = b")
        .fails("(?s)Encountered \"-\" at .*")
        .withDialect(BIG_QUERY)
        .ok("UPDATE `bigquery-public-data`.foo SET a = b");

    sql("delete from bigquery^-^public-data.foo where a = 5")
        .fails("(?s)Encountered \"-\" at .*")
        .withDialect(BIG_QUERY)
        .ok("DELETE FROM `bigquery-public-data`.foo\n"
            + "WHERE (a = 5)");

    final String mergeSql = "merge into bigquery^-^public-data.emps e\n"
        + "using (\n"
        + "  select *\n"
        + "  from bigquery-public-data.tempemps\n"
        + "  where deptno is null) t\n"
        + "on e.empno = t.empno\n"
        + "when matched then\n"
        + "  update set name = t.name, deptno = t.deptno,\n"
        + "    salary = t.salary * .1\n"
        + "when not matched then\n"
        + "    insert (name, dept, salary)\n"
        + "    values(t.name, 10, t.salary * .15)";
    final String mergeExpected = "MERGE INTO `bigquery-public-data`.emps AS e\n"
        + "USING (SELECT *\n"
        + "FROM `bigquery-public-data`.tempemps\n"
        + "WHERE (deptno IS NULL)) AS t\n"
        + "ON (e.empno = t.empno)\n"
        + "WHEN MATCHED THEN"
        + " UPDATE SET name = t.name, deptno = t.deptno,"
        + " salary = (t.salary * 0.1)\n"
        + "WHEN NOT MATCHED THEN"
        + " INSERT (name, dept, salary)"
        + " VALUES (t.name, 10, (t.salary * 0.15))";
    sql(mergeSql)
        .fails("(?s)Encountered \"-\" at .*")
        .withDialect(BIG_QUERY)
        .ok(mergeExpected);

    // Hyphenated identifiers may not contain spaces, even in BigQuery.
    sql("select * from bigquery ^-^ foo - bar as t where x < y")
        .fails("(?s)Encountered \"-\" at .*")
        .withDialect(BIG_QUERY)
        .fails("(?s)Encountered \"-\" at .*");
  }

  @Test void testHyphenatedColumnName() {
    // While BigQuery allows hyphenated table names, no dialect allows
    // hyphenated column names; they are parsed as arithmetic minus.
    final String expected = "SELECT (`FOO` - `BAR`)\n"
        + "FROM `EMP`";
    final String expectedBigQuery = "SELECT (foo - bar)\n"
        + "FROM emp";
    sql("select foo-bar from emp")
        .ok(expected)
        .withDialect(BIG_QUERY)
        .ok(expectedBigQuery);
  }

  @Test void testDecimalLiteral() {
    sql("select DECIMAL '99.999'")
        .ok("SELECT 99.999");
    sql("select DECIMAL '   99.999'")
        .ok("SELECT 99.999");
    sql("select DECIMAL '   99.999   '")
        .ok("SELECT 99.999");
    sql("select DECIMAL '+99.999'")
        .ok("SELECT 99.999");
    sql("select DECIMAL '-99.999'")
        .ok("SELECT -99.999");
    sql("select DECIMAL'-99.999'")
        .ok("SELECT -99.999");
    sql("select DECIMAL'99.999'")
        .ok("SELECT 99.999");
    sql("select DECIMAL'.999'")
        .ok("SELECT 0.999");
    sql("select DECIMAL'999.'")
        .ok("SELECT 999");
    sql("select DECIMAL'999'")
        .ok("SELECT 999");
    sql("select DECIMAL '2.11E-2'")
        .ok("SELECT 0.0211");
    sql("select DECIMAL '2.11E2'")
        .ok("SELECT 211");
    sql("select DECIMAL '.11E-2'")
        .ok("SELECT 0.0011");
    sql("select DECIMAL ^''^")
        .fails("(?s)Literal '' can not be parsed to type 'DECIMAL'.*");
    sql("select DECIMAL ^'-'^")
        .fails("(?s)Literal '-' can not be parsed to type 'DECIMAL'.*");
    sql("select DECIMAL ^'foo'^")
        .fails("(?s)Literal 'foo' can not be parsed to type 'DECIMAL'.*");

    // Test with bigquery
    sql("select DECIMAL \"2.11E-2\"")
        .withDialect(BIG_QUERY)
        .ok("SELECT 0.0211");
    sql("select DECIMAL \"999\"")
        .withDialect(BIG_QUERY)
        .ok("SELECT 999");
  }

  @Test void testDerivedColumnList() {
    sql("select * from emp as e (empno, gender) where true")
        .ok("SELECT *\n"
            + "FROM `EMP` AS `E` (`EMPNO`, `GENDER`)\n"
            + "WHERE TRUE");
  }

  @Test void testDerivedColumnListInJoin() {
    final String sql = "select * from emp as e (empno, gender)\n"
        + " join dept as d (deptno, dname) on emp.deptno = dept.deptno";
    final String expected = "SELECT *\n"
        + "FROM `EMP` AS `E` (`EMPNO`, `GENDER`)\n"
        + "INNER JOIN `DEPT` AS `D` (`DEPTNO`, `DNAME`) ON (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)";
    sql(sql).ok(expected);
  }

  /** Test case that does not reproduce but is related to
   * <a href="https://issues.apache.org/jira/browse/CALCITE-2637">[CALCITE-2637]
   * Prefix '-' operator failed between BETWEEN and AND</a>. */
  @Test void testBetweenAnd() {
    final String sql = "select * from emp\n"
        + "where deptno between - DEPTNO + 1 and 5";
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`DEPTNO` BETWEEN ASYMMETRIC ((- `DEPTNO`) + 1) AND 5)";
    sql(sql).ok(expected);
  }

  @Test void testBetweenAnd2() {
    final String sql = "select * from emp\n"
        + "where deptno between - DEPTNO + 1 and - empno - 3";
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`DEPTNO` BETWEEN ASYMMETRIC ((- `DEPTNO`) + 1)"
        + " AND ((- `EMPNO`) - 3))";
    sql(sql).ok(expected);
  }

  @Disabled
  @Test void testDerivedColumnListNoAs() {
    sql("select * from emp e (empno, gender) where true").ok("foo");
  }

  // jdbc syntax
  @Disabled
  @Test void testEmbeddedCall() {
    expr("{call foo(?, ?)}")
        .ok("foo");
  }

  @Disabled
  @Test void testEmbeddedFunction() {
    expr("{? = call bar (?, ?)}")
        .ok("foo");
  }

  @Test void testColumnAliasWithAs() {
    sql("select 1 as foo from emp")
        .ok("SELECT 1 AS `FOO`\n"
            + "FROM `EMP`");
  }

  @Test void testColumnAliasWithoutAs() {
    sql("select 1 foo from emp")
        .ok("SELECT 1 AS `FOO`\n"
            + "FROM `EMP`");
  }

  @Test void testEmbeddedDate() {
    expr("{d '1998-10-22'}")
        .ok("DATE '1998-10-22'");
  }

  @Test void testEmbeddedTime() {
    expr("{t '16:22:34'}")
        .ok("TIME '16:22:34'");
  }

  @Test void testEmbeddedTimestamp() {
    expr("{ts '1998-10-22 16:22:34'}")
        .ok("TIMESTAMP '1998-10-22 16:22:34'");
  }

  @Test void testNot() {
    sql("select not true, not false, not null, not unknown from t")
        .ok("SELECT (NOT TRUE), (NOT FALSE), (NOT NULL), (NOT UNKNOWN)\n"
            + "FROM `T`");
  }

  @Test void testBooleanPrecedenceAndAssociativity() {
    sql("select * from t where true and false")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (TRUE AND FALSE)");

    sql("select * from t where null or unknown and unknown")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (NULL OR (UNKNOWN AND UNKNOWN))");

    sql("select * from t where true and (true or true) or false")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((TRUE AND (TRUE OR TRUE)) OR FALSE)");

    sql("select * from t where 1 and true")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (1 AND TRUE)");
  }

  @Test void testLessThanAssociativity() {
    expr("NOT a = b")
        .ok("(NOT (`A` = `B`))");

    // comparison operators are left-associative
    expr("x < y < z")
        .ok("((`X` < `Y`) < `Z`)");
    expr("x < y <= z = a")
        .ok("(((`X` < `Y`) <= `Z`) = `A`)");
    expr("a = x < y <= z = a")
        .ok("((((`A` = `X`) < `Y`) <= `Z`) = `A`)");

    // IS NULL has lower precedence than comparison
    expr("a = x is null")
        .ok("((`A` = `X`) IS NULL)");
    expr("a = x is not null")
        .ok("((`A` = `X`) IS NOT NULL)");

    // BETWEEN, IN, LIKE have higher precedence than comparison
    expr("a = x between y = b and z = c")
        .ok("((`A` = (`X` BETWEEN ASYMMETRIC (`Y` = `B`) AND `Z`)) = `C`)");
    expr("a = x like y = b")
        .ok("((`A` = (`X` LIKE `Y`)) = `B`)");
    expr("a = x not like y = b")
        .ok("((`A` = (`X` NOT LIKE `Y`)) = `B`)");
    expr("a = x similar to y = b")
        .ok("((`A` = (`X` SIMILAR TO `Y`)) = `B`)");
    expr("a = x not similar to y = b")
        .ok("((`A` = (`X` NOT SIMILAR TO `Y`)) = `B`)");
    expr("a = x not in (y, z) = b")
        .ok("((`A` = (`X` NOT IN (`Y`, `Z`))) = `B`)");

    // LIKE has higher precedence than IS NULL
    expr("a like b is null")
        .ok("((`A` LIKE `B`) IS NULL)");
    expr("a not like b is not null")
        .ok("((`A` NOT LIKE `B`) IS NOT NULL)");

    // = has higher precedence than NOT
    expr("NOT a = b")
        .ok("(NOT (`A` = `B`))");
    expr("NOT a = NOT b")
        .ok("(NOT (`A` = (NOT `B`)))");

    // IS NULL has higher precedence than NOT
    expr("NOT a IS NULL")
        .ok("(NOT (`A` IS NULL))");
    expr("NOT a = b IS NOT NULL")
        .ok("(NOT ((`A` = `B`) IS NOT NULL))");

    // NOT has higher precedence than AND, which  has higher precedence than OR
    expr("NOT a AND NOT b")
        .ok("((NOT `A`) AND (NOT `B`))");
    expr("NOT a OR NOT b")
        .ok("((NOT `A`) OR (NOT `B`))");
    expr("NOT a = b AND NOT c = d OR NOT e = f")
        .ok("(((NOT (`A` = `B`)) AND (NOT (`C` = `D`))) OR (NOT (`E` = `F`)))");
    expr("NOT a = b OR NOT c = d AND NOT e = f")
        .ok("((NOT (`A` = `B`)) OR ((NOT (`C` = `D`)) AND (NOT (`E` = `F`))))");
    expr("NOT NOT a = b OR NOT NOT c = d")
        .ok("((NOT (NOT (`A` = `B`))) OR (NOT (NOT (`C` = `D`))))");
  }

  @Test void testIsBooleans() {
    String[] inOuts = {"NULL", "TRUE", "FALSE", "UNKNOWN"};

    for (String inOut : inOuts) {
      sql("select * from t where nOt fAlSe Is " + inOut)
          .ok("SELECT *\n"
              + "FROM `T`\n"
              + "WHERE (NOT (FALSE IS " + inOut + "))");

      sql("select * from t where c1=1.1 IS NOT " + inOut)
          .ok("SELECT *\n"
              + "FROM `T`\n"
              + "WHERE ((`C1` = 1.1) IS NOT " + inOut + ")");
    }
  }

  @Test void testIsBooleanPrecedenceAndAssociativity() {
    sql("select * from t where x is unknown is not unknown")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((`X` IS UNKNOWN) IS NOT UNKNOWN)");

    sql("select 1 from t where not true is unknown")
        .ok("SELECT 1\n"
            + "FROM `T`\n"
            + "WHERE (NOT (TRUE IS UNKNOWN))");

    sql("select * from t where x is unknown is not unknown is false is not false"
        + " is true is not true is null is not null")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((((((((`X` IS UNKNOWN) IS NOT UNKNOWN) IS FALSE) IS NOT FALSE) IS TRUE) IS NOT TRUE) IS NULL) IS NOT NULL)");

    // combine IS postfix operators with infix (AND) and prefix (NOT) ops
    final String sql = "select * from t "
        + "where x is unknown is false "
        + "and x is unknown is true "
        + "or not y is unknown is not null";
    final String expected = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE ((((`X` IS UNKNOWN) IS FALSE)"
        + " AND ((`X` IS UNKNOWN) IS TRUE))"
        + " OR (NOT ((`Y` IS UNKNOWN) IS NOT NULL)))";
    sql(sql).ok(expected);
  }

  @Test void testEqualNotEqual() {
    expr("'abc'=123")
        .ok("('abc' = 123)");
    expr("'abc'<>123")
        .ok("('abc' <> 123)");
    expr("'abc'<>123='def'<>456")
        .ok("((('abc' <> 123) = 'def') <> 456)");
    expr("'abc'<>123=('def'<>456)")
        .ok("(('abc' <> 123) = ('def' <> 456))");
  }

  @Test void testBangEqualIsBad() {
    // Quoth www.ocelot.ca:
    //   "Other relators besides '=' are what you'd expect if
    //   you've used any programming language: > and >= and < and <=. The
    //   only potential point of confusion is that the operator for 'not
    //   equals' is <> as in BASIC. There are many texts which will tell
    //   you that != is SQL's not-equals operator; those texts are false;
    //   it's one of those unstampoutable urban myths."
    // Therefore, we only support != with certain SQL conformance levels.
    expr("'abc'^!=^123")
        .fails("Bang equal '!=' is not allowed under the current SQL conformance level");
  }

  @Test void testBetween() {
    sql("select * from t where price between 1 and 2")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`PRICE` BETWEEN ASYMMETRIC 1 AND 2)");

    sql("select * from t where price between symmetric 1 and 2")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`PRICE` BETWEEN SYMMETRIC 1 AND 2)");

    sql("select * from t where price not between symmetric 1 and 2")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`PRICE` NOT BETWEEN SYMMETRIC 1 AND 2)");

    sql("select * from t where price between ASYMMETRIC 1 and 2+2*2")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`PRICE` BETWEEN ASYMMETRIC 1 AND (2 + (2 * 2)))");

    final String sql0 = "select * from t\n"
        + " where price > 5\n"
        + " and price not between 1 + 2 and 3 * 4 AnD price is null";
    final String expected0 = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (((`PRICE` > 5) "
        + "AND (`PRICE` NOT BETWEEN ASYMMETRIC (1 + 2) AND (3 * 4))) "
        + "AND (`PRICE` IS NULL))";
    sql(sql0).ok(expected0);

    final String sql1 = "select * from t\n"
        + "where price > 5\n"
        + "and price between 1 + 2 and 3 * 4 + price is null";
    final String expected1 = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE ((`PRICE` > 5) "
        + "AND ((`PRICE` BETWEEN ASYMMETRIC (1 + 2) AND ((3 * 4) + `PRICE`)) "
        + "IS NULL))";
    sql(sql1).ok(expected1);

    final String sql2 = "select * from t\n"
        + "where price > 5\n"
        + "and price between 1 + 2 and 3 * 4 or price is null";
    final String expected2 = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (((`PRICE` > 5) "
        + "AND (`PRICE` BETWEEN ASYMMETRIC (1 + 2) AND (3 * 4))) "
        + "OR (`PRICE` IS NULL))";
    sql(sql2).ok(expected2);

    final String sql3 = "values a between c and d and e and f between g and h";
    final String expected3 = "VALUES ("
        + "ROW((((`A` BETWEEN ASYMMETRIC `C` AND `D`) AND `E`)"
        + " AND (`F` BETWEEN ASYMMETRIC `G` AND `H`))))";
    sql(sql3).ok(expected3);

    sql("values a between b or c^")
        .fails(".*BETWEEN operator has no terminating AND");

    sql("values a ^between^")
        .fails("(?s).*Encountered \"between <EOF>\" at line 1, column 10.*");

    sql("values a between symmetric 1^")
        .fails(".*BETWEEN operator has no terminating AND");

    // precedence of BETWEEN is higher than AND and OR, but lower than '+'
    sql("values a between b and c + 2 or d and e")
        .ok("VALUES (ROW(((`A` BETWEEN ASYMMETRIC `B` AND (`C` + 2)) OR (`D` AND `E`))))");

    // '=' has slightly lower precedence than BETWEEN; both are left-assoc
    sql("values x = a between b and c = d = e")
        .ok("VALUES (ROW((((`X` = (`A` BETWEEN ASYMMETRIC `B` AND `C`)) = `D`) = `E`)))");

    // AND doesn't match BETWEEN if it's between parentheses!
    sql("values a between b or (c and d) or e and f")
        .ok("VALUES (ROW((`A` BETWEEN ASYMMETRIC ((`B` OR (`C` AND `D`)) OR `E`) AND `F`)))");
  }

  @Test void testOperateOnColumn() {
    sql("select c1*1,c2  + 2,c3/3,c4-4,c5*c4  from t")
        .ok("SELECT (`C1` * 1), (`C2` + 2), (`C3` / 3), (`C4` - 4), (`C5` * `C4`)\n"
            + "FROM `T`");
  }

  @Test void testRow() {
    sql("select t.r.\"EXPR$1\", t.r.\"EXPR$0\" from (select (1,2) r from sales.depts) t")
        .ok("SELECT `T`.`R`.`EXPR$1`, `T`.`R`.`EXPR$0`\n"
            + "FROM (SELECT (ROW(1, 2)) AS `R`\n"
            + "FROM `SALES`.`DEPTS`) AS `T`");

    sql("select t.r.\"EXPR$1\".\"EXPR$2\" "
        + "from (select ((1,2),(3,4,5)) r from sales.depts) t")
        .ok("SELECT `T`.`R`.`EXPR$1`.`EXPR$2`\n"
            + "FROM (SELECT (ROW((ROW(1, 2)), (ROW(3, 4, 5)))) AS `R`\n"
            + "FROM `SALES`.`DEPTS`) AS `T`");

    sql("select t.r.\"EXPR$1\".\"EXPR$2\" "
        + "from (select ((1,2),(3,4,5,6)) r from sales.depts) t")
        .ok("SELECT `T`.`R`.`EXPR$1`.`EXPR$2`\n"
            + "FROM (SELECT (ROW((ROW(1, 2)), (ROW(3, 4, 5, 6)))) AS `R`\n"
            + "FROM `SALES`.`DEPTS`) AS `T`");

    // Conformance DEFAULT and LENIENT support explicit row value constructor
    final String selectRow = "select ^row(t1a, t2a)^ from t1";
    final String expected = "SELECT (ROW(`T1A`, `T2A`))\n"
        + "FROM `T1`";
    sql(selectRow)
        .withConformance(SqlConformanceEnum.DEFAULT)
        .ok(expected);
    sql(selectRow)
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected);

    final String pattern = "ROW expression encountered in illegal context";
    sql(selectRow)
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .fails(pattern);
    sql(selectRow)
        .withConformance(SqlConformanceEnum.ORACLE_12)
        .fails(pattern);
    sql(selectRow)
        .withConformance(SqlConformanceEnum.STRICT_2003)
        .fails(pattern);
    sql(selectRow)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .fails(pattern);

    final String whereRow = "select 1 from t2 where ^row (x, y)^ < row (a, b)";
    final String whereExpected = "SELECT 1\n"
        + "FROM `T2`\n"
        + "WHERE ((ROW(`X`, `Y`)) < (ROW(`A`, `B`)))";
    sql(whereRow)
        .withConformance(SqlConformanceEnum.DEFAULT)
        .ok(whereExpected);
    sql(whereRow)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .fails(pattern);

    final String whereRow2 = "select 1 from t2 where ^(x, y)^ < (a, b)";
    sql(whereRow2)
        .withConformance(SqlConformanceEnum.DEFAULT)
        .ok(whereExpected);

    // After this point, SqlUnparserTest has problems.
    // We generate ROW in a dialect that does not allow ROW in all contexts.
    // So bail out.
    assumeFalse(fixture().tester.isUnparserTest());
    sql(whereRow2)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(whereExpected);
  }

  @Test void testRowValueExpression() {
    final String expected0 = "INSERT INTO \"EMPS\"\n"
            + "VALUES (ROW(1, 'Fred')),\n"
            + "(ROW(2, 'Eric'))";
    String sql = "insert into emps values (1,'Fred'),(2, 'Eric')";
    sql(sql)
        .withDialect(CALCITE)
        .ok(expected0);

    final String expected1 = "INSERT INTO `emps`\n"
            + "VALUES (1, 'Fred'),\n"
            + "(2, 'Eric')";
    sql(sql)
        .withDialect(MYSQL)
        .ok(expected1);

    final String expected2 = "INSERT INTO \"EMPS\"\n"
            + "VALUES (1, 'Fred'),\n"
            + "(2, 'Eric')";
    sql(sql)
        .withDialect(ORACLE)
        .ok(expected2);

    final String expected3 = "INSERT INTO [EMPS]\n"
            + "VALUES (1, 'Fred'),\n"
            + "(2, 'Eric')";
    sql(sql)
        .withDialect(MSSQL)
        .ok(expected3);

    expr("ROW(EMP.EMPNO, EMP.ENAME)").ok("(ROW(`EMP`.`EMPNO`, `EMP`.`ENAME`))");
    expr("ROW(EMP.EMPNO + 1, EMP.ENAME)").ok("(ROW((`EMP`.`EMPNO` + 1), `EMP`.`ENAME`))");
    expr("ROW((select deptno from dept where dept.deptno = emp.deptno), EMP.ENAME)")
        .ok("(ROW((SELECT `DEPTNO`\n"
            + "FROM `DEPT`\n"
            + "WHERE (`DEPT`.`DEPTNO` = `EMP`.`DEPTNO`)), `EMP`.`ENAME`))");
  }

  @Test void testRowWithDot() {
    sql("select (1,2).a from c.t")
        .ok("SELECT ((ROW(1, 2)).`A`)\n"
            + "FROM `C`.`T`");
    sql("select row(1,2).a from c.t")
        .ok("SELECT ((ROW(1, 2)).`A`)\n"
            + "FROM `C`.`T`");
    sql("select tbl.foo(0).col.bar from tbl")
        .ok("SELECT ((`TBL`.`FOO`(0).`COL`).`BAR`)\n"
            + "FROM `TBL`");
  }

  @Test void testDotAfterParenthesizedIdentifier() {
    sql("select (a).c.d from c.t")
        .ok("SELECT ((`A`.`C`).`D`)\n"
            + "FROM `C`.`T`");
  }

  @Test void testPeriod() {
    // We don't have a PERIOD constructor currently;
    // ROW constructor is sufficient for now.
    expr("period (date '1969-01-05', interval '2-3' year to month)")
        .ok("(ROW(DATE '1969-01-05', INTERVAL '2-3' YEAR TO MONTH))");
  }

  @Test void testOverlaps() {
    final String[] ops = {
        "overlaps", "equals", "precedes", "succeeds",
        "immediately precedes", "immediately succeeds"
    };
    final String[] periods = {"period ", ""};
    for (String period : periods) {
      for (String op : ops) {
        checkPeriodPredicate(new Checker(op, period));
      }
    }
  }

  void checkPeriodPredicate(Checker checker) {
    checker.checkExp("$p(x,xx) $op $p(y,yy)",
        "(PERIOD (`X`, `XX`) $op PERIOD (`Y`, `YY`))");

    checker.checkExp(
        "$p(x,xx) $op $p(y,yy) or false",
        "((PERIOD (`X`, `XX`) $op PERIOD (`Y`, `YY`)) OR FALSE)");

    checker.checkExp(
        "true and not $p(x,xx) $op $p(y,yy) or false",
        "((TRUE AND (NOT (PERIOD (`X`, `XX`) $op PERIOD (`Y`, `YY`)))) OR FALSE)");

    if (checker.period.isEmpty()) {
      checker.checkExp("$p(x,xx,xxx) $op $p(y,yy) or false",
          "((PERIOD (`X`, `XX`) $op PERIOD (`Y`, `YY`)) OR FALSE)");
    } else {
      // 3-argument rows are valid in the parser, rejected by the validator
      checker.checkExpFails("$p(x,xx^,^xxx) $op $p(y,yy) or false",
          "(?s).*Encountered \",\" at .*");
    }
  }

  /** Parses a list of statements (that contains only one statement). */
  @Test void testStmtListWithSelect() {
    final String expected = "SELECT *\n"
        + "FROM `EMP`,\n"
        + "`DEPT`";
    sql("select * from emp, dept").list().ok(expected);
  }

  @Test void testStmtListWithSelectAndSemicolon() {
    final String expected = "SELECT *\n"
        + "FROM `EMP`,\n"
        + "`DEPT`";
    sql("select * from emp, dept;").list().ok(expected);
  }

  @Test void testStmtListWithTwoSelect() {
    final String expected = "SELECT *\n"
        + "FROM `EMP`,\n"
        + "`DEPT`";
    sql("select * from emp, dept ; select * from emp, dept").list()
        .ok(expected, expected);
  }

  @Test void testStmtListWithTwoSelectSemicolon() {
    final String expected = "SELECT *\n"
        + "FROM `EMP`,\n"
        + "`DEPT`";
    sql("select * from emp, dept ; select * from emp, dept;").list()
        .ok(expected, expected);
  }

  @Test void testStmtListWithSelectDelete() {
    final String expected = "SELECT *\n"
        + "FROM `EMP`,\n"
        + "`DEPT`";
    final String expected1 = "DELETE FROM `EMP`";
    sql("select * from emp, dept; delete from emp").list()
        .ok(expected, expected1);
  }

  @Test void testStmtListWithSelectDeleteUpdate() {
    final String sql = "select * from emp, dept; "
        + "delete from emp; "
        + "update emps set empno = empno + 1";
    final String expected = "SELECT *\n"
        + "FROM `EMP`,\n"
        + "`DEPT`";
    final String expected1 = "DELETE FROM `EMP`";
    final String expected2 = "UPDATE `EMPS` SET `EMPNO` = (`EMPNO` + 1)";
    sql(sql).list().ok(expected, expected1, expected2);
  }

  @Test void testStmtListWithSemiColonInComment() {
    final String sql = ""
        + "select * from emp, dept; // comment with semicolon ; values 1\n"
        + "values 2";
    final String expected = "SELECT *\n"
        + "FROM `EMP`,\n"
        + "`DEPT`";
    final String expected1 = "VALUES (ROW(2))";
    sql(sql).list().ok(expected, expected1);
  }

  @Test void testStmtListWithSemiColonInWhere() {
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`NAME` LIKE 'toto;')";
    final String expected1 = "DELETE FROM `EMP`";
    sql("select * from emp where name like 'toto;'; delete from emp").list()
        .ok(expected, expected1);
  }

  @Test void testStmtListWithInsertSelectInsert() {
    final String sql = "insert into dept (name, deptno) values ('a', 123); "
        + "select * from emp where name like 'toto;'; "
        + "insert into dept (name, deptno) values ('b', 123);";
    final String expected = "INSERT INTO `DEPT` (`NAME`, `DEPTNO`)\n"
        + "VALUES (ROW('a', 123))";
    final String expected1 = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`NAME` LIKE 'toto;')";
    final String expected2 = "INSERT INTO `DEPT` (`NAME`, `DEPTNO`)\n"
        + "VALUES (ROW('b', 123))";
    sql(sql).list().ok(expected, expected1, expected2);
  }

  /** Should fail since the first statement lacks semicolon. */
  @Test void testStmtListWithoutSemiColon1() {
    sql("select * from emp where name like 'toto' "
        + "^delete^ from emp")
        .list()
        .fails("(?s).*Encountered \"delete\" at .*");
  }

  /** Should fail since the third statement lacks semicolon. */
  @Test void testStmtListWithoutSemiColon2() {
    sql("select * from emp where name like 'toto'; "
        + "delete from emp; "
        + "insert into dept (name, deptno) values ('a', 123) "
        + "^select^ * from dept")
        .list()
        .fails("(?s).*Encountered \"select\" at .*");
  }

  @Test void testIsDistinctFrom() {
    sql("select x is distinct from y from t")
        .ok("SELECT (`X` IS DISTINCT FROM `Y`)\n"
            + "FROM `T`");

    sql("select * from t where x is distinct from y")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`X` IS DISTINCT FROM `Y`)");

    sql("select * from t where x is distinct from (4,5,6)")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`X` IS DISTINCT FROM (ROW(4, 5, 6)))");

    sql("select * from t where x is distinct from row (4,5,6)")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`X` IS DISTINCT FROM (ROW(4, 5, 6)))");

    sql("select * from t where true is distinct from true")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (TRUE IS DISTINCT FROM TRUE)");

    sql("select * from t where true is distinct from true is true")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((TRUE IS DISTINCT FROM TRUE) IS TRUE)");
  }

  @Test void testIsNotDistinct() {
    sql("select x is not distinct from y from t")
        .ok("SELECT (`X` IS NOT DISTINCT FROM `Y`)\n"
            + "FROM `T`");

    sql("select * from t where true is not distinct from true")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (TRUE IS NOT DISTINCT FROM TRUE)");
  }

  @Test void testFloor() {
    expr("floor(1.5)")
        .ok("FLOOR(1.5)");
    expr("floor(x)")
        .ok("FLOOR(`X`)");

    expr("floor(x to second)")
        .ok("FLOOR(`X` TO SECOND)");
    expr("floor(x to epoch)")
        .ok("FLOOR(`X` TO EPOCH)");
    expr("floor(x to minute)")
        .ok("FLOOR(`X` TO MINUTE)");
    expr("floor(x to hour)")
        .ok("FLOOR(`X` TO HOUR)");
    expr("floor(x to day)")
        .ok("FLOOR(`X` TO DAY)");
    expr("floor(x to dow)")
        .ok("FLOOR(`X` TO DOW)");
    expr("floor(x to doy)")
        .ok("FLOOR(`X` TO DOY)");
    expr("floor(x to week)")
        .ok("FLOOR(`X` TO WEEK)");
    expr("floor(x to month)")
        .ok("FLOOR(`X` TO MONTH)");
    expr("floor(x to quarter)")
        .ok("FLOOR(`X` TO QUARTER)");
    expr("floor(x to year)")
        .ok("FLOOR(`X` TO YEAR)");
    expr("floor(x to decade)")
        .ok("FLOOR(`X` TO DECADE)");
    expr("floor(x to century)")
        .ok("FLOOR(`X` TO CENTURY)");
    expr("floor(x to millennium)")
        .ok("FLOOR(`X` TO MILLENNIUM)");

    expr("floor(x + interval '1:20' minute to second)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND))");
    expr("floor(x + interval '1:20' minute to second to second)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO SECOND)");
    expr("floor(x + interval '1:20' minute to second to epoch)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO EPOCH)");
    expr("floor(x + interval '1:20' hour to minute)")
        .ok("FLOOR((`X` + INTERVAL '1:20' HOUR TO MINUTE))");
    expr("floor(x + interval '1:20' hour to minute to minute)")
        .ok("FLOOR((`X` + INTERVAL '1:20' HOUR TO MINUTE) TO MINUTE)");
    expr("floor(x + interval '1:20' minute to second to hour)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO HOUR)");
    expr("floor(x + interval '1:20' minute to second to day)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DAY)");
    expr("floor(x + interval '1:20' minute to second to dow)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DOW)");
    expr("floor(x + interval '1:20' minute to second to doy)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DOY)");
    expr("floor(x + interval '1:20' minute to second to week)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO WEEK)");
    expr("floor(x + interval '1:20' minute to second to month)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO MONTH)");
    expr("floor(x + interval '1:20' minute to second to quarter)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO QUARTER)");
    expr("floor(x + interval '1:20' minute to second to year)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO YEAR)");
    expr("floor(x + interval '1:20' minute to second to decade)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DECADE)");
    expr("floor(x + interval '1:20' minute to second to century)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO CENTURY)");
    expr("floor(x + interval '1:20' minute to second to millennium)")
        .ok("FLOOR((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO MILLENNIUM)");
  }

  @Test void testCeil() {
    expr("ceil(3453.2)")
        .ok("CEIL(3453.2)");
    expr("ceil(x)")
        .ok("CEIL(`X`)");
    expr("ceil(x to second)")
        .ok("CEIL(`X` TO SECOND)");
    expr("ceil(x to epoch)")
        .ok("CEIL(`X` TO EPOCH)");
    expr("ceil(x to minute)")
        .ok("CEIL(`X` TO MINUTE)");
    expr("ceil(x to hour)")
        .ok("CEIL(`X` TO HOUR)");
    expr("ceil(x to day)")
        .ok("CEIL(`X` TO DAY)");
    expr("ceil(x to dow)")
        .ok("CEIL(`X` TO DOW)");
    expr("ceil(x to doy)")
        .ok("CEIL(`X` TO DOY)");
    expr("ceil(x to week)")
        .ok("CEIL(`X` TO WEEK)");
    expr("ceil(x to month)")
        .ok("CEIL(`X` TO MONTH)");
    expr("ceil(x to quarter)")
        .ok("CEIL(`X` TO QUARTER)");
    expr("ceil(x to year)")
        .ok("CEIL(`X` TO YEAR)");
    expr("ceil(x to decade)")
        .ok("CEIL(`X` TO DECADE)");
    expr("ceil(x to century)")
        .ok("CEIL(`X` TO CENTURY)");
    expr("ceil(x to millennium)")
        .ok("CEIL(`X` TO MILLENNIUM)");

    expr("ceil(x + interval '1:20' minute to second)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND))");
    expr("ceil(x + interval '1:20' minute to second to second)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO SECOND)");
    expr("ceil(x + interval '1:20' minute to second to epoch)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO EPOCH)");
    expr("ceil(x + interval '1:20' hour to minute)")
        .ok("CEIL((`X` + INTERVAL '1:20' HOUR TO MINUTE))");
    expr("ceil(x + interval '1:20' hour to minute to minute)")
        .ok("CEIL((`X` + INTERVAL '1:20' HOUR TO MINUTE) TO MINUTE)");
    expr("ceil(x + interval '1:20' minute to second to hour)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO HOUR)");
    expr("ceil(x + interval '1:20' minute to second to day)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DAY)");
    expr("ceil(x + interval '1:20' minute to second to dow)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DOW)");
    expr("ceil(x + interval '1:20' minute to second to doy)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DOY)");
    expr("ceil(x + interval '1:20' minute to second to week)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO WEEK)");
    expr("ceil(x + interval '1:20' minute to second to month)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO MONTH)");
    expr("ceil(x + interval '1:20' minute to second to quarter)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO QUARTER)");
    expr("ceil(x + interval '1:20' minute to second to year)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO YEAR)");
    expr("ceil(x + interval '1:20' minute to second to decade)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO DECADE)");
    expr("ceil(x + interval '1:20' minute to second to century)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO CENTURY)");
    expr("ceil(x + interval '1:20' minute to second to millennium)")
        .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO MILLENNIUM)");
  }

  @Test void testCast() {
    expr("cast(x as boolean)")
        .ok("CAST(`X` AS BOOLEAN)");
    expr("cast(x as integer)")
        .ok("CAST(`X` AS INTEGER)");
    expr("cast(x as varchar(1))")
        .ok("CAST(`X` AS VARCHAR(1))");
    expr("cast(x as date)")
        .ok("CAST(`X` AS DATE)");
    expr("cast(x as time)")
        .ok("CAST(`X` AS TIME)");
    expr("cast(x as time without time zone)")
        .ok("CAST(`X` AS TIME)");
    expr("cast(x as time with local time zone)")
        .ok("CAST(`X` AS TIME WITH LOCAL TIME ZONE)");
    expr("cast(x as timestamp without time zone)")
        .ok("CAST(`X` AS TIMESTAMP)");
    expr("cast(x as timestamp with local time zone)")
        .ok("CAST(`X` AS TIMESTAMP WITH LOCAL TIME ZONE)");
    expr("cast(x as time(0))")
        .ok("CAST(`X` AS TIME(0))");
    expr("cast(x as time(0) without time zone)")
        .ok("CAST(`X` AS TIME(0))");
    expr("cast(x as time(0) with local time zone)")
        .ok("CAST(`X` AS TIME(0) WITH LOCAL TIME ZONE)");
    expr("cast(x as timestamp(0))")
        .ok("CAST(`X` AS TIMESTAMP(0))");
    expr("cast(x as timestamp(0) without time zone)")
        .ok("CAST(`X` AS TIMESTAMP(0))");
    expr("cast(x as timestamp(0) with local time zone)")
        .ok("CAST(`X` AS TIMESTAMP(0) WITH LOCAL TIME ZONE)");
    expr("cast(x as timestamp)")
        .ok("CAST(`X` AS TIMESTAMP)");
    expr("cast(x as decimal(1,1))")
        .ok("CAST(`X` AS DECIMAL(1, 1))");
    expr("cast(x as char(1))")
        .ok("CAST(`X` AS CHAR(1))");
    expr("cast(x as binary(1))")
        .ok("CAST(`X` AS BINARY(1))");
    expr("cast(x as varbinary(1))")
        .ok("CAST(`X` AS VARBINARY(1))");
    expr("cast(x as tinyint)")
        .ok("CAST(`X` AS TINYINT)");
    expr("cast(x as smallint)")
        .ok("CAST(`X` AS SMALLINT)");
    expr("cast(x as bigint)")
        .ok("CAST(`X` AS BIGINT)");
    expr("cast(x as real)")
        .ok("CAST(`X` AS REAL)");
    expr("cast(x as double)")
        .ok("CAST(`X` AS DOUBLE)");
    expr("cast(x as decimal)")
        .ok("CAST(`X` AS DECIMAL)");
    expr("cast(x as decimal(0))")
        .ok("CAST(`X` AS DECIMAL(0))");
    expr("cast(x as decimal(1,2))")
        .ok("CAST(`X` AS DECIMAL(1, 2))");

    expr("cast('foo' as bar)")
        .ok("CAST('foo' AS `BAR`)");
  }

  @Test void testCastFails() {
    expr("cast(x as time with ^time^ zone)")
        .fails("(?s).*Encountered \"time\" at .*");
    expr("cast(x as time(0) with ^time^ zone)")
        .fails("(?s).*Encountered \"time\" at .*");
    expr("cast(x as timestamp with ^time^ zone)")
        .fails("(?s).*Encountered \"time\" at .*");
    expr("cast(x as timestamp(0) with ^time^ zone)")
        .fails("(?s).*Encountered \"time\" at .*");
    expr("cast(x as varchar(10) ^with^ local time zone)")
        .fails("(?s).*Encountered \"with\" at line 1, column 23.\n.*");
    expr("cast(x as varchar(10) ^without^ time zone)")
        .fails("(?s).*Encountered \"without\" at line 1, column 23.\n.*");
  }

  /** Test for MSSQL CONVERT parsing, with focus on iffy DATE type and
   * testing that the extra "style" operand is parsed
   * Other tests are defined in functions.iq
   */
  @Test void testMssqlConvert() {
    expr("CONVERT(VARCHAR(5), 'xx')")
        .same();
    expr("CONVERT(VARCHAR(5), 'xx')")
        .same();
    expr("CONVERT(VARCHAR(5), NULL)")
        .same();
    expr("CONVERT(VARCHAR(5), NULL, NULL)")
        .same();
    expr("CONVERT(DATE, 'xx', 121)")
        .same();
    expr("CONVERT(DATE, 'xx')")
        .same();
  }

  @Test void testLikeAndSimilar() {
    sql("select * from t where x like '%abc%'")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`X` LIKE '%abc%')");

    sql("select * from t where x+1 not siMilaR to '%abc%' ESCAPE 'e'")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((`X` + 1) NOT SIMILAR TO '%abc%' ESCAPE 'e')");

    // LIKE has higher precedence than AND
    sql("select * from t where price > 5 and x+2*2 like y*3+2 escape (select*from t)")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((`PRICE` > 5) AND ((`X` + (2 * 2)) LIKE ((`Y` * 3) + 2) ESCAPE (SELECT *\n"
            + "FROM `T`)))");

    sql("values a and b like c")
        .ok("VALUES (ROW((`A` AND (`B` LIKE `C`))))");

    // LIKE has higher precedence than AND
    sql("values a and b like c escape d and e")
        .ok("VALUES (ROW(((`A` AND (`B` LIKE `C` ESCAPE `D`)) AND `E`)))");

    // LIKE has same precedence as '='; LIKE is right-assoc, '=' is left
    sql("values a = b like c = d")
        .ok("VALUES (ROW(((`A` = (`B` LIKE `C`)) = `D`)))");

    // Nested LIKE
    sql("values a like b like c escape d")
        .ok("VALUES (ROW((`A` LIKE (`B` LIKE `C` ESCAPE `D`))))");
    sql("values a like b like c escape d and false")
        .ok("VALUES (ROW(((`A` LIKE (`B` LIKE `C` ESCAPE `D`)) AND FALSE)))");
    sql("values a like b like c like d escape e escape f")
        .ok("VALUES (ROW((`A` LIKE (`B` LIKE (`C` LIKE `D` ESCAPE `E`) ESCAPE `F`))))");

    // Mixed LIKE and SIMILAR TO
    sql("values a similar to b like c similar to d escape e escape f")
        .ok("VALUES (ROW((`A` SIMILAR TO (`B` LIKE (`C` SIMILAR TO `D` ESCAPE `E`) ESCAPE `F`))))");

    if (isReserved("ESCAPE")) {
      sql("select * from t where ^escape^ 'e'")
          .fails("(?s).*Encountered \"escape\" at .*");
    }

    // LIKE with +
    sql("values a like b + c escape d")
        .ok("VALUES (ROW((`A` LIKE (`B` + `C`) ESCAPE `D`)))");

    // LIKE with ||
    sql("values a like b || c escape d")
        .ok("VALUES (ROW((`A` LIKE (`B` || `C`) ESCAPE `D`)))");

    // ESCAPE with no expression
    if (isReserved("ESCAPE")) {
      sql("values a ^like^ escape d")
          .fails("(?s).*Encountered \"like escape\" at .*");
    }

    // ESCAPE with no expression
    if (isReserved("ESCAPE")) {
      sql("values a like b || c ^escape^ and false")
          .fails("(?s).*Encountered \"escape and\" at line 1, column 22.*");
    }

    // basic SIMILAR TO
    sql("select * from t where x similar to '%abc%'")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`X` SIMILAR TO '%abc%')");

    sql("select * from t where x+1 not siMilaR to '%abc%' ESCAPE 'e'")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((`X` + 1) NOT SIMILAR TO '%abc%' ESCAPE 'e')");

    // SIMILAR TO has higher precedence than AND
    sql("select * from t where price > 5 and x+2*2 SIMILAR TO y*3+2 escape (select*from t)")
        .ok("SELECT *\n"
            + "FROM `T`\n"
            + "WHERE ((`PRICE` > 5) AND ((`X` + (2 * 2)) SIMILAR TO ((`Y` * 3) + 2) ESCAPE (SELECT *\n"
            + "FROM `T`)))");

    // Mixed LIKE and SIMILAR TO
    sql("values a similar to b like c similar to d escape e escape f")
        .ok("VALUES (ROW((`A` SIMILAR TO (`B` LIKE (`C` SIMILAR TO `D` ESCAPE `E`) ESCAPE `F`))))");

    // SIMILAR TO with sub-query
    sql("values a similar to (select * from t where a like b escape c) escape d")
        .ok("VALUES (ROW((`A` SIMILAR TO (SELECT *\n"
            + "FROM `T`\n"
            + "WHERE (`A` LIKE `B` ESCAPE `C`)) ESCAPE `D`)))");
  }

  @Test void testIlike() {
    // The ILIKE operator is only valid when the PostgreSQL function library is
    // enabled ('fun=postgresql'). But the parser can always parse it.
    final String expected = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (`X` NOT ILIKE '%abc%')";
    final String sql = "select * from t where x not ilike '%abc%'";
    sql(sql).ok(expected);

    final String sql1 = "select * from t where x ilike '%abc%'";
    final String expected1 = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (`X` ILIKE '%abc%')";
    sql(sql1).ok(expected1);
  }

  @Test void testRlike() {
    // The RLIKE operator is valid when the HIVE or SPARK function library is
    // enabled ('fun=spark' or 'fun=hive'). But the parser can always parse it.
    final String expected = "SELECT `COLA`\n"
        + "FROM `T`\n"
        + "WHERE (MAX(`EMAIL`) RLIKE '.+@.+\\\\..+')";
    final String sql = "select cola from t where max(email) rlike '.+@.+\\\\..+'";
    sql(sql).ok(expected);

    final String expected1 = "SELECT `COLA`\n"
        + "FROM `T`\n"
        + "WHERE (MAX(`EMAIL`) NOT RLIKE '.+@.+\\\\..+')";
    final String sql1 = "select cola from t where max(email) not rlike '.+@.+\\\\..+'";
    sql(sql1).ok(expected1);
  }

  @Test void testArithmeticOperators() {
    expr("1-2+3*4/5/6-7")
        .ok("(((1 - 2) + (((3 * 4) / 5) / 6)) - 7)");
    expr("power(2,3)")
        .ok("POWER(2, 3)");
    expr("aBs(-2.3e-2)")
        .ok("ABS(-2.3E-2)");
    expr("MOD(5             ,\t\f\r\n2)")
        .ok("MOD(5, 2)");
    expr("ln(5.43  )")
        .ok("LN(5.43)");
    expr("log10(- -.2  )")
        .ok("LOG10(0.2)");
  }

  @Test void testExists() {
    sql("select * from dept where exists (select 1 from emp where emp.deptno = dept.deptno)")
        .ok("SELECT *\n"
            + "FROM `DEPT`\n"
            + "WHERE (EXISTS (SELECT 1\n"
            + "FROM `EMP`\n"
            + "WHERE (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)))");
  }

  @Test void testExistsInWhere() {
    sql("select * from emp where 1 = 2 and exists (select 1 from dept) and 3 = 4")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE (((1 = 2) AND (EXISTS (SELECT 1\n"
            + "FROM `DEPT`))) AND (3 = 4))");
  }

  @Test void testUnique() {
    sql("select * from dept where unique (select 1 from emp where emp.deptno = dept.deptno)")
        .ok("SELECT *\n"
            + "FROM `DEPT`\n"
            + "WHERE (UNIQUE (SELECT 1\n"
            + "FROM `EMP`\n"
            + "WHERE (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)))");
  }

  @Test void testUniqueInWhere() {
    sql("select * from emp where 1 = 2 and unique (select 1 from dept) and 3 = 4")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE (((1 = 2) AND (UNIQUE (SELECT 1\n"
            + "FROM `DEPT`))) AND (3 = 4))");
  }

  @Test void testNotUnique() {
    sql("select * from dept where not not unique (select * from emp) and true")
        .ok("SELECT *\n"
            + "FROM `DEPT`\n"
            + "WHERE ((NOT (NOT (UNIQUE (SELECT *\n"
            + "FROM `EMP`)))) AND TRUE)");
  }

  @Test void testFromWithAs() {
    sql("select 1 from emp as e where 1")
        .ok("SELECT 1\n"
            + "FROM `EMP` AS `E`\n"
            + "WHERE 1");
  }

  @Test void testConcat() {
    expr("'a' || 'b'").ok("('a' || 'b')");
  }

  @Test void testCStyleEscapedString() {
    expr("E'Apache\\tCalcite'")
        .ok("_UTF16'Apache\tCalcite'");
    expr("E'Apache\\bCalcite'")
        .ok("_UTF16'Apache\bCalcite'");
    expr("E'Apache\\fCalcite'")
        .ok("_UTF16'Apache\fCalcite'");
    expr("E'Apache\\nCalcite'")
        .ok("_UTF16'Apache\nCalcite'");
    expr("E'Apache\\rCalcite'")
        .ok("_UTF16'Apache\rCalcite'");
    expr("E'\\t\\n\\f'")
        .ok("_UTF16'\t\n\f'");
    expr("E'\\Apache Calcite'")
        .ok("_UTF16'\\Apache Calcite'");

    expr("E'\141'")
        .ok("_UTF16'a'");
    expr("E'\141\141\141'")
        .ok("_UTF16'aaa'");
    expr("E'\1411'")
        .ok("_UTF16'a1'");

    expr("E'\\x61'")
        .ok("_UTF16'a'");
    expr("E'\\x61\\x61\\x61'")
        .ok("_UTF16'aaa'");
    expr("E'\\x61\\x61\\x61'")
        .ok("_UTF16'aaa'");
    expr("E'\\xDg0000'")
        .ok("_UTF16'\rg0000'");

    expr("E'\\u0061'")
        .ok("_UTF16'a'");
    expr("E'\\u0061\\u0061\\u0061'")
        .ok("_UTF16'aaa'");

    expr("E'\\U00000061'")
        .ok("_UTF16'a'");
    expr("E'\\U00000061\\U00000061\\U00000061'")
        .ok("_UTF16'aaa'");

    expr("E'\\0'")
        .ok("_UTF16'\u0000'");
    expr("E'\\07'")
        .ok("_UTF16'\u0007'");
    expr("E'\\07'")
        .ok("_UTF16'\u0007'");

    expr("E'a\\'a\\'a\\''")
        .ok("_UTF16'a''a''a'''");
    expr("E'a''a''a'''")
        .ok("_UTF16'a''a''a'''");

    expr("E^'\\'^")
        .fails("(?s).*Encountered .*");
    expr("E^'a\\'^")
        .fails("(?s).*Encountered.*");
    expr("E'a\\''^a^'")
        .fails("(?s).*Encountered.*");

    expr("^E'A\\U0061'^")
        .fails(RESOURCE.unicodeEscapeMalformed(1).str());
    expr("^E'AB\\U0000006G'^")
        .fails(RESOURCE.unicodeEscapeMalformed(2).str());
  }

  @Test void testReverseSolidus() {
    expr("'\\'").same();
  }

  @Test void testSubstring() {
    expr("substring('a'\nFROM \t  1)")
        .ok("SUBSTRING('a', 1)");
    expr("substring('a' FROM 1 FOR 3)")
        .ok("SUBSTRING('a', 1, 3)");
    expr("substring('a' FROM 'reg' FOR '\\')")
        .ok("SUBSTRING('a', 'reg', '\\')");

    expr("substring('a', 'reg', '\\')")
        .ok("SUBSTRING('a', 'reg', '\\')");
    expr("substring('a', 1, 2)")
        .ok("SUBSTRING('a', 1, 2)");
    expr("substring('a' , 1)")
        .ok("SUBSTRING('a', 1)");
  }

  @Test void testFunction() {
    sql("select substring('Eggs and ham', 1, 3 + 2) || ' benedict' from emp")
        .ok("SELECT (SUBSTRING('Eggs and ham', 1, (3 + 2)) || ' benedict')\n"
            + "FROM `EMP`");
    expr("log10(1)\r\n"
        + "+power(2, mod(\r\n"
        + "3\n"
        + "\t\t\f\n"
        + ",ln(4))*log10(5)-6*log10(7/abs(8)+9))*power(10,11)")
        .ok("(LOG10(1) + (POWER(2, ((MOD(3, LN(4)) * LOG10(5))"
            + " - (6 * LOG10(((7 / ABS(8)) + 9))))) * POWER(10, 11)))");
  }

  @Test void testFunctionWithDistinct() {
    expr("count(DISTINCT 1)").ok("COUNT(DISTINCT 1)");
    expr("count(ALL 1)").ok("COUNT(ALL 1)");
    expr("count(1)").ok("COUNT(1)");
    sql("select count(1), count(distinct 2) from emp")
        .ok("SELECT COUNT(1), COUNT(DISTINCT 2)\n"
            + "FROM `EMP`");
  }

  @Test void testFunctionCallWithDot() {
    expr("foo(a,b).c")
        .ok("(`FOO`(`A`, `B`).`C`)");
  }

  @Test void testFunctionInFunction() {
    expr("ln(power(2,2))")
        .ok("LN(POWER(2, 2))");
  }

  @Test void testFunctionNamedArgument() {
    expr("foo(x => 1)")
        .ok("`FOO`(`X` => 1)");
    expr("foo(x => 1, \"y\" => 'a', z => x <= y)")
        .ok("`FOO`(`X` => 1, `y` => 'a', `Z` => (`X` <= `Y`))");
    expr("foo(x.y ^=>^ 1)")
        .fails("(?s).*Encountered \"=>\" at .*");
    expr("foo(a => 1, x.y ^=>^ 2, c => 3)")
        .fails("(?s).*Encountered \"=>\" at .*");
  }

  @Test void testFunctionDefaultArgument() {
    sql("foo(1, DEFAULT, default, 'default', \"default\", 3)").expression()
        .ok("`FOO`(1, DEFAULT, DEFAULT, 'default', `default`, 3)");
    sql("foo(DEFAULT)").expression()
        .ok("`FOO`(DEFAULT)");
    sql("foo(x => 1, DEFAULT)").expression()
        .ok("`FOO`(`X` => 1, DEFAULT)");
    sql("foo(y => DEFAULT, x => 1)").expression()
        .ok("`FOO`(`Y` => DEFAULT, `X` => 1)");
    sql("foo(x => 1, y => DEFAULT)").expression()
        .ok("`FOO`(`X` => 1, `Y` => DEFAULT)");
    sql("select sum(DISTINCT DEFAULT) from t group by x")
        .ok("SELECT SUM(DISTINCT DEFAULT)\n"
            + "FROM `T`\n"
            + "GROUP BY `X`");
    expr("foo(x ^+^ DEFAULT)")
        .fails("(?s).*Encountered \"\\+ DEFAULT\" at .*");
    expr("foo(0, x ^+^ DEFAULT + y)")
        .fails("(?s).*Encountered \"\\+ DEFAULT\" at .*");
    expr("foo(0, DEFAULT ^+^ y)")
        .fails("(?s).*Encountered \"\\+\" at .*");
  }

  @Test void testDefault() {
    sql("select ^DEFAULT^ from emp")
        .fails("(?s)Incorrect syntax near the keyword 'DEFAULT' at .*");
    sql("select cast(empno ^+^ DEFAULT as double) from emp")
        .fails("(?s)Encountered \"\\+ DEFAULT\" at .*");
    sql("select empno ^+^ DEFAULT + deptno from emp")
        .fails("(?s)Encountered \"\\+ DEFAULT\" at .*");
    sql("select power(0, DEFAULT ^+^ empno) from emp")
        .fails("(?s)Encountered \"\\+\" at .*");
    sql("select * from emp join dept on ^DEFAULT^")
        .fails("(?s)Incorrect syntax near the keyword 'DEFAULT' at .*");
    sql("select * from emp where empno ^>^ DEFAULT or deptno < 10")
        .fails("(?s)Encountered \"> DEFAULT\" at .*");
    sql("select * from emp order by ^DEFAULT^ desc")
        .fails("(?s)Incorrect syntax near the keyword 'DEFAULT' at .*");
    final String expected = "INSERT INTO `DEPT` (`NAME`, `DEPTNO`)\n"
        + "VALUES (ROW('a', DEFAULT))";
    sql("insert into dept (name, deptno) values ('a', DEFAULT)")
        .ok(expected);
    sql("insert into dept (name, deptno) values ('a', 1 ^+^ DEFAULT)")
        .fails("(?s)Encountered \"\\+ DEFAULT\" at .*");
    sql("insert into dept (name, deptno) select 'a', ^DEFAULT^ from (values 0)")
        .fails("(?s)Incorrect syntax near the keyword 'DEFAULT' at .*");
  }

  @Test void testAggregateFilter() {
    final String sql = "select\n"
        + " sum(sal) filter (where gender = 'F') as femaleSal,\n"
        + " sum(sal) filter (where true) allSal,\n"
        + " count(distinct deptno) filter (where (deptno < 40))\n"
        + "from emp";
    final String expected = "SELECT"
        + " SUM(`SAL`) FILTER (WHERE (`GENDER` = 'F')) AS `FEMALESAL`,"
        + " SUM(`SAL`) FILTER (WHERE TRUE) AS `ALLSAL`,"
        + " COUNT(DISTINCT `DEPTNO`) FILTER (WHERE (`DEPTNO` < 40))\n"
        + "FROM `EMP`";
    sql(sql).ok(expected);
  }

  @Test void testGroup() {
    sql("select deptno, min(foo) as x from emp group by deptno, gender")
        .ok("SELECT `DEPTNO`, MIN(`FOO`) AS `X`\n"
            + "FROM `EMP`\n"
            + "GROUP BY `DEPTNO`, `GENDER`");
  }

  @Test void testGroupEmpty() {
    sql("select count(*) from emp group by ()")
        .ok("SELECT COUNT(*)\n"
            + "FROM `EMP`\n"
            + "GROUP BY ()");

    sql("select count(*) from emp group by () having 1 = 2 order by 3")
        .ok("SELECT COUNT(*)\n"
            + "FROM `EMP`\n"
            + "GROUP BY ()\n"
            + "HAVING (1 = 2)\n"
            + "ORDER BY 3");

    // Used to be invalid, valid now that we support grouping sets.
    sql("select 1 from emp group by (), x")
        .ok("SELECT 1\n"
            + "FROM `EMP`\n"
            + "GROUP BY (), `X`");

    // Used to be invalid, valid now that we support grouping sets.
    sql("select 1 from emp group by x, ()")
        .ok("SELECT 1\n"
            + "FROM `EMP`\n"
            + "GROUP BY `X`, ()");

    // parentheses do not an empty GROUP BY make
    sql("select 1 from emp group by (empno + deptno)")
        .ok("SELECT 1\n"
            + "FROM `EMP`\n"
            + "GROUP BY (`EMPNO` + `DEPTNO`)");
  }

  @Test void testHavingAfterGroup() {
    final String sql = "select deptno from emp group by deptno, emp\n"
        + "having count(*) > 5 and 1 = 2 order by 5, 2";
    final String expected = "SELECT `DEPTNO`\n"
        + "FROM `EMP`\n"
        + "GROUP BY `DEPTNO`, `EMP`\n"
        + "HAVING ((COUNT(*) > 5) AND (1 = 2))\n"
        + "ORDER BY 5, 2";
    sql(sql).ok(expected);
  }

  @Test void testHavingBeforeGroupFails() {
    final String sql = "select deptno from emp\n"
        + "having count(*) > 5 and deptno < 4 ^group^ by deptno, emp";
    sql(sql).fails("(?s).*Encountered \"group\" at .*");
  }

  @Test void testHavingNoGroup() {
    sql("select deptno from emp having count(*) > 5")
        .ok("SELECT `DEPTNO`\n"
            + "FROM `EMP`\n"
            + "HAVING (COUNT(*) > 5)");
  }

  @Test void testGroupingSets() {
    sql("select deptno from emp\n"
        + "group by grouping sets (deptno, (deptno, gender), ())")
        .ok("SELECT `DEPTNO`\n"
            + "FROM `EMP`\n"
            + "GROUP BY GROUPING SETS(`DEPTNO`, (`DEPTNO`, `GENDER`), ())");

    sql("select deptno from emp\n"
        + "group by grouping sets ((deptno, gender), (deptno), (), gender)")
        .ok("SELECT `DEPTNO`\n"
            + "FROM `EMP`\n"
            + "GROUP BY GROUPING SETS((`DEPTNO`, `GENDER`), `DEPTNO`, (), `GENDER`)");

    // Grouping sets must have parentheses
    sql("select deptno from emp\n"
        + "group by grouping sets ^deptno^, (deptno, gender), ()")
        .fails("(?s).*Encountered \"deptno\" at line 2, column 24.\n"
            + "Was expecting:\n"
            + "    \"\\(\" .*");

    // Nested grouping sets, cube, rollup, grouping sets all OK
    sql("select deptno from emp\n"
        + "group by grouping sets (deptno, grouping sets (e, d), (),\n"
        + "  cube (x, y), rollup(p, q))\n"
        + "order by a")
        .ok("SELECT `DEPTNO`\n"
            + "FROM `EMP`\n"
            + "GROUP BY GROUPING SETS(`DEPTNO`, GROUPING SETS(`E`, `D`), (), CUBE(`X`, `Y`), ROLLUP(`P`, `Q`))\n"
            + "ORDER BY `A`");

    sql("select deptno from emp\n"
        + "group by grouping sets (())")
        .ok("SELECT `DEPTNO`\n"
            + "FROM `EMP`\n"
            + "GROUP BY GROUPING SETS(())");
  }

  @Test void testGroupByCube() {
    final String sql = "select deptno from emp\n"
        + "group by cube ((a, b), (c, d))";
    final String expected = "SELECT `DEPTNO`\n"
        + "FROM `EMP`\n"
        + "GROUP BY CUBE((`A`, `B`), (`C`, `D`))";
    sql(sql).ok(expected);
  }

  @Test void testGroupByAllOrDistinct() {
    final String sql = "select deptno from emp\n"
        + "group by all cube (a, b), rollup (a, b)";
    final String expected = "SELECT `DEPTNO`\n"
        + "FROM `EMP`\n"
        + "GROUP BY CUBE(`A`, `B`), ROLLUP(`A`, `B`)";
    sql(sql).ok(expected);

    final String sql1 = "select deptno from emp\n"
        + "group by distinct cube (a, b), rollup (a, b)";
    final String expected1 = "SELECT `DEPTNO`\n"
        + "FROM `EMP`\n"
        + "GROUP BY DISTINCT CUBE(`A`, `B`), ROLLUP(`A`, `B`)";
    sql(sql1).ok(expected1);

    final String sql2 = "select deptno from emp\n"
        + "group by cube (a, b), rollup (a, b)";
    final String expected2 = "SELECT `DEPTNO`\n"
        + "FROM `EMP`\n"
        + "GROUP BY CUBE(`A`, `B`), ROLLUP(`A`, `B`)";
    sql(sql2).ok(expected2);
  }

  @Test void testGroupByCube2() {
    final String sql = "select deptno from emp\n"
        + "group by cube ((a, b), (c, d)) order by a";
    final String expected = "SELECT `DEPTNO`\n"
        + "FROM `EMP`\n"
        + "GROUP BY CUBE((`A`, `B`), (`C`, `D`))\n"
        + "ORDER BY `A`";
    sql(sql).ok(expected);

    final String sql2 = "select deptno from emp\n"
        + "group by cube (^)";
    sql(sql2).fails("(?s)Encountered \"\\)\" at .*");
  }

  @Test void testGroupByRollup() {
    final String sql = "select deptno from emp\n"
        + "group by rollup (deptno, deptno + 1, gender)";
    final String expected = "SELECT `DEPTNO`\n"
        + "FROM `EMP`\n"
        + "GROUP BY ROLLUP(`DEPTNO`, (`DEPTNO` + 1), `GENDER`)";
    sql(sql).ok(expected);

    // Nested rollup not ok
    final String sql1 = "select deptno from emp\n"
        + "group by rollup (deptno^, rollup(e, d))";
    sql(sql1).fails("(?s)Encountered \", rollup\" at .*");
  }

  @Test void testGrouping() {
    final String sql = "select deptno, grouping(deptno) from emp\n"
        + "group by grouping sets (deptno, (deptno, gender), ())";
    final String expected = "SELECT `DEPTNO`, GROUPING(`DEPTNO`)\n"
        + "FROM `EMP`\n"
        + "GROUP BY GROUPING SETS(`DEPTNO`, (`DEPTNO`, `GENDER`), ())";
    sql(sql).ok(expected);
  }

  @Test void testWith() {
    final String sql = "with femaleEmps as (select * from emps where gender = 'F')"
        + "select deptno from femaleEmps";
    final String expected = "WITH `FEMALEEMPS` AS (SELECT *\n"
        + "FROM `EMPS`\n"
        + "WHERE (`GENDER` = 'F')) SELECT `DEPTNO`\n"
        + "FROM `FEMALEEMPS`";
    sql(sql).ok(expected);
  }

  @Test void testWith2() {
    final String sql = "with femaleEmps as (select * from emps where gender = 'F'),\n"
        + "marriedFemaleEmps(x, y) as (select * from femaleEmps where maritaStatus = 'M')\n"
        + "select deptno from femaleEmps";
    final String expected = "WITH `FEMALEEMPS` AS (SELECT *\n"
        + "FROM `EMPS`\n"
        + "WHERE (`GENDER` = 'F')), `MARRIEDFEMALEEMPS` (`X`, `Y`) AS (SELECT *\n"
        + "FROM `FEMALEEMPS`\n"
        + "WHERE (`MARITASTATUS` = 'M')) SELECT `DEPTNO`\n"
        + "FROM `FEMALEEMPS`";
    sql(sql).ok(expected);
  }

  @Test void testWithFails() {
    final String sql = "with femaleEmps as ^select^ *\n"
        + "from emps where gender = 'F'\n"
        + "select deptno from femaleEmps";
    sql(sql).fails("(?s)Encountered \"select\" at .*");
  }

  @Test void testWithValues() {
    final String sql = "with v(i,c) as (values (1, 'a'), (2, 'bb'))\n"
        + "select c, i from v";
    final String expected = "WITH `V` (`I`, `C`) AS (VALUES (ROW(1, 'a')),\n"
        + "(ROW(2, 'bb'))) SELECT `C`, `I`\n"
        + "FROM `V`";
    sql(sql).ok(expected);
  }

  @Test void testWithNestedFails() {
    // SQL standard does not allow WITH to contain WITH
    final String sql = "with emp2 as (select * from emp)\n"
        + "^with^ dept2 as (select * from dept)\n"
        + "select 1 as uno from emp, dept";
    sql(sql).fails("(?s)Encountered \"with\" at .*");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5299">[CALCITE-5299]
   * JDBC adapter sometimes adds unnecessary parentheses around SELECT in WITH body</a>. */
  @Test void testWithSelect() {
    final String sql = "with emp2 as (select * from emp)\n"
        + "select * from emp2\n";
    final String expected = "WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP`) SELECT *\n"
        + "FROM `EMP2`";
    sql(sql).ok(expected);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5299">[CALCITE-5299]
   * JDBC adapter sometimes adds unnecessary parentheses around SELECT in WITH body</a>. */
  @Test void testWithOrderBy() {
    final String sql = "with emp2 as (select * from emp)\n"
        + "select * from emp2 order by deptno\n";
    final String expected = "WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP`) SELECT *\n"
        + "FROM `EMP2`\n"
        + "ORDER BY `DEPTNO`";
    sql(sql).ok(expected);
  }

  @Test void testWithNestedInSubQuery() {
    // SQL standard does not allow sub-query to contain WITH but we do
    final String sql = "with emp2 as (select * from emp)\n"
        + "(\n"
        + "  with dept2 as (select * from dept)\n"
        + "  select 1 as uno from empDept)";
    final String expected = "WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP`) (WITH `DEPT2` AS (SELECT *\n"
        + "FROM `DEPT`) SELECT 1 AS `UNO`\n"
        + "FROM `EMPDEPT`)";
    sql(sql).ok(expected);
  }

  @Test void testWithUnion() {
    // Per the standard WITH ... SELECT ... UNION is valid even without parens.
    final String sql = "with emp2 as (select * from emp)\n"
        + "select * from emp2\n"
        + "union\n"
        + "select * from emp2\n";
    final String expected = "WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP`) SELECT *\n"
        + "FROM `EMP2`\n"
        + "UNION\n"
        + "SELECT *\n"
        + "FROM `EMP2`";
    sql(sql).ok(expected);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5252">[CALCITE-5252]
   * JDBC adapter sometimes miss parentheses around SELECT in WITH_ITEM body</a>. */
  @Test void testWithAsUnion() {
    final String sql = "with emp2 as (select * from emp union select * from emp)\n"
        + "select * from emp2\n";
    final String expected = "WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP`\n"
        + "UNION\n"
        + "SELECT *\n"
        + "FROM `EMP`) SELECT *\n"
        + "FROM `EMP2`";
    sql(sql).ok(expected);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5252">[CALCITE-5252]
   * JDBC adapter sometimes miss parentheses around SELECT in WITH_ITEM body</a>. */
  @Test void testWithAsOrderBy() {
    final String sql = "with emp2 as (select * from emp order by deptno)\n"
        + "select * from emp2\n";
    final String expected = "WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP`\n"
        + "ORDER BY `DEPTNO`) SELECT *\n"
        + "FROM `EMP2`";
    sql(sql).ok(expected);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5252">[CALCITE-5252]
   * JDBC adapter sometimes miss parentheses around SELECT in WITH_ITEM body</a>. */
  @Test void testWithAsJoin() {
    final String sql = "with emp2 as (select * from emp e1 join emp e2 on e1.deptno = e2.deptno)\n"
        + "select * from emp2\n";
    final String expected = "WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP` AS `E1`\n"
        + "INNER JOIN `EMP` AS `E2` ON (`E1`.`DEPTNO` = `E2`.`DEPTNO`)) SELECT *\n"
        + "FROM `EMP2`";
    sql(sql).ok(expected);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5252">[CALCITE-5252]
   * JDBC adapter sometimes miss parentheses around SELECT in WITH_ITEM body</a>. */
  @Test void testWithAsNestedInSubQuery() {
    final String sql = "with emp3 as (with emp2 as (select * from emp) select * from emp2)\n"
        + "select * from emp3\n";
    final String expected = "WITH `EMP3` AS (WITH `EMP2` AS (SELECT *\n"
        + "FROM `EMP`) SELECT *\n"
        + "FROM `EMP2`) SELECT *\n"
        + "FROM `EMP3`";
    sql(sql).ok(expected);
  }

  @Test void testIdentifier() {
    expr("ab").ok("`AB`");
    expr("     \"a  \"\" b!c\"").ok("`a  \" b!c`");
    expr("     ^`^a  \" b!c`")
        .fails("(?s).*Encountered.*");
    expr("\"x`y`z\"").ok("`x``y``z`");
    expr("^`^x`y`z`")
        .fails("(?s).*Encountered.*");

    expr("myMap[field] + myArray[1 + 2]")
        .ok("(`MYMAP`[`FIELD`] + `MYARRAY`[(1 + 2)])");

    sql("VALUES a").node(isQuoted(0, false));
    sql("VALUES \"a\"").node(isQuoted(0, true));
    sql("VALUES \"a\".\"b\"").node(isQuoted(1, true));
    sql("VALUES \"a\".b").node(isQuoted(1, false));
  }

  @Test void testBackTickIdentifier() {
    SqlParserFixture f = fixture()
        .withConfig(c -> c.withQuoting(Quoting.BACK_TICK))
        .expression();
    f.sql("ab").ok("`AB`");
    f.sql("     `a  \" b!c`").ok("`a  \" b!c`");
    f.sql("     ^\"^a  \"\" b!c\"")
        .fails("(?s).*Encountered.*");

    f.sql("^\"^x`y`z\"").fails("(?s).*Encountered.*");
    f.sql("`x``y``z`").same();
    f.sql("`x\\`^y^\\`z`").fails("(?s).*Encountered.*");

    f.sql("myMap[field] + myArray[1 + 2]")
        .ok("(`MYMAP`[`FIELD`] + `MYARRAY`[(1 + 2)])");

    f = f.expression(false);
    f.sql("VALUES a").node(isQuoted(0, false));
    f.sql("VALUES `a`").node(isQuoted(0, true));
    f.sql("VALUES `a``b`").node(isQuoted(0, true));
  }

  @Test void testBackTickBackslashIdentifier() {
    SqlParserFixture f = fixture()
        .withConfig(c -> c.withQuoting(Quoting.BACK_TICK_BACKSLASH))
        .expression();
    f.sql("ab").ok("`AB`");
    f.sql("     `a  \" b!c`").ok("`a  \" b!c`");
    f.sql("     \"a  \"^\" b!c\"^")
        .fails("(?s).*Encountered.*");

    // BACK_TICK_BACKSLASH identifiers implies
    // BigQuery dialect, which implies double-quoted character literals.
    f.sql("^\"^x`y`z\"").ok("'x`y`z'");
    f.sql("`x`^`y`^`z`").fails("(?s).*Encountered.*");
    f.sql("`x\\`y\\`z`").ok("`x``y``z`");

    f.sql("myMap[field] + myArray[1 + 2]")
        .ok("(`MYMAP`[`FIELD`] + `MYARRAY`[(1 + 2)])");

    f = f.expression(false);
    f.sql("VALUES a").node(isQuoted(0, false));
    f.sql("VALUES `a`").node(isQuoted(0, true));
    f.sql("VALUES `a\\`b`").node(isQuoted(0, true));
  }

  @Test void testBracketIdentifier() {
    SqlParserFixture f = fixture()
        .withConfig(c -> c.withQuoting(Quoting.BRACKET))
        .expression();
    f.sql("ab").ok("`AB`");
    f.sql("     [a  \" b!c]").ok("`a  \" b!c`");
    f.sql("     ^`^a  \" b!c`")
        .fails("(?s).*Encountered.*");
    f.sql("     ^\"^a  \"\" b!c\"")
        .fails("(?s).*Encountered.*");

    f.sql("[x`y`z]").ok("`x``y``z`");
    f.sql("^\"^x`y`z\"")
        .fails("(?s).*Encountered.*");
    f.sql("^`^x``y``z`")
        .fails("(?s).*Encountered.*");

    f.sql("[anything [even brackets]] is].[ok]")
        .ok("`anything [even brackets] is`.`ok`");

    // What would be a call to the 'item' function in DOUBLE_QUOTE and BACK_TICK
    // is a table alias.
    f = f.expression(false);
    f.sql("select * from myMap[field], myArray[1 + 2]")
        .ok("SELECT *\n"
            + "FROM `MYMAP` AS `field`,\n"
            + "`MYARRAY` AS `1 + 2`");
    f.sql("select * from myMap [field], myArray [1 + 2]")
        .ok("SELECT *\n"
            + "FROM `MYMAP` AS `field`,\n"
            + "`MYARRAY` AS `1 + 2`");

    f.sql("VALUES a").node(isQuoted(0, false));
    f.sql("VALUES [a]").node(isQuoted(0, true));
  }

  @Test void testBackTickQuery() {
    sql("select `x`.`b baz` from `emp` as `x` where `x`.deptno in (10, 20)")
        .withConfig(c -> c.withQuoting(Quoting.BACK_TICK))
        .ok("SELECT `x`.`b baz`\n"
            + "FROM `emp` AS `x`\n"
            + "WHERE (`x`.`DEPTNO` IN (10, 20))");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-4080">[CALCITE-4080]
   * Allow character literals as column aliases, if
   * SqlConformance.allowCharLiteralAlias()</a>. */
  @Test void testSingleQuotedAlias() {
    final String expectingAlias = "Expecting alias, found character literal";

    final String sql1 = "select 1 as ^'a b'^ from t";
    sql(sql1)
        .withConformance(SqlConformanceEnum.DEFAULT)
        .fails(expectingAlias);
    final String sql1b = "SELECT 1 AS `a b`\n"
        + "FROM `T`";
    sql(sql1)
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(sql1b);
    sql(sql1)
        .withConformance(SqlConformanceEnum.BIG_QUERY)
        .ok(sql1b);
    sql(sql1)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(sql1b);

    // valid on MSSQL (alias contains a single quote)
    final String sql2 = "with t as (select 1 as ^'x''y'^)\n"
        + "select [x'y] from t as [u]";
    final SqlParserFixture f2 = sql(sql2)
        .withConfig(c -> c.withQuoting(Quoting.BRACKET)
            .withConformance(SqlConformanceEnum.DEFAULT));
    f2.fails(expectingAlias);
    final String sql2b = "WITH `T` AS (SELECT 1 AS `x'y`) SELECT `x'y`\n"
        + "FROM `T` AS `u`";
    f2.withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(sql2b);
    f2.withConformance(SqlConformanceEnum.BIG_QUERY)
        .ok(sql2b);
    f2.withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(sql2b);

    // also valid on MSSQL
    final String sql3 = "with [t] as (select 1 as [x]) select [x] from [t]";
    final String sql3b = "WITH `t` AS (SELECT 1 AS `x`) SELECT `x`\n"
        + "FROM `t`";
    final SqlParserFixture f3 = sql(sql3)
        .withConfig(c -> c.withQuoting(Quoting.BRACKET)
            .withConformance(SqlConformanceEnum.DEFAULT));
    f3.ok(sql3b);
    f3.withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(sql3b);
    f3.withConformance(SqlConformanceEnum.BIG_QUERY)
        .ok(sql3b);
    f3.withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(sql3b);

    // char literal as table alias is invalid on MSSQL (and others)
    final String sql4 = "with t as (select 1 as x) select x from t as ^'u'^";
    final String sql4b = "(?s)Encountered \"\\\\'u\\\\'\" at .*";
    final SqlParserFixture f4 = sql(sql4)
        .withConfig(c -> c.withQuoting(Quoting.BRACKET)
            .withConformance(SqlConformanceEnum.DEFAULT));
    f4.fails(sql4b);
    f4.withConformance(SqlConformanceEnum.MYSQL_5)
        .fails(sql4b);
    f4.withConformance(SqlConformanceEnum.BIG_QUERY)
        .fails(sql4b);
    f4.withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .fails(sql4b);

    // char literal as table alias (without AS) is invalid on MSSQL (and others)
    final String sql5 = "with t as (select 1 as x) select x from t ^'u'^";
    final String sql5b = "(?s)Encountered \"\\\\'u\\\\'\" at .*";
    final SqlParserFixture f5 = sql(sql5)
        .withConfig(c -> c.withQuoting(Quoting.BRACKET)
            .withConformance(SqlConformanceEnum.DEFAULT));
    f5.fails(sql5b);
    f5.withConformance(SqlConformanceEnum.MYSQL_5)
        .fails(sql5b);
    f5.withConformance(SqlConformanceEnum.BIG_QUERY)
        .fails(sql5b);
    f5.withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .fails(sql5b);
  }

  @Test void testInList() {
    sql("select * from emp where deptno in (10, 20) and gender = 'F'")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE ((`DEPTNO` IN (10, 20)) AND (`GENDER` = 'F'))");
  }

  @Test void testInListEmptyFails() {
    sql("select * from emp where deptno in (^)^ and gender = 'F'")
        .fails("(?s).*Encountered \"\\)\" at line 1, column 36\\..*");
  }

  @Test void testInQuery() {
    sql("select * from emp where deptno in (select deptno from dept)")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE (`DEPTNO` IN (SELECT `DEPTNO`\n"
            + "FROM `DEPT`))");
  }

  @Test void testSomeEveryAndIntersectionAggQuery() {
    sql("select some(deptno = 10), every(deptno > 0), intersection(multiset[1,2]) from dept")
        .ok("SELECT SOME((`DEPTNO` = 10)), EVERY((`DEPTNO` > 0)), INTERSECTION((MULTISET[1, 2]))\n"
            + "FROM `DEPT`");
  }

  /**
   * Tricky for the parser - looks like "IN (scalar, scalar)" but isn't.
   */
  @Test void testInQueryWithComma() {
    sql("select * from emp where deptno in (select deptno from dept group by 1, 2)")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE (`DEPTNO` IN (SELECT `DEPTNO`\n"
            + "FROM `DEPT`\n"
            + "GROUP BY 1, 2))");
  }

  @Test void testInSetop() {
    sql("select * from emp where deptno in (\n"
        + "(select deptno from dept union select * from dept)"
        + "except\n"
        + "select * from dept) and false")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE ((`DEPTNO` IN (SELECT `DEPTNO`\n"
            + "FROM `DEPT`\n"
            + "UNION\n"
            + "SELECT *\n"
            + "FROM `DEPT`\n"
            + "EXCEPT\n"
            + "SELECT *\n"
            + "FROM `DEPT`)) AND FALSE)");
  }

  @Test void testSome() {
    final String sql = "select * from emp\n"
        + "where sal > some (select comm from emp)";
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`SAL` > SOME (SELECT `COMM`\n"
        + "FROM `EMP`))";
    sql(sql).ok(expected);

    // ANY is a synonym for SOME
    final String sql2 = "select * from emp\n"
        + "where sal > any (select comm from emp)";
    sql(sql2).ok(expected);

    final String sql3 = "select * from emp\n"
        + "where name like (select ^some^ name from emp)";
    sql(sql3).fails("(?s).*Encountered \"some name\" at .*");

    final String sql4 = "select * from emp\n"
        + "where name like some (select name from emp)";
    final String expected4 = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`NAME` LIKE SOME((SELECT `NAME`\n"
        +  "FROM `EMP`)))";
    sql(sql4).ok(expected4);

    final String sql5 = "select * from emp where empno = any (10,20)";
    final String expected5 = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`EMPNO` = SOME (10, 20))";
    sql(sql5).ok(expected5);
  }

  @Test void testAll() {
    final String sql = "select * from emp\n"
        + "where sal <= all (select comm from emp) or sal > 10";
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE ((`SAL` <= ALL (SELECT `COMM`\n"
        + "FROM `EMP`)) OR (`SAL` > 10))";
    sql(sql).ok(expected);
  }

  @Test void testAllList() {
    final String sql = "select * from emp\n"
        + "where sal <= all (12, 20, 30)";
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`SAL` <= ALL (12, 20, 30))";
    sql(sql).ok(expected);
  }

  @Test void testUnion() {
    sql("select * from a union select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "UNION\n"
            + "SELECT *\n"
            + "FROM `A`");
    sql("select * from a union all select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "UNION ALL\n"
            + "SELECT *\n"
            + "FROM `A`");
    sql("select * from a union distinct select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "UNION\n"
            + "SELECT *\n"
            + "FROM `A`");
  }

  @Test void testUnionOrder() {
    sql("select a, b from t "
        + "union all "
        + "select x, y from u "
        + "order by 1 asc, 2 desc")
        .ok("SELECT `A`, `B`\n"
            + "FROM `T`\n"
            + "UNION ALL\n"
            + "SELECT `X`, `Y`\n"
            + "FROM `U`\n"
            + "ORDER BY 1, 2 DESC");
  }

  @Test void testOrderUnion() {
    // ORDER BY inside UNION not allowed
    sql("select a from t order by a\n"
        + "^union^ all\n"
        + "select b from t order by b")
        .fails("(?s).*Encountered \"union\" at .*");
  }

  @Test void testLimitUnion() {
    // LIMIT inside UNION not allowed
    sql("select a from t limit 10\n"
        + "^union^ all\n"
        + "select b from t order by b")
        .fails("(?s).*Encountered \"union\" at .*");
  }

  @Test void testLimitUnion2() {
    // LIMIT is allowed in a parenthesized sub-query inside UNION;
    // the result probably has more parentheses than strictly necessary.
    final String sql = "(select a from t limit 10)\n"
        + "union all\n"
        + "(select b from t offset 20)";
    final String expected = "(SELECT `A`\n"
        + "FROM `T`\n"
        + "FETCH NEXT 10 ROWS ONLY)\n"
        + "UNION ALL\n"
        + "(SELECT `B`\n"
        + "FROM `T`\n"
        + "OFFSET 20 ROWS)";
    sql(sql).ok(expected);
  }

  @Test void testUnionOffset() {
    // Note that the second sub-query has parentheses, to ensure that ORDER BY,
    // OFFSET, FETCH are associated with just that sub-query, not the UNION.
    final String sql = "select a from t\n"
        + "union all\n"
        + "(select b from t order by b offset 3 fetch next 5 rows only)";
    final String expected = "SELECT `A`\n"
        + "FROM `T`\n"
        + "UNION ALL\n"
        + "(SELECT `B`\n"
        + "FROM `T`\n"
        + "ORDER BY `B`\n"
        + "OFFSET 3 ROWS\n"
        + "FETCH NEXT 5 ROWS ONLY)";
    sql(sql).ok(expected);

    // as above, just ORDER BY
    final String sql2 = "select a from t\n"
        + "union all\n"
        + "(select b from t order by b)";
    final String expected2 = "SELECT `A`\n"
        + "FROM `T`\n"
        + "UNION ALL\n"
        + "(SELECT `B`\n"
        + "FROM `T`\n"
        + "ORDER BY `B`)";
    sql(sql2).ok(expected2);

    // as above, just OFFSET
    final String sql3 = "select a from t\n"
        + "union all\n"
        + "(select b from t offset 3)";
    final String expected3 = "SELECT `A`\n"
        + "FROM `T`\n"
        + "UNION ALL\n"
        + "(SELECT `B`\n"
        + "FROM `T`\n"
        + "OFFSET 3 ROWS)";
    sql(sql3).ok(expected3);

    // as above, just FETCH
    final String sql4 = "select a from t\n"
        + "union all\n"
        + "(select b from t fetch next 5 rows only)";
    final String expected4 = "SELECT `A`\n"
        + "FROM `T`\n"
        + "UNION ALL\n"
        + "(SELECT `B`\n"
        + "FROM `T`\n"
        + "FETCH NEXT 5 ROWS ONLY)";
    sql(sql4).ok(expected4);

    // as above, just FETCH and OFFSET
    final String sql5 = "select a from t\n"
        + "union all\n"
        + "(select b from t offset 3 fetch next 5 rows only)";
    final String expected5 = "SELECT `A`\n"
        + "FROM `T`\n"
        + "UNION ALL\n"
        + "(SELECT `B`\n"
        + "FROM `T`\n"
        + "OFFSET 3 ROWS\n"
        + "FETCH NEXT 5 ROWS ONLY)";
    sql(sql5).ok(expected5);

    // as previous, INTERSECT
    final String sql6 = "select a from t\n"
        + "intersect\n"
        + "(select b from t offset 3 fetch next 5 rows only)";
    final String expected6 = "SELECT `A`\n"
        + "FROM `T`\n"
        + "INTERSECT\n"
        + "(SELECT `B`\n"
        + "FROM `T`\n"
        + "OFFSET 3 ROWS\n"
        + "FETCH NEXT 5 ROWS ONLY)";
    sql(sql6).ok(expected6);
  }

  @Test void testUnionIntersect() {
    // Note that the union sub-query has parentheses.
    final String sql = "(select * from a union select * from b)\n"
        + "intersect select * from c";
    final String expected = "(SELECT *\n"
        + "FROM `A`\n"
        + "UNION\n"
        + "SELECT *\n"
        + "FROM `B`)\n"
        + "INTERSECT\n"
        + "SELECT *\n"
        + "FROM `C`";
    sql(sql).ok(expected);
  }

  @Test void testUnionOfNonQueryFails() {
    sql("select 1 from emp union ^2^ + 5")
        .fails("Non-query expression encountered in illegal context");
  }

  /**
   * In modern SQL, a query can occur almost everywhere that an expression
   * can. This test tests the few exceptions.
   */
  @Test void testQueryInIllegalContext() {
    sql("select 0, multiset[^(^select * from emp), 2] from dept")
        .fails("Query expression encountered in illegal context");
    sql("select 0, multiset[1, ^(^select * from emp), 2, 3] from dept")
        .fails("Query expression encountered in illegal context");
  }

  @Test void testExcept() {
    sql("select * from a except select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "EXCEPT\n"
            + "SELECT *\n"
            + "FROM `A`");
    sql("select * from a except all select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "EXCEPT ALL\n"
            + "SELECT *\n"
            + "FROM `A`");
    sql("select * from a except distinct select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "EXCEPT\n"
            + "SELECT *\n"
            + "FROM `A`");
  }

  /** Tests MINUS, which is equivalent to EXCEPT but only supported in some
   * conformance levels (e.g. ORACLE). */
  @Test void testSetMinus() {
    final String pattern =
        "MINUS is not allowed under the current SQL conformance level";
    final String sql = "select col1 from table1 ^MINUS^ select col1 from table2";
    sql(sql).fails(pattern);

    final String expected = "SELECT `COL1`\n"
        + "FROM `TABLE1`\n"
        + "EXCEPT\n"
        + "SELECT `COL1`\n"
        + "FROM `TABLE2`";
    sql(sql)
        .withConformance(SqlConformanceEnum.ORACLE_10)
        .ok(expected);

    final String sql2 =
        "select col1 from table1 MINUS ALL select col1 from table2";
    final String expected2 = "SELECT `COL1`\n"
        + "FROM `TABLE1`\n"
        + "EXCEPT ALL\n"
        + "SELECT `COL1`\n"
        + "FROM `TABLE2`";
    sql(sql2)
        .withConformance(SqlConformanceEnum.ORACLE_10)
        .ok(expected2);
  }

  /** MINUS is a <b>reserved</b> keyword in Calcite in all conformances, even
   * in the default conformance, where it is not allowed as an alternative to
   * EXCEPT. (It is reserved in Oracle but not in any version of the SQL
   * standard.) */
  @Test void testMinusIsReserved() {
    sql("select ^minus^ from t")
        .fails("(?s).*Encountered \"minus\" at .*");
    sql("select ^minus^ select")
        .fails("(?s).*Encountered \"minus\" at .*");
    sql("select * from t as ^minus^ where x < y")
        .fails("(?s).*Encountered \"minus\" at .*");
  }

  @Test void testIntersect() {
    sql("select * from a intersect select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "INTERSECT\n"
            + "SELECT *\n"
            + "FROM `A`");
    sql("select * from a intersect all select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "INTERSECT ALL\n"
            + "SELECT *\n"
            + "FROM `A`");
    sql("select * from a intersect distinct select * from a")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "INTERSECT\n"
            + "SELECT *\n"
            + "FROM `A`");
  }

  @Test void testJoinCross() {
    sql("select * from a as a2 cross join b")
        .ok("SELECT *\n"
            + "FROM `A` AS `A2`\n"
            + "CROSS JOIN `B`");
  }

  @Test void testJoinOn() {
    sql("select * from a left join b on 1 = 1 and 2 = 2 where 3 = 3")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "LEFT JOIN `B` ON ((1 = 1) AND (2 = 2))\n"
            + "WHERE (3 = 3)");
  }

  @Test void testJoinOnParentheses() {
    if (!Bug.TODO_FIXED) {
      return;
    }
    sql("select * from a\n"
        + " left join (b join c as c1 on 1 = 1) on 2 = 2\n"
        + "where 3 = 3")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "LEFT JOIN (`B` INNER JOIN `C` AS `C1` ON (1 = 1)) ON (2 = 2)\n"
            + "WHERE (3 = 3)");
  }

  /**
   * Same as {@link #testJoinOnParentheses()} but fancy aliases.
   */
  @Test void testJoinOnParenthesesPlus() {
    if (!Bug.TODO_FIXED) {
      return;
    }
    sql("select * from a\n"
        + " left join (b as b1 (x, y) join (select * from c) c1 on 1 = 1) on 2 = 2\n"
        + "where 3 = 3")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "LEFT JOIN (`B` AS `B1` (`X`, `Y`) INNER JOIN (SELECT *\n"
            + "FROM `C`) AS `C1` ON (1 = 1)) ON (2 = 2)\n"
            + "WHERE (3 = 3)");
  }

  @Test void testExplicitTableInJoin() {
    sql("select * from a left join (table b) on 2 = 2 where 3 = 3")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "LEFT JOIN (TABLE `B`) ON (2 = 2)\n"
            + "WHERE (3 = 3)");
  }

  @Test void testSubQueryInJoin() {
    if (!Bug.TODO_FIXED) {
      return;
    }
    sql("select * from (select * from a cross join b) as ab\n"
        + " left join ((table c) join d on 2 = 2) on 3 = 3\n"
        + " where 4 = 4")
        .ok("SELECT *\n"
            + "FROM (SELECT *\n"
            + "FROM `A`\n"
            + "CROSS JOIN `B`) AS `AB`\n"
            + "LEFT JOIN ((TABLE `C`) INNER JOIN `D` ON (2 = 2)) ON (3 = 3)\n"
            + "WHERE (4 = 4)");
  }

  @Test void testOuterJoinNoiseWord() {
    sql("select * from a left outer join b on 1 = 1 and 2 = 2 where 3 = 3")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "LEFT JOIN `B` ON ((1 = 1) AND (2 = 2))\n"
            + "WHERE (3 = 3)");
  }

  @Test void testJoinQuery() {
    sql("select * from a join (select * from b) as b2 on true")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "INNER JOIN (SELECT *\n"
            + "FROM `B`) AS `B2` ON TRUE");
  }

  @Test void testFullInnerJoinFails() {
    // cannot have more than one of INNER, FULL, LEFT, RIGHT, CROSS
    sql("select * from a ^full^ inner join b")
        .fails("(?s).*Encountered \"full inner\" at line .*");
  }

  @Test void testFullOuterJoin() {
    // OUTER is an optional extra to LEFT, RIGHT, or FULL
    sql("select * from a full outer join b")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "FULL JOIN `B`");
  }

  @Test void testInnerOuterJoinFails() {
    sql("select * from a ^inner^ outer join b")
        .fails("(?s).*Encountered \"inner outer\" at line .*");
  }

  @Test void testJoinAssociativity() {
    // joins are left-associative
    // 1. no parens needed
    String expected = "SELECT *\n"
        + "FROM `A`\n"
        + "NATURAL LEFT JOIN `B`\n"
        + "LEFT JOIN `C` ON (`B`.`C1` = `C`.`C1`)";
    sql("select * from (a natural left join b) left join c on b.c1 = c.c1")
        .ok(expected);

    // 2. parens needed
    String expected2 = "SELECT *\n"
        + "FROM `A`\n"
        + "NATURAL LEFT JOIN (`B` LEFT JOIN `C` ON (`B`.`C1` = `C`.`C1`))";
    sql("select * from a natural left join (b left join c on b.c1 = c.c1)")
        .ok(expected2);

    // 3. same as 1
    sql("select * from a natural left join b left join c on b.c1 = c.c1")
        .ok(expected);

    // bushy
    String sql4 = "select *\n"
        + "from (a cross join b)\n"
        + "cross join (c cross join d)";
    String expected4 = "SELECT *\n"
        + "FROM `A`\n"
        + "CROSS JOIN `B`\n"
        + "CROSS JOIN (`C` CROSS JOIN `D`)";
    sql(sql4).ok(expected4);
  }

  // Note: "select * from a natural cross join b" is actually illegal SQL
  // ("cross" is the only join type which cannot be modified with the
  // "natural") but the parser allows it; we and catch it at validate time
  @Test void testNaturalCrossJoin() {
    sql("select * from a natural cross join b")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "NATURAL CROSS JOIN `B`");
  }

  @Test void testJoinUsing() {
    sql("select * from a join b using (x)")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "INNER JOIN `B` USING (`X`)");
    sql("select * from a join b using (^)^ where c = d")
        .fails("(?s).*Encountered \"[)]\" at line 1, column 31.*");
  }

  /** Tests CROSS APPLY, which is equivalent to CROSS JOIN and LEFT JOIN but
   * only supported in some conformance levels (e.g. SQL Server). */
  @Test void testApply() {
    final String pattern =
        "APPLY operator is not allowed under the current SQL conformance level";
    final String sql = "select * from dept\n"
        + "cross apply table(ramp(deptno)) as t(a^)^";
    sql(sql).fails(pattern);

    final String expected = "SELECT *\n"
        + "FROM `DEPT`\n"
        + "CROSS JOIN LATERAL TABLE(`RAMP`(`DEPTNO`)) AS `T` (`A`)";
    sql(sql)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(expected);

    // Supported in Oracle 12 but not Oracle 10
    sql(sql)
        .withConformance(SqlConformanceEnum.ORACLE_10)
        .fails(pattern);

    sql(sql)
        .withConformance(SqlConformanceEnum.ORACLE_12)
        .ok(expected);
  }

  /** Tests OUTER APPLY. */
  @Test void testOuterApply() {
    final String sql = "select * from dept outer apply table(ramp(deptno))";
    final String expected = "SELECT *\n"
        + "FROM `DEPT`\n"
        + "LEFT JOIN LATERAL TABLE(`RAMP`(`DEPTNO`)) ON TRUE";
    sql(sql)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(expected);
  }

  @Test void testOuterApplySubQuery() {
    final String sql = "select * from dept\n"
        + "outer apply (select * from emp where emp.deptno = dept.deptno)";
    final String expected = "SELECT *\n"
        + "FROM `DEPT`\n"
        + "LEFT JOIN LATERAL (SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)) ON TRUE";
    sql(sql)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(expected);
  }

  @Test void testOuterApplyValues() {
    final String sql = "select * from dept\n"
        + "outer apply (select * from emp where emp.deptno = dept.deptno)";
    final String expected = "SELECT *\n"
        + "FROM `DEPT`\n"
        + "LEFT JOIN LATERAL (SELECT *\n"
        + "FROM `EMP`\n"
        + "WHERE (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)) ON TRUE";
    sql(sql)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(expected);
  }

  /** Even in SQL Server conformance mode, we do not yet support
   * 'function(args)' as an abbreviation for 'table(function(args)'. */
  @Test void testOuterApplyFunctionFails() {
    final String sql = "select * from dept outer apply ramp(deptno^)^)";
    sql(sql)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .fails("(?s).*Encountered \"\\)\" at .*");
  }

  @Test void testCrossOuterApply() {
    final String sql = "select * from dept\n"
        + "cross apply table(ramp(deptno)) as t(a)\n"
        + "outer apply table(ramp2(a))";
    final String expected = "SELECT *\n"
        + "FROM `DEPT`\n"
        + "CROSS JOIN LATERAL TABLE(`RAMP`(`DEPTNO`)) AS `T` (`A`)\n"
        + "LEFT JOIN LATERAL TABLE(`RAMP2`(`A`)) ON TRUE";
    sql(sql)
        .withConformance(SqlConformanceEnum.SQL_SERVER_2008)
        .ok(expected);
  }

  @Test void testTableSample() {
    final String sql0 = "select * from ("
        + "  select * "
        + "  from emp "
        + "  join dept on emp.deptno = dept.deptno"
        + "  where gender = 'F'"
        + "  order by sal) tablesample substitute('medium')";
    final String expected0 = "SELECT *\n"
        + "FROM (SELECT *\n"
        + "FROM `EMP`\n"
        + "INNER JOIN `DEPT` ON (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)\n"
        + "WHERE (`GENDER` = 'F')\n"
        + "ORDER BY `SAL`) TABLESAMPLE SUBSTITUTE('MEDIUM')";
    sql(sql0).ok(expected0);

    final String sql1 = "select * "
        + "from emp as x tablesample substitute('medium') "
        + "join dept tablesample substitute('lar' /* split */ 'ge') on x.deptno = dept.deptno";
    final String expected1 = "SELECT *\n"
        + "FROM `EMP` AS `X` TABLESAMPLE SUBSTITUTE('MEDIUM')\n"
        + "INNER JOIN `DEPT` TABLESAMPLE SUBSTITUTE('LARGE') ON (`X`.`DEPTNO` = `DEPT`.`DEPTNO`)";
    sql(sql1).ok(expected1);

    final String sql2 = "select * "
        + "from emp as x tablesample bernoulli(50)";
    final String expected2 = "SELECT *\n"
        + "FROM `EMP` AS `X` TABLESAMPLE BERNOULLI(50.0)";
    sql(sql2).ok(expected2);

    final String sql3 = "select * "
        + "from emp as x "
        + "tablesample bernoulli(50) REPEATABLE(10) ";
    final String expected3 = "SELECT *\n"
        + "FROM `EMP` AS `X` TABLESAMPLE BERNOULLI(50.0) REPEATABLE(10)";
    sql(sql3).ok(expected3);

    // test repeatable with invalid int literal.
    sql("select * "
        + "from emp as x "
        + "tablesample bernoulli(50) REPEATABLE(^100000000000000000000^) ")
        .fails("Literal '100000000000000000000' "
            + "can not be parsed to type 'java\\.lang\\.Integer'");

    // test repeatable with invalid negative int literal.
    sql("select * "
        + "from emp as x "
        + "tablesample bernoulli(50) REPEATABLE(-^100000000000000000000^) ")
        .fails("Literal '100000000000000000000' "
            + "can not be parsed to type 'java\\.lang\\.Integer'");

    // test bernoulli sample percentage with zero.
    final String sql4 = "select * "
        + "from emp as x tablesample bernoulli(0)";
    final String expected4 = "SELECT *\n"
        + "FROM `EMP` AS `X` TABLESAMPLE BERNOULLI(0)";
    sql(sql4).ok(expected4);

    // test system sample percentage with zero.
    final String sql5 = "select * "
        + "from emp as x tablesample system(0)";
    final String expected5 = "SELECT *\n"
        + "FROM `EMP` AS `X` TABLESAMPLE SYSTEM(0)";
    sql(sql5).ok(expected5);
  }

  @Test void testLiteral() {
    expr("'foo'").same();
    expr("100").same();
    sql("select 1 as uno, 'x' as x, null as n from emp")
        .ok("SELECT 1 AS `UNO`, 'x' AS `X`, NULL AS `N`\n"
            + "FROM `EMP`");

    // Even though it looks like a date, it's just a string.
    expr("'2004-06-01'").same();
    expr("-.25")
        .ok("-0.25");
    expr("TIMESTAMP '2004-06-01 15:55:55'").same();
    expr("TIMESTAMP '2004-06-01 15:55:55.900'").same();
    expr("TIMESTAMP '2004-06-01 15:55:55.1234'").same();
    expr("TIMESTAMP '2004-06-01 15:55:55.1236'").same();
    expr("TIMESTAMP '2004-06-01 15:55:55.9999'").same();
    expr("NULL").same();
  }

  @Test void testContinuedLiteral() {
    expr("'abba'\n'abba'").same();
    expr("'abba'\n'0001'").same();
    expr("N'yabba'\n'dabba'\n'doo'")
        .ok("_ISO-8859-1'yabba'\n'dabba'\n'doo'");
    expr("_iso-8859-1'yabba'\n'dabba'\n'don''t'")
        .ok("_ISO-8859-1'yabba'\n'dabba'\n'don''t'");

    expr("x'01aa'\n'03ff'")
        .ok("X'01AA'\n'03FF'");

    // a bad hexstring
    sql("x'01aa'\n^'vvvv'^")
        .fails("Binary literal string must contain only characters '0' - '9', 'A' - 'F'");
  }

  /** Tests that ambiguity between extended string literals and character string
   * aliases is always resolved in favor of extended string literals. */
  @Test void testContinuedLiteralAlias() {
    final String expectingAlias = "Expecting alias, found character literal";

    // Not ambiguous, because of 'as'.
    final String sql0 = "select 1 an_alias,\n"
        + "  x'01'\n"
        + "  'ab' as x\n"
        + "from t";
    final String sql0b = "SELECT 1 AS `AN_ALIAS`, X'01'\n"
        + "'AB' AS `X`\n"
        + "FROM `T`";
    sql(sql0)
        .withConformance(SqlConformanceEnum.DEFAULT)
        .ok(sql0b);
    sql(sql0)
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(sql0b);
    sql(sql0)
        .withConformance(SqlConformanceEnum.BIG_QUERY)
        .ok(sql0b);

    // Is 'ab' an alias or is it part of the x'01' 'ab' continued binary string
    // literal? It's ambiguous, but we prefer the latter.
    final String sql1 = "select 1 ^'an alias'^,\n"
        + "  x'01'\n"
        + "  'ab'\n"
        + "from t";
    final String sql1b = "SELECT 1 AS `an alias`, X'01'\n"
        + "'AB'\n"
        + "FROM `T`";
    sql(sql1)
        .withConformance(SqlConformanceEnum.DEFAULT)
        .fails(expectingAlias);
    sql(sql1)
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(sql1b);
    sql(sql1)
        .withConformance(SqlConformanceEnum.BIG_QUERY)
        .ok(sql1b);

    // Parser prefers continued character and binary string literals over
    // character string aliases, regardless of whether the dialect allows
    // character string aliases.
    final String sql2 = "select 'continued'\n"
        + "  'char literal, not alias',\n"
        + "  x'01'\n"
        + "  'ab'\n"
        + "from t";
    final String sql2b = "SELECT 'continued'\n"
        + "'char literal, not alias', X'01'\n"
        + "'AB'\n"
        + "FROM `T`";
    sql(sql2)
        .withConformance(SqlConformanceEnum.DEFAULT)
        .ok(sql2b);
    sql(sql2)
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(sql2b);
    sql(sql2)
        .withConformance(SqlConformanceEnum.BIG_QUERY)
        .ok(sql2b);
  }

  @Test void testMixedFrom() {
    sql("select * from a join b using (x), c join d using (y)")
        .ok("SELECT *\n"
            + "FROM `A`\n"
            + "INNER JOIN `B` USING (`X`),\n"
            + "`C`\n"
            + "INNER JOIN `D` USING (`Y`)");
  }

  @Test void testMixedStar() {
    sql("select emp.*, 1 as foo from emp, dept")
        .ok("SELECT `EMP`.*, 1 AS `FOO`\n"
            + "FROM `EMP`,\n"
            + "`DEPT`");
  }

  @Test void testSchemaTableStar() {
    sql("select schem.emp.*, emp.empno * dept.deptno\n"
        + "from schem.emp, dept")
        .ok("SELECT `SCHEM`.`EMP`.*, (`EMP`.`EMPNO` * `DEPT`.`DEPTNO`)\n"
            + "FROM `SCHEM`.`EMP`,\n"
            + "`DEPT`");
  }

  @Test void testCatalogSchemaTableStar() {
    sql("select cat.schem.emp.* from cat.schem.emp")
        .ok("SELECT `CAT`.`SCHEM`.`EMP`.*\n"
            + "FROM `CAT`.`SCHEM`.`EMP`");
  }

  @Test void testAliasedStar() {
    // OK in parser; validator will give error
    sql("select emp.* as foo from emp")
        .ok("SELECT `EMP`.* AS `FOO`\n"
            + "FROM `EMP`");
  }

  @Test void testNotExists() {
    sql("select * from dept where not not exists (select * from emp) and true")
        .ok("SELECT *\n"
            + "FROM `DEPT`\n"
            + "WHERE ((NOT (NOT (EXISTS (SELECT *\n"
            + "FROM `EMP`)))) AND TRUE)");
  }

  @Test void testOrder() {
    sql("select * from emp order by empno, gender desc, deptno asc, empno asc, name desc")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "ORDER BY `EMPNO`, `GENDER` DESC, `DEPTNO`, `EMPNO`, `NAME` DESC");
  }

  @Test void testOrderNullsFirst() {
    final String sql = "select * from emp\n"
        + "order by gender desc nulls last,\n"
        + " deptno asc nulls first,\n"
        + " empno nulls last";
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "ORDER BY `GENDER` DESC NULLS LAST, `DEPTNO` NULLS FIRST,"
        + " `EMPNO` NULLS LAST";
    sql(sql).ok(expected);
  }

  @Test void testOrderInternal() {
    sql("(select * from emp order by empno) union select * from emp")
        .ok("(SELECT *\n"
            + "FROM `EMP`\n"
            + "ORDER BY `EMPNO`)\n"
            + "UNION\n"
            + "SELECT *\n"
            + "FROM `EMP`");

    sql("select * from (select * from t order by x, y) where a = b")
        .ok("SELECT *\n"
            + "FROM (SELECT *\n"
            + "FROM `T`\n"
            + "ORDER BY `X`, `Y`)\n"
            + "WHERE (`A` = `B`)");
  }

  @Test void testOrderIllegalInExpression() {
    sql("select (select 1 from foo order by x,y) from t where a = b")
        .ok("SELECT (SELECT 1\n"
            + "FROM `FOO`\n"
            + "ORDER BY `X`, `Y`)\n"
            + "FROM `T`\n"
            + "WHERE (`A` = `B`)");
    sql("select (1 ^order^ by x, y) from t where a = b")
        .fails("ORDER BY unexpected");
  }

  @Test void testOrderOffsetFetch() {
    sql("select a from foo order by b, c offset 1 row fetch first 2 row only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "OFFSET 1 ROWS\n"
            + "FETCH NEXT 2 ROWS ONLY");
    // as above, but ROWS rather than ROW
    sql("select a from foo order by b, c offset 1 rows fetch first 2 rows only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "OFFSET 1 ROWS\n"
            + "FETCH NEXT 2 ROWS ONLY");
    // as above, but NEXT (means same as FIRST)
    sql("select a from foo order by b, c offset 1 rows fetch next 3 rows only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "OFFSET 1 ROWS\n"
            + "FETCH NEXT 3 ROWS ONLY");
    // as above, but omit the ROWS noise word after OFFSET. This is not
    // compatible with SQL:2008 but allows the Postgres syntax
    // "LIMIT ... OFFSET".
    sql("select a from foo order by b, c offset 1 fetch next 3 rows only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "OFFSET 1 ROWS\n"
            + "FETCH NEXT 3 ROWS ONLY");
    // as above, omit OFFSET
    sql("select a from foo order by b, c fetch next 3 rows only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "FETCH NEXT 3 ROWS ONLY");
    // FETCH, no ORDER BY or OFFSET
    sql("select a from foo fetch next 4 rows only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "FETCH NEXT 4 ROWS ONLY");
    // OFFSET, no ORDER BY or FETCH
    sql("select a from foo offset 1 row")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "OFFSET 1 ROWS");
    // OFFSET and FETCH, no ORDER BY
    sql("select a from foo offset 1 row fetch next 3 rows only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "OFFSET 1 ROWS\n"
            + "FETCH NEXT 3 ROWS ONLY");
    // OFFSET and FETCH, with dynamic parameters
    sql("select a from foo offset ? row fetch next ? rows only")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "OFFSET ? ROWS\n"
            + "FETCH NEXT ? ROWS ONLY");
    // missing ROWS after FETCH
    sql("select a from foo offset 1 fetch next 3 ^only^")
        .fails("(?s).*Encountered \"only\" at .*");
    // FETCH before OFFSET is illegal
    sql("select a from foo fetch next 3 rows only ^offset^ 1")
        .fails("(?s).*Encountered \"offset\" at .*");
  }

  /**
   * "LIMIT ... OFFSET ..." is the postgres equivalent of SQL:2008
   * "OFFSET ... FETCH". It all maps down to a parse tree that looks like
   * SQL:2008.
   */
  @Test void testLimit() {
    sql("select a from foo order by b, c limit 2 offset 1")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "OFFSET 1 ROWS\n"
            + "FETCH NEXT 2 ROWS ONLY");
    sql("select a from foo order by b, c limit 2")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "FETCH NEXT 2 ROWS ONLY");
    sql("select a from foo order by b, c offset 1")
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "OFFSET 1 ROWS");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-4463">[CALCITE-4463]
   * JDBC adapter for Spark generates incorrect ORDER BY syntax</a>.
   *
   * <p>Similar to {@link #testLimit}, but parses and unparses in the Spark
   * dialect, which uses LIMIT and OFFSET rather than OFFSET and FETCH. */
  @Test void testLimitSpark() {
    final String sql1 = "select a from foo order by b, c limit 2 offset 1";
    final String expected1 = "SELECT A\n"
        + "FROM FOO\n"
        + "ORDER BY B, C\n"
        + "LIMIT 2\n"
        + "OFFSET 1";
    sql(sql1).withDialect(SparkSqlDialect.DEFAULT).ok(expected1);

    final String sql2 = "select a from foo order by b, c limit 2";
    final String expected2 = "SELECT A\n"
        + "FROM FOO\n"
        + "ORDER BY B, C\n"
        + "LIMIT 2";
    sql(sql2).withDialect(SparkSqlDialect.DEFAULT).ok(expected2);

    final String sql3 = "select a from foo order by b, c offset 1";
    final String expected3 = "SELECT A\n"
        + "FROM FOO\n"
        + "ORDER BY B, C\n"
        + "OFFSET 1";
    sql(sql3).withDialect(SparkSqlDialect.DEFAULT).ok(expected3);

    final String sql4 = "select a from foo offset 10";
    final String expected4 = "SELECT A\n"
        + "FROM FOO\n"
        + "OFFSET 10";
    sql(sql4).withDialect(SparkSqlDialect.DEFAULT).ok(expected4);

    final String sql5 = "select a from foo\n"
        + "union\n"
        + "select b from baz\n"
        + "limit 3";
    final String expected5 = "SELECT A\n"
        + "FROM FOO\n"
        + "UNION\n"
        + "SELECT B\n"
        + "FROM BAZ\n"
        + "LIMIT 3";
    sql(sql5).withDialect(SparkSqlDialect.DEFAULT).ok(expected5);
  }

  /** Test case that does not reproduce but is related to
   * <a href="https://issues.apache.org/jira/browse/CALCITE-1238">[CALCITE-1238]
   * Unparsing LIMIT without ORDER BY after validation</a>. */
  @Test void testLimitWithoutOrder() {
    final String expected = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "FETCH NEXT 2 ROWS ONLY";
    sql("select a from foo limit 2")
        .ok(expected);
  }

  @Test void testLimitOffsetWithoutOrder() {
    final String expected = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "OFFSET 1 ROWS\n"
        + "FETCH NEXT 2 ROWS ONLY";
    sql("select a from foo limit 2 offset 1")
        .ok(expected);
  }

  @Test void testLimitStartCount() {
    final String error = "'LIMIT start, count' is not allowed under the "
        + "current SQL conformance level";
    sql("select a from foo ^limit 1,2^")
        .withConformance(SqlConformanceEnum.DEFAULT)
        .fails(error);

    // "limit all" is equivalent to no limit
    final String expected0 = "SELECT `A`\n"
        + "FROM `FOO`";
    sql("select a from foo limit all")
        .withConformance(SqlConformanceEnum.DEFAULT)
        .ok(expected0);

    final String expected1 = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "ORDER BY `X`";
    sql("select a from foo order by x limit all")
        .withConformance(SqlConformanceEnum.DEFAULT)
        .ok(expected1);

    final String expected2 = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "OFFSET 2 ROWS\n"
        + "FETCH NEXT 3 ROWS ONLY";
    sql("select a from foo limit 2,3")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected2);

    // "offset 4" overrides the earlier "2"
    final String expected3 = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "OFFSET 4 ROWS\n"
        + "FETCH NEXT 3 ROWS ONLY";
    sql("select a from foo limit 2,3 offset 4")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected3);

    // "fetch next 4" is invalid after "limit 3"
    sql("select a from foo limit 2,3 ^fetch^ next 4 rows only")
        .withConformance(SqlConformanceEnum.LENIENT)
        .fails("(?s).*Encountered \"fetch\" at line 1.*");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5086">[CALCITE-5086]
   * Calcite supports OFFSET start LIMIT count expression</a>. */
  @Test void testOffsetStartLimitCount() {
    final String error = "'OFFSET start LIMIT count' is not allowed under the "
        + "current SQL conformance level";
    sql("select a from foo order by b, c offset 1 limit 2")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok("SELECT `A`\n"
            + "FROM `FOO`\n"
            + "ORDER BY `B`, `C`\n"
            + "OFFSET 1 ROWS\n"
            + "FETCH NEXT 2 ROWS ONLY");

    sql("select a from foo order by b, c ^offset 1 limit 2^")
        .withConformance(SqlConformanceEnum.DEFAULT)
        .fails(error);

    // "limit 2" overrides the earlier "offset 4"
    final String expected3 = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "OFFSET 2 ROWS\n"
        + "FETCH NEXT 3 ROWS ONLY";
    sql("select a from foo offset 4 limit 2,3")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected3);

    // "fetch next 4" is illegal following "limit 3"
    sql("select a from foo offset 2 limit 3 ^fetch^ next 4 rows only")
        .withConformance(SqlConformanceEnum.LENIENT)
        .fails("(?s).*Encountered \"fetch\" at line 1.*");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5184">[CALCITE-5184]
   * Support "LIMIT start, ALL" (in MySQL conformance)</a>.
   *
   * @see SqlConformance#isLimitStartCountAllowed() */
  @Test void testLimitStartAll() {
    String expected = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "OFFSET 2 ROWS";
    sql("select a from foo ^limit 2, all^")
        .withConformance(SqlConformanceEnum.DEFAULT)
        .fails("'LIMIT start, ALL' is not allowed under the "
            + "current SQL conformance level")
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(expected)
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected);

    // 'limit 2, all' will override 'offset 1' (if allowed by conformance)
    sql("select a from foo ^offset 1 limit 2, all^")
        .withConformance(SqlConformanceEnum.DEFAULT)
        .fails("'LIMIT start, ALL' is not allowed under the "
            + "current SQL conformance level")
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .fails("'OFFSET start LIMIT count' is not allowed under the "
            + "current SQL conformance level")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected);

    // 'offset 1' will override 'limit 2, all'
    String expected2 = "SELECT `A`\n"
        + "FROM `FOO`\n"
        + "OFFSET 1 ROWS";
    sql("select a from foo limit 2, all offset 1")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected2);
  }

  @Test void testSqlInlineComment() {
    sql("select 1 from t --this is a comment\n")
        .ok("SELECT 1\n"
            + "FROM `T`");
    sql("select 1 from t--\n")
        .ok("SELECT 1\n"
            + "FROM `T`");
    sql("select 1 from t--this is a comment\n"
        + "where a>b-- this is comment\n")
        .ok("SELECT 1\n"
            + "FROM `T`\n"
            + "WHERE (`A` > `B`)");
    sql("select 1 from t\n--select")
        .ok("SELECT 1\n"
            + "FROM `T`");
  }

  @Test void testMultilineComment() {
    // on single line
    sql("select 1 /* , 2 */, 3 from t")
        .ok("SELECT 1, 3\n"
            + "FROM `T`");

    // on several lines
    sql("select /* 1,\n"
        + " 2,\n"
        + " */ 3 from t")
        .ok("SELECT 3\n"
            + "FROM `T`");

    // stuff inside comment
    sql("values ( /** 1, 2 + ** */ 3)")
        .ok("VALUES (ROW(3))");

    // comment in string is preserved
    sql("values ('a string with /* a comment */ in it')")
        .ok("VALUES (ROW('a string with /* a comment */ in it'))");

    // SQL:2003, 5.2, syntax rule # 8 "There shall be no <separator>
    // separating the <minus sign>s of a <simple comment introducer>".

    sql("values (- -1\n"
        + ")")
        .ok("VALUES (ROW(1))");

    sql("values (--1+\n"
        + "2)")
        .ok("VALUES (ROW(2))");

    // end of multiline comment without start
    if (Bug.FRG73_FIXED) {
      sql("values (1 */ 2)")
          .fails("xx");
    }

    // SQL:2003, 5.2, syntax rule #10 "Within a <bracket comment context>,
    // any <solidus> immediately followed by an <asterisk> without any
    // intervening <separator> shall be considered to be the <bracketed
    // comment introducer> for a <separator> that is a <bracketed
    // comment>".

    // comment inside a comment
    // Spec is unclear what should happen, but currently it crashes the
    // parser, and that's bad
    if (Bug.FRG73_FIXED) {
      sql("values (1 + /* comment /* inner comment */ */ 2)").ok("xx");
    }

    // single-line comment inside multiline comment is illegal
    //
    // SQL-2003, 5.2: "Note 63 - Conforming programs should not place
    // <simple comment> within a <bracketed comment> because if such a
    // <simple comment> contains the sequence of characters "*/" without
    // a preceding "/*" in the same <simple comment>, it will prematurely
    // terminate the containing <bracketed comment>.
    if (Bug.FRG73_FIXED) {
      final String sql = "values /* multiline contains -- singline */\n"
          + " (1)";
      sql(sql).fails("xxx");
    }

    // non-terminated multi-line comment inside single-line comment
    if (Bug.FRG73_FIXED) {
      // Test should fail, and it does, but it should give "*/" as the
      // erroneous token.
      final String sql = "values ( -- rest of line /* a comment\n"
          + " 1, ^*/^ 2)";
      sql(sql).fails("Encountered \"/\\*\" at");
    }

    sql("values (1 + /* comment -- rest of line\n"
        + " rest of comment */ 2)")
        .ok("VALUES (ROW((1 + 2)))");

    // multiline comment inside single-line comment
    sql("values -- rest of line /* a comment */\n"
        + "(1)")
        .ok("VALUES (ROW(1))");

    // non-terminated multiline comment inside single-line comment
    sql("values -- rest of line /* a comment\n"
        + "(1)")
        .ok("VALUES (ROW(1))");

    // even if comment abuts the tokens at either end, it becomes a space
    sql("values ('abc'/* a comment*/'def')")
        .ok("VALUES (ROW('abc'\n'def'))");

    // comment which starts as soon as it has begun
    sql("values /**/ (1)")
        .ok("VALUES (ROW(1))");
  }

  // expressions
  @Test void testParseNumber() {
    // Exacts
    expr("1").same();
    expr("+1.").ok("1");
    expr("-1").same();
    expr("- -1").ok("1");
    expr("1.0").same();
    expr("-3.2").same();
    expr("1.").ok("1");
    expr(".1").ok("0.1");
    expr("2500000000").same();
    expr("5000000000").same();

    // Approximates
    expr("1e1").ok("1E1");
    expr("+1e1").ok("1E1");
    expr("1.1e1").ok("1.1E1");
    expr("1.1e+1").ok("1.1E1");
    expr("1.1e-1").ok("1.1E-1");
    expr("+1.1e-1").ok("1.1E-1");
    expr("1.E3").ok("1E3");
    expr("1.e-3").ok("1E-3");
    expr("1.e+3").ok("1E3");
    expr(".5E3").ok("5E2");
    expr("+.5e3").ok("5E2");
    expr("-.5E3").ok("-5E2");
    expr(".5e-32").ok("5E-33");

    // Mix integer/decimals/approx
    expr("3. + 2").ok("(3 + 2)");
    expr("1++2+3").ok("((1 + 2) + 3)");
    expr("1- -2").ok("(1 - -2)");
    expr("1++2.3e-4++.5e-6++.7++8").ok("((((1 + 2.3E-4) + 5E-7) + 0.7) + 8)");
    expr("1- -2.3e-4 - -.5e-6  -\n"
        + "-.7++8")
        .ok("((((1 - -2.3E-4) - -5E-7) - -0.7) + 8)");
    expr("1+-2.*-3.e-1/-4")
        .ok("(1 + ((-2 * -3E-1) / -4))");
  }

  @Test void testParseNumberFails() {
    sql("SELECT 0.5e1^.1^ from t")
        .fails("(?s).*Encountered .*\\.1.* at line 1.*");
  }

  @Test void testMinusPrefixInExpression() {
    expr("-(1+2)")
        .ok("(- (1 + 2))");
  }

  // operator precedence
  @Test void testPrecedence0() {
    expr("1 + 2 * 3 * 4 + 5")
        .ok("((1 + ((2 * 3) * 4)) + 5)");
  }

  @Test void testPrecedence1() {
    expr("1 + 2 * (3 * (4 + 5))")
        .ok("(1 + (2 * (3 * (4 + 5))))");
  }

  @Test void testPrecedence2() {
    expr("- - 1").ok("1"); // special case for unary minus
  }

  @Test void testPrecedence2b() {
    expr("not not 1").ok("(NOT (NOT 1))"); // two prefixes
  }

  @Test void testPrecedence3() {
    expr("- 1 is null").ok("(-1 IS NULL)"); // prefix vs. postfix
  }

  @Test void testPrecedence4() {
    expr("1 - -2").ok("(1 - -2)"); // infix, prefix '-'
  }

  @Test void testPrecedence5() {
    expr("1++2").ok("(1 + 2)"); // infix, prefix '+'
    expr("1+ +2").ok("(1 + 2)"); // infix, prefix '+'
  }

  @Test void testPrecedenceSetOps() {
    final String sql = "select * from a union "
        + "select * from b intersect "
        + "select * from c intersect "
        + "select * from d except "
        + "select * from e except "
        + "select * from f union "
        + "select * from g";
    final String expected = "SELECT *\n"
        + "FROM `A`\n"
        + "UNION\n"
        + "SELECT *\n"
        + "FROM `B`\n"
        + "INTERSECT\n"
        + "SELECT *\n"
        + "FROM `C`\n"
        + "INTERSECT\n"
        + "SELECT *\n"
        + "FROM `D`\n"
        + "EXCEPT\n"
        + "SELECT *\n"
        + "FROM `E`\n"
        + "EXCEPT\n"
        + "SELECT *\n"
        + "FROM `F`\n"
        + "UNION\n"
        + "SELECT *\n"
        + "FROM `G`";
    sql(sql).ok(expected);
  }

  @Test void testQueryInFrom() {
    // one query with 'as', the other without
    sql("select * from (select * from emp) as e join (select * from dept) d")
        .ok("SELECT *\n"
            + "FROM (SELECT *\n"
            + "FROM `EMP`) AS `E`\n"
            + "INNER JOIN (SELECT *\n"
            + "FROM `DEPT`) AS `D`");
  }

  @Test void testQuotesInString() {
    expr("'a''b'").same();
    expr("'''x'").same();
    expr("''").same();
    expr("'Quoted strings aren''t \"hard\"'").same();
  }

  @Test void testScalarQueryInWhere() {
    sql("select * from emp where 3 = (select count(*) from dept where dept.deptno = emp.deptno)")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE (3 = (SELECT COUNT(*)\n"
            + "FROM `DEPT`\n"
            + "WHERE (`DEPT`.`DEPTNO` = `EMP`.`DEPTNO`)))");
  }

  @Test void testScalarQueryInSelect() {
    sql("select x, (select count(*) from dept where dept.deptno = emp.deptno) from emp")
        .ok("SELECT `X`, (SELECT COUNT(*)\n"
            + "FROM `DEPT`\n"
            + "WHERE (`DEPT`.`DEPTNO` = `EMP`.`DEPTNO`))\n"
            + "FROM `EMP`");
  }

  @Test void testSelectList() {
    sql("select * from emp, dept")
        .ok("SELECT *\n"
            + "FROM `EMP`,\n"
            + "`DEPT`");
  }

  @Test void testSelectWithoutFrom() {
    sql("select 2+2")
        .ok("SELECT (2 + 2)");
  }

  @Test void testSelectWithoutFrom2() {
    sql("select 2+2 as x, 'a' as y")
        .ok("SELECT (2 + 2) AS `X`, 'a' AS `Y`");
  }

  @Test void testSelectDistinctWithoutFrom() {
    sql("select distinct 2+2 as x, 'a' as y")
        .ok("SELECT DISTINCT (2 + 2) AS `X`, 'a' AS `Y`");
  }

  @Test void testSelectWithoutFromWhereFails() {
    sql("select 2+2 as x ^where^ 1 > 2")
        .fails("(?s).*Encountered \"where\" at line .*");
  }

  @Test void testSelectWithoutFromGroupByFails() {
    sql("select 2+2 as x ^group^ by 1, 2")
        .fails("(?s).*Encountered \"group\" at line .*");
  }

  @Test void testSelectWithoutFromHavingFails() {
    sql("select 2+2 as x ^having^ 1 > 2")
        .fails("(?s).*Encountered \"having\" at line .*");
  }

  @Test void testSelectList3() {
    sql("select 1, emp.*, 2 from emp")
        .ok("SELECT 1, `EMP`.*, 2\n"
            + "FROM `EMP`");
  }

  @Test void testSelectList4() {
    sql("select ^from^ emp")
        .fails("(?s).*Encountered \"from\" at line .*");
  }

  @Test void testStar() {
    sql("select * from emp")
        .ok("SELECT *\n"
            + "FROM `EMP`");
  }

  @Test void testCompoundStar() {
    final String sql = "select sales.emp.address.zipcode,\n"
        + " sales.emp.address.*\n"
        + "from sales.emp";
    final String expected = "SELECT `SALES`.`EMP`.`ADDRESS`.`ZIPCODE`,"
        + " `SALES`.`EMP`.`ADDRESS`.*\n"
        + "FROM `SALES`.`EMP`";
    sql(sql).ok(expected);
  }

  @Test void testSelectDistinct() {
    sql("select distinct foo from bar")
        .ok("SELECT DISTINCT `FOO`\n"
            + "FROM `BAR`");
  }

  @Test void testSelectAll() {
    // "unique" is the default -- so drop the keyword
    sql("select * from (select all foo from bar) as xyz")
        .ok("SELECT *\n"
            + "FROM (SELECT ALL `FOO`\n"
            + "FROM `BAR`) AS `XYZ`");
  }

  @Test void testSelectStream() {
    sql("select stream foo from bar")
        .ok("SELECT STREAM `FOO`\n"
            + "FROM `BAR`");
  }

  @Test void testSelectStreamDistinct() {
    sql("select stream distinct foo from bar")
        .ok("SELECT STREAM DISTINCT `FOO`\n"
            + "FROM `BAR`");
  }

  @Test void testWhere() {
    sql("select * from emp where empno > 5 and gender = 'F'")
        .ok("SELECT *\n"
            + "FROM `EMP`\n"
            + "WHERE ((`EMPNO` > 5) AND (`GENDER` = 'F'))");
  }

  @Test void testNestedSelect() {
    sql("select * from (select * from emp)")
        .ok("SELECT *\n"
            + "FROM (SELECT *\n"
            + "FROM `EMP`)");
  }

  @Test void testValues() {
    final String expected = "VALUES (ROW(1, 'two'))";
    final String pattern =
        "VALUE is not allowed under the current SQL conformance level";
    sql("values(1,'two')")
        .ok(expected);
    sql("value(1,'two')")
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(expected)
        .node(not(isDdl()));
    sql("^value^(1,'two')")
        .fails(pattern);
  }

  @Test void testValuesExplicitRow() {
    sql("values row(1,'two')")
        .ok("VALUES (ROW(1, 'two'))");
  }

  @Test void testFromValues() {
    sql("select * from (values(1,'two'), 3, (4, 'five'))")
        .ok("SELECT *\n"
            + "FROM (VALUES (ROW(1, 'two')),\n"
            + "(ROW(3)),\n"
            + "(ROW(4, 'five')))");
  }

  @Test void testFromValuesWithoutParens() {
    sql("select 1 from ^values^('x')")
        .fails("(?s)Encountered \"values\" at line 1, column 15\\.\n"
            + "Was expecting one of:\n"
            + "    \"LATERAL\" \\.\\.\\.\n"
            + "    \"TABLE\" \\.\\.\\.\n"
            + "    \"UNNEST\" \\.\\.\\.\n"
            + "    <IDENTIFIER> \\.\\.\\.\n"
            + "    <HYPHENATED_IDENTIFIER> \\.\\.\\.\n"
            + "    <QUOTED_IDENTIFIER> \\.\\.\\.\n"
            + "    <BACK_QUOTED_IDENTIFIER> \\.\\.\\.\n"
            + "    <BIG_QUERY_BACK_QUOTED_IDENTIFIER> \\.\\.\\.\n"
            + "    <BRACKET_QUOTED_IDENTIFIER> \\.\\.\\.\n"
            + "    <UNICODE_QUOTED_IDENTIFIER> \\.\\.\\.\n"
            + "    \"\\(\" \\.\\.\\.\n.*");
  }

  @Test void testEmptyValues() {
    sql("select * from (values(^)^)")
        .fails("(?s).*Encountered \"\\)\" at .*");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-493">[CALCITE-493]
   * Add EXTEND clause, for defining columns and their types at query/DML
   * time</a>. */
  @Test void testTableExtend() {
    sql("select * from emp extend (x int, y varchar(10) not null)")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10))");
    sql("select * from emp extend (x int, y varchar(10) not null) where true")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10))\n"
            + "WHERE TRUE");
    // with table alias
    sql("select * from emp extend (x int, y varchar(10) not null) as t")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10)) AS `T`");
    // as previous, without AS
    sql("select * from emp extend (x int, y varchar(10) not null) t")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10)) AS `T`");
    // with table alias and column alias list
    sql("select * from emp extend (x int, y varchar(10) not null) as t(a, b)")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10)) AS `T` (`A`, `B`)");
    // as previous, without AS
    sql("select * from emp extend (x int, y varchar(10) not null) t(a, b)")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10)) AS `T` (`A`, `B`)");
    // omit EXTEND
    sql("select * from emp (x int, y varchar(10) not null) t(a, b)")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10)) AS `T` (`A`, `B`)");
    sql("select * from emp (x int, y varchar(10) not null) where x = y")
        .ok("SELECT *\n"
            + "FROM `EMP` EXTEND (`X` INTEGER, `Y` VARCHAR(10))\n"
            + "WHERE (`X` = `Y`)");
  }

  @Test void testExplicitTable() {
    sql("table emp")
        .ok("(TABLE `EMP`)");

    sql("table ^123^")
        .fails("(?s)Encountered \"123\" at line 1, column 7\\.\n.*");
  }

  @Test void testExplicitTableOrdered() {
    sql("table emp order by name")
        .ok("(TABLE `EMP`)\n"
            + "ORDER BY `NAME`");
  }

  @Test void testSelectFromExplicitTable() {
    sql("select * from (table emp)")
        .ok("SELECT *\n"
            + "FROM (TABLE `EMP`)");
  }

  @Test void testSelectFromBareExplicitTableFails() {
    sql("select * from table ^emp^")
        .fails("(?s).*Encountered \"emp\" at .*");

    sql("select * from (table (^select^ empno from emp))")
        .fails("(?s)Encountered \"select\".*");
  }

  @Test void testCollectionTable() {
    sql("select * from table(ramp(3, 4))")
        .ok("SELECT *\n"
            + "FROM TABLE(`RAMP`(3, 4))");
  }

  @Test void testDescriptor() {
    sql("select * from table(ramp(descriptor(column_name)))")
        .ok("SELECT *\n"
            + "FROM TABLE(`RAMP`(DESCRIPTOR(`COLUMN_NAME`)))");
    sql("select * from table(ramp(descriptor(\"COLUMN_NAME\")))")
        .ok("SELECT *\n"
            + "FROM TABLE(`RAMP`(DESCRIPTOR(`COLUMN_NAME`)))");
    sql("select * from table(ramp(descriptor(column_name1, column_name2, column_name3)))")
        .ok("SELECT *\n"
            + "FROM TABLE(`RAMP`(DESCRIPTOR(`COLUMN_NAME1`, `COLUMN_NAME2`, `COLUMN_NAME3`)))");
  }

  @Test void testCollectionTableWithCursorParam() {
    sql("select * from table(dedup(cursor(select * from emps),'name'))")
        .ok("SELECT *\n"
            + "FROM TABLE(`DEDUP`((CURSOR ((SELECT *\n"
            + "FROM `EMPS`))), 'name'))");
  }

  @Test void testCollectionTableWithColumnListParam() {
    sql("select * from table(dedup(cursor(select * from emps),"
        + "row(empno, name)))")
        .ok("SELECT *\n"
            + "FROM TABLE(`DEDUP`((CURSOR ((SELECT *\n"
            + "FROM `EMPS`))), (ROW(`EMPNO`, `NAME`))))");
  }

  @Test void testLateral() {
    // Bad: LATERAL table
    sql("select * from lateral ^emp^")
        .fails("(?s)Encountered \"emp\" at .*");
    sql("select * from lateral table ^emp^ as e")
        .fails("(?s)Encountered \"emp\" at .*");

    // Bad: LATERAL TABLE schema.table
    sql("select * from lateral table ^scott^.emp")
        .fails("(?s)Encountered \"scott\" at .*");
    final String expected = "SELECT *\n"
        + "FROM LATERAL TABLE(`RAMP`(1))";

    // Good: LATERAL TABLE function(arg, arg)
    sql("select * from lateral table(ramp(1))")
        .ok(expected);
    sql("select * from lateral table(ramp(1)) as t")
        .ok(expected + " AS `T`");
    sql("select * from lateral table(ramp(1)) as t(x)")
        .ok(expected + " AS `T` (`X`)");
    // Bad: Parentheses make it look like a sub-query
    sql("select * from lateral (^table (ramp(1))^)")
        .fails("Expected query or join");

    // Good: LATERAL (subQuery)
    final String expected2 = "SELECT *\n"
        + "FROM LATERAL (SELECT *\n"
        + "FROM `EMP`)";
    sql("select * from lateral (select * from emp)")
        .ok(expected2);
    sql("select * from lateral (select * from emp) as t")
        .ok(expected2 + " AS `T`");
    sql("select * from lateral (select * from emp) as t(x)")
        .ok(expected2 + " AS `T` (`X`)");
  }

  @Test void testTemporalTable() {
    final String sql0 = "select stream * from orders, products\n"
        + "for system_time as of TIMESTAMP '2011-01-02 00:00:00'";
    final String expected0 = "SELECT STREAM *\n"
        + "FROM `ORDERS`,\n"
        + "`PRODUCTS` FOR SYSTEM_TIME AS OF TIMESTAMP '2011-01-02 00:00:00'";
    sql(sql0).ok(expected0);

    // Can not use explicit LATERAL keyword.
    final String sql1 = "select stream * from orders, LATERAL ^products_temporal^\n"
        + "for system_time as of TIMESTAMP '2011-01-02 00:00:00'";
    final String error = "(?s)Encountered \"products_temporal\" at line .*";
    sql(sql1).fails(error);

    // Inner join with a specific timestamp
    final String sql2 = "select stream * from orders join products_temporal\n"
        + "for system_time as of timestamp '2011-01-02 00:00:00'\n"
        + "on orders.productid = products_temporal.productid";
    final String expected2 = "SELECT STREAM *\n"
        + "FROM `ORDERS`\n"
        + "INNER JOIN `PRODUCTS_TEMPORAL` "
        + "FOR SYSTEM_TIME AS OF TIMESTAMP '2011-01-02 00:00:00' "
        + "ON (`ORDERS`.`PRODUCTID` = `PRODUCTS_TEMPORAL`.`PRODUCTID`)";
    sql(sql2).ok(expected2);

    // Left join with a timestamp field
    final String sql3 = "select stream * from orders left join products_temporal\n"
        + "for system_time as of orders.rowtime "
        + "on orders.productid = products_temporal.productid";
    final String expected3 = "SELECT STREAM *\n"
        + "FROM `ORDERS`\n"
        + "LEFT JOIN `PRODUCTS_TEMPORAL` "
        + "FOR SYSTEM_TIME AS OF `ORDERS`.`ROWTIME` "
        + "ON (`ORDERS`.`PRODUCTID` = `PRODUCTS_TEMPORAL`.`PRODUCTID`)";
    sql(sql3).ok(expected3);

    // Left join with a timestamp expression
    final String sql4 = "select stream * from orders left join products_temporal\n"
        + "for system_time as of orders.rowtime - INTERVAL '3' DAY "
        + "on orders.productid = products_temporal.productid";
    final String expected4 = "SELECT STREAM *\n"
        + "FROM `ORDERS`\n"
        + "LEFT JOIN `PRODUCTS_TEMPORAL` "
        + "FOR SYSTEM_TIME AS OF (`ORDERS`.`ROWTIME` - INTERVAL '3' DAY) "
        + "ON (`ORDERS`.`PRODUCTID` = `PRODUCTS_TEMPORAL`.`PRODUCTID`)";
    sql(sql4).ok(expected4);
  }

  @Test void testCollectionTableWithLateral() {
    final String sql = "select * from dept, lateral table(ramp(dept.deptno))";
    final String expected = "SELECT *\n"
        + "FROM `DEPT`,\n"
        + "LATERAL TABLE(`RAMP`(`DEPT`.`DEPTNO`))";
    sql(sql).ok(expected);
  }

  @Test void testCollectionTableWithLateral2() {
    final String sql = "select * from dept as d\n"
        + "cross join lateral table(ramp(dept.deptno)) as r";
    final String expected = "SELECT *\n"
        + "FROM `DEPT` AS `D`\n"
        + "CROSS JOIN LATERAL TABLE(`RAMP`(`DEPT`.`DEPTNO`)) AS `R`";
    sql(sql).ok(expected);
  }

  @Test void testCollectionTableWithLateral3() {
    // LATERAL before first table in FROM clause doesn't achieve anything, but
    // it's valid.
    final String sql = "select * from lateral table(ramp(dept.deptno)), dept";
    final String expected = "SELECT *\n"
        + "FROM LATERAL TABLE(`RAMP`(`DEPT`.`DEPTNO`)),\n"
        + "`DEPT`";
    sql(sql).ok(expected);
  }

  @Test void testTableFunction() {
    final String sql = "select * from table(score(table orders))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`SCORE`((TABLE `ORDERS`)))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithPartitionKey() {
    // test one partition key for input table
    final String sql = "select * from table(topn(table orders partition by productid, 3))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`TOPN`(((TABLE `ORDERS`) PARTITION BY `PRODUCTID`), 3))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithMultiplePartitionKeys() {
    // test multiple partition keys for input table
    final String sql =
        "select * from table(topn(table orders partition by (orderId, productid), 3))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`TOPN`(((TABLE `ORDERS`) PARTITION BY `ORDERID`, `PRODUCTID`), 3))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithOrderKey() {
    // test one order key for input table
    final String sql =
        "select * from table(topn(table orders order by orderId, 3))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`TOPN`(((TABLE `ORDERS`) ORDER BY `ORDERID`), 3))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithMultipleOrderKeys() {
    // test multiple order keys for input table
    final String sql =
        "select * from table(topn(table orders order by (orderId, productid), 3))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`TOPN`(((TABLE `ORDERS`) ORDER BY `ORDERID`, `PRODUCTID`), 3))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithComplexOrderBy() {
    // test complex order-by clause for input table
    final String sql =
        "select * from table(topn(table orders order by (orderId desc, productid asc), 3))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`TOPN`(((TABLE `ORDERS`) ORDER BY `ORDERID` DESC, `PRODUCTID`), 3))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithPartitionKeyAndOrderKey() {
    // test partition by clause and order by clause for input table
    final String sql =
        "select * from table(topn(table orders partition by productid order by orderId, 3))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`TOPN`(((TABLE `ORDERS`) PARTITION BY `PRODUCTID` ORDER BY `ORDERID`), 3))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithSubQuery() {
    // test partition by clause and order by clause for subquery
    final String sql =
        "select * from table(topn(select * from Orders partition by productid "
            + "order by orderId, 3))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`TOPN`(((SELECT *\n"
        + "FROM `ORDERS`) PARTITION BY `PRODUCTID` ORDER BY `ORDERID`), 3))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithMultipleInputTables() {
    final String sql = "select * from table(similarlity(table emp, table emp_b))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`SIMILARLITY`((TABLE `EMP`), (TABLE `EMP_B`)))";
    sql(sql).ok(expected);
  }

  @Test void testTableFunctionWithMultipleInputTablesAndSubClauses() {
    final String sql = "select * from table("
        + "similarlity("
        + "  table emp partition by deptno order by empno, "
        + "  table emp_b partition by deptno order by empno))";
    final String expected = "SELECT *\n"
        + "FROM TABLE(`SIMILARLITY`(((TABLE `EMP`) PARTITION BY `DEPTNO` ORDER BY `EMPNO`), "
        + "((TABLE `EMP_B`) PARTITION BY `DEPTNO` ORDER BY `EMPNO`)))";
    sql(sql).ok(expected);
  }

  @Test void testIllegalCursors() {
    sql("select ^cursor^(select * from emps) from emps")
        .fails("CURSOR expression encountered in illegal context");
    sql("call list(^cursor^(select * from emps))")
        .fails("CURSOR expression encountered in illegal context");
    sql("select f(^cursor^(select * from emps)) from emps")
        .fails("CURSOR expression encountered in illegal context");
  }

  @Test void testExplain() {
    final String sql = "explain plan for select * from emps";
    final String expected = "EXPLAIN PLAN"
        + " INCLUDING ATTRIBUTES WITH IMPLEMENTATION FOR\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql(sql).ok(expected);
  }

  @Test void testExplainAsXml() {
    final String sql = "explain plan as xml for select * from emps";
    final String expected = "EXPLAIN PLAN"
        + " INCLUDING ATTRIBUTES WITH IMPLEMENTATION AS XML FOR\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql(sql).ok(expected);
  }

  @Test void testExplainAsDot() {
    final String sql = "explain plan as dot for select * from emps";
    final String expected = "EXPLAIN PLAN"
        + " INCLUDING ATTRIBUTES WITH IMPLEMENTATION AS DOT FOR\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql(sql).ok(expected);
  }

  @Test void testExplainAsJson() {
    final String sql = "explain plan as json for select * from emps";
    final String expected = "EXPLAIN PLAN"
        + " INCLUDING ATTRIBUTES WITH IMPLEMENTATION AS JSON FOR\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql(sql).ok(expected);
  }

  @Test void testExplainWithImpl() {
    sql("explain plan with implementation for select * from emps")
        .ok("EXPLAIN PLAN INCLUDING ATTRIBUTES WITH IMPLEMENTATION FOR\n"
            + "SELECT *\n"
            + "FROM `EMPS`");
  }

  @Test void testExplainWithoutImpl() {
    sql("explain plan without implementation for select * from emps")
        .ok("EXPLAIN PLAN INCLUDING ATTRIBUTES WITHOUT IMPLEMENTATION FOR\n"
            + "SELECT *\n"
            + "FROM `EMPS`");
  }

  @Test void testExplainWithType() {
    sql("explain plan with type for (values (true))")
        .ok("EXPLAIN PLAN INCLUDING ATTRIBUTES WITH TYPE FOR\n"
            + "(VALUES (ROW(TRUE)))");
  }

  @Test void testExplainJsonFormat() {
    SqlExplain sqlExplain =
        (SqlExplain) sql("explain plan as json for select * from emps").node();
    assertThat(sqlExplain.isJson(), is(true));
  }

  @Test void testDescribeSchema() {
    sql("describe schema A")
        .ok("DESCRIBE SCHEMA `A`");
    // Currently DESCRIBE DATABASE, DESCRIBE CATALOG become DESCRIBE SCHEMA.
    // See [CALCITE-1221] Implement DESCRIBE DATABASE, CATALOG, STATEMENT
    sql("describe database A")
        .ok("DESCRIBE SCHEMA `A`");
    sql("describe catalog A")
        .ok("DESCRIBE SCHEMA `A`");
  }

  @Test void testDescribeTable() {
    sql("describe emps")
        .ok("DESCRIBE TABLE `EMPS`");
    sql("describe \"emps\"")
        .ok("DESCRIBE TABLE `emps`");
    sql("describe s.emps")
        .ok("DESCRIBE TABLE `S`.`EMPS`");
    sql("describe db.c.s.emps")
        .ok("DESCRIBE TABLE `DB`.`C`.`S`.`EMPS`");
    sql("describe emps col1")
        .ok("DESCRIBE TABLE `EMPS` `COL1`");

    // BigQuery allows hyphens in schema (project) names
    sql("describe foo-bar.baz")
        .withDialect(BIG_QUERY)
        .ok("DESCRIBE TABLE `foo-bar`.baz");
    sql("describe table foo-bar.baz")
        .withDialect(BIG_QUERY)
        .ok("DESCRIBE TABLE `foo-bar`.baz");

    // table keyword is OK
    sql("describe table emps col1")
        .ok("DESCRIBE TABLE `EMPS` `COL1`");
    // character literal for column name not ok
    sql("describe emps ^'col_'^")
        .fails("(?s).*Encountered \"\\\\'col_\\\\'\" at .*");
    // composite column name not ok
    sql("describe emps c1^.^c2")
        .fails("(?s).*Encountered \"\\.\" at .*");
  }

  @Test void testDescribeStatement() {
    // Currently DESCRIBE STATEMENT becomes EXPLAIN.
    // See [CALCITE-1221] Implement DESCRIBE DATABASE, CATALOG, STATEMENT
    final String expected0 = ""
        + "EXPLAIN PLAN INCLUDING ATTRIBUTES WITH IMPLEMENTATION FOR\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("describe statement select * from emps").ok(expected0);
    final String expected1 = ""
        + "EXPLAIN PLAN INCLUDING ATTRIBUTES WITH IMPLEMENTATION FOR\n"
        + "(SELECT *\n"
        + "FROM `EMPS`\n"
        + "ORDER BY 2)";
    sql("describe statement select * from emps order by 2").ok(expected1);
    sql("describe select * from emps").ok(expected0);
    sql("describe (select * from emps)").ok(expected0);
    sql("describe statement (select * from emps)").ok(expected0);
    final String expected2 = ""
        + "EXPLAIN PLAN INCLUDING ATTRIBUTES WITH IMPLEMENTATION FOR\n"
        + "SELECT `DEPTNO`\n"
        + "FROM `EMPS`\n"
        + "UNION\n"
        + "SELECT `DEPTNO`\n"
        + "FROM `DEPTS`";
    sql("describe select deptno from emps union select deptno from depts").ok(expected2);
    final String expected3 = ""
        + "EXPLAIN PLAN INCLUDING ATTRIBUTES WITH IMPLEMENTATION FOR\n"
        + "INSERT INTO `EMPS`\n"
        + "VALUES (ROW(1, 'a'))";
    sql("describe insert into emps values (1, 'a')").ok(expected3);
    // only allow query or DML, not explain, inside describe
    sql("describe ^explain^ plan for select * from emps")
        .fails("(?s).*Encountered \"explain\" at .*");
    sql("describe statement ^explain^ plan for select * from emps")
        .fails("(?s).*Encountered \"explain\" at .*");
  }

  @Test void testSelectIsNotDdl() {
    sql("select 1 from t")
        .node(not(isDdl()));
  }

  @Test void testInsertSelect() {
    final String expected = "INSERT INTO `EMPS`\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("insert into emps select * from emps")
        .ok(expected)
        .node(not(isDdl()));
  }

  @Test void testInsertUnion() {
    final String expected = "INSERT INTO `EMPS`\n"
        + "SELECT *\n"
        + "FROM `EMPS1`\n"
        + "UNION\n"
        + "SELECT *\n"
        + "FROM `EMPS2`";
    sql("insert into emps select * from emps1 union select * from emps2")
        .ok(expected);
  }

  @Test void testInsertValues() {
    final String expected = "INSERT INTO `EMPS`\n"
        + "VALUES (ROW(1, 'Fredkin'))";
    final String pattern =
        "VALUE is not allowed under the current SQL conformance level";
    sql("insert into emps values (1,'Fredkin')")
        .ok(expected)
        .node(not(isDdl()));
    sql("insert into emps value (1, 'Fredkin')")
        .withConformance(SqlConformanceEnum.MYSQL_5)
        .ok(expected)
        .node(not(isDdl()));
    sql("insert into emps ^value^ (1, 'Fredkin')")
        .fails(pattern);
  }

  @Test void testInsertValuesDefault() {
    final String expected = "INSERT INTO `EMPS`\n"
        + "VALUES (ROW(1, DEFAULT, 'Fredkin'))";
    sql("insert into emps values (1,DEFAULT,'Fredkin')")
        .ok(expected)
        .node(not(isDdl()));
  }

  @Test void testInsertValuesRawDefault() {
    final String expected = "INSERT INTO `EMPS`\n"
        + "VALUES (ROW(DEFAULT))";
    sql("insert into emps values ^default^")
        .fails("(?s).*Encountered \"default\" at .*");
    sql("insert into emps values (default)")
        .ok(expected)
        .node(not(isDdl()));
  }

  @Test void testInsertColumnList() {
    final String expected = "INSERT INTO `EMPS` (`X`, `Y`)\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("insert into emps(x,y) select * from emps")
        .ok(expected);
  }

  @Test void testInsertCaseSensitiveColumnList() {
    final String expected = "INSERT INTO `emps` (`x`, `y`)\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("insert into \"emps\"(\"x\",\"y\") select * from emps")
        .ok(expected);
  }

  @Test void testInsertExtendedColumnList() {
    String expected = "INSERT INTO `EMPS` EXTEND (`Z` BOOLEAN) (`X`, `Y`)\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("insert into emps(z boolean)(x,y) select * from emps")
        .ok(expected);
    expected = "INSERT INTO `EMPS` EXTEND (`Z` BOOLEAN) (`X`, `Y`, `Z`)\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("insert into emps(x, y, z boolean) select * from emps")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected);
  }

  @Test void testUpdateExtendedColumnList() {
    final String expected = "UPDATE `EMPDEFAULTS` EXTEND (`EXTRA` BOOLEAN, `NOTE` VARCHAR)"
        + " SET `DEPTNO` = 1"
        + ", `EXTRA` = TRUE"
        + ", `EMPNO` = 20"
        + ", `ENAME` = 'Bob'"
        + ", `NOTE` = 'legion'\n"
        + "WHERE (`DEPTNO` = 10)";
    sql("update empdefaults(extra BOOLEAN, note VARCHAR)"
        + " set deptno = 1, extra = true, empno = 20, ename = 'Bob', note = 'legion'"
        + " where deptno = 10")
        .ok(expected);
  }


  @Test void testUpdateCaseSensitiveExtendedColumnList() {
    final String expected = "UPDATE `EMPDEFAULTS` EXTEND (`extra` BOOLEAN, `NOTE` VARCHAR)"
        + " SET `DEPTNO` = 1"
        + ", `extra` = TRUE"
        + ", `EMPNO` = 20"
        + ", `ENAME` = 'Bob'"
        + ", `NOTE` = 'legion'\n"
        + "WHERE (`DEPTNO` = 10)";
    sql("update empdefaults(\"extra\" BOOLEAN, note VARCHAR)"
        + " set deptno = 1, \"extra\" = true, empno = 20, ename = 'Bob', note = 'legion'"
        + " where deptno = 10")
        .ok(expected);
  }

  @Test void testInsertCaseSensitiveExtendedColumnList() {
    String expected = "INSERT INTO `emps` EXTEND (`z` BOOLEAN) (`x`, `y`)\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("insert into \"emps\"(\"z\" boolean)(\"x\",\"y\") select * from emps")
        .ok(expected);
    expected = "INSERT INTO `emps` EXTEND (`z` BOOLEAN) (`x`, `y`, `z`)\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql("insert into \"emps\"(\"x\", \"y\", \"z\" boolean) select * from emps")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok(expected);
  }

  @Test void testExplainInsert() {
    final String expected = "EXPLAIN PLAN INCLUDING ATTRIBUTES"
        + " WITH IMPLEMENTATION FOR\n"
        + "INSERT INTO `EMPS1`\n"
        + "SELECT *\n"
        + "FROM `EMPS2`";
    sql("explain plan for insert into emps1 select * from emps2")
        .ok(expected)
        .node(not(isDdl()));
  }

  @Test void testUpsertValues() {
    final String expected = "UPSERT INTO `EMPS`\n"
        + "VALUES (ROW(1, 'Fredkin'))";
    final String sql = "upsert into emps values (1,'Fredkin')";
    if (isReserved("UPSERT")) {
      sql(sql)
          .ok(expected)
          .node(not(isDdl()));
    }
  }

  @Test void testUpsertSelect() {
    final String sql = "upsert into emps select * from emp as e";
    final String expected = "UPSERT INTO `EMPS`\n"
        + "SELECT *\n"
        + "FROM `EMP` AS `E`";
    if (isReserved("UPSERT")) {
      sql(sql).ok(expected);
    }
  }

  @Test void testExplainUpsert() {
    final String sql = "explain plan for upsert into emps1 values (1, 2)";
    final String expected = "EXPLAIN PLAN INCLUDING ATTRIBUTES"
        + " WITH IMPLEMENTATION FOR\n"
        + "UPSERT INTO `EMPS1`\n"
        + "VALUES (ROW(1, 2))";
    if (isReserved("UPSERT")) {
      sql(sql).ok(expected);
    }
  }

  @Test void testDelete() {
    sql("delete from emps")
        .ok("DELETE FROM `EMPS`")
        .node(not(isDdl()));
  }

  @Test void testDeleteWhere() {
    sql("delete from emps where empno=12")
        .ok("DELETE FROM `EMPS`\n"
            + "WHERE (`EMPNO` = 12)");
  }

  @Test void testUpdate() {
    sql("update emps set empno = empno + 1, sal = sal - 1 where empno=12")
        .ok("UPDATE `EMPS` SET `EMPNO` = (`EMPNO` + 1)"
            + ", `SAL` = (`SAL` - 1)\n"
            + "WHERE (`EMPNO` = 12)");
  }

  @Test void testMergeSelectSource() {
    final String sql = "merge into emps e "
        + "using (select * from tempemps where deptno is null) t "
        + "on e.empno = t.empno "
        + "when matched then update "
        + "set name = t.name, deptno = t.deptno, salary = t.salary * .1 "
        + "when not matched then insert (name, dept, salary) "
        + "values(t.name, 10, t.salary * .15)";
    final String expected = "MERGE INTO `EMPS` AS `E`\n"
        + "USING (SELECT *\n"
        + "FROM `TEMPEMPS`\n"
        + "WHERE (`DEPTNO` IS NULL)) AS `T`\n"
        + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
        + "WHEN MATCHED THEN UPDATE SET `NAME` = `T`.`NAME`"
        + ", `DEPTNO` = `T`.`DEPTNO`"
        + ", `SALARY` = (`T`.`SALARY` * 0.1)\n"
        + "WHEN NOT MATCHED THEN INSERT (`NAME`, `DEPT`, `SALARY`) "
        + "VALUES (ROW(`T`.`NAME`, 10, (`T`.`SALARY` * 0.15)))";
    sql(sql).ok(expected)
        .node(not(isDdl()));
  }

  /** Same as testMergeSelectSource but set with compound identifier. */
  @Test void testMergeSelectSource2() {
    final String sql = "merge into emps e "
        + "using (select * from tempemps where deptno is null) t "
        + "on e.empno = t.empno "
        + "when matched then update "
        + "set e.name = t.name, e.deptno = t.deptno, e.salary = t.salary * .1 "
        + "when not matched then insert (name, dept, salary) "
        + "values(t.name, 10, t.salary * .15)";
    final String expected = "MERGE INTO `EMPS` AS `E`\n"
        + "USING (SELECT *\n"
        + "FROM `TEMPEMPS`\n"
        + "WHERE (`DEPTNO` IS NULL)) AS `T`\n"
        + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
        + "WHEN MATCHED THEN UPDATE SET `E`.`NAME` = `T`.`NAME`"
        + ", `E`.`DEPTNO` = `T`.`DEPTNO`"
        + ", `E`.`SALARY` = (`T`.`SALARY` * 0.1)\n"
        + "WHEN NOT MATCHED THEN INSERT (`NAME`, `DEPT`, `SALARY`) "
        + "VALUES (ROW(`T`.`NAME`, 10, (`T`.`SALARY` * 0.15)))";
    sql(sql).ok(expected)
        .node(not(isDdl()));
  }

  @Test void testMergeTableRefSource() {
    final String sql = "merge into emps e "
        + "using tempemps as t "
        + "on e.empno = t.empno "
        + "when matched then update "
        + "set name = t.name, deptno = t.deptno, salary = t.salary * .1 "
        + "when not matched then insert (name, dept, salary) "
        + "values(t.name, 10, t.salary * .15)";
    final String expected = "MERGE INTO `EMPS` AS `E`\n"
        + "USING `TEMPEMPS` AS `T`\n"
        + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
        + "WHEN MATCHED THEN UPDATE SET `NAME` = `T`.`NAME`"
        + ", `DEPTNO` = `T`.`DEPTNO`"
        + ", `SALARY` = (`T`.`SALARY` * 0.1)\n"
        + "WHEN NOT MATCHED THEN INSERT (`NAME`, `DEPT`, `SALARY`) "
        + "VALUES (ROW(`T`.`NAME`, 10, (`T`.`SALARY` * 0.15)))";
    sql(sql).ok(expected);
  }

  /** Same with testMergeTableRefSource but set with compound identifier. */
  @Test void testMergeTableRefSource2() {
    final String sql = "merge into emps e "
        + "using tempemps as t "
        + "on e.empno = t.empno "
        + "when matched then update "
        + "set e.name = t.name, e.deptno = t.deptno, e.salary = t.salary * .1 "
        + "when not matched then insert (name, dept, salary) "
        + "values(t.name, 10, t.salary * .15)";
    final String expected = "MERGE INTO `EMPS` AS `E`\n"
        + "USING `TEMPEMPS` AS `T`\n"
        + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
        + "WHEN MATCHED THEN UPDATE SET `E`.`NAME` = `T`.`NAME`"
        + ", `E`.`DEPTNO` = `T`.`DEPTNO`"
        + ", `E`.`SALARY` = (`T`.`SALARY` * 0.1)\n"
        + "WHEN NOT MATCHED THEN INSERT (`NAME`, `DEPT`, `SALARY`) "
        + "VALUES (ROW(`T`.`NAME`, 10, (`T`.`SALARY` * 0.15)))";
    sql(sql).ok(expected);
  }

  @Test void testMergeMismatchedParentheses() {
    // Invalid; more '(' than ')'
    final String sql1 = "merge into emps as e\n"
        + "using temps as t on e.empno = t.empno\n"
        + "when not matched\n"
        + "then insert (a, b) (values (1, 2^)^";
    sql(sql1).fails("(?s)Encountered \"<EOF>\" at .*");

    // Invalid; more ')' than '('
    final String sql1b = "merge into emps as e\n"
        + "using temps as t on e.empno = t.empno\n"
        + "when not matched\n"
        + "then insert (a, b) values (1, 2)^)^";
    sql(sql1b).fails("(?s)Encountered \"\\)\" at .*");

    // As sql1, with extra ')', therefore valid
    final String sql2 = "merge into emps as e\n"
        + "using temps as t on e.empno = t.empno\n"
        + "when not matched\n"
        + "then insert (a, b) (values (1, 2))";
    final String expected = "MERGE INTO `EMPS` AS `E`\n"
        + "USING `TEMPS` AS `T`\n"
        + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
        + "WHEN NOT MATCHED THEN INSERT (`A`, `B`) VALUES (ROW(1, 2))";
    sql(sql2).ok(expected);

    // As sql1, removing unmatched '(', therefore valid
    final String sql3 = "merge into emps as e\n"
        + "using temps as t on e.empno = t.empno\n"
        + "when not matched\n"
        + "then insert (a, b) values (1, 2)";
    sql(sql3).ok(expected);
  }

  @Test void testBitStringNotImplemented() {
    // Bit-string is longer part of the SQL standard. We do not support it.
    sql("select (B^'1011'^ || 'foobar') from (values (true))")
        .fails("(?s).*Encountered \"\\\\'1011\\\\'\" at .*");
  }

  @Test void testHexAndBinaryString() {
    expr("x''=X'2'")
        .ok("(X'' = X'2')");
    expr("x'fffff'=X''")
        .ok("(X'FFFFF' = X'')");
    expr("x'1' \t\t\f\r\n"
        + "'2'--hi this is a comment'FF'\r\r\t\f\n"
        + "'34'")
        .ok("X'1'\n'2'\n'34'");
    expr("x'1' \t\t\f\r\n"
        + "'000'--\n"
        + "'01'")
        .ok("X'1'\n'000'\n'01'");
    expr("x'1234567890abcdef'=X'fFeEdDcCbBaA'")
        .ok("(X'1234567890ABCDEF' = X'FFEEDDCCBBAA')");

    // Check the inital zeros don't get trimmed somehow
    expr("x'001'=X'000102'")
        .ok("(X'001' = X'000102')");
  }

  @Test void testHexAndBinaryStringFails() {
    sql("select ^x'FeedGoats'^ from t")
        .fails("Binary literal string must contain only characters '0' - '9', 'A' - 'F'");
    sql("select ^x'abcdefG'^ from t")
        .fails("Binary literal string must contain only characters '0' - '9', 'A' - 'F'");
    sql("select x'1' ^x'2'^ from t")
        .fails("(?s).*Encountered .x.*2.* at line 1, column 13.*");

    // valid syntax, but should fail in the validator
    sql("select x'1' '2' from t")
        .ok("SELECT X'1'\n"
            + "'2'\n"
            + "FROM `T`");
  }

  @Test void testStringLiteral() {
    expr("_latin1'hi'")
        .ok("_LATIN1'hi'");
    expr("N'is it a plane? no it''s superman!'")
        .ok("_ISO-8859-1'is it a plane? no it''s superman!'");
    expr("n'lowercase n'")
        .ok("_ISO-8859-1'lowercase n'");
    expr("'boring string'").same();
    expr("_iSo-8859-1'bye'")
        .ok("_ISO-8859-1'bye'");
    expr("'three'\n' blind'\n' mice'").same();
    expr("'three' -- comment\n' blind'\n' mice'")
        .ok("'three'\n' blind'\n' mice'");
    expr("N'bye' \t\r\f\f\n' bye'")
        .ok("_ISO-8859-1'bye'\n' bye'");
    expr("_iso-8859-1'bye'\n\n--\n-- this is a comment\n' bye'")
        .ok("_ISO-8859-1'bye'\n' bye'");
    expr("_utf8'hi'")
        .ok("_UTF8'hi'");

    // newline in string literal
    expr("'foo\rbar'").same();
    expr("'foo\nbar'").same();

    expr("'foo\r\nbar'")
        // prevent test infrastructure from converting '\r\n' to '\n'
        .withConvertToLinux(false)
        .same();
  }

  @Test void testStringLiteralFails() {
    sql("select (N ^'space'^)")
        .fails("(?s).*Encountered .*space.* at line 1, column ...*");
    sql("select (_latin1\n^'newline'^)")
        .fails("(?s).*Encountered.*newline.* at line 2, column ...*");
    sql("select ^_unknown-charset''^ from (values(true))")
        .fails("Unknown character set 'unknown-charset'");

    // valid syntax, but should give a validator error
    sql("select (N'1' '2') from t")
        .ok("SELECT _ISO-8859-1'1'\n"
            + "'2'\n"
            + "FROM `T`");
  }

  @Test void testStringLiteralChain() {
    final String fooBar =
        "'foo'\n"
            + "'bar'";
    final String fooBarBaz =
        "'foo'\n"
            + "'bar'\n"
            + "'baz'";
    expr("   'foo'\r'bar'")
        .ok(fooBar);
    expr("   'foo'\r\n'bar'")
        .ok(fooBar);
    expr("   'foo'\r\n\r\n'bar'\n'baz'")
        .ok(fooBarBaz);
    expr("   'foo' /* a comment */ 'bar'")
        .ok(fooBar);
    expr("   'foo' -- a comment\r\n 'bar'")
        .ok(fooBar);

    // String literals not separated by comment or newline are OK in
    // parser, should fail in validator.
    expr("   'foo' 'bar'")
        .ok(fooBar);
  }

  @Test void testStringLiteralDoubleQuoted() {
    sql("select `deptno` as d, ^\"^deptno\" as d2 from emp")
        .withDialect(MYSQL)
        .fails("(?s)Encountered \"\\\\\"\" at .*")
        .withDialect(BIG_QUERY)
        .ok("SELECT deptno AS d, 'deptno' AS d2\n"
            + "FROM emp");

    // MySQL uses single-quotes as escapes; BigQuery uses backslashes
    sql("select 'Let''s call the dog \"Elvis\"!'")
        .withDialect(MYSQL)
        .node(isCharLiteral("Let's call the dog \"Elvis\"!"));

    sql("select 'Let\\'\\'s call the dog \"Elvis\"!'")
        .withDialect(BIG_QUERY)
        .node(isCharLiteral("Let''s call the dog \"Elvis\"!"));

    sql("select 'Let\\'s ^call^ the dog \"Elvis\"!'")
        .withDialect(MYSQL)
        .fails("(?s)Encountered \"call\" at .*")
        .withDialect(BIG_QUERY)
        .node(isCharLiteral("Let's call the dog \"Elvis\"!"));

    // Oracle uses double-quotes as escapes in identifiers;
    // BigQuery uses backslashes as escapes in double-quoted character literals.
    sql("select \"Let's call the dog \\\"Elvis^\\^\"!\"")
        .withDialect(ORACLE)
        .fails("(?s)Lexical error at line 1, column 35\\.  "
            + "Encountered: \"\\\\\\\\\" \\(92\\), after : \"\".*")
        .withDialect(BIG_QUERY)
        .node(isCharLiteral("Let's call the dog \"Elvis\"!"));
  }

  private static Matcher<SqlNode> isCharLiteral(String s) {
    return new CustomTypeSafeMatcher<SqlNode>(s) {
      @Override protected boolean matchesSafely(SqlNode item) {
        final SqlNodeList selectList;
        return item instanceof SqlSelect
            && (selectList = ((SqlSelect) item).getSelectList()).size() == 1
            && selectList.get(0) instanceof SqlLiteral
            && ((SqlLiteral) selectList.get(0)).getValueAs(String.class)
            .equals(s);
      }
    };
  }

  @VisibleForTesting
  @Test public void testCaseExpression() {
    // implicit simple "ELSE NULL" case
    expr("case \t col1 when 1 then 'one' end")
        .ok("(CASE WHEN (`COL1` = 1) THEN 'one' ELSE NULL END)");

    // implicit searched "ELSE NULL" case
    expr("case when nbr is false then 'one' end")
        .ok("(CASE WHEN (`NBR` IS FALSE) THEN 'one' ELSE NULL END)");

    // multiple WHENs
    expr("case col1 when\n1.2 then 'one' when 2 then 'two' else 'three' end")
        .ok("(CASE WHEN (`COL1` = 1.2) THEN 'one' WHEN (`COL1` = 2) THEN 'two' ELSE 'three' END)");

    // sub-queries as case expression operands
    expr("case (select * from emp) when 1 then 2 end")
        .ok("(CASE WHEN ((SELECT *\n"
            + "FROM `EMP`) = 1) THEN 2 ELSE NULL END)");
    expr("case 1 when (select * from emp) then 2 end")
        .ok("(CASE WHEN (1 = (SELECT *\n"
            + "FROM `EMP`)) THEN 2 ELSE NULL END)");
    expr("case 1 when 2 then (select * from emp) end")
        .ok("(CASE WHEN (1 = 2) THEN (SELECT *\n"
            + "FROM `EMP`) ELSE NULL END)");
    expr("case 1 when 2 then 3 else (select * from emp) end")
        .ok("(CASE WHEN (1 = 2) THEN 3 ELSE (SELECT *\n"
            + "FROM `EMP`) END)");
    expr("case x when 2, 4 then 3 else 4 end")
        .ok("(CASE WHEN (`X` IN (2, 4)) THEN 3 ELSE 4 END)");
    // comma-list must not be empty
    sql("case x when 2, 4 then 3 when ^then^ 5 else 4 end")
        .fails("(?s)Encountered \"then\" at .*");
    // commas not allowed in boolean case
    sql("case when b1, b2 ^when^ 2, 4 then 3 else 4 end")
        .fails("(?s)Encountered \"when\" at .*");
  }

  @Test void testCaseExpressionFails() {
    // Missing 'END'
    sql("select case col1 when 1 then 'one' ^from^ t")
        .fails("(?s).*from.*");

    // Wrong 'WHEN'
    sql("select case col1 ^when1^ then 'one' end from t")
        .fails("(?s).*when1.*");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-4802">[CALCITE-4802]
   * Babel parser doesn't parse IF(condition, then, else) statements </a>.
   */
  @Test void testIf() {
    expr("if(true, 1, 0)")
        .ok("`IF`(TRUE, 1, 0)");

    sql("select 1 as if")
        .ok("SELECT 1 AS `IF`");
  }

  @Test void testNullIf() {
    expr("nullif(v1,v2)")
        .ok("NULLIF(`V1`, `V2`)");
    if (isReserved("NULLIF")) {
      expr("1 + ^nullif^ + 3")
          .fails("(?s)Encountered \"nullif \\+\" at line 1, column 5.*");
    }
  }

  @Test void testCoalesce() {
    expr("coalesce(v1)")
        .ok("COALESCE(`V1`)");
    expr("coalesce(v1,v2)")
        .ok("COALESCE(`V1`, `V2`)");
    expr("coalesce(v1,v2,v3)")
        .ok("COALESCE(`V1`, `V2`, `V3`)");
  }

  @Test void testLiteralCollate() {
    if (!Bug.FRG78_FIXED) {
      return;
    }

    expr("'string' collate latin1$sv_SE$mega_strength")
        .ok("'string' COLLATE ISO-8859-1$sv_SE$mega_strength");
    expr("'a long '\n'string' collate latin1$sv_SE$mega_strength")
        .ok("'a long ' 'string' COLLATE ISO-8859-1$sv_SE$mega_strength");
    expr("x collate iso-8859-6$ar_LB$1")
        .ok("`X` COLLATE ISO-8859-6$ar_LB$1");
    expr("x.y.z collate shift_jis$ja_JP$2")
        .ok("`X`.`Y`.`Z` COLLATE SHIFT_JIS$ja_JP$2");
    expr("'str1'='str2' collate latin1$sv_SE")
        .ok("('str1' = 'str2' COLLATE ISO-8859-1$sv_SE$primary)");
    expr("'str1' collate latin1$sv_SE>'str2'")
        .ok("('str1' COLLATE ISO-8859-1$sv_SE$primary > 'str2')");
    expr("'str1' collate latin1$sv_SE<='str2' collate latin1$sv_FI")
        .ok("('str1' COLLATE ISO-8859-1$sv_SE$primary <= 'str2' COLLATE ISO-8859-1$sv_FI$primary)");
  }

  @Test void testCharLength() {
    expr("char_length('string')")
        .ok("CHAR_LENGTH('string')");
    expr("character_length('string')")
        .ok("CHARACTER_LENGTH('string')");
  }

  @Test void testPosition() {
    expr("posiTion('mouse' in 'house')")
        .ok("POSITION('mouse' IN 'house')");
  }

  @Test void testReplace() {
    expr("replace('x', 'y', 'z')")
        .ok("REPLACE('x', 'y', 'z')");
  }

  @Test void testDateLiteral() {
    final String expected = "SELECT DATE '1980-01-01'\n"
        + "FROM `T`";
    sql("select date '1980-01-01' from t").ok(expected);
    final String expected1 = "SELECT TIME '00:00:00'\n"
        + "FROM `T`";
    sql("select time '00:00:00' from t").ok(expected1);
    final String expected2 = "SELECT TIMESTAMP '1980-01-01 00:00:00'\n"
        + "FROM `T`";
    sql("select timestamp '1980-01-01 00:00:00' from t").ok(expected2);
    final String expected3 = "SELECT INTERVAL '3' DAY\n"
        + "FROM `T`";
    sql("select interval '3' day from t").ok(expected3);
    final String expected4 = "SELECT INTERVAL '5:6' HOUR TO MINUTE\n"
        + "FROM `T`";
    sql("select interval '5:6' hour to minute from t").ok(expected4);
  }

  /** Tests that on BigQuery, DATE, TIME and TIMESTAMP literals can use
   * single- or double-quoted strings. */
  @Test void testDateLiteralBigQuery() {
    final SqlParserFixture f = fixture().withDialect(BIG_QUERY);
    f.sql("select date '2020-10-10'")
        .ok("SELECT DATE '2020-10-10'");
    f.sql("select date\"2020-10-10\"")
        .ok("SELECT DATE '2020-10-10'");
    f.sql("select timestamp '2018-02-17 13:22:04'")
        .ok("SELECT TIMESTAMP '2018-02-17 13:22:04'");
    f.sql("select timestamp \"2018-02-17 13:22:04\"")
        .ok("SELECT TIMESTAMP '2018-02-17 13:22:04'");
    f.sql("select time '13:22:04'")
        .ok("SELECT TIME '13:22:04'");
    f.sql("select time \"13:22:04\"")
        .ok("SELECT TIME '13:22:04'");
  }

  @Test void testIntervalLiteralBigQuery() {
    final SqlParserFixture f = fixture().withDialect(BIG_QUERY)
        .expression(true);
    f.sql("interval '1' day")
        .ok("INTERVAL '1' DAY");
    f.sql("interval \"1\" day")
        .ok("INTERVAL '1' DAY");
    f.sql("interval '1:2:3' hour to second")
        .ok("INTERVAL '1:2:3' HOUR TO SECOND");
    f.sql("interval \"1:2:3\" hour to second")
        .ok("INTERVAL '1:2:3' HOUR TO SECOND");
  }

  // check date/time functions.
  @Test void testTimeDate() {
    // CURRENT_TIME - returns time w/ timezone
    expr("CURRENT_TIME(3)").same();

    // checkFails("SELECT CURRENT_TIME() FROM foo",
    //     "SELECT CURRENT_TIME() FROM `FOO`");

    expr("CURRENT_TIME").same();
    expr("CURRENT_TIME(x+y)")
        .ok("CURRENT_TIME((`X` + `Y`))");

    // LOCALTIME returns time w/o TZ
    expr("LOCALTIME(3)").same();

    // checkFails("SELECT LOCALTIME() FROM foo",
    //     "SELECT LOCALTIME() FROM `FOO`");

    expr("LOCALTIME").same();
    expr("LOCALTIME(x+y)")
        .ok("LOCALTIME((`X` + `Y`))");

    // LOCALTIMESTAMP - returns timestamp w/o TZ
    expr("LOCALTIMESTAMP(3)").same();

    // checkFails("SELECT LOCALTIMESTAMP() FROM foo",
    //     "SELECT LOCALTIMESTAMP() FROM `FOO`");

    expr("LOCALTIMESTAMP").same();
    expr("LOCALTIMESTAMP(x+y)")
        .ok("LOCALTIMESTAMP((`X` + `Y`))");

    // CURRENT_DATE - returns DATE
    expr("CURRENT_DATE(3)").same();

    // checkFails("SELECT CURRENT_DATE() FROM foo",
    //     "SELECT CURRENT_DATE() FROM `FOO`");
    expr("CURRENT_DATE").same();

    // checkFails("SELECT CURRENT_DATE(x+y) FROM foo",
    //     "CURRENT_DATE((`X` + `Y`))");

    // CURRENT_TIMESTAMP - returns timestamp w/ TZ
    expr("CURRENT_TIMESTAMP(3)").same();

    // checkFails("SELECT CURRENT_TIMESTAMP() FROM foo",
    //     "SELECT CURRENT_TIMESTAMP() FROM `FOO`");

    expr("CURRENT_TIMESTAMP").same();
    expr("CURRENT_TIMESTAMP(x+y)")
        .ok("CURRENT_TIMESTAMP((`X` + `Y`))");

    // Date literals
    expr("DATE '2004-12-01'").same();

    // Time literals
    expr("TIME '12:01:01'").same();
    expr("TIME '12:01:01.'").same();
    expr("TIME '12:01:01.000'").same();
    expr("TIME '12:01:01.001'").same();
    expr("TIME '12:01:01.01023456789'").same();

    // Timestamp literals
    expr("TIMESTAMP '2004-12-01 12:01:01'").same();
    expr("TIMESTAMP '2004-12-01 12:01:01.1'").same();
    expr("TIMESTAMP '2004-12-01 12:01:01.'").same();
    expr("TIMESTAMP  '2004-12-01 12:01:01.010234567890'")
        .ok("TIMESTAMP '2004-12-01 12:01:01.010234567890'");
    expr("TIMESTAMP '2004-12-01 12:01:01.01023456789'").same();

    // Datetime, Timestamp with local time zone literals.
    expr("DATETIME '2004-12-01 12:01:01'")
        .same();

    // Value strings that are illegal for their type are considered valid at
    // parse time, invalid at validate time. See SqlValidatorTest.testLiteral.
    expr("^DATE '12/21/99'^").same();
    expr("^TIME '1230:33'^").same();
    expr("^TIME '12:00:00 PM'^").same();
    expr("TIMESTAMP '12-21-99, 12:30:00'").same();
    expr("TIMESTAMP WITH LOCAL TIME ZONE '12-21-99, 12:30:00'").same();
    expr("DATETIME '12-21-99, 12:30:00'").same();
  }

  /**
   * Tests for casting to/from date/time types.
   */
  @Test void testDateTimeCast() {
    //   checkExp("CAST(DATE '2001-12-21' AS CHARACTER VARYING)",
    // "CAST(2001-12-21)");
    expr("CAST('2001-12-21' AS DATE)").same();
    expr("CAST(12 AS DATE)").same();
    sql("CAST('2000-12-21' AS DATE ^NOT^ NULL)")
        .fails("(?s).*Encountered \"NOT\" at line 1, column 27.*");
    sql("CAST('foo' as ^1^)")
        .fails("(?s).*Encountered \"1\" at line 1, column 15.*");
    expr("Cast(DATE '2004-12-21' AS VARCHAR(10))")
        .ok("CAST(DATE '2004-12-21' AS VARCHAR(10))");
  }

  @Test void testTrim() {
    expr("trim('mustache' FROM 'beard')")
        .ok("TRIM(BOTH 'mustache' FROM 'beard')");
    expr("trim('mustache')")
        .ok("TRIM(BOTH ' ' FROM 'mustache')");
    expr("trim(TRAILING FROM 'mustache')")
        .ok("TRIM(TRAILING ' ' FROM 'mustache')");
    expr("trim(bOth 'mustache' FROM 'beard')")
        .ok("TRIM(BOTH 'mustache' FROM 'beard')");
    expr("trim( lEaDing       'mustache' FROM 'beard')")
        .ok("TRIM(LEADING 'mustache' FROM 'beard')");
    expr("trim(\r\n\ttrailing\n  'mustache' FROM 'beard')")
        .ok("TRIM(TRAILING 'mustache' FROM 'beard')");
    expr("trim (coalesce(cast(null as varchar(2)))||"
        + "' '||coalesce('junk ',''))")
        .ok("TRIM(BOTH ' ' FROM ((COALESCE(CAST(NULL AS VARCHAR(2))) || "
            + "' ') || COALESCE('junk ', '')))");

    sql("trim(^from^ 'beard')")
        .fails("(?s).*'FROM' without operands preceding it is illegal.*");
  }

  @Test void testConvertAndTranslate() {
    expr("convert('abc', utf8, utf16)")
        .ok("CONVERT('abc', `UTF8`, `UTF16`)");
    sql("select convert(name, latin1, gbk) as newName from t")
        .ok("SELECT CONVERT(`NAME`, `LATIN1`, `GBK`) AS `NEWNAME`\n"
            + "FROM `T`");

    expr("convert('abc' using utf8)")
        .ok("TRANSLATE('abc', `UTF8`)");
    sql("select convert(name using gbk) as newName from t")
        .ok("SELECT TRANSLATE(`NAME`, `GBK`) AS `NEWNAME`\n"
            + "FROM `T`");

    expr("translate('abc' using lazy_translation)")
        .ok("TRANSLATE('abc', `LAZY_TRANSLATION`)");
    sql("select translate(name using utf8) as newName from t")
        .ok("SELECT TRANSLATE(`NAME`, `UTF8`) AS `NEWNAME`\n"
            + "FROM `T`");
  }

  @Test void testTranslate3() {
    expr("translate('aaabbbccc', 'ab', '+-')")
        .ok("TRANSLATE('aaabbbccc', 'ab', '+-')");
  }

  @Test void testOverlay() {
    expr("overlay('ABCdef' placing 'abc' from 1)")
        .ok("OVERLAY('ABCdef' PLACING 'abc' FROM 1)");
    expr("overlay('ABCdef' placing 'abc' from 1 for 3)")
        .ok("OVERLAY('ABCdef' PLACING 'abc' FROM 1 FOR 3)");
  }

  @Test void testJdbcFunctionCall() {
    expr("{fn apa(1,'1')}")
        .ok("{fn APA(1, '1') }");
    expr("{ Fn apa(log10(ln(1))+2)}")
        .ok("{fn APA((LOG10(LN(1)) + 2)) }");
    expr("{fN apa(*)}")
        .ok("{fn APA(*) }");
    expr("{   FN\t\r\n apa()}")
        .ok("{fn APA() }");
    expr("{fn insert()}")
        .ok("{fn INSERT() }");
    expr("{fn convert(foo, SQL_VARCHAR)}")
        .ok("{fn CONVERT(`FOO`, SQL_VARCHAR) }");
    expr("{fn convert(log10(100), integer)}")
        .ok("{fn CONVERT(LOG10(100), SQL_INTEGER) }");
    expr("{fn convert(1, SQL_INTERVAL_YEAR)}")
        .ok("{fn CONVERT(1, SQL_INTERVAL_YEAR) }");
    expr("{fn convert(1, SQL_INTERVAL_YEAR_TO_MONTH)}")
        .ok("{fn CONVERT(1, SQL_INTERVAL_YEAR_TO_MONTH) }");
    expr("{fn convert(1, ^sql_interval_year_to_day^)}")
        .fails("(?s)Encountered \"sql_interval_year_to_day\" at line 1, column 16\\.\n.*");
    expr("{fn convert(1, sql_interval_day)}")
        .ok("{fn CONVERT(1, SQL_INTERVAL_DAY) }");
    expr("{fn convert(1, sql_interval_day_to_minute)}")
        .ok("{fn CONVERT(1, SQL_INTERVAL_DAY_TO_MINUTE) }");
    expr("{fn convert(^)^}")
        .fails("(?s)Encountered \"\\)\" at.*");
    expr("{fn convert(\"123\", SMALLINT^(^3)}")
        .fails("(?s)Encountered \"\\(\" at.*");
    // Regular types (without SQL_) are OK for regular types, but not for
    // intervals.
    expr("{fn convert(1, INTEGER)}")
        .ok("{fn CONVERT(1, SQL_INTEGER) }");
    expr("{fn convert(1, VARCHAR)}")
        .ok("{fn CONVERT(1, SQL_VARCHAR) }");
    expr("{fn convert(1, VARCHAR^(^5))}")
        .fails("(?s)Encountered \"\\(\" at.*");
    expr("{fn convert(1, ^INTERVAL^ YEAR TO MONTH)}")
        .fails("(?s)Encountered \"INTERVAL\" at.*");
    expr("{fn convert(1, ^INTERVAL^ YEAR)}")
        .fails("(?s)Encountered \"INTERVAL\" at.*");
  }

  @Test void testWindowReference() {
    expr("sum(sal) over (w)")
        .ok("(SUM(`SAL`) OVER (`W`))");

    // Only 1 window reference allowed
    expr("sum(sal) over (w ^w1^ partition by deptno)")
        .fails("(?s)Encountered \"w1\" at.*");
  }

  @Test void testWindowInSubQuery() {
    final String sql = "select * from (\n"
        + " select sum(x) over w, sum(y) over w\n"
        + " from s\n"
        + " window w as (range interval '1' minute preceding))";
    final String expected = "SELECT *\n"
        + "FROM (SELECT (SUM(`X`) OVER `W`), (SUM(`Y`) OVER `W`)\n"
        + "FROM `S`\n"
        + "WINDOW `W` AS (RANGE INTERVAL '1' MINUTE PRECEDING))";
    sql(sql).ok(expected);
  }

  @Test void testWindowSpec() {
    // Correct syntax
    final String sql1 = "select count(z) over w as foo\n"
        + "from Bids\n"
        + "window w as (partition by y + yy, yyy\n"
        + " order by x\n"
        + " rows between 2 preceding and 2 following)";
    final String expected1 = "SELECT (COUNT(`Z`) OVER `W`) AS `FOO`\n"
        + "FROM `BIDS`\n"
        + "WINDOW `W` AS (PARTITION BY (`Y` + `YY`), `YYY` ORDER BY `X` ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING)";
    sql(sql1).ok(expected1);

    final String sql2 = "select count(*) over w\n"
        + "from emp window w as (rows 2 preceding)";
    final String expected2 = "SELECT (COUNT(*) OVER `W`)\n"
        + "FROM `EMP`\n"
        + "WINDOW `W` AS (ROWS 2 PRECEDING)";
    sql(sql2).ok(expected2);

    // Chained string literals are valid syntax. They are unlikely to be
    // semantically valid, because intervals are usually numeric or
    // datetime.
    // Note: literal chain is not yet replaced with combined literal
    // since we are just parsing, and not validating the sql.
    final String sql3 = "select count(*) over w from emp window w as (\n"
        + "  rows 'foo' 'bar'\n"
        + "       'baz' preceding)";
    final String expected3 = "SELECT (COUNT(*) OVER `W`)\n"
        + "FROM `EMP`\n"
        + "WINDOW `W` AS (ROWS 'foo'\n'bar'\n'baz' PRECEDING)";
    sql(sql3).ok(expected3);

    // Partition clause out of place. Found after ORDER BY
    final String sql4 = "select count(z) over w as foo\n"
        + "from Bids\n"
        + "window w as (partition by y order by x ^partition^ by y)";
    sql(sql4)
        .fails("(?s).*Encountered \"partition\".*");

    final String sql5 = "select count(z) over w as foo\n"
        + "from Bids window w as (order by x ^partition^ by y)";
    sql(sql5)
        .fails("(?s).*Encountered \"partition\".*");

    // Cannot partition by sub-query
    sql("select sum(a) over (partition by ^(^select 1 from t), x) from t2")
        .fails("Query expression encountered in illegal context");

    // AND is required in BETWEEN clause of window frame
    final String sql7 = "select sum(x) over\n"
        + " (order by x range between unbounded preceding ^unbounded^ following)";
    sql(sql7)
        .fails("(?s).*Encountered \"unbounded\".*");

    // WINDOW keyword is not permissible.
    sql("select sum(x) over ^window^ (order by x) from bids")
        .fails("(?s).*Encountered \"window\".*");

    // ORDER BY must be before Frame spec
    sql("select sum(x) over (rows 2 preceding ^order^ by x) from emp")
        .fails("(?s).*Encountered \"order\".*");
  }

  @Test void testWindowSpecPartial() {
    // ALLOW PARTIAL is the default, and is omitted when the statement is
    // unparsed.
    sql("select sum(x) over (order by x allow partial) from bids")
        .ok("SELECT (SUM(`X`) OVER (ORDER BY `X`))\n"
            + "FROM `BIDS`");

    sql("select sum(x) over (order by x) from bids")
        .ok("SELECT (SUM(`X`) OVER (ORDER BY `X`))\n"
            + "FROM `BIDS`");

    sql("select sum(x) over (order by x disallow partial) from bids")
        .ok("SELECT (SUM(`X`) OVER (ORDER BY `X` DISALLOW PARTIAL))\n"
            + "FROM `BIDS`");

    sql("select sum(x) over (order by x) from bids")
        .ok("SELECT (SUM(`X`) OVER (ORDER BY `X`))\n"
            + "FROM `BIDS`");
  }

  @Test void testQualify() {
    final String sql = "SELECT empno, ename,\n"
        + " ROW_NUMBER() over (partition by ename order by deptno) as rn\n"
        + "FROM emp\n"
        + "QUALIFY rn = 1";
    final String expected = "SELECT `EMPNO`, `ENAME`,"
        + " (ROW_NUMBER() OVER (PARTITION BY `ENAME` ORDER BY `DEPTNO`)) AS `RN`\n"
        + "FROM `EMP`\n"
        + "QUALIFY (`RN` = 1)";
    sql(sql).ok(expected);
  }

  @Test void testQualifyWithoutAlias() {
    final String sql = "SELECT empno, ename\n"
        + "FROM emp\n"
        + "QUALIFY ROW_NUMBER() over (partition by ename order by deptno) = 1";
    final String expected = "SELECT `EMPNO`, `ENAME`\n"
        + "FROM `EMP`\n"
        + "QUALIFY ((ROW_NUMBER() OVER (PARTITION BY `ENAME` ORDER BY `DEPTNO`)) = 1)";
    sql(sql).ok(expected);
  }

  @Test void testQualifyWithWindowClause() {
    final String sql = "SELECT empno, ename,\n"
        + " SUM(deptno) OVER myWindow as sumDeptNo\n"
        + "FROM emp\n"
        + "WINDOW myWindow AS (PARTITION BY ename ORDER BY empno)\n"
        + "QUALIFY sumDeptNo = 1";
    final String expected = "SELECT `EMPNO`, `ENAME`,"
        + " (SUM(`DEPTNO`) OVER `MYWINDOW`) AS `SUMDEPTNO`\n"
        + "FROM `EMP`\n"
        + "WINDOW `MYWINDOW` AS (PARTITION BY `ENAME` ORDER BY `EMPNO`)\n"
        + "QUALIFY (`SUMDEPTNO` = 1)";
    sql(sql).ok(expected);
  }

  @Test void testQualifyWithEverything() {
    final String sql = "SELECT DISTINCT ename,\n"
        + " SUM(deptno) OVER (PARTITION BY ename) as r\n"
        + "FROM emp\n"
        + "WHERE deptno > 3\n"
        + "GROUP BY ename, deptno\n"
        + "HAVING SUM(empno) > 4\n"
        + "QUALIFY sumDeptNo = 1\n"
        + "ORDER BY ename\n"
        + "LIMIT 5\n";
    final String expected = "SELECT DISTINCT `ENAME`,"
        + " (SUM(`DEPTNO`) OVER (PARTITION BY `ENAME`)) AS `R`\n"
        + "FROM `EMP`\n"
        + "WHERE (`DEPTNO` > 3)\n"
        + "GROUP BY `ENAME`, `DEPTNO`\n"
        + "HAVING (SUM(`EMPNO`) > 4)\n"
        + "QUALIFY (`SUMDEPTNO` = 1)\n"
        + "ORDER BY `ENAME`\n"
        + "FETCH NEXT 5 ROWS ONLY";
    sql(sql).ok(expected);
  }

  @Test void testQualifyIllegalAfterOrder() {
    final String sql = "SELECT x\n"
        + "FROM t\n"
        + "ORDER BY 1 DESC\n"
        + "^QUALIFY^ x = 1";
    sql(sql).fails("(?s).*Encountered \"QUALIFY\" at .*");
  }

  @Test void testNullTreatment() {
    sql("select lead(x) respect nulls over (w) from t")
        .ok("SELECT (LEAD(`X`) RESPECT NULLS OVER (`W`))\n"
            + "FROM `T`");
    sql("select deptno, sum(sal) respect nulls from emp group by deptno")
        .ok("SELECT `DEPTNO`, SUM(`SAL`) RESPECT NULLS\n"
            + "FROM `EMP`\n"
            + "GROUP BY `DEPTNO`");
    sql("select deptno, sum(sal) ignore nulls from emp group by deptno")
        .ok("SELECT `DEPTNO`, SUM(`SAL`) IGNORE NULLS\n"
            + "FROM `EMP`\n"
            + "GROUP BY `DEPTNO`");
    final String sql = "select col1,\n"
        + " collect(col2) ignore nulls\n"
        + "   within group (order by col3)\n"
        + "   filter (where 1 = 0)\n"
        + "   over (rows 10 preceding)\n"
        + " as c\n"
        + "from t\n"
        + "order by col1 limit 10";
    final String expected = "SELECT `COL1`, (COLLECT(`COL2`) IGNORE NULLS"
        + " WITHIN GROUP (ORDER BY `COL3`)"
        + " FILTER (WHERE (1 = 0)) OVER (ROWS 10 PRECEDING)) AS `C`\n"
        + "FROM `T`\n"
        + "ORDER BY `COL1`\n"
        + "FETCH NEXT 10 ROWS ONLY";
    sql(sql).ok(expected);

    // See [CALCITE-2993] ParseException may be thrown for legal
    // SQL queries due to incorrect "LOOKAHEAD(1)" hints
    sql("select lead(x) ignore from t")
        .ok("SELECT LEAD(`X`) AS `IGNORE`\n"
            + "FROM `T`");
    sql("select lead(x) respect from t")
        .ok("SELECT LEAD(`X`) AS `RESPECT`\n"
            + "FROM `T`");
  }

  @Test void testAs() {
    // AS is optional for column aliases
    sql("select x y from t")
        .ok("SELECT `X` AS `Y`\n"
            + "FROM `T`");

    sql("select x AS y from t")
        .ok("SELECT `X` AS `Y`\n"
            + "FROM `T`");
    sql("select sum(x) y from t group by z")
        .ok("SELECT SUM(`X`) AS `Y`\n"
            + "FROM `T`\n"
            + "GROUP BY `Z`");

    // Even after OVER
    sql("select count(z) over w foo from Bids window w as (order by x)")
        .ok("SELECT (COUNT(`Z`) OVER `W`) AS `FOO`\n"
            + "FROM `BIDS`\n"
            + "WINDOW `W` AS (ORDER BY `X`)");

    // AS is optional for table correlation names
    final String expected = "SELECT `X`\n"
        + "FROM `T` AS `T1`";
    sql("select x from t as t1").ok(expected);
    sql("select x from t t1").ok(expected);

    // AS is required in WINDOW declaration
    sql("select sum(x) over w from bids window w ^(order by x)")
        .fails("(?s).*Encountered \"\\(\".*");

    // Error if OVER and AS are in wrong order
    sql("select count(*) as foo ^over^ w from Bids window w (order by x)")
        .fails("(?s).*Encountered \"over\".*");
  }

  @Test void testAsAliases() {
    sql("select x from t as t1 (a, b) where foo")
        .ok("SELECT `X`\n"
            + "FROM `T` AS `T1` (`A`, `B`)\n"
            + "WHERE `FOO`");

    sql("select x from (values (1, 2), (3, 4)) as t1 (\"a\", b) where \"a\" > b")
        .ok("SELECT `X`\n"
            + "FROM (VALUES (ROW(1, 2)),\n"
            + "(ROW(3, 4))) AS `T1` (`a`, `B`)\n"
            + "WHERE (`a` > `B`)");

    // must have at least one column
    sql("select x from (values (1, 2), (3, 4)) as t1 (^)^")
        .fails("(?s).*Encountered \"\\)\" at .*");

    // cannot have expressions
    sql("select x from t as t1 (x ^+^ y)")
        .fails("(?s).*Was expecting one of:\n"
            + "    \"\\)\" \\.\\.\\.\n"
            + "    \",\" \\.\\.\\..*");

    // cannot have compound identifiers
    sql("select x from t as t1 (x^.^y)")
        .fails("(?s).*Was expecting one of:\n"
            + "    \"\\)\" \\.\\.\\.\n"
            + "    \",\" \\.\\.\\..*");
  }

  @Test void testOver() {
    expr("sum(sal) over ()")
        .ok("(SUM(`SAL`) OVER ())");
    expr("sum(sal) over (partition by x, y)")
        .ok("(SUM(`SAL`) OVER (PARTITION BY `X`, `Y`))");
    expr("sum(sal) over (order by x desc, y asc)")
        .ok("(SUM(`SAL`) OVER (ORDER BY `X` DESC, `Y`))");
    expr("sum(sal) over (rows 5 preceding)")
        .ok("(SUM(`SAL`) OVER (ROWS 5 PRECEDING))");
    expr("sum(sal) over (range between interval '1' second preceding\n"
        + " and interval '1' second following)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN INTERVAL '1' SECOND PRECEDING "
            + "AND INTERVAL '1' SECOND FOLLOWING))");
    expr("sum(sal) over (range between interval '1:03' hour preceding\n"
        + " and interval '2' minute following)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN INTERVAL '1:03' HOUR PRECEDING "
            + "AND INTERVAL '2' MINUTE FOLLOWING))");
    expr("sum(sal) over (range between interval '5' day preceding\n"
        + " and current row)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN INTERVAL '5' DAY PRECEDING "
            + "AND CURRENT ROW))");
    expr("sum(sal) over (range interval '5' day preceding)")
        .ok("(SUM(`SAL`) OVER (RANGE INTERVAL '5' DAY PRECEDING))");
    expr("sum(sal) over (range between unbounded preceding and current row)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN UNBOUNDED PRECEDING "
            + "AND CURRENT ROW))");
    expr("sum(sal) over (range unbounded preceding)")
        .ok("(SUM(`SAL`) OVER (RANGE UNBOUNDED PRECEDING))");
    expr("sum(sal) over (range between current row and unbounded preceding)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN CURRENT ROW "
            + "AND UNBOUNDED PRECEDING))");
    expr("sum(sal) over (range between current row and unbounded following)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN CURRENT ROW "
            + "AND UNBOUNDED FOLLOWING))");
    expr("sum(sal) over (range between 6 preceding\n"
        + " and interval '1:03' hour preceding)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN 6 PRECEDING "
            + "AND INTERVAL '1:03' HOUR PRECEDING))");
    expr("sum(sal) over (range between interval '1' second following\n"
        + " and interval '5' day following)")
        .ok("(SUM(`SAL`) OVER (RANGE BETWEEN INTERVAL '1' SECOND FOLLOWING "
            + "AND INTERVAL '5' DAY FOLLOWING))");
  }

  @Test void testElementFunc() {
    expr("element(a)")
        .ok("ELEMENT(`A`)");
  }

  @Test void testCardinalityFunc() {
    expr("cardinality(a)")
        .ok("CARDINALITY(`A`)");
  }

  @Test void testMemberOf() {
    expr("a member of b")
        .ok("(`A` MEMBER OF `B`)");
    expr("a member of multiset[b]")
        .ok("(`A` MEMBER OF (MULTISET[`B`]))");
  }

  @Test void testSubMultisetrOf() {
    expr("a submultiset of b")
        .ok("(`A` SUBMULTISET OF `B`)");
  }

  @Test void testIsASet() {
    expr("b is a set")
        .ok("(`B` IS A SET)");
    expr("a is a set")
        .ok("(`A` IS A SET)");
  }

  @Test void testMultiset() {
    expr("multiset[1]")
        .ok("(MULTISET[1])");
    expr("multiset[1,2.3]")
        .ok("(MULTISET[1, 2.3])");
    expr("multiset[1,    '2']")
        .ok("(MULTISET[1, '2'])");
    expr("multiset[ROW(1,2)]")
        .ok("(MULTISET[(ROW(1, 2))])");
    expr("multiset[ROW(1,2),ROW(3,4)]")
        .ok("(MULTISET[(ROW(1, 2)), (ROW(3, 4))])");

    expr("multiset(select*from T)")
        .ok("(MULTISET ((SELECT *\n"
            + "FROM `T`)))");
  }

  @Test void testMultisetUnion() {
    expr("a multiset union b")
        .ok("(`A` MULTISET UNION ALL `B`)");
    expr("a multiset union all b")
        .ok("(`A` MULTISET UNION ALL `B`)");
    expr("a multiset union distinct b")
        .ok("(`A` MULTISET UNION DISTINCT `B`)");
  }

  @Test void testMultisetExcept() {
    expr("a multiset EXCEPT b")
        .ok("(`A` MULTISET EXCEPT ALL `B`)");
    expr("a multiset EXCEPT all b")
        .ok("(`A` MULTISET EXCEPT ALL `B`)");
    expr("a multiset EXCEPT distinct b")
        .ok("(`A` MULTISET EXCEPT DISTINCT `B`)");
  }

  @Test void testMultisetIntersect() {
    expr("a multiset INTERSECT b")
        .ok("(`A` MULTISET INTERSECT ALL `B`)");
    expr("a multiset INTERSECT all b")
        .ok("(`A` MULTISET INTERSECT ALL `B`)");
    expr("a multiset INTERSECT distinct b")
        .ok("(`A` MULTISET INTERSECT DISTINCT `B`)");
  }

  @Test void testMultisetMixed() {
    expr("multiset[1] MULTISET union b")
        .ok("((MULTISET[1]) MULTISET UNION ALL `B`)");
    final String sql = "a MULTISET union b "
        + "multiset intersect c "
        + "multiset except d "
        + "multiset union e";
    final String expected = "(((`A` MULTISET UNION ALL "
        + "(`B` MULTISET INTERSECT ALL `C`)) "
        + "MULTISET EXCEPT ALL `D`) MULTISET UNION ALL `E`)";
    expr(sql).ok(expected);
  }

  @Test void testMapItem() {
    expr("a['foo']")
        .ok("`A`['foo']");
    expr("a['x' || 'y']")
        .ok("`A`[('x' || 'y')]");
    expr("a['foo'] ['bar']")
        .ok("`A`['foo']['bar']");
    expr("a['foo']['bar']")
        .ok("`A`['foo']['bar']");
  }

  @Test void testMapItemPrecedence() {
    expr("1 + a['foo'] * 3")
        .ok("(1 + (`A`['foo'] * 3))");
    expr("1 * a['foo'] + 3")
        .ok("((1 * `A`['foo']) + 3)");
    expr("a['foo']['bar']")
        .ok("`A`['foo']['bar']");
    expr("a[b['foo' || 'bar']]")
        .ok("`A`[`B`[('foo' || 'bar')]]");
  }

  @Test void testArrayElement() {
    expr("a[1]")
        .ok("`A`[1]");
    expr("a[b[1]]")
        .ok("`A`[`B`[1]]");
    expr("a[b[1 + 2] + 3]")
        .ok("`A`[(`B`[(1 + 2)] + 3)]");
  }

  @Test void testArrayElementWithDot() {
    expr("a[1+2].b.c[2].d")
        .ok("(((`A`[(1 + 2)].`B`).`C`)[2].`D`)");
    expr("a[b[1]].c.f0[d[1]]")
        .ok("((`A`[`B`[1]].`C`).`F0`)[`D`[1]]");
  }

  @Test void testArrayValueConstructor() {
    expr("array[1, 2]").ok("(ARRAY[1, 2])");
    expr("array [1, 2]").ok("(ARRAY[1, 2])"); // with space

    // parser allows empty array; validator will reject it
    expr("array[]")
        .ok("(ARRAY[])");
    expr("array[(1, 'a'), (2, 'b')]")
        .ok("(ARRAY[(ROW(1, 'a')), (ROW(2, 'b'))])");
  }

  @Test void testArrayFunction() {
    expr("array()").ok("ARRAY()");
    expr("array(1)").ok("ARRAY(1)");
  }

  @Test void testArrayQueryConstructor() {
    sql("SELECT array(SELECT x FROM (VALUES(1)) x)")
        .ok("SELECT (ARRAY ((SELECT `X`\n"
            + "FROM (VALUES (ROW(1))) AS `X`)))");
    sql("SELECT array(SELECT x FROM (VALUES(1)) x ORDER BY x)")
        .ok("SELECT (ARRAY (SELECT `X`\n"
            + "FROM (VALUES (ROW(1))) AS `X`\n"
            + "ORDER BY `X`))");
    sql("SELECT array(SELECT x FROM (VALUES(1)) x, ^SELECT^ x FROM (VALUES(1)) x)")
      .fails("(?s)Incorrect syntax near the keyword 'SELECT' at.*");
    sql("SELECT array(1, ^SELECT^ x FROM (VALUES(1)) x)")
      .fails("(?s)Incorrect syntax near the keyword 'SELECT'.*");
  }

  @Test void testCastAsCollectionType() {
    // test array type.
    expr("cast(a as int array)")
        .ok("CAST(`A` AS INTEGER ARRAY)");
    expr("cast(a as varchar(5) array)")
        .ok("CAST(`A` AS VARCHAR(5) ARRAY)");
    expr("cast(a as int array array)")
        .ok("CAST(`A` AS INTEGER ARRAY ARRAY)");
    expr("cast(a as varchar(5) array array)")
        .ok("CAST(`A` AS VARCHAR(5) ARRAY ARRAY)");
    expr("cast(a as int array^<^10>)")
        .fails("(?s).*Encountered \"<\" at line 1, column 20.\n.*");
    // test multiset type.
    expr("cast(a as int multiset)")
        .ok("CAST(`A` AS INTEGER MULTISET)");
    expr("cast(a as varchar(5) multiset)")
        .ok("CAST(`A` AS VARCHAR(5) MULTISET)");
    expr("cast(a as int multiset array)")
        .ok("CAST(`A` AS INTEGER MULTISET ARRAY)");
    expr("cast(a as varchar(5) multiset array)")
        .ok("CAST(`A` AS VARCHAR(5) MULTISET ARRAY)");
    // test row type nested in collection type.
    expr("cast(a as row(f0 int array multiset, f1 varchar(5) array) array multiset)")
        .ok("CAST(`A` AS "
            + "ROW(`F0` INTEGER ARRAY MULTISET, "
            + "`F1` VARCHAR(5) ARRAY) "
            + "ARRAY MULTISET)");
    // test UDT collection type.
    expr("cast(a as MyUDT array multiset)")
        .ok("CAST(`A` AS `MYUDT` ARRAY MULTISET)");
  }

  @Test void testCastAsRowType() {
    expr("cast(a as row(f0 int, f1 varchar))")
        .ok("CAST(`A` AS ROW(`F0` INTEGER, `F1` VARCHAR))");
    expr("cast(a as row(f0 int not null, f1 varchar null))")
        .ok("CAST(`A` AS ROW(`F0` INTEGER, `F1` VARCHAR NULL))");
    // test nested row type.
    expr("cast(a as row("
        + "f0 row(ff0 int not null, ff1 varchar null) null, "
        + "f1 timestamp not null))")
        .ok("CAST(`A` AS ROW("
            + "`F0` ROW(`FF0` INTEGER, `FF1` VARCHAR NULL) NULL, "
            + "`F1` TIMESTAMP))");
    // test row type in collection data types.
    expr("cast(a as row(f0 bigint not null, f1 decimal null) array)")
        .ok("CAST(`A` AS ROW(`F0` BIGINT, `F1` DECIMAL NULL) ARRAY)");
    expr("cast(a as row(f0 varchar not null, f1 timestamp null) multiset)")
        .ok("CAST(`A` AS ROW(`F0` VARCHAR, `F1` TIMESTAMP NULL) MULTISET)");
  }

  @Test void testMapValueConstructor() {
    expr("map[1, 'x', 2, 'y']")
        .ok("(MAP[1, 'x', 2, 'y'])");
    expr("map [1, 'x', 2, 'y']")
        .ok("(MAP[1, 'x', 2, 'y'])");
    expr("map[]")
        .ok("(MAP[])");
  }

  @Test void testVisitSqlInsertWithSqlShuttle() {
    final String sql = "insert into emps select * from emps";
    final SqlNode sqlNode = sql(sql).node();
    final SqlNode sqlNodeVisited = sqlNode.accept(new SqlShuttle() {
      @Override public SqlNode visit(SqlIdentifier identifier) {
        // Copy the identifier in order to return a new SqlInsert.
        return identifier.clone(identifier.getParserPosition());
      }
    });
    assertNotSame(sqlNodeVisited, sqlNode);
    assertThat(sqlNodeVisited.getKind(), is(SqlKind.INSERT));
  }

  @Test void testSqlInsertSqlBasicCallToString() {
    final String sql0 = "insert into emps select * from emps";
    final SqlNode sqlNode0 = sql(sql0).node();
    final SqlNode sqlNodeVisited0 = sqlNode0.accept(new SqlShuttle() {
      @Override public SqlNode visit(SqlIdentifier identifier) {
        // Copy the identifier in order to return a new SqlInsert.
        return identifier.clone(identifier.getParserPosition());
      }
    });
    final String str0 = "INSERT INTO `EMPS`\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    assertThat(str0, is(toLinux(sqlNodeVisited0.toString())));

    final String sql1 = "insert into emps select empno from emps";
    final SqlNode sqlNode1 = sql(sql1).node();
    final SqlNode sqlNodeVisited1 = sqlNode1.accept(new SqlShuttle() {
      @Override public SqlNode visit(SqlIdentifier identifier) {
        // Copy the identifier in order to return a new SqlInsert.
        return identifier.clone(identifier.getParserPosition());
      }
    });
    final String str1 = "INSERT INTO `EMPS`\n"
        + "SELECT `EMPNO`\n"
        + "FROM `EMPS`";
    assertThat(str1, is(toLinux(sqlNodeVisited1.toString())));
  }

  @Test void testVisitSqlMatchRecognizeWithSqlShuttle() {
    final String sql = "select *\n"
        + "from emp \n"
        + "match_recognize (\n"
        + "  pattern (strt down+ up+)\n"
        + "  define\n"
        + "    down as down.sal < PREV(down.sal),\n"
        + "    up as up.sal > PREV(up.sal)\n"
        + ") mr";
    final SqlNode sqlNode = sql(sql).node();
    final SqlNode sqlNodeVisited = sqlNode.accept(new SqlShuttle() {
      @Override public SqlNode visit(SqlIdentifier identifier) {
        // Copy the identifier in order to return a new SqlMatchRecognize.
        return identifier.clone(identifier.getParserPosition());
      }
    });
    assertNotSame(sqlNodeVisited, sqlNode);
  }

  /**
   * Runs tests for each of the thirteen different main types of INTERVAL
   * qualifiers (YEAR, YEAR TO MONTH, etc.) Tests in this section fall into
   * two categories:
   *
   * <ul>
   * <li>xxxPositive: tests that should pass parser and validator</li>
   * <li>xxxFailsValidation: tests that should pass parser but fail validator
   * </li>
   * </ul>
   *
   * <p>A substantially identical set of tests exists in SqlValidatorTest, and
   * any changes here should be synchronized there.
   */
  @Test void testIntervalLiterals() {
    final SqlParserFixture f = fixture();
    final IntervalTest.Fixture intervalFixture = new IntervalTest.Fixture() {
      @Override public IntervalTest.Fixture2 expr(String sql) {
        final SqlParserFixture f2 = f.sql(sql).expression(true);
        return getFixture2(f2, f2.sap.sql);
      }

      @Override public IntervalTest.Fixture2 wholeExpr(String sql) {
        return expr(sql);
      }

      private IntervalTest.Fixture2 getFixture2(SqlParserFixture f2,
          String expectedSql) {
        return new IntervalTest.Fixture2() {
          @Override public void fails(String message) {
            f2.compare(expectedSql);
          }

          @Override public void columnType(String expectedType) {
            f2.compare(expectedSql);
          }

          @Override public IntervalTest.Fixture2 assertParse(
              String expectedSql) {
            return getFixture2(f2, expectedSql);
          }
        };
      }
    };

    new IntervalTest(intervalFixture).testAll();
  }

  @Test void testUnparseableIntervalQualifiers() {
    // No qualifier
    expr("interval '1^'^")
        .fails("Encountered \"<EOF>\" at line 1, column 12\\.\n"
            + "Was expecting one of:\n"
            + "    \"DAY\" \\.\\.\\.\n"
            + "    \"DAYS\" \\.\\.\\.\n"
            + "    \"HOUR\" \\.\\.\\.\n"
            + "    \"HOURS\" \\.\\.\\.\n"
            + "    \"MINUTE\" \\.\\.\\.\n"
            + "    \"MINUTES\" \\.\\.\\.\n"
            + "    \"MONTH\" \\.\\.\\.\n"
            + "    \"MONTHS\" \\.\\.\\.\n"
            + "    \"QUARTER\" \\.\\.\\.\n"
            + "    \"QUARTERS\" \\.\\.\\.\n"
            + "    \"SECOND\" \\.\\.\\.\n"
            + "    \"SECONDS\" \\.\\.\\.\n"
            + "    \"WEEK\" \\.\\.\\.\n"
            + "    \"WEEKS\" \\.\\.\\.\n"
            + "    \"YEAR\" \\.\\.\\.\n"
            + "    \"YEARS\" \\.\\.\\.\n"
            + "    ");

    // illegal qualifiers, no precision in either field
    expr("interval '1' year ^to^ year")
        .fails("(?s)Encountered \"to year\" at line 1, column 19.\n"
            + "Was expecting one of:\n"
            + "    <EOF> \n"
            + "    \"\\(\" \\.\\.\\.\n"
            + "    \"\\.\" \\.\\.\\..*");
    expr("interval '1-2' year ^to^ day")
        .fails(ANY);
    expr("interval '1-2' year ^to^ hour")
        .fails(ANY);
    expr("interval '1-2' year ^to^ minute")
        .fails(ANY);
    expr("interval '1-2' year ^to^ second")
        .fails(ANY);

    expr("interval '1-2' month ^to^ year")
        .fails(ANY);
    expr("interval '1-2' month ^to^ month")
        .fails(ANY);
    expr("interval '1-2' month ^to^ day")
        .fails(ANY);
    expr("interval '1-2' month ^to^ hour")
        .fails(ANY);
    expr("interval '1-2' month ^to^ minute")
        .fails(ANY);
    expr("interval '1-2' month ^to^ second")
        .fails(ANY);

    expr("interval '1-2' day ^to^ year")
        .fails(ANY);
    expr("interval '1-2' day ^to^ month")
        .fails(ANY);
    expr("interval '1-2' day ^to^ day")
        .fails(ANY);

    expr("interval '1-2' hour ^to^ year")
        .fails(ANY);
    expr("interval '1-2' hour ^to^ month")
        .fails(ANY);
    expr("interval '1-2' hour ^to^ day")
        .fails(ANY);
    expr("interval '1-2' hour ^to^ hour")
        .fails(ANY);

    expr("interval '1-2' minute ^to^ year")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ month")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ day")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ hour")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ minute")
        .fails(ANY);

    expr("interval '1-2' second ^to^ year")
        .fails(ANY);
    expr("interval '1-2' second ^to^ month")
        .fails(ANY);
    expr("interval '1-2' second ^to^ day")
        .fails(ANY);
    expr("interval '1-2' second ^to^ hour")
        .fails(ANY);
    expr("interval '1-2' second ^to^ minute")
        .fails(ANY);
    expr("interval '1-2' second ^to^ second")
        .fails(ANY);

    // illegal qualifiers, including precision in start field
    expr("interval '1' year(3) ^to^ year")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ day")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ hour")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ minute")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ second")
        .fails(ANY);

    expr("interval '1-2' month(3) ^to^ year")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ month")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ day")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ hour")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ minute")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ second")
        .fails(ANY);

    expr("interval '1-2' day(3) ^to^ year")
        .fails(ANY);
    expr("interval '1-2' day(3) ^to^ month")
        .fails(ANY);

    expr("interval '1-2' hour(3) ^to^ year")
        .fails(ANY);
    expr("interval '1-2' hour(3) ^to^ month")
        .fails(ANY);
    expr("interval '1-2' hour(3) ^to^ day")
        .fails(ANY);

    expr("interval '1-2' minute(3) ^to^ year")
        .fails(ANY);
    expr("interval '1-2' minute(3) ^to^ month")
        .fails(ANY);
    expr("interval '1-2' minute(3) ^to^ day")
        .fails(ANY);
    expr("interval '1-2' minute(3) ^to^ hour")
        .fails(ANY);

    expr("interval '1-2' second(3) ^to^ year")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ month")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ day")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ hour")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ minute")
        .fails(ANY);

    // illegal qualifiers, including precision in end field
    expr("interval '1' year ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' year to month^(^2)")
        .fails(ANY);
    expr("interval '1-2' year ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' year ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' year ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' year ^to^ second(2)")
        .fails(ANY);
    expr("interval '1-2' year ^to^ second(2,6)")
        .fails(ANY);

    expr("interval '1-2' month ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' month ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' month ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' month ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' month ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' month ^to^ second(2)")
        .fails(ANY);
    expr("interval '1-2' month ^to^ second(2,6)")
        .fails(ANY);

    expr("interval '1-2' day ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' day ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' day ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' day to hour^(^2)")
        .fails(ANY);
    expr("interval '1-2' day to minute^(^2)")
        .fails(ANY);
    expr("interval '1-2' day to second(2^,^6)")
        .fails(ANY);

    expr("interval '1-2' hour ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' hour ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' hour ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' hour ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' hour to minute^(^2)")
        .fails(ANY);
    expr("interval '1-2' hour to second(2^,^6)")
        .fails(ANY);

    expr("interval '1-2' minute ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' minute ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' minute to second(2^,^6)")
        .fails(ANY);

    expr("interval '1-2' second ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' second ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' second ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' second ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' second ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' second ^to^ second(2)")
        .fails(ANY);
    expr("interval '1-2' second ^to^ second(2,6)")
        .fails(ANY);

    // illegal qualifiers, including precision in start and end field
    expr("interval '1' year(3) ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' year(3) to month^(^2)")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ second(2)")
        .fails(ANY);
    expr("interval '1-2' year(3) ^to^ second(2,6)")
        .fails(ANY);

    expr("interval '1-2' month(3) ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ second(2)")
        .fails(ANY);
    expr("interval '1-2' month(3) ^to^ second(2,6)")
        .fails(ANY);
  }

  @Test void testUnparseableIntervalQualifiers2() {
    expr("interval '1-2' day(3) ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' day(3) ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' day(3) ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' day(3) to hour^(^2)")
        .fails(ANY);
    expr("interval '1-2' day(3) to minute^(^2)")
        .fails(ANY);
    expr("interval '1-2' day(3) to second(2^,^6)")
        .fails(ANY);

    expr("interval '1-2' hour(3) ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' hour(3) ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' hour(3) ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' hour(3) ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' hour(3) to minute^(^2)")
        .fails(ANY);
    expr("interval '1-2' hour(3) to second(2^,^6)")
        .fails(ANY);

    expr("interval '1-2' minute(3) ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' minute(3) ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' minute(3) ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' minute(3) ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' minute(3) ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' minute(3) to second(2^,^6)")
        .fails(ANY);

    expr("interval '1-2' second(3) ^to^ year(2)")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ month(2)")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ day(2)")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ hour(2)")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ minute(2)")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ second(2)")
        .fails(ANY);
    expr("interval '1-2' second(3) ^to^ second(2,6)")
        .fails(ANY);

    // precision of -1 (< minimum allowed)
    expr("INTERVAL '0' YEAR(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0-0' YEAR(^-^1) TO MONTH")
        .fails(ANY);
    expr("INTERVAL '0' MONTH(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0' DAY(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0 0' DAY(^-^1) TO HOUR")
        .fails(ANY);
    expr("INTERVAL '0 0' DAY(^-^1) TO MINUTE")
        .fails(ANY);
    expr("INTERVAL '0 0:0:0' DAY(^-^1) TO SECOND")
        .fails(ANY);
    expr("INTERVAL '0 0:0:0' DAY TO SECOND(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0' HOUR(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0:0' HOUR(^-^1) TO MINUTE")
        .fails(ANY);
    expr("INTERVAL '0:0:0' HOUR(^-^1) TO SECOND")
        .fails(ANY);
    expr("INTERVAL '0:0:0' HOUR TO SECOND(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0' MINUTE(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0:0' MINUTE(^-^1) TO SECOND")
        .fails(ANY);
    expr("INTERVAL '0:0' MINUTE TO SECOND(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0' SECOND(^-^1)")
        .fails(ANY);
    expr("INTERVAL '0' SECOND(1, ^-^1)")
        .fails(ANY);

    // These may actually be legal per SQL2003, as the first field is
    // "more significant" than the last, but we do not support them
    expr("interval '1' day(3) ^to^ day")
        .fails(ANY);
    expr("interval '1' hour(3) ^to^ hour")
        .fails(ANY);
    expr("interval '1' minute(3) ^to^ minute")
        .fails(ANY);
    expr("interval '1' second(3) ^to^ second")
        .fails(ANY);
    expr("interval '1' second(3,1) ^to^ second")
        .fails(ANY);
    expr("interval '1' second(2,3) ^to^ second")
        .fails(ANY);
    expr("interval '1' second(2,2) ^to^ second(3)")
        .fails(ANY);

    // Invalid units
    expr("INTERVAL '2' ^MILLENNIUM^")
        .fails(ANY);
    expr("INTERVAL '1-2' ^MILLENNIUM^ TO CENTURY")
        .fails(ANY);
    expr("INTERVAL '10' ^CENTURY^")
        .fails(ANY);
    expr("INTERVAL '10' ^DECADE^")
        .fails(ANY);
  }

  /** Tests that plural time units are allowed when not in strict mode. */
  @Test void testIntervalPluralUnits() {
    expr("interval '2' years")
        .hasWarning(checkWarnings("YEARS"))
        .ok("INTERVAL '2' YEAR");
    expr("interval '2:1' years to months")
        .hasWarning(checkWarnings("YEARS", "MONTHS"))
        .ok("INTERVAL '2:1' YEAR TO MONTH");
    expr("interval '2' days")
        .hasWarning(checkWarnings("DAYS"))
        .ok("INTERVAL '2' DAY");
    expr("interval '2:1' days to hours")
        .hasWarning(checkWarnings("DAYS", "HOURS"))
        .ok("INTERVAL '2:1' DAY TO HOUR");
    expr("interval '2:1' day to hours")
        .hasWarning(checkWarnings("HOURS"))
        .ok("INTERVAL '2:1' DAY TO HOUR");
    expr("interval '2:1' days to hour")
        .hasWarning(checkWarnings("DAYS"))
        .ok("INTERVAL '2:1' DAY TO HOUR");
    expr("interval '1:1' minutes to seconds")
        .hasWarning(checkWarnings("MINUTES", "SECONDS"))
        .ok("INTERVAL '1:1' MINUTE TO SECOND");
  }

  private static Consumer<List<? extends Throwable>> checkWarnings(
      String... tokens) {
    final List<String> messages = new ArrayList<>();
    for (String token : tokens) {
      messages.add("Warning: use of non-standard feature '" + token + "'");
    }
    return throwables -> {
      assertThat(throwables, hasSize(messages.size()));
      for (Pair<? extends Throwable, String> pair : Pair.zip(throwables, messages)) {
        assertThat(pair.left.getMessage(), containsString(pair.right));
      }
    };
  }

  @Test void testMiscIntervalQualifier() {
    expr("interval '-' day")
        .ok("INTERVAL '-' DAY");

    expr("interval '1 2:3:4.567' day to hour ^to^ second")
        .fails("(?s)Encountered \"to\" at.*");
    expr("interval '1:2' minute to second(2^,^ 2)")
        .fails("(?s)Encountered \",\" at.*");
    expr("interval '1:x' hour to minute")
        .ok("INTERVAL '1:x' HOUR TO MINUTE");
    expr("interval '1:x:2' hour to second")
        .ok("INTERVAL '1:x:2' HOUR TO SECOND");
  }

  @Test void testIntervalExpression() {
    expr("interval 0 day").ok("INTERVAL 0 DAY");
    expr("interval 0 days").ok("INTERVAL 0 DAY");
    expr("interval -10 days").ok("INTERVAL (- 10) DAY");
    expr("interval -10 days").ok("INTERVAL (- 10) DAY");
    // parser requires parentheses for expressions other than numeric
    // literal or identifier
    expr("interval 1 ^+^ x.y days")
        .fails("(?s)Encountered \"\\+\" at .*");
    expr("interval (1 + x.y) days")
        .ok("INTERVAL (1 + `X`.`Y`) DAY");
    expr("interval -x second(3)")
        .ok("INTERVAL (- `X`) SECOND(3)");
    expr("interval -x.y second(3)")
        .ok("INTERVAL (- `X`.`Y`) SECOND(3)");
    expr("interval 1 day ^to^ hour")
        .fails("(?s)Encountered \"to\" at .*");
    expr("interval '1 1' day to hour").ok("INTERVAL '1 1' DAY TO HOUR");
  }

  @Test void testIntervalOperators() {
    expr("-interval '1' day")
        .ok("(- INTERVAL '1' DAY)");
    expr("interval '1' day + interval '1' day")
        .ok("(INTERVAL '1' DAY + INTERVAL '1' DAY)");
    expr("interval '1' day - interval '1:2:3' hour to second")
        .ok("(INTERVAL '1' DAY - INTERVAL '1:2:3' HOUR TO SECOND)");

    expr("interval -'1' day")
        .ok("INTERVAL -'1' DAY");
    expr("interval '-1' day")
        .ok("INTERVAL '-1' DAY");
    expr("interval 'wael was here^'^")
        .fails("(?s)Encountered \"<EOF>\".*");

    // ok in parser, not in validator
    expr("interval 'wael was here' HOUR")
        .ok("INTERVAL 'wael was here' HOUR");
  }

  @Test void testDateMinusDate() {
    expr("(date1 - date2) HOUR")
        .ok("((`DATE1` - `DATE2`) HOUR)");
    expr("(date1 - date2) YEAR TO MONTH")
        .ok("((`DATE1` - `DATE2`) YEAR TO MONTH)");
    expr("(date1 - date2) HOUR > interval '1' HOUR")
        .ok("(((`DATE1` - `DATE2`) HOUR) > INTERVAL '1' HOUR)");
    expr("^(date1 + date2) second^")
        .fails("(?s).*Illegal expression. "
            + "Was expecting ..DATETIME - DATETIME. INTERVALQUALIFIER.*");
    expr("^(date1,date2,date2) second^")
        .fails("(?s).*Illegal expression. "
            + "Was expecting ..DATETIME - DATETIME. INTERVALQUALIFIER.*");
  }

  @Test void testExtract() {
    expr("extract(year from x)")
        .ok("EXTRACT(YEAR FROM `X`)");
    expr("extract(month from x)")
        .ok("EXTRACT(MONTH FROM `X`)");
    expr("extract(day from x)")
        .ok("EXTRACT(DAY FROM `X`)");
    expr("extract(hour from x)")
        .ok("EXTRACT(HOUR FROM `X`)");
    expr("extract(minute from x)")
        .ok("EXTRACT(MINUTE FROM `X`)");
    expr("extract(second from x)")
        .ok("EXTRACT(SECOND FROM `X`)");
    expr("extract(dow from x)")
        .ok("EXTRACT(DOW FROM `X`)");
    expr("extract(doy from x)")
        .ok("EXTRACT(DOY FROM `X`)");
    expr("extract(week from x)")
        .ok("EXTRACT(WEEK FROM `X`)");
    expr("extract(epoch from x)")
        .ok("EXTRACT(EPOCH FROM `X`)");
    expr("extract(quarter from x)")
        .ok("EXTRACT(QUARTER FROM `X`)");
    expr("extract(decade from x)")
        .ok("EXTRACT(DECADE FROM `X`)");
    expr("extract(century from x)")
        .ok("EXTRACT(CENTURY FROM `X`)");
    expr("extract(millennium from x)")
        .ok("EXTRACT(MILLENNIUM FROM `X`)");

    expr("extract(day ^to^ second from x)")
        .fails("(?s)Encountered \"to\".*");
  }

  /** Tests that EXTRACT, FLOOR, CEIL, DATE_TRUNC functions accept abbreviations for
   * time units (such as "Y" for "YEAR") when configured via
   * {@link Config#timeUnitCodes()}. */
  @Test protected void testTimeUnitCodes() {
    // YEAR is a built-in time frame. When unparsed, it looks like a keyword.
    // (Note no backticks around YEAR.)
    expr("floor(d to year)")
        .ok("FLOOR(`D` TO YEAR)");
    // Y is an extension time frame. (Or rather, it could be, if you configure
    // it.) When unparsed, it looks like an identifier (backticks around Y.)
    expr("floor(d to y)")
        .ok("FLOOR(`D` TO `Y`)");

    // As for FLOOR, so for CEIL.
    expr("ceil(d to year)").ok("CEIL(`D` TO YEAR)");
    expr("ceil(d to y)").ok("CEIL(`D` TO `Y`)");

    // CEILING is a synonym for CEIL.
    expr("ceiling(d to year)").ok("CEIL(`D` TO YEAR)");
    expr("ceiling(d to y)").ok("CEIL(`D` TO `Y`)");

    // As for FLOOR, so for EXTRACT.
    expr("extract(year from d)").ok("EXTRACT(YEAR FROM `D`)");
    expr("extract(y from d)").ok("EXTRACT(`Y` FROM `D`)");

    // MICROSECOND, NANOSECOND used to be native for EXTRACT but not for FLOOR
    // or CEIL. Now they are native, and so appear as keywords, without
    // backticks.
    expr("floor(d to nanosecond)").ok("FLOOR(`D` TO NANOSECOND)");
    expr("floor(d to microsecond)").ok("FLOOR(`D` TO MICROSECOND)");
    expr("ceil(d to nanosecond)").ok("CEIL(`D` TO NANOSECOND)");
    expr("ceiling(d to microsecond)").ok("CEIL(`D` TO MICROSECOND)");
    expr("extract(nanosecond from d)").ok("EXTRACT(NANOSECOND FROM `D`)");
    expr("extract(microsecond from d)").ok("EXTRACT(MICROSECOND FROM `D`)");

    // As for FLOOR, so for DATE_TRUNC.
    expr("date_trunc(d , year)").ok("(DATE_TRUNC(`D`, YEAR))");
    expr("date_trunc(d , y)").ok("(DATE_TRUNC(`D`, `Y`))");
    expr("date_trunc(d , week(tuesday))").ok("(DATE_TRUNC(`D`, `WEEK_TUESDAY`))");
  }

  @Test void testGeometry() {
    expr("cast(null as ^geometry^)")
        .fails("Geo-spatial extensions and the GEOMETRY data type are not enabled");
    expr("cast(null as geometry)")
        .withConformance(SqlConformanceEnum.LENIENT)
        .ok("CAST(NULL AS GEOMETRY)");
  }

  @Test void testIntervalArithmetics() {
    expr("TIME '23:59:59' - interval '1' hour ")
        .ok("(TIME '23:59:59' - INTERVAL '1' HOUR)");
    expr("TIMESTAMP '2000-01-01 23:59:59.1' - interval '1' hour ")
        .ok("(TIMESTAMP '2000-01-01 23:59:59.1' - INTERVAL '1' HOUR)");
    expr("DATE '2000-01-01' - interval '1' hour ")
        .ok("(DATE '2000-01-01' - INTERVAL '1' HOUR)");

    expr("TIME '23:59:59' + interval '1' hour ")
        .ok("(TIME '23:59:59' + INTERVAL '1' HOUR)");
    expr("TIMESTAMP '2000-01-01 23:59:59.1' + interval '1' hour ")
        .ok("(TIMESTAMP '2000-01-01 23:59:59.1' + INTERVAL '1' HOUR)");
    expr("DATE '2000-01-01' + interval '1' hour ")
        .ok("(DATE '2000-01-01' + INTERVAL '1' HOUR)");

    expr("interval '1' hour + TIME '23:59:59' ")
        .ok("(INTERVAL '1' HOUR + TIME '23:59:59')");

    expr("interval '1' hour * 8")
        .ok("(INTERVAL '1' HOUR * 8)");
    expr("1 * interval '1' hour")
        .ok("(1 * INTERVAL '1' HOUR)");
    expr("interval '1' hour / 8")
        .ok("(INTERVAL '1' HOUR / 8)");
  }

  @Test void testIntervalCompare() {
    expr("interval '1' hour = interval '1' second")
        .ok("(INTERVAL '1' HOUR = INTERVAL '1' SECOND)");
    expr("interval '1' hour <> interval '1' second")
        .ok("(INTERVAL '1' HOUR <> INTERVAL '1' SECOND)");
    expr("interval '1' hour < interval '1' second")
        .ok("(INTERVAL '1' HOUR < INTERVAL '1' SECOND)");
    expr("interval '1' hour <= interval '1' second")
        .ok("(INTERVAL '1' HOUR <= INTERVAL '1' SECOND)");
    expr("interval '1' hour > interval '1' second")
        .ok("(INTERVAL '1' HOUR > INTERVAL '1' SECOND)");
    expr("interval '1' hour >= interval '1' second")
        .ok("(INTERVAL '1' HOUR >= INTERVAL '1' SECOND)");
  }

  @Test void testCastToInterval() {
    expr("cast(x as interval year)")
        .ok("CAST(`X` AS INTERVAL YEAR)");
    expr("cast(x as interval month)")
        .ok("CAST(`X` AS INTERVAL MONTH)");
    expr("cast(x as interval year to month)")
        .ok("CAST(`X` AS INTERVAL YEAR TO MONTH)");
    expr("cast(x as interval day)")
        .ok("CAST(`X` AS INTERVAL DAY)");
    expr("cast(x as interval hour)")
        .ok("CAST(`X` AS INTERVAL HOUR)");
    expr("cast(x as interval minute)")
        .ok("CAST(`X` AS INTERVAL MINUTE)");
    expr("cast(x as interval second)")
        .ok("CAST(`X` AS INTERVAL SECOND)");
    expr("cast(x as interval day to hour)")
        .ok("CAST(`X` AS INTERVAL DAY TO HOUR)");
    expr("cast(x as interval day to minute)")
        .ok("CAST(`X` AS INTERVAL DAY TO MINUTE)");
    expr("cast(x as interval day to second)")
        .ok("CAST(`X` AS INTERVAL DAY TO SECOND)");
    expr("cast(x as interval hour to minute)")
        .ok("CAST(`X` AS INTERVAL HOUR TO MINUTE)");
    expr("cast(x as interval hour to second)")
        .ok("CAST(`X` AS INTERVAL HOUR TO SECOND)");
    expr("cast(x as interval minute to second)")
        .ok("CAST(`X` AS INTERVAL MINUTE TO SECOND)");
    expr("cast(interval '3-2' year to month as CHAR(5))")
        .ok("CAST(INTERVAL '3-2' YEAR TO MONTH AS CHAR(5))");
  }

  @Test void testCastToVarchar() {
    expr("cast(x as varchar(5))")
        .ok("CAST(`X` AS VARCHAR(5))");
    expr("cast(x as varchar)")
        .ok("CAST(`X` AS VARCHAR)");
    expr("cast(x as varBINARY(5))")
        .ok("CAST(`X` AS VARBINARY(5))");
    expr("cast(x as varbinary)")
        .ok("CAST(`X` AS VARBINARY)");
  }

  @Test void testTimestampAdd() {
    final String sql = "select * from t\n"
        + "where timestampadd(month, 5, hiredate) < curdate";
    final String expected = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (TIMESTAMPADD(MONTH, 5, `HIREDATE`) < `CURDATE`)";
    sql(sql).ok(expected);

    // SQL_TSI_MONTH is treated as a user-defined time frame, hence appears as
    // an identifier (with backticks) when unparsed. It will be resolved to
    // MICROSECOND during validation.
    final String sql2 = "select * from t\n"
        + "where timestampadd(sql_tsi_month, 5, hiredate) < curdate";
    final String expected2 = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (TIMESTAMPADD(`SQL_TSI_MONTH`, 5, `HIREDATE`) < `CURDATE`)";
    sql(sql2).ok(expected2);

    // Previously a parse error, now an identifier that the validator will find
    // to be invalid.
    expr("timestampadd(incorrect, 1, current_timestamp)")
        .ok("TIMESTAMPADD(`INCORRECT`, 1, CURRENT_TIMESTAMP)");
  }

  @Test void testTimestampDiff() {
    final String sql = "select * from t\n"
        + "where timestampdiff(microsecond, 5, hiredate) < curdate";
    final String expected = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (TIMESTAMPDIFF(MICROSECOND, 5, `HIREDATE`) < `CURDATE`)";
    sql(sql).ok(expected);

    // FRAC_SECOND is treated as a user-defined time frame, hence appears as
    // an identifier (with backticks) when unparsed. It will be resolved to
    // MICROSECOND during validation.
    final String sql2 = "select * from t\n"
        + "where timestampdiff(frac_second, 5, hiredate) < curdate";
    final String expected2 = "SELECT *\n"
        + "FROM `T`\n"
        + "WHERE (TIMESTAMPDIFF(`FRAC_SECOND`, 5, `HIREDATE`) < `CURDATE`)";
    sql(sql2).ok(expected2);

    // Previously a parse error, now an identifier that the validator will find
    // to be invalid.
    expr("timestampdiff(incorrect, current_timestamp, current_timestamp)")
        .ok("TIMESTAMPDIFF(`INCORRECT`, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)");
  }

  @Test void testTimeTrunc() {
    final String sql = "select time_trunc(TIME '15:30:00', hour) from t";
    final String expected = "SELECT TIME_TRUNC(TIME '15:30:00', HOUR)\n"
        + "FROM `T`";
    sql(sql).ok(expected);

    // Syntactically valid; validator will complain because WEEK is for DATE or
    // TIMESTAMP but not for TIME.
    final String sql2 = "select time_trunc(time '15:30:00', week) from t";
    final String expected2 = "SELECT TIME_TRUNC(TIME '15:30:00', WEEK)\n"
        + "FROM `T`";
    sql(sql2).ok(expected2);

    // note backticks
    final String sql3 = "select time_trunc(time '15:30:00', incorrect) from t";
    final String expected3 = "SELECT TIME_TRUNC(TIME '15:30:00', `INCORRECT`)\n"
        + "FROM `T`";
    sql(sql3).ok(expected3);
  }

  @Test void testTimestampTrunc() {
    final String sql = "select timestamp_trunc(timestamp '2008-12-25 15:30:00', week) from t";
    final String expected = "SELECT TIMESTAMP_TRUNC(TIMESTAMP '2008-12-25 15:30:00', WEEK)\n"
        + "FROM `T`";
    sql(sql).ok(expected);

    // note backticks
    final String sql3 = "select timestamp_trunc(time '15:30:00', incorrect) from t";
    String expected3 = "SELECT TIMESTAMP_TRUNC(TIME '15:30:00', `INCORRECT`)\n"
        + "FROM `T`";
    sql(sql3).ok(expected3);
  }

  @Test void testUnnest() {
    sql("select*from unnest(x)")
        .ok("SELECT *\n"
            + "FROM UNNEST(`X`)");
    sql("select*from unnest(x) AS T")
        .ok("SELECT *\n"
            + "FROM UNNEST(`X`) AS `T`");

    // UNNEST cannot be first word in query
    sql("^unnest^(x)")
        .fails("(?s)Encountered \"unnest\" at.*");

    // UNNEST with more than one argument
    final String sql = "select * from dept,\n"
        + "unnest(dept.employees, dept.managers)";
    final String expected = "SELECT *\n"
        + "FROM `DEPT`,\n"
        + "UNNEST(`DEPT`.`EMPLOYEES`, `DEPT`.`MANAGERS`)";
    sql(sql).ok(expected);

    // LATERAL UNNEST is not valid
    sql("select * from dept, lateral ^unnest^(dept.employees)")
        .fails("(?s)Encountered \"unnest\" at .*");

    // Does not generate extra parentheses around UNNEST because UNNEST is
    // a table expression.
    final String sql1 = ""
        + "SELECT\n"
        + "  item.name,\n"
        + "  relations.*\n"
        + "FROM dfs.tmp item\n"
        + "JOIN (\n"
        + "  SELECT * FROM UNNEST(item.related) i(rels)\n"
        + ") relations\n"
        + "ON TRUE";
    final String expected1 = "SELECT `ITEM`.`NAME`, `RELATIONS`.*\n"
        + "FROM `DFS`.`TMP` AS `ITEM`\n"
        + "INNER JOIN (SELECT *\n"
        + "FROM UNNEST(`ITEM`.`RELATED`) AS `I` (`RELS`)) AS `RELATIONS` ON TRUE";
    sql(sql1).ok(expected1);
  }

  @Test void testUnnestWithOrdinality() {
    sql("select * from unnest(x) with ordinality")
        .ok("SELECT *\n"
            + "FROM UNNEST(`X`) WITH ORDINALITY");
    sql("select*from unnest(x) with ordinality AS T")
        .ok("SELECT *\n"
            + "FROM UNNEST(`X`) WITH ORDINALITY AS `T`");
    sql("select*from unnest(x) with ordinality AS T(c, o)")
        .ok("SELECT *\n"
            + "FROM UNNEST(`X`) WITH ORDINALITY AS `T` (`C`, `O`)");
    sql("select*from unnest(x) as T ^with^ ordinality")
        .fails("(?s)Encountered \"with\" at .*");
  }

  @Test void testParensInFrom() {
    // UNNEST may not occur within parentheses.
    // FIXME should fail at "unnest"
    sql("select *from (^unnest(x)^)")
        .fails("Expected query or join");

    // <table-name> may not occur within parentheses.
    // TODO: Postgres gives "syntax error at ')'", which might be better
    sql("select * from (^emp^)")
        .fails("(?s)Expected query or join.*");

    // <table-name> may not occur within parentheses.
    // TODO: Postgres gives "syntax error at ')'", which might be better
    sql("select * from (^emp as x^)")
        .fails("Expected query or join");

    // <table-name> may not occur within parentheses.
    sql("select * from (^emp^) as x")
        .fails("Expected query or join");

    // Parentheses around JOINs are OK, and sometimes necessary.
    String sql1 = "select *\n"
        + "from (emp join dept using (deptno))";
    String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "INNER JOIN `DEPT` USING (`DEPTNO`)";
    sql(sql1).ok(expected);

    String sql2 = "select *\n"
        + "from (emp join dept using (deptno))\n"
        + "join foo using (x)";
    String expected2 = "SELECT *\n"
        + "FROM `EMP`\n"
        + "INNER JOIN `DEPT` USING (`DEPTNO`)\n"
        + "INNER JOIN `FOO` USING (`X`)";
    sql(sql2).ok(expected2);

    // In Postgres and Standard SQL, you can alias a join:
    //   "select x.i from (t cross join u) as x"
    // is syntactically and semantically valid; but
    //   "select t.i from (t cross join u) as x"
    // is semantically invalid.
    // TODO: Support this in Calcite.
    sql("select * from (t cross ^join^ u) as x")
        .fails("Join expression encountered in illegal context");
    sql("select *\n"
        + "from (t cross ^join^ u)\n"
        + "  tablesample substitute('medium')")
        .fails("Join expression encountered in illegal context");
    sql("select *\n"
        + "from (t cross ^join^ u)\n"
        + "PIVOT (sum(sal) AS sal FOR job in ('CLERK' AS c))")
        .fails("Join expression encountered in illegal context");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-35">[CALCITE-35]
   * Support parenthesized sub-clause in JOIN</a>. */
  @Test void testParenthesizedJoins() {
    final String sql = "SELECT * FROM "
        + "(((S.C c INNER JOIN S.N n ON n.id = c.id) "
        + "INNER JOIN S.A a ON (NOT a.isactive)) "
        + "INNER JOIN S.T t ON t.id = a.id)";
    final String expected = "SELECT *\n"
        + "FROM `S`.`C` AS `C`\n"
        + "INNER JOIN `S`.`N` AS `N` ON (`N`.`ID` = `C`.`ID`)\n"
        + "INNER JOIN `S`.`A` AS `A` ON (NOT `A`.`ISACTIVE`)\n"
        + "INNER JOIN `S`.`T` AS `T` ON (`T`.`ID` = `A`.`ID`)";
    sql(sql).ok(expected);

    final String sql2 = "select *\n"
        + "from a\n"
        + " join (b join c on b.x = c.x)\n"
        + " on a.y = c.y";
    final String expected2 = "SELECT *\n"
        + "FROM `A`\n"
        + "INNER JOIN (`B` INNER JOIN `C` ON (`B`.`X` = `C`.`X`))"
        + " ON (`A`.`Y` = `C`.`Y`)";
    sql(sql2).ok(expected2);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-5194">[CALCITE-5194]
   * Cannot parse parenthesized UNION in FROM</a>. */
  @Test void testParenthesizedUnionInFrom() {
    final String sql = "select *\n"
        + "from (\n"
        + "  (select x from a)\n"
        + "  union\n"
        + "  (select y from b))";
    final String expected = "SELECT *\n"
        + "FROM (SELECT `X`\n"
        + "FROM `A`\n"
        + "UNION\n"
        + "SELECT `Y`\n"
        + "FROM `B`)";
    sql(sql).ok(expected);
  }

  @Test void testParenthesizedUnionAndJoinInFrom() {
    final String sql = "select *\n"
        + "from (\n"
        + "  (select x from a) as a"
        + "  cross join\n"
        + "  (select x from a\n"
        + "  union\n"
        + "  select y from b) as b)";
    final String expected = "SELECT *\n"
        + "FROM (SELECT `X`\n"
        + "FROM `A`) AS `A`\n"
        + "CROSS JOIN (SELECT `X`\n"
        + "FROM `A`\n"
        + "UNION\n"
        + "SELECT `Y`\n"
        + "FROM `B`) AS `B`";
    sql(sql).ok(expected);
  }

  /** As {@link #testParenthesizedUnionAndJoinInFrom()}
   * but the UNION is the first input to the JOIN. */
  @Test void testParenthesizedUnionAndJoinInFrom2() {
    final String sql = "select *\n"
        + "from (\n"
        + "  (select x from a\n"
        + "  union\n"
        + "  select y from b) as b\n"
        + "  cross join\n"
        + "  (select x from a) as a)";
    final String expected = "SELECT *\n"
        + "FROM (SELECT `X`\n"
        + "FROM `A`\n"
        + "UNION\n"
        + "SELECT `Y`\n"
        + "FROM `B`) AS `B`\n"
        + "CROSS JOIN (SELECT `X`\n"
        + "FROM `A`) AS `A`";
    sql(sql).ok(expected);
  }

  /** As {@link #testParenthesizedUnionAndJoinInFrom2()}
   * but INNER JOIN rather than CROSS JOIN. */
  @Test void testParenthesizedUnionAndJoinInFrom3() {
    final String sql = "select *\n"
        + "from (\n"
        + "  (select x from a\n"
        + "  union\n"
        + "  select y from b) as b\n"
        + "  join\n"
        + "  (select x from a) as a on b.x = a.x)";
    final String expected = "SELECT *\n"
        + "FROM (SELECT `X`\n"
        + "FROM `A`\n"
        + "UNION\n"
        + "SELECT `Y`\n"
        + "FROM `B`) AS `B`\n"
        + "INNER JOIN (SELECT `X`\n"
        + "FROM `A`) AS `A` ON (`B`.`X` = `A`.`X`)";
    sql(sql).ok(expected);
  }

  @Test void testParenthesizedUnion() {
    final String sql = "(select x from a\n"
        + "  union\n"
        + "  select y from b)\n"
        + "except\n"
        + "(select z from c)";
    final String expected = "SELECT `X`\n"
        + "FROM `A`\n"
        + "UNION\n"
        + "SELECT `Y`\n"
        + "FROM `B`\n"
        + "EXCEPT\n"
        + "SELECT `Z`\n"
        + "FROM `C`";
    sql(sql).ok(expected);
  }

  @Test void testFromExpr() {
    String sql0 = "select * from a cross join b";
    String sql1 = "select * from (a cross join b)";
    String expected = "SELECT *\n"
        + "FROM `A`\n"
        + "CROSS JOIN `B`";
    sql(sql0).ok(expected);
    sql(sql1).ok(expected);
  }

  /** Tests parsing parenthesized queries. */
  @Test void testParenthesizedQueries() {
    final String expected = "SELECT *\n"
        + "FROM (SELECT *\n"
        + "FROM `TAB`) AS `X`";
    final String sql1 = "SELECT *\n"
        + "FROM (((SELECT * FROM tab))) X";
    final String sql2 = "SELECT *\n"
        + "FROM ((((((((((((SELECT * FROM tab)))))))))))) X";
    sql(sql1).ok(expected);
    sql(sql2).ok(expected);

    final String sql3 = "SELECT *\n"
        + "FROM ((((((((((((SELECT * FROM t)))\n"
        + "  cross join ((table t2)))))))))))";
    final String expected3 = "SELECT *\n"
        + "FROM (SELECT *\n"
        + "FROM `T`)\n"
        + "CROSS JOIN (TABLE `T2`)";
    sql(sql3).ok(expected3);

    // Adding an alias to the previous query makes it invalid
    // (The error message and location could be improved)
    final String sql4 = "SELECT *\n"
        + "FROM ((((((((((((SELECT * FROM t)))\n"
        + "  cross ^join^ ((table t2))))))))))) X";
    final String sql5 = "SELECT *\n"
        + "FROM ((((((((((((SELECT * FROM t)))\n"
        + "  cross ^join^ ((table t2))))))))))) as X";
    final String sql6 = "SELECT *\n"
        + "FROM ((((((((((((SELECT * FROM t)))\n"
        + "  cross ^join^ ((table t2))))))))))) as X (a, b, c)";
    final String message = "Join expression encountered in illegal context";
    sql(sql4).fails(message);
    sql(sql5).fails(message);
    sql(sql6).fails(message);
  }

  @Test void testProcedureCall() {
    sql("call blubber(5)")
        .ok("CALL `BLUBBER`(5)");
    sql("call \"blubber\"(5)")
        .ok("CALL `blubber`(5)");
    sql("call whale.blubber(5)")
        .ok("CALL `WHALE`.`BLUBBER`(5)");
  }

  @Test void testNewSpecification() {
    expr("new udt()")
        .ok("(NEW `UDT`())");
    expr("new my.udt(1, 'hey')")
        .ok("(NEW `MY`.`UDT`(1, 'hey'))");
    expr("new udt() is not null")
        .ok("((NEW `UDT`()) IS NOT NULL)");
    expr("1 + new udt()")
        .ok("(1 + (NEW `UDT`()))");
  }

  @Test void testMultisetCast() {
    expr("cast(multiset[1] as double multiset)")
        .ok("CAST((MULTISET[1]) AS DOUBLE MULTISET)");
  }

  @Test void testAddCarets() {
    assertEquals(
        "values (^foo^)",
        SqlParserUtil.addCarets("values (foo)", 1, 9, 1, 12));
    assertEquals(
        "abc^def",
        SqlParserUtil.addCarets("abcdef", 1, 4, 1, 4));
    assertEquals(
        "abcdef^",
        SqlParserUtil.addCarets("abcdef", 1, 7, 1, 7));
  }

  @Test void testSnapshotForSystemTimeWithAlias() {
    sql("SELECT * FROM orders LEFT JOIN products FOR SYSTEM_TIME AS OF "
        + "orders.proctime as products ON orders.product_id = products.pro_id")
        .ok("SELECT *\n"
            + "FROM `ORDERS`\n"
            + "LEFT JOIN `PRODUCTS` FOR SYSTEM_TIME AS OF `ORDERS`.`PROCTIME` AS `PRODUCTS` ON (`ORDERS`"
            + ".`PRODUCT_ID` = `PRODUCTS`.`PRO_ID`)");
  }

  @Test protected void testMetadata() {
    SqlAbstractParserImpl.Metadata metadata = sql("").parser().getMetadata();
    assertThat(metadata.isReservedFunctionName("ABS"), is(true));
    assertThat(metadata.isReservedFunctionName("FOO"), is(false));

    assertThat(metadata.isContextVariableName("CURRENT_USER"), is(true));
    assertThat(metadata.isContextVariableName("CURRENT_CATALOG"), is(true));
    assertThat(metadata.isContextVariableName("CURRENT_SCHEMA"), is(true));
    assertThat(metadata.isContextVariableName("ABS"), is(false));
    assertThat(metadata.isContextVariableName("FOO"), is(false));

    assertThat(metadata.isNonReservedKeyword("A"), is(true));
    assertThat(metadata.isNonReservedKeyword("KEY"), is(true));
    assertThat(metadata.isNonReservedKeyword("SELECT"), is(false));
    assertThat(metadata.isNonReservedKeyword("FOO"), is(false));
    assertThat(metadata.isNonReservedKeyword("ABS"), is(false));

    assertThat(metadata.isKeyword("ABS"), is(true));
    assertThat(metadata.isKeyword("CURRENT_USER"), is(true));
    assertThat(metadata.isKeyword("CURRENT_CATALOG"), is(true));
    assertThat(metadata.isKeyword("CURRENT_SCHEMA"), is(true));
    assertThat(metadata.isKeyword("KEY"), is(true));
    assertThat(metadata.isKeyword("SELECT"), is(true));
    assertThat(metadata.isKeyword("HAVING"), is(true));
    assertThat(metadata.isKeyword("A"), is(true));
    assertThat(metadata.isKeyword("BAR"), is(false));

    assertThat(metadata.isReservedWord("SELECT"), is(true));
    assertThat(metadata.isReservedWord("CURRENT_CATALOG"), is(true));
    assertThat(metadata.isReservedWord("CURRENT_SCHEMA"), is(true));
    assertThat(metadata.isReservedWord("KEY"), is(false));

    String jdbcKeywords = metadata.getJdbcKeywords();
    assertThat(jdbcKeywords.contains(",COLLECT,"), is(true));
    assertThat(!jdbcKeywords.contains(",SELECT,"), is(true));
  }

  @Test void testTabStop() {
    sql("SELECT *\n\tFROM mytable")
        .ok("SELECT *\n"
            + "FROM `MYTABLE`");

    // make sure that the tab stops do not affect the placement of the
    // error tokens
    sql("SELECT *\tFROM mytable\t\tWHERE x ^=^ = y AND b = 1")
        .fails("(?s).*Encountered \"= =\" at line 1, column 32\\..*");
  }

  @Test void testLongIdentifiers() {
    StringBuilder ident128Builder = new StringBuilder();
    for (int i = 0; i < 128; i++) {
      ident128Builder.append((char) ('a' + (i % 26)));
    }
    String ident128 = ident128Builder.toString();
    String ident128Upper = ident128.toUpperCase(Locale.US);
    String ident129 = "x" + ident128;
    String ident129Upper = ident129.toUpperCase(Locale.US);

    sql("select * from " + ident128)
        .ok("SELECT *\n"
            + "FROM `" + ident128Upper + "`");
    sql("select * from ^" + ident129 + "^")
        .fails("Length of identifier '" + ident129Upper
            + "' must be less than or equal to 128 characters");

    sql("select " + ident128 + " from mytable")
        .ok("SELECT `" + ident128Upper + "`\n"
            + "FROM `MYTABLE`");
    sql("select ^" + ident129 + "^ from mytable")
        .fails("Length of identifier '" + ident129Upper
            + "' must be less than or equal to 128 characters");
  }

  /**
   * Tests that you can't quote the names of builtin functions.
   *
   * <p>See
   * {@code org.apache.calcite.test.SqlValidatorTest#testQuotedFunction()}.
   */
  @Test void testQuotedFunction() {
    expr("\"CAST\"(1 ^as^ double)")
        .fails("(?s).*Encountered \"as\" at .*");
    expr("\"POSITION\"('b' ^in^ 'alphabet')")
        .fails("(?s).*Encountered \"in \\\\'alphabet\\\\'\" at .*");
    expr("\"OVERLAY\"('a' ^PLAcing^ 'b' from 1)")
        .fails("(?s).*Encountered \"PLAcing\" at.*");
    expr("\"SUBSTRING\"('a' ^from^ 1)")
        .fails("(?s).*Encountered \"from\" at .*");
  }

  /** Tests applying a member function of a specific type as a suffix
   * function. */
  @Test void testMemberFunction() {
    sql("SELECT myColumn.func(a, b) FROM tbl")
        .ok("SELECT `MYCOLUMN`.`FUNC`(`A`, `B`)\n"
            + "FROM `TBL`");
    sql("SELECT myColumn.mySubField.func() FROM tbl")
        .ok("SELECT `MYCOLUMN`.`MYSUBFIELD`.`FUNC`()\n"
            + "FROM `TBL`");
    sql("SELECT tbl.myColumn.mySubField.func() FROM tbl")
        .ok("SELECT `TBL`.`MYCOLUMN`.`MYSUBFIELD`.`FUNC`()\n"
            + "FROM `TBL`");
    sql("SELECT tbl.foo(0).col.bar(2, 3) FROM tbl")
        .ok("SELECT ((`TBL`.`FOO`(0).`COL`).`BAR`(2, 3))\n"
            + "FROM `TBL`");
  }

  @Test void testUnicodeLiteral() {
    // Note that here we are constructing a SQL statement which directly
    // contains Unicode characters (not SQL Unicode escape sequences).  The
    // escaping here is Java-only, so by the time it gets to the SQL
    // parser, the literal already contains Unicode characters.
    String in1 =
        "values _UTF16'"
            + ConversionUtil.TEST_UNICODE_STRING + "'";
    String out1 =
        "VALUES (ROW(_UTF16'"
            + ConversionUtil.TEST_UNICODE_STRING + "'))";
    sql(in1).ok(out1);

    // Without the U& prefix, escapes are left unprocessed
    String in2 =
        "values '"
            + ConversionUtil.TEST_UNICODE_SQL_ESCAPED_LITERAL + "'";
    String out2 =
        "VALUES (ROW('"
            + ConversionUtil.TEST_UNICODE_SQL_ESCAPED_LITERAL + "'))";
    sql(in2).ok(out2);

    // Likewise, even with the U& prefix, if some other escape
    // character is specified, then the backslash-escape
    // sequences are not interpreted
    String in3 =
        "values U&'"
            + ConversionUtil.TEST_UNICODE_SQL_ESCAPED_LITERAL
            + "' UESCAPE '!'";
    String out3 =
        "VALUES (ROW(_UTF16'"
            + ConversionUtil.TEST_UNICODE_SQL_ESCAPED_LITERAL + "'))";
    sql(in3).ok(out3);
  }

  @Test void testUnicodeEscapedLiteral() {
    // Note that here we are constructing a SQL statement which
    // contains SQL-escaped Unicode characters to be handled
    // by the SQL parser.
    String in =
        "values U&'"
            + ConversionUtil.TEST_UNICODE_SQL_ESCAPED_LITERAL + "'";
    String out =
        "VALUES (ROW(_UTF16'"
            + ConversionUtil.TEST_UNICODE_STRING + "'))";
    sql(in).ok(out);

    // Verify that we can override with an explicit escape character
    sql(in.replace("\\", "!") + "UESCAPE '!'").ok(out);
  }

  @Test void testIllegalUnicodeEscape() {
    expr("U&'abc' UESCAPE '!!'")
        .fails(".*must be exactly one character.*");
    expr("U&'abc' UESCAPE ''")
        .fails(".*must be exactly one character.*");
    expr("U&'abc' UESCAPE '0'")
        .fails(".*hex digit.*");
    expr("U&'abc' UESCAPE 'a'")
        .fails(".*hex digit.*");
    expr("U&'abc' UESCAPE 'F'")
        .fails(".*hex digit.*");
    expr("U&'abc' UESCAPE ' '")
        .fails(".*whitespace.*");
    expr("U&'abc' UESCAPE '+'")
        .fails(".*plus sign.*");
    expr("U&'abc' UESCAPE '\"'")
        .fails(".*double quote.*");
    expr("'abc' UESCAPE ^'!'^")
        .fails(".*without Unicode literal introducer.*");
    expr("^U&'\\0A'^")
        .fails(".*is not exactly four hex digits.*");
    expr("^U&'\\wxyz'^")
        .fails(".*is not exactly four hex digits.*");
  }

  @Test void testSqlOptions() {
    SqlNode node = sql("alter system set schema = true").node();
    SqlSetOption opt = (SqlSetOption) node;
    assertThat(opt.getScope(), equalTo("SYSTEM"));
    SqlPrettyWriter writer = new SqlPrettyWriter();
    assertThat(writer.format(opt.getName()), equalTo("\"SCHEMA\""));
    writer = new SqlPrettyWriter();
    assertThat(writer.format(opt.getValue()), equalTo("TRUE"));
    writer = new SqlPrettyWriter();
    assertThat(writer.format(opt),
        equalTo("ALTER SYSTEM SET \"SCHEMA\" = TRUE"));

    sql("alter system set \"a number\" = 1")
        .ok("ALTER SYSTEM SET `a number` = 1")
        .node(isDdl());
    sql("alter system set flag = false")
        .ok("ALTER SYSTEM SET `FLAG` = FALSE");
    sql("alter system set approx = -12.3450")
        .ok("ALTER SYSTEM SET `APPROX` = -12.3450");
    sql("alter system set onOff = on")
        .ok("ALTER SYSTEM SET `ONOFF` = `ON`");
    sql("alter system set onOff = off")
        .ok("ALTER SYSTEM SET `ONOFF` = `OFF`");
    sql("alter system set baz = foo")
        .ok("ALTER SYSTEM SET `BAZ` = `FOO`");


    sql("alter system set \"a\".\"number\" = 1")
        .ok("ALTER SYSTEM SET `a`.`number` = 1");
    sql("set approx = -12.3450")
        .ok("SET `APPROX` = -12.3450")
        .node(isDdl());

    node = sql("reset schema").node();
    opt = (SqlSetOption) node;
    assertThat(opt.getScope(), equalTo(null));
    writer = new SqlPrettyWriter();
    assertThat(writer.format(opt.getName()), equalTo("\"SCHEMA\""));
    assertThat(opt.getValue(), equalTo(null));
    writer = new SqlPrettyWriter();
    assertThat(writer.format(opt),
        equalTo("RESET \"SCHEMA\""));

    sql("alter system RESET flag")
        .ok("ALTER SYSTEM RESET `FLAG`");
    sql("reset onOff")
        .ok("RESET `ONOFF`")
        .node(isDdl());
    sql("reset \"this\".\"is\".\"sparta\"")
        .ok("RESET `this`.`is`.`sparta`");
    sql("alter system reset all")
        .ok("ALTER SYSTEM RESET `ALL`");
    sql("reset all")
        .ok("RESET `ALL`");

    // expressions not allowed
    sql("alter system set aString = 'abc' ^||^ 'def' ")
        .fails("(?s)Encountered \"\\|\\|\" at line 1, column 34\\..*");

    // multiple assignments not allowed
    sql("alter system set x = 1^,^ y = 2")
        .fails("(?s)Encountered \",\" at line 1, column 23\\..*");
  }

  @Test void testSequence() {
    sql("select next value for my_schema.my_seq from t")
        .ok("SELECT (NEXT VALUE FOR `MY_SCHEMA`.`MY_SEQ`)\n"
            + "FROM `T`");
    sql("select next value for my_schema.my_seq as s from t")
        .ok("SELECT (NEXT VALUE FOR `MY_SCHEMA`.`MY_SEQ`) AS `S`\n"
            + "FROM `T`");
    sql("select next value for my_seq as s from t")
        .ok("SELECT (NEXT VALUE FOR `MY_SEQ`) AS `S`\n"
            + "FROM `T`");
    sql("select 1 + next value for s + current value for s from t")
        .ok("SELECT ((1 + (NEXT VALUE FOR `S`)) + (CURRENT VALUE FOR `S`))\n"
            + "FROM `T`");
    sql("select 1 from t where next value for my_seq < 10")
        .ok("SELECT 1\n"
            + "FROM `T`\n"
            + "WHERE ((NEXT VALUE FOR `MY_SEQ`) < 10)");
    sql("select 1 from t\n"
        + "where next value for my_seq < 10 fetch next 3 rows only")
        .ok("SELECT 1\n"
            + "FROM `T`\n"
            + "WHERE ((NEXT VALUE FOR `MY_SEQ`) < 10)\n"
            + "FETCH NEXT 3 ROWS ONLY");
    sql("insert into t values next value for my_seq, current value for my_seq")
        .ok("INSERT INTO `T`\n"
            + "VALUES (ROW((NEXT VALUE FOR `MY_SEQ`))),\n"
            + "(ROW((CURRENT VALUE FOR `MY_SEQ`)))");
    sql("insert into t values (1, current value for my_seq)")
        .ok("INSERT INTO `T`\n"
            + "VALUES (ROW(1, (CURRENT VALUE FOR `MY_SEQ`)))");
  }

  @Test void testPivot() {
    final String sql = "SELECT * FROM emp\n"
        + "PIVOT (sum(sal) AS sal FOR job in ('CLERK' AS c))";
    final String expected = "SELECT *\n"
        + "FROM `EMP` PIVOT (SUM(`SAL`) AS `SAL`"
        + " FOR `JOB` IN ('CLERK' AS `C`))";
    sql(sql).ok(expected);

    // As previous, but parentheses around singleton column.
    final String sql2 = "SELECT * FROM emp\n"
        + "PIVOT (sum(sal) AS sal FOR (job) in ('CLERK' AS c))";
    sql(sql2).ok(expected);
  }

  /** As {@link #testPivot()} but composite FOR and two composite values. */
  @Test void testPivotComposite() {
    final String sql = "SELECT * FROM emp\n"
        + "PIVOT (sum(sal) AS sal FOR (job, deptno) IN\n"
        + " (('CLERK', 10) AS c10, ('MANAGER', 20) AS m20))";
    final String expected = "SELECT *\n"
        + "FROM `EMP` PIVOT (SUM(`SAL`) AS `SAL` FOR (`JOB`, `DEPTNO`)"
        + " IN (('CLERK', 10) AS `C10`, ('MANAGER', 20) AS `M20`))";
    sql(sql).ok(expected);
  }

  /** Pivot with no values. */
  @Test void testPivotWithoutValues() {
    final String sql = "SELECT * FROM emp\n"
        + "PIVOT (sum(sal) AS sal FOR job IN ())";
    final String expected = "SELECT *\n"
        + "FROM `EMP` PIVOT (SUM(`SAL`) AS `SAL` FOR `JOB` IN ())";
    sql(sql).ok(expected);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-4746">[CALCITE-4746]
   * Pivots with pivotAgg without alias fail with Babel Parser Implementation</a>.*/
  @Test void testPivotWithoutAlias() {
    final String sql = "SELECT * FROM emp\n"
        + "PIVOT (sum(sal) FOR job in ('CLERK'))";
    final String expected = "SELECT *\n"
        + "FROM `EMP` PIVOT (SUM(`SAL`)"
        + " FOR `JOB` IN ('CLERK'))";
    sql(sql).ok(expected);
  }

  /** In PIVOT, FOR clause must contain only simple identifiers. */
  @Test void testPivotErrorExpressionInFor() {
    final String sql = "SELECT * FROM emp\n"
        + "PIVOT (sum(sal) AS sal FOR deptno ^-^10 IN (10, 20)";
    sql(sql).fails("(?s)Encountered \"-\" at .*");
  }

  /** As {@link #testPivotErrorExpressionInFor()} but more than one column. */
  @Test void testPivotErrorExpressionInCompositeFor() {
    final String sql = "SELECT * FROM emp\n"
        + "PIVOT (sum(sal) AS sal FOR (job, deptno ^-^10)\n"
        + " IN (('CLERK', 10), ('MANAGER', 20))";
    sql(sql).fails("(?s)Encountered \"-\" at .*");
  }

  /** More complex PIVOT case (multiple aggregates, composite FOR, multiple
   * values with and without aliases). */
  @Test void testPivot2() {
    final String sql = "SELECT *\n"
        + "FROM (SELECT deptno, job, sal\n"
        + "    FROM   emp)\n"
        + "PIVOT (SUM(sal) AS sum_sal, COUNT(*) AS \"COUNT\"\n"
        + "    FOR (job, deptno)\n"
        + "    IN (('CLERK', 10),\n"
        + "        ('MANAGER', 20) mgr20,\n"
        + "        ('ANALYST', 10) AS \"a10\"))\n"
        + "ORDER BY deptno";
    final String expected = "SELECT *\n"
        + "FROM (SELECT `DEPTNO`, `JOB`, `SAL`\n"
        + "FROM `EMP`) PIVOT (SUM(`SAL`) AS `SUM_SAL`, COUNT(*) AS `COUNT` "
        + "FOR (`JOB`, `DEPTNO`) "
        + "IN (('CLERK', 10),"
        + " ('MANAGER', 20) AS `MGR20`,"
        + " ('ANALYST', 10) AS `a10`))\n"
        + "ORDER BY `DEPTNO`";
    sql(sql).ok(expected);
  }

  @Test void testUnpivot() {
    final String sql = "SELECT *\n"
        + "FROM emp_pivoted\n"
        + "UNPIVOT (\n"
        + "  (sum_sal, count_star)\n"
        + "  FOR (job, deptno)\n"
        + "  IN ((c10_ss, c10_c) AS ('CLERK', 10),\n"
        + "      (c20_ss, c20_c) AS ('CLERK', 20),\n"
        + "      (a20_ss, a20_c) AS ('ANALYST', 20)))";
    final String expected = "SELECT *\n"
        + "FROM `EMP_PIVOTED` "
        + "UNPIVOT EXCLUDE NULLS ((`SUM_SAL`, `COUNT_STAR`)"
        + " FOR (`JOB`, `DEPTNO`)"
        + " IN ((`C10_SS`, `C10_C`) AS ('CLERK', 10),"
        + " (`C20_SS`, `C20_C`) AS ('CLERK', 20),"
        + " (`A20_SS`, `A20_C`) AS ('ANALYST', 20)))";
    sql(sql).ok(expected);
  }

  @Test void testPivotThroughShuttle() {
    final String sql = ""
        + "SELECT *\n"
        + "FROM (SELECT job, deptno FROM \"EMP\")\n"
        + "PIVOT (COUNT(*) AS \"COUNT\" FOR deptno IN (10, 50, 20))";
    final String expected = ""
        + "SELECT *\n"
        + "FROM (SELECT `JOB`, `DEPTNO`\n"
        + "FROM `EMP`) PIVOT (COUNT(*) AS `COUNT` FOR `DEPTNO` IN (10, 50, 20))";
    final SqlNode sqlNode = sql(sql).node();

    final SqlNode shuttled = sqlNode.accept(new SqlShuttle() {
      @Override public @Nullable SqlNode visit(final SqlCall call) {
        // Handler always creates a new copy of 'call'
        CallCopyingArgHandler argHandler = new CallCopyingArgHandler(call, true);
        call.getOperator().acceptCall(this, call, false, argHandler);
        return argHandler.result();
      }
    });
    assertThat(toLinux(shuttled.toString()), is(expected));
  }

  @Test void testMatchRecognize1() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    partition by type, price\n"
        + "    order by type asc, price desc\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PARTITION BY `TYPE`, `PRICE`\n"
        + "ORDER BY `TYPE`, `PRICE` DESC\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize2() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down+ up+$)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)) $)\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize3() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (^^strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (^ ((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize4() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (^^strt down+ up+$)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (^ ((`STRT` (`DOWN` +)) (`UP` +)) $)\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize5() {
    final String sql = "select *\n"
        + "  from (select * from t) match_recognize\n"
        + "  (\n"
        + "    pattern (strt down* up?)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM (SELECT *\n"
        + "FROM `T`) MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` *)) (`UP` ?)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize6() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt {-down-} up?)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` ({- `DOWN` -})) (`UP` ?)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize7() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down{2} up{3,})\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` { 2 })) (`UP` { 3, })))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize8() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down{,2} up{3,5})\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` { , 2 })) (`UP` { 3, 5 })))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize9() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt {-down+-} {-up*-})\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` ({- (`DOWN` +) -})) ({- (`UP` *) -})))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize10() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern ( A B C | A C B | B A C | B C A | C A B | C B A)\n"
        + "    define\n"
        + "      A as A.price > PREV(A.price),\n"
        + "      B as B.price < prev(B.price),\n"
        + "      C as C.price > prev(C.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN ((((((((`A` `B`) `C`) | ((`A` `C`) `B`)) | ((`B` `A`) `C`)) "
        + "| ((`B` `C`) `A`)) | ((`C` `A`) `B`)) | ((`C` `B`) `A`)))\n"
        + "DEFINE "
        + "`A` AS (`A`.`PRICE` > PREV(`A`.`PRICE`, 1)), "
        + "`B` AS (`B`.`PRICE` < PREV(`B`.`PRICE`, 1)), "
        + "`C` AS (`C`.`PRICE` > PREV(`C`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognize11() {
    final String sql = "select *\n"
        + "  from t match_recognize (\n"
        + "    pattern ( \"a\" \"b c\")\n"
        + "    define\n"
        + "      \"A\" as A.price > PREV(A.price),\n"
        + "      \"b c\" as \"b c\".foo\n"
        + "  ) as mr(c1, c2) join e as x on foo = baz";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN ((`a` `b c`))\n"
        + "DEFINE `A` AS (`A`.`PRICE` > PREV(`A`.`PRICE`, 1)),"
        + " `b c` AS `b c`.`FOO`) AS `MR` (`C1`, `C2`)\n"
        + "INNER JOIN `E` AS `X` ON (`FOO` = `BAZ`)";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeDefineClause() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > NEXT(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > NEXT(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeDefineClause2() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < FIRST(down.price),\n"
        + "      up as up.price > LAST(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < FIRST(`DOWN`.`PRICE`, 0)), "
        + "`UP` AS (`UP`.`PRICE` > LAST(`UP`.`PRICE`, 0))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeDefineClause3() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price,1),\n"
        + "      up as up.price > LAST(up.price + up.TAX)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > LAST((`UP`.`PRICE` + `UP`.`TAX`), 0))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeDefineClause4() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price,1),\n"
        + "      up as up.price > PREV(LAST(up.price + up.TAX),3)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(LAST((`UP`.`PRICE` + `UP`.`TAX`), 0), 3))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeMeasures1() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures "
        + "   MATCH_NUMBER() as match_num,"
        + "   CLASSIFIER() as var_match,"
        + "   STRT.ts as start_ts,"
        + "   LAST(DOWN.ts) as bottom_ts,"
        + "   LAST(up.ts) as end_ts"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES (MATCH_NUMBER()) AS `MATCH_NUM`, "
        + "(CLASSIFIER()) AS `VAR_MATCH`, "
        + "`STRT`.`TS` AS `START_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "LAST(`UP`.`TS`, 0) AS `END_TS`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeMeasures2() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures STRT.ts as start_ts,"
        + "  FINAL LAST(DOWN.ts) as bottom_ts,"
        + "   LAST(up.ts) as end_ts"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES `STRT`.`TS` AS `START_TS`, "
        + "FINAL LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "LAST(`UP`.`TS`, 0) AS `END_TS`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeMeasures3() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures STRT.ts as start_ts,"
        + "  RUNNING LAST(DOWN.ts) as bottom_ts,"
        + "   LAST(up.ts) as end_ts"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES `STRT`.`TS` AS `START_TS`, "
        + "RUNNING LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "LAST(`UP`.`TS`, 0) AS `END_TS`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeMeasures4() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures "
        + "  FINAL count(up.ts) as up_ts,"
        + "  FINAL count(ts) as total_ts,"
        + "  RUNNING count(ts) as cnt_ts,"
        + "  price - strt.price as price_dif"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES FINAL COUNT(`UP`.`TS`) AS `UP_TS`, "
        + "FINAL COUNT(`TS`) AS `TOTAL_TS`, "
        + "RUNNING COUNT(`TS`) AS `CNT_TS`, "
        + "(`PRICE` - `STRT`.`PRICE`) AS `PRICE_DIF`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))) AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeMeasures5() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures "
        + "  FIRST(STRT.ts) as strt_ts,"
        + "  LAST(DOWN.ts) as down_ts,"
        + "  AVG(DOWN.ts) as avg_down_ts"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES FIRST(`STRT`.`TS`, 0) AS `STRT_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `DOWN_TS`, "
        + "AVG(`DOWN`.`TS`) AS `AVG_DOWN_TS`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeMeasures6() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures "
        + "  FIRST(STRT.ts) as strt_ts,"
        + "  LAST(DOWN.ts) as down_ts,"
        + "  FINAL SUM(DOWN.ts) as sum_down_ts"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES FIRST(`STRT`.`TS`, 0) AS `STRT_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `DOWN_TS`, "
        + "FINAL SUM(`DOWN`.`TS`) AS `SUM_DOWN_TS`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizePatternSkip1() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "     after match skip to next row\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "AFTER MATCH SKIP TO NEXT ROW\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizePatternSkip2() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "     after match skip past last row\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "AFTER MATCH SKIP PAST LAST ROW\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizePatternSkip3() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "     after match skip to FIRST down\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "AFTER MATCH SKIP TO FIRST `DOWN`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizePatternSkip4() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "     after match skip to LAST down\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "AFTER MATCH SKIP TO LAST `DOWN`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizePatternSkip5() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "     after match skip to down\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "AFTER MATCH SKIP TO LAST `DOWN`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-2993">[CALCITE-2993]
   * ParseException may be thrown for legal SQL queries due to incorrect
   * "LOOKAHEAD(1)" hints</a>. */
  @Test void testMatchRecognizePatternSkip6() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "     after match skip to last\n"
        + "    pattern (strt down+ up+)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "AFTER MATCH SKIP TO LAST `LAST`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeSubset1() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    pattern (strt down+ up+)\n"
        + "    subset stdn = (strt, down)"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "SUBSET (`STDN` = (`STRT`, `DOWN`))\n"
        + "DEFINE "
        + "`DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeSubset2() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures STRT.ts as start_ts,"
        + "   LAST(DOWN.ts) as bottom_ts,"
        + "   AVG(stdn.price) as stdn_avg"
        + "    pattern (strt down+ up+)\n"
        + "    subset stdn = (strt, down)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES `STRT`.`TS` AS `START_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "AVG(`STDN`.`PRICE`) AS `STDN_AVG`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "SUBSET (`STDN` = (`STRT`, `DOWN`))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeSubset3() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures STRT.ts as start_ts,"
        + "   LAST(DOWN.ts) as bottom_ts,"
        + "   AVG(stdn.price) as stdn_avg"
        + "    pattern (strt down+ up+)\n"
        + "    subset stdn = (strt, down), stdn2 = (strt, down)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES `STRT`.`TS` AS `START_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "AVG(`STDN`.`PRICE`) AS `STDN_AVG`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "SUBSET (`STDN` = (`STRT`, `DOWN`)), (`STDN2` = (`STRT`, `DOWN`))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeRowsPerMatch1() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures STRT.ts as start_ts,"
        + "   LAST(DOWN.ts) as bottom_ts,"
        + "   AVG(stdn.price) as stdn_avg"
        + "   ONE ROW PER MATCH"
        + "    pattern (strt down+ up+)\n"
        + "    subset stdn = (strt, down), stdn2 = (strt, down)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES `STRT`.`TS` AS `START_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "AVG(`STDN`.`PRICE`) AS `STDN_AVG`\n"
        + "ONE ROW PER MATCH\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "SUBSET (`STDN` = (`STRT`, `DOWN`)), (`STDN2` = (`STRT`, `DOWN`))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeRowsPerMatch2() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "   measures STRT.ts as start_ts,"
        + "   LAST(DOWN.ts) as bottom_ts,"
        + "   AVG(stdn.price) as stdn_avg"
        + "   ALL ROWS PER MATCH"
        + "    pattern (strt down+ up+)\n"
        + "    subset stdn = (strt, down), stdn2 = (strt, down)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "MEASURES `STRT`.`TS` AS `START_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "AVG(`STDN`.`PRICE`) AS `STDN_AVG`\n"
        + "ALL ROWS PER MATCH\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +)))\n"
        + "SUBSET (`STDN` = (`STRT`, `DOWN`)), (`STDN2` = (`STRT`, `DOWN`))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testMatchRecognizeWithin() {
    final String sql = "select *\n"
        + "  from t match_recognize\n"
        + "  (\n"
        + "    order by rowtime\n"
        + "    measures STRT.ts as start_ts,\n"
        + "      LAST(DOWN.ts) as bottom_ts,\n"
        + "      AVG(stdn.price) as stdn_avg\n"
        + "    pattern (strt down+ up+) within interval '3' second\n"
        + "    subset stdn = (strt, down), stdn2 = (strt, down)\n"
        + "    define\n"
        + "      down as down.price < PREV(down.price),\n"
        + "      up as up.price > prev(up.price)\n"
        + "  ) mr";
    final String expected = "SELECT *\n"
        + "FROM `T` MATCH_RECOGNIZE(\n"
        + "ORDER BY `ROWTIME`\n"
        + "MEASURES `STRT`.`TS` AS `START_TS`, "
        + "LAST(`DOWN`.`TS`, 0) AS `BOTTOM_TS`, "
        + "AVG(`STDN`.`PRICE`) AS `STDN_AVG`\n"
        + "PATTERN (((`STRT` (`DOWN` +)) (`UP` +))) WITHIN INTERVAL '3' SECOND\n"
        + "SUBSET (`STDN` = (`STRT`, `DOWN`)), (`STDN2` = (`STRT`, `DOWN`))\n"
        + "DEFINE `DOWN` AS (`DOWN`.`PRICE` < PREV(`DOWN`.`PRICE`, 1)), "
        + "`UP` AS (`UP`.`PRICE` > PREV(`UP`.`PRICE`, 1))"
        + ") AS `MR`";
    sql(sql).ok(expected);
  }

  @Test void testWithinGroupClause1() {
    final String sql = "select col1,\n"
        + " collect(col2) within group (order by col3)\n"
        + "from t\n"
        + "order by col1 limit 10";
    final String expected = "SELECT `COL1`,"
        + " COLLECT(`COL2`) WITHIN GROUP (ORDER BY `COL3`)\n"
        + "FROM `T`\n"
        + "ORDER BY `COL1`\n"
        + "FETCH NEXT 10 ROWS ONLY";
    sql(sql).ok(expected);
  }

  @Test void testWithinGroupClause2() {
    final String sql = "select collect(col2) within group (order by col3)\n"
        + "from t\n"
        + "order by col1 limit 10";
    final String expected = "SELECT"
        + " COLLECT(`COL2`) WITHIN GROUP (ORDER BY `COL3`)\n"
        + "FROM `T`\n"
        + "ORDER BY `COL1`\n"
        + "FETCH NEXT 10 ROWS ONLY";
    sql(sql).ok(expected);
  }

  @Test void testWithinGroupClause3() {
    final String sql = "select collect(col2) within group (^)^ "
        + "from t order by col1 limit 10";
    sql(sql).fails("(?s).*Encountered \"\\)\" at line 1, column 36\\..*");
  }

  @Test void testWithinGroupClause4() {
    final String sql = "select col1,\n"
        + " collect(col2) within group (order by col3, col4)\n"
        + "from t\n"
        + "order by col1 limit 10";
    final String expected = "SELECT `COL1`,"
        + " COLLECT(`COL2`) WITHIN GROUP (ORDER BY `COL3`, `COL4`)\n"
        + "FROM `T`\n"
        + "ORDER BY `COL1`\n"
        + "FETCH NEXT 10 ROWS ONLY";
    sql(sql).ok(expected);
  }

  @Test void testWithinGroupClause5() {
    final String sql = "select col1,\n"
        + " collect(col2) within group (\n"
        + "  order by col3 desc nulls first, col4 asc nulls last)\n"
        + "from t\n"
        + "order by col1 limit 10";
    final String expected = "SELECT `COL1`, COLLECT(`COL2`) "
        + "WITHIN GROUP (ORDER BY `COL3` DESC NULLS FIRST, `COL4` NULLS LAST)\n"
        + "FROM `T`\n"
        + "ORDER BY `COL1`\n"
        + "FETCH NEXT 10 ROWS ONLY";
    sql(sql).ok(expected);
  }

  @Test void testStringAgg() {
    final String sql = "select\n"
        + "  string_agg(ename order by deptno, ename) as c1,\n"
        + "  string_agg(ename, '; ' order by deptno, ename desc) as c2,\n"
        + "  string_agg(ename) as c3,\n"
        + "  string_agg(ename, ':') as c4,\n"
        + "  string_agg(ename, ':' ignore nulls) as c5\n"
        + "from emp group by gender";
    final String expected = "SELECT"
        + " STRING_AGG(`ENAME` ORDER BY `DEPTNO`, `ENAME`) AS `C1`,"
        + " STRING_AGG(`ENAME`, '; ' ORDER BY `DEPTNO`, `ENAME` DESC) AS `C2`,"
        + " STRING_AGG(`ENAME`) AS `C3`,"
        + " STRING_AGG(`ENAME`, ':') AS `C4`,"
        + " STRING_AGG(`ENAME`, ':') IGNORE NULLS AS `C5`\n"
        + "FROM `EMP`\n"
        + "GROUP BY `GENDER`";
    sql(sql).ok(expected);
  }

  @Test void testArrayAgg() {
    final String sql = "select\n"
        + "  array_agg(ename respect nulls order by deptno, ename) as c1,\n"
        + "  array_concat_agg(ename order by deptno, ename desc) as c2,\n"
        + "  array_agg(ename) as c3,\n"
        + "  array_concat_agg(ename) within group (order by ename) as c4\n"
        + "from emp group by gender";
    final String expected = "SELECT"
        + " ARRAY_AGG(`ENAME` ORDER BY `DEPTNO`, `ENAME`) RESPECT NULLS AS `C1`,"
        + " ARRAY_CONCAT_AGG(`ENAME` ORDER BY `DEPTNO`, `ENAME` DESC) AS `C2`,"
        + " ARRAY_AGG(`ENAME`) AS `C3`,"
        + " ARRAY_CONCAT_AGG(`ENAME`) WITHIN GROUP (ORDER BY `ENAME`) AS `C4`\n"
        + "FROM `EMP`\n"
        + "GROUP BY `GENDER`";
    sql(sql).ok(expected);
  }

  @Test void testGroupConcat() {
    final String sql = "select\n"
        + "  group_concat(ename order by deptno, ename desc) as c2,\n"
        + "  group_concat(ename) as c3,\n"
        + "  group_concat(ename order by deptno, ename desc separator ',') as c4\n"
        + "from emp group by gender";
    final String expected = "SELECT"
        + " GROUP_CONCAT(`ENAME` ORDER BY `DEPTNO`, `ENAME` DESC) AS `C2`,"
        + " GROUP_CONCAT(`ENAME`) AS `C3`,"
        + " GROUP_CONCAT(`ENAME` ORDER BY `DEPTNO`, `ENAME` DESC SEPARATOR ',') AS `C4`\n"
        + "FROM `EMP`\n"
        + "GROUP BY `GENDER`";
    sql(sql).ok(expected);
  }

  @Test void testWithinDistinct() {
    final String sql = "select col1,\n"
        + " sum(col2) within distinct (col3 + col4, col5)\n"
        + "from t\n"
        + "order by col1 limit 10";
    final String expected = "SELECT `COL1`,"
        + " (SUM(`COL2`) WITHIN DISTINCT ((`COL3` + `COL4`), `COL5`))\n"
        + "FROM `T`\n"
        + "ORDER BY `COL1`\n"
        + "FETCH NEXT 10 ROWS ONLY";
    sql(sql).ok(expected);
  }

  @Test void testWithinDistinct2() {
    final String sql = "select col1,\n"
        + " sum(col2) within distinct (col3 + col4, col5)\n"
        + "   within group (order by col6 desc)\n"
        + "   filter (where col7 < col8) as sum2\n"
        + "from t\n"
        + "group by col9";
    final String expected = "SELECT `COL1`,"
        + " (SUM(`COL2`) WITHIN DISTINCT ((`COL3` + `COL4`), `COL5`))"
        + " WITHIN GROUP (ORDER BY `COL6` DESC)"
        + " FILTER (WHERE (`COL7` < `COL8`)) AS `SUM2`\n"
        + "FROM `T`\n"
        + "GROUP BY `COL9`";
    sql(sql).ok(expected);
  }

  @Test void testJsonValueExpressionOperator() {
    expr("foo format json")
        .ok("`FOO` FORMAT JSON");
    // Currently, encoding js not valid
    expr("foo format json encoding utf8")
        .ok("`FOO` FORMAT JSON");
    expr("foo format json encoding utf16")
        .ok("`FOO` FORMAT JSON");
    expr("foo format json encoding utf32")
        .ok("`FOO` FORMAT JSON");
    expr("null format json")
        .ok("NULL FORMAT JSON");
    // Test case to eliminate choice conflict on token <FORMAT>
    sql("select foo format from tab")
        .ok("SELECT `FOO` AS `FORMAT`\n"
            + "FROM `TAB`");
    // Test case to eliminate choice conflict on token <ENCODING>
    sql("select foo format json encoding from tab")
        .ok("SELECT `FOO` FORMAT JSON AS `ENCODING`\n"
            + "FROM `TAB`");
  }

  @Test void testJsonExists() {
    expr("json_exists('{\"foo\": \"bar\"}', 'lax $.foo')")
        .ok("JSON_EXISTS('{\"foo\": \"bar\"}', 'lax $.foo')");
    expr("json_exists('{\"foo\": \"bar\"}', 'lax $.foo' error on error)")
        .ok("JSON_EXISTS('{\"foo\": \"bar\"}', 'lax $.foo' ERROR ON ERROR)");
  }

  @Test void testJsonValue() {
    expr("json_value('{\"foo\": \"100\"}', 'lax $.foo' "
        + "returning integer)")
        .ok("JSON_VALUE('{\"foo\": \"100\"}', 'lax $.foo' "
            + "RETURNING INTEGER)");
    expr("json_value('{\"foo\": \"100\"}', 'lax $.foo' "
        + "returning integer default 10 on empty error on error)")
        .ok("JSON_VALUE('{\"foo\": \"100\"}', 'lax $.foo' "
            + "RETURNING INTEGER DEFAULT 10 ON EMPTY ERROR ON ERROR)");
  }

  @Test void testJsonQuery() {
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' WITHOUT ARRAY WRAPPER)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' WITH WRAPPER)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITH UNCONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' WITH UNCONDITIONAL WRAPPER)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITH UNCONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' WITH CONDITIONAL WRAPPER)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITH CONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' NULL ON EMPTY)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' ERROR ON EMPTY)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER ERROR ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' EMPTY ARRAY ON EMPTY)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER EMPTY ARRAY ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' EMPTY OBJECT ON EMPTY)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER EMPTY OBJECT ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' NULL ON ERROR)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' ERROR ON ERROR)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER NULL ON EMPTY ERROR ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' EMPTY ARRAY ON ERROR)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER NULL ON EMPTY EMPTY ARRAY ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' EMPTY OBJECT ON ERROR)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER NULL ON EMPTY EMPTY OBJECT ON ERROR)");
    expr("json_query('{\"foo\": \"bar\"}', 'lax $' EMPTY ARRAY ON EMPTY "
        + "EMPTY OBJECT ON ERROR)")
        .ok("JSON_QUERY('{\"foo\": \"bar\"}', "
            + "'lax $' WITHOUT ARRAY WRAPPER EMPTY ARRAY ON EMPTY EMPTY OBJECT ON ERROR)");
  }

  @Test void testJsonObject() {
    expr("json_object('foo': 'bar')")
        .ok("JSON_OBJECT(KEY 'foo' VALUE 'bar' NULL ON NULL)");
    expr("json_object('foo': 'bar', 'foo2': 'bar2')")
        .ok("JSON_OBJECT(KEY 'foo' VALUE 'bar', KEY 'foo2' VALUE 'bar2' NULL ON NULL)");
    expr("json_object('foo' value 'bar')")
        .ok("JSON_OBJECT(KEY 'foo' VALUE 'bar' NULL ON NULL)");
    expr("json_object(key 'foo' value 'bar')")
        .ok("JSON_OBJECT(KEY 'foo' VALUE 'bar' NULL ON NULL)");
    expr("json_object('foo': null)")
        .ok("JSON_OBJECT(KEY 'foo' VALUE NULL NULL ON NULL)");
    expr("json_object('foo': null absent on null)")
        .ok("JSON_OBJECT(KEY 'foo' VALUE NULL ABSENT ON NULL)");
    expr("json_object('foo': json_object('foo': 'bar') format json)")
        .ok("JSON_OBJECT(KEY 'foo' VALUE "
            + "JSON_OBJECT(KEY 'foo' VALUE 'bar' NULL ON NULL) "
            + "FORMAT JSON NULL ON NULL)");

    if (!Bug.TODO_FIXED) {
      return;
    }
    // "LOOKAHEAD(2) list = JsonNameAndValue()" does not generate
    // valid LOOKAHEAD codes for the case "key: value".
    //
    // You can see the generated codes that are located at method
    // SqlParserImpl#JsonObjectFunctionCall. Looking ahead fails
    // immediately after seeking the tokens <KEY> and <COLON>.
    expr("json_object(key: value)")
        .ok("JSON_OBJECT(KEY `KEY` VALUE `VALUE` NULL ON NULL)");
  }

  @Test void testJsonType() {
    expr("json_type('11.56')")
        .ok("JSON_TYPE('11.56')");
    expr("json_type('{}')")
        .ok("JSON_TYPE('{}')");
    expr("json_type(null)")
        .ok("JSON_TYPE(NULL)");
    expr("json_type('[\"foo\",null]')")
        .ok("JSON_TYPE('[\"foo\",null]')");
    expr("json_type('{\"foo\": \"100\"}')")
        .ok("JSON_TYPE('{\"foo\": \"100\"}')");
  }

  @Test void testJsonDepth() {
    expr("json_depth('11.56')")
        .ok("JSON_DEPTH('11.56')");
    expr("json_depth('{}')")
        .ok("JSON_DEPTH('{}')");
    expr("json_depth(null)")
        .ok("JSON_DEPTH(NULL)");
    expr("json_depth('[\"foo\",null]')")
        .ok("JSON_DEPTH('[\"foo\",null]')");
    expr("json_depth('{\"foo\": \"100\"}')")
        .ok("JSON_DEPTH('{\"foo\": \"100\"}')");
  }

  @Test void testJsonLength() {
    expr("json_length('{\"foo\": \"bar\"}')")
        .ok("JSON_LENGTH('{\"foo\": \"bar\"}')");
    expr("json_length('{\"foo\": \"bar\"}', 'lax $')")
        .ok("JSON_LENGTH('{\"foo\": \"bar\"}', 'lax $')");
    expr("json_length('{\"foo\": \"bar\"}', 'strict $')")
        .ok("JSON_LENGTH('{\"foo\": \"bar\"}', 'strict $')");
    expr("json_length('{\"foo\": \"bar\"}', 'invalid $')")
        .ok("JSON_LENGTH('{\"foo\": \"bar\"}', 'invalid $')");
  }

  @Test void testJsonKeys() {
    expr("json_keys('{\"foo\": \"bar\"}', 'lax $')")
        .ok("JSON_KEYS('{\"foo\": \"bar\"}', 'lax $')");
    expr("json_keys('{\"foo\": \"bar\"}', 'strict $')")
        .ok("JSON_KEYS('{\"foo\": \"bar\"}', 'strict $')");
    expr("json_keys('{\"foo\": \"bar\"}', 'invalid $')")
        .ok("JSON_KEYS('{\"foo\": \"bar\"}', 'invalid $')");
  }

  @Test void testJsonRemove() {
    expr("json_remove('[\"a\", [\"b\", \"c\"], \"d\"]', '$')")
        .ok("JSON_REMOVE('[\"a\", [\"b\", \"c\"], \"d\"]', '$')");
    expr("json_remove('[\"a\", [\"b\", \"c\"], \"d\"]', '$[1]', '$[0]')")
        .ok("JSON_REMOVE('[\"a\", [\"b\", \"c\"], \"d\"]', '$[1]', '$[0]')");
  }

  @Test void testJsonObjectAgg() {
    expr("json_objectagg(k_column: v_column)")
        .ok("JSON_OBJECTAGG(KEY `K_COLUMN` VALUE `V_COLUMN` NULL ON NULL)");
    expr("json_objectagg(k_column value v_column)")
        .ok("JSON_OBJECTAGG(KEY `K_COLUMN` VALUE `V_COLUMN` NULL ON NULL)");
    expr("json_objectagg(key k_column value v_column)")
        .ok("JSON_OBJECTAGG(KEY `K_COLUMN` VALUE `V_COLUMN` NULL ON NULL)");
    expr("json_objectagg(k_column: null)")
        .ok("JSON_OBJECTAGG(KEY `K_COLUMN` VALUE NULL NULL ON NULL)");
    expr("json_objectagg(k_column: null absent on null)")
        .ok("JSON_OBJECTAGG(KEY `K_COLUMN` VALUE NULL ABSENT ON NULL)");
    expr("json_objectagg(k_column: json_object(k_column: v_column) format json)")
        .ok("JSON_OBJECTAGG(KEY `K_COLUMN` VALUE "
            + "JSON_OBJECT(KEY `K_COLUMN` VALUE `V_COLUMN` NULL ON NULL) "
            + "FORMAT JSON NULL ON NULL)");
  }

  @Test void testJsonArray() {
    expr("json_array('foo')")
        .ok("JSON_ARRAY('foo' ABSENT ON NULL)");
    expr("json_array(null)")
        .ok("JSON_ARRAY(NULL ABSENT ON NULL)");
    expr("json_array(null null on null)")
        .ok("JSON_ARRAY(NULL NULL ON NULL)");
    expr("json_array(json_array('foo', 'bar') format json)")
        .ok("JSON_ARRAY(JSON_ARRAY('foo', 'bar' ABSENT ON NULL) FORMAT JSON ABSENT ON NULL)");
  }

  @Test void testJsonPretty() {
    expr("json_pretty('foo')")
        .ok("JSON_PRETTY('foo')");
    expr("json_pretty(null)")
        .ok("JSON_PRETTY(NULL)");
  }

  @Test void testJsonStorageSize() {
    expr("json_storage_size('foo')")
        .ok("JSON_STORAGE_SIZE('foo')");
    expr("json_storage_size(null)")
        .ok("JSON_STORAGE_SIZE(NULL)");
  }

  @Test void testJsonArrayAgg1() {
    expr("json_arrayagg(\"column\")")
        .ok("JSON_ARRAYAGG(`column` ABSENT ON NULL)");
    expr("json_arrayagg(\"column\" null on null)")
        .ok("JSON_ARRAYAGG(`column` NULL ON NULL)");
    expr("json_arrayagg(json_array(\"column\") format json)")
        .ok("JSON_ARRAYAGG(JSON_ARRAY(`column` ABSENT ON NULL) FORMAT JSON ABSENT ON NULL)");
  }

  @Test void testJsonArrayAgg2() {
    expr("json_arrayagg(\"column\" order by \"column\")")
        .ok("JSON_ARRAYAGG(`column` ABSENT ON NULL) WITHIN GROUP (ORDER BY `column`)");
    expr("json_arrayagg(\"column\") within group (order by \"column\")")
        .ok("JSON_ARRAYAGG(`column` ABSENT ON NULL) WITHIN GROUP (ORDER BY `column`)");
    sql("^json_arrayagg(\"column\" order by \"column\") within group (order by \"column\")^")
        .fails("(?s).*Including both WITHIN GROUP\\(\\.\\.\\.\\) and inside ORDER BY "
            + "in a single JSON_ARRAYAGG call is not allowed.*");
  }

  @Test void testJsonPredicate() {
    expr("'{}' is json")
        .ok("('{}' IS JSON VALUE)");
    expr("'{}' is json value")
        .ok("('{}' IS JSON VALUE)");
    expr("'{}' is json object")
        .ok("('{}' IS JSON OBJECT)");
    expr("'[]' is json array")
        .ok("('[]' IS JSON ARRAY)");
    expr("'100' is json scalar")
        .ok("('100' IS JSON SCALAR)");
    expr("'{}' is not json")
        .ok("('{}' IS NOT JSON VALUE)");
    expr("'{}' is not json value")
        .ok("('{}' IS NOT JSON VALUE)");
    expr("'{}' is not json object")
        .ok("('{}' IS NOT JSON OBJECT)");
    expr("'[]' is not json array")
        .ok("('[]' IS NOT JSON ARRAY)");
    expr("'100' is not json scalar")
        .ok("('100' IS NOT JSON SCALAR)");
  }

  @Test void testParseWithReader() throws Exception {
    String query = "select * from dual";
    SqlParser sqlParserReader = sqlParser(new StringReader(query), b -> b);
    SqlNode node1 = sqlParserReader.parseQuery();
    SqlNode node2 = sql(query).node();
    assertEquals(node2.toString(), node1.toString());
  }

  @Test void testConfigureFromDialect() {
    // Calcite's default converts unquoted identifiers to upper case
    sql("select unquotedColumn from \"double\"\"QuotedTable\"")
        .withDialect(CALCITE)
        .ok("SELECT \"UNQUOTEDCOLUMN\"\n"
            + "FROM \"double\"\"QuotedTable\"");
    // MySQL leaves unquoted identifiers unchanged
    sql("select unquotedColumn from `double``QuotedTable`")
        .withDialect(MYSQL)
        .ok("SELECT `unquotedColumn`\n"
            + "FROM `double``QuotedTable`");
    // Oracle converts unquoted identifiers to upper case
    sql("select unquotedColumn from \"double\"\"QuotedTable\"")
        .withDialect(ORACLE)
        .ok("SELECT \"UNQUOTEDCOLUMN\"\n"
            + "FROM \"double\"\"QuotedTable\"");
    // PostgreSQL converts unquoted identifiers to lower case
    sql("select unquotedColumn from \"double\"\"QuotedTable\"")
        .withDialect(POSTGRESQL)
        .ok("SELECT \"unquotedcolumn\"\n"
            + "FROM \"double\"\"QuotedTable\"");
    // Redshift converts all identifiers to lower case
    sql("select unquotedColumn from \"double\"\"QuotedTable\"")
        .withDialect(REDSHIFT)
        .ok("SELECT \"unquotedcolumn\"\n"
            + "FROM \"double\"\"quotedtable\"");
    // BigQuery leaves quoted and unquoted identifiers unchanged
    sql("select unquotedColumn from `double\\`QuotedTable`")
        .withDialect(BIG_QUERY)
        .ok("SELECT unquotedColumn\n"
            + "FROM `double\\`QuotedTable`");
  }

  /** Test case for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-4230">[CALCITE-4230]
   * In Babel for BigQuery, split quoted table names that contain dots</a>. */
  @Test void testSplitIdentifier() {
    final String sql = "select *\n"
        + "from `bigquery-public-data.samples.natality`";
    final String sql2 = "select *\n"
        + "from `bigquery-public-data`.`samples`.`natality`";
    final String expectedSplit = "SELECT *\n"
        + "FROM `bigquery-public-data`.samples.natality";
    final String expectedNoSplit = "SELECT *\n"
        + "FROM `bigquery-public-data.samples.natality`";
    final String expectedSplitMysql = "SELECT *\n"
        + "FROM `bigquery-public-data`.`samples`.`natality`";
    // In BigQuery, an identifier containing dots is split into sub-identifiers.
    sql(sql)
        .withDialect(BIG_QUERY)
        .ok(expectedSplit);
    // In MySQL, identifiers are not split.
    sql(sql)
        .withDialect(MYSQL)
        .ok(expectedNoSplit);
    // Query with split identifiers produces split AST. No surprise there.
    sql(sql2)
        .withDialect(BIG_QUERY)
        .ok(expectedSplit);
    // Similar to previous; we just quote simple identifiers on unparse.
    sql(sql2)
        .withDialect(MYSQL)
        .ok(expectedSplitMysql);
  }

  @Test void testParenthesizedSubQueries() {
    final String expected = "SELECT *\n"
        + "FROM (SELECT *\n"
        + "FROM `TAB`) AS `X`";

    final String sql1 = "SELECT * FROM (((SELECT * FROM tab))) X";
    sql(sql1).ok(expected);

    final String sql2 = "SELECT * FROM ((((((((((((SELECT * FROM tab)))))))))))) X";
    sql(sql2).ok(expected);
  }

  @Test void testQueryHint() {
    final String sql1 = "select "
        + "/*+ properties(k1='v1', k2='v2', 'a.b.c'='v3'), "
        + "no_hash_join, Index(idx1, idx2), "
        + "repartition(3) */ "
        + "empno, ename, deptno from emps";
    final String expected1 = "SELECT\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2', 'a.b.c' = 'v3'), "
        + "`NO_HASH_JOIN`, "
        + "`INDEX`(`IDX1`, `IDX2`), "
        + "`REPARTITION`(3) */\n"
        + "`EMPNO`, `ENAME`, `DEPTNO`\n"
        + "FROM `EMPS`";
    sql(sql1).ok(expected1);
    // Hint item right after the token "/*+"
    final String sql2 = "select /*+properties(k1='v1', k2='v2')*/ empno from emps";
    final String expected2 = "SELECT\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2') */\n"
        + "`EMPNO`\n"
        + "FROM `EMPS`";
    sql(sql2).ok(expected2);
    // Hint item without parentheses
    final String sql3 = "select /*+ simple_hint */ empno, ename, deptno from emps limit 2";
    final String expected3 = "SELECT\n"
        + "/*+ `SIMPLE_HINT` */\n"
        + "`EMPNO`, `ENAME`, `DEPTNO`\n"
        + "FROM `EMPS`\n"
        + "FETCH NEXT 2 ROWS ONLY";
    sql(sql3).ok(expected3);
  }

  @Test void testTableHintsInQuery() {
    final String hint = "/*+ PROPERTIES(K1 ='v1', K2 ='v2'), INDEX(IDX0, IDX1) */";
    final String sql1 = String.format(Locale.ROOT, "select * from t %s", hint);
    final String expected1 = "SELECT *\n"
        + "FROM `T`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), `INDEX`(`IDX0`, `IDX1`) */";
    sql(sql1).ok(expected1);
    final String sql2 = String.format(Locale.ROOT, "select * from\n"
        + "(select * from t %s union all select * from t %s )", hint, hint);
    final String expected2 = "SELECT *\n"
        + "FROM (SELECT *\n"
        + "FROM `T`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
        + "UNION ALL\n"
        + "SELECT *\n"
        + "FROM `T`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), `INDEX`(`IDX0`, `IDX1`) */)";
    sql(sql2).ok(expected2);
    final String sql3 = String.format(Locale.ROOT, "select * from t %s join t %s", hint, hint);
    final String expected3 = "SELECT *\n"
        + "FROM `T`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
        + "INNER JOIN `T`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), `INDEX`(`IDX0`, `IDX1`) */";
    sql(sql3).ok(expected3);
  }

  @Test void testTableHintsInInsert() {
    final String sql = "insert into emps\n"
        + "/*+ PROPERTIES(k1='v1', k2='v2'), INDEX(idx0, idx1) */\n"
        + "select * from emps";
    final String expected = "INSERT INTO `EMPS`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), `INDEX`(`IDX0`, `IDX1`) */\n"
        + "SELECT *\n"
        + "FROM `EMPS`";
    sql(sql).ok(expected);
  }

  @Test void testTableHintsInDelete() {
    final String sql = "delete from emps\n"
        + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */\n"
        + "where empno=12";
    final String expected = "DELETE FROM `EMPS`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), `INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */\n"
        + "WHERE (`EMPNO` = 12)";
    sql(sql).ok(expected);
  }

  @Test void testTableHintsInUpdate() {
    final String sql = "update emps\n"
        + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */\n"
        + "set empno = empno + 1, sal = sal - 1\n"
        + "where empno=12";
    final String expected = "UPDATE `EMPS`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), "
        + "`INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */ "
        + "SET `EMPNO` = (`EMPNO` + 1)"
        + ", `SAL` = (`SAL` - 1)\n"
        + "WHERE (`EMPNO` = 12)";
    sql(sql).ok(expected);
  }

  @Test void testTableHintsInMerge() {
    final String sql = "merge into emps\n"
        + "/*+ properties(k1='v1', k2='v2'), index(idx1, idx2), no_hash_join */ e\n"
        + "using tempemps as t\n"
        + "on e.empno = t.empno\n"
        + "when matched then update\n"
        + "set name = t.name, deptno = t.deptno, salary = t.salary * .1\n"
        + "when not matched then insert (name, dept, salary)\n"
        + "values(t.name, 10, t.salary * .15)";
    final String expected = "MERGE INTO `EMPS`\n"
        + "/*+ `PROPERTIES`(`K1` = 'v1', `K2` = 'v2'), "
        + "`INDEX`(`IDX1`, `IDX2`), `NO_HASH_JOIN` */ "
        + "AS `E`\n"
        + "USING `TEMPEMPS` AS `T`\n"
        + "ON (`E`.`EMPNO` = `T`.`EMPNO`)\n"
        + "WHEN MATCHED THEN UPDATE SET `NAME` = `T`.`NAME`"
        + ", `DEPTNO` = `T`.`DEPTNO`"
        + ", `SALARY` = (`T`.`SALARY` * 0.1)\n"
        + "WHEN NOT MATCHED THEN INSERT (`NAME`, `DEPT`, `SALARY`) "
        + "VALUES (ROW(`T`.`NAME`, 10, (`T`.`SALARY` * 0.15)))";
    sql(sql).ok(expected);
  }

  @Test void testHintThroughShuttle() {
    final String sql = "select * from emp /*+ options('key1' = 'val1') */";
    final SqlNode sqlNode = sql(sql).node();
    final SqlNode shuttled = sqlNode.accept(new SqlShuttle() {
      @Override public SqlNode visit(SqlIdentifier identifier) {
        // Copy the identifier in order to return a new SqlTableRef.
        return identifier.clone(identifier.getParserPosition());
      }
    });
    final String expected = "SELECT *\n"
        + "FROM `EMP`\n"
        + "/*+ `OPTIONS`('key1' = 'val1') */";
    assertThat(toLinux(shuttled.toString()), is(expected));
  }

  @Test void testInvalidHintFormat() {
    final String sql1 = "select "
        + "/*+ properties(^k1^=123, k2='v2'), no_hash_join() */ "
        + "empno, ename, deptno from emps";
    sql(sql1).fails("(?s).*Encountered \"k1 = 123\" at .*");
    final String sql2 = "select "
        + "/*+ properties(k1, k2^=^'v2'), no_hash_join */ "
        + "empno, ename, deptno from emps";
    sql(sql2).fails("(?s).*Encountered \"=\" at line 1, column 29.\n.*");
    final String sql3 = "select "
        + "/*+ no_hash_join() */ "
        + "empno, ename, deptno from emps";
    // Allow empty options.
    final String expected3 = "SELECT\n"
        + "/*+ `NO_HASH_JOIN` */\n"
        + "`EMPNO`, `ENAME`, `DEPTNO`\n"
        + "FROM `EMPS`";
    sql(sql3).ok(expected3);
    final String sql4 = "select "
        + "/*+ properties(^a^.b.c=123, k2='v2') */"
        + "empno, ename, deptno from emps";
    sql(sql4).fails("(?s).*Encountered \"a .\" at .*");
  }

  /** Tests {@link Hoist}. */
  @Test protected void testHoist() {
    final String sql = "select 1 as x,\n"
        + "  'ab' || 'c' as y\n"
        + "from emp /* comment with 'quoted string'? */ as e\n"
        + "where deptno < 40\n"
        + "and hiredate > date '2010-05-06'";
    final Hoist.Hoisted hoisted = Hoist.create(Hoist.config()).hoist(sql);

    // Simple toString converts each variable to '?N'
    final String expected = "select ?0 as x,\n"
        + "  ?1 || ?2 as y\n"
        + "from emp /* comment with 'quoted string'? */ as e\n"
        + "where deptno < ?3\n"
        + "and hiredate > ?4";
    assertThat(hoisted, hasToString(expected));

    // As above, using the function explicitly.
    assertThat(hoisted.substitute(Hoist::ordinalString), is(expected));

    // Simple toString converts each variable to '?N'
    final String expected1 = "select 1 as x,\n"
        + "  ?1 || ?2 as y\n"
        + "from emp /* comment with 'quoted string'? */ as e\n"
        + "where deptno < 40\n"
        + "and hiredate > date '2010-05-06'";
    assertThat(hoisted.substitute(Hoist::ordinalStringIfChar), is(expected1));

    // Custom function converts variables to '[N:TYPE:VALUE]'
    final String expected2 = "select [0:DECIMAL:1] as x,\n"
        + "  [1:CHAR:ab] || [2:CHAR:c] as y\n"
        + "from emp /* comment with 'quoted string'? */ as e\n"
        + "where deptno < [3:DECIMAL:40]\n"
        + "and hiredate > [4:DATE:2010-05-06]";
    assertThat(hoisted.substitute(SqlParserTest::varToStr), is(expected2));
  }

  protected static String varToStr(Hoist.Variable v) {
    if (v.node instanceof SqlLiteral) {
      SqlLiteral literal = (SqlLiteral) v.node;
      SqlTypeName typeName = literal.getTypeName();
      return "[" + v.ordinal
          + ":"
          + (typeName == SqlTypeName.UNKNOWN
              ? ((SqlUnknownLiteral) literal).tag
              : typeName.getName())
          + ":" + literal.toValue()
          + "]";
    } else {
      return "[" + v.ordinal + "]";
    }
  }

  //~ Inner Interfaces -------------------------------------------------------

  /**
   * Callback to control how test actions are performed.
   */
  protected interface Tester {
    void checkList(SqlTestFactory factory, StringAndPos sap,
        @Nullable SqlDialect dialect, UnaryOperator<String> converter,
        List<String> expected);

    void check(SqlTestFactory factory, StringAndPos sap,
        @Nullable SqlDialect dialect, UnaryOperator<String> converter,
        String expected, Consumer<SqlParser> parserChecker);

    void checkExp(SqlTestFactory factory, StringAndPos sap,
        UnaryOperator<String> converter, String expected,
        Consumer<SqlParser> parserChecker);

    void checkFails(SqlTestFactory factory, StringAndPos sap,
        boolean list, String expectedMsgPattern);

    /** Tests that an expression throws an exception that matches the given
     * pattern. */
    void checkExpFails(SqlTestFactory factory, StringAndPos sap,
        String expectedMsgPattern);

    void checkNode(SqlTestFactory factory, StringAndPos sap,
        Matcher<SqlNode> matcher);

    /** Whether this is a sub-class that tests un-parsing as well as parsing. */
    default boolean isUnparserTest() {
      return false;
    }
  }

  //~ Inner Classes ----------------------------------------------------------

  /**
   * Default implementation of {@link Tester}.
   */
  protected static class TesterImpl implements Tester {
    static final TesterImpl DEFAULT = new TesterImpl();

    private static void check0(SqlNode sqlNode,
        SqlWriterConfig sqlWriterConfig,
        UnaryOperator<String> converter,
        String expected) {
      final String actual = sqlNode.toSqlString(c -> sqlWriterConfig).getSql();
      TestUtil.assertEqualsVerbose(expected, converter.apply(actual));
    }

    @Override public void checkList(SqlTestFactory factory, StringAndPos sap,
        @Nullable SqlDialect dialect, UnaryOperator<String> converter,
        List<String> expected) {
      final SqlNodeList sqlNodeList = parseStmtsAndHandleEx(factory, sap.sql);
      assertThat(sqlNodeList, hasSize(expected.size()));

      final SqlWriterConfig sqlWriterConfig =
          SQL_WRITER_CONFIG.withDialect(
              Util.first(dialect, AnsiSqlDialect.DEFAULT));
      for (int i = 0; i < sqlNodeList.size(); i++) {
        SqlNode sqlNode = sqlNodeList.get(i);
        check0(sqlNode, sqlWriterConfig, converter, expected.get(i));
      }
    }

    @Override public void check(SqlTestFactory factory, StringAndPos sap,
        @Nullable SqlDialect dialect, UnaryOperator<String> converter,
        String expected, Consumer<SqlParser> parserChecker) {
      final SqlNode sqlNode =
          parseStmtAndHandleEx(factory, sap.sql, parserChecker);
      final SqlWriterConfig sqlWriterConfig =
          SQL_WRITER_CONFIG.withDialect(
              Util.first(dialect, AnsiSqlDialect.DEFAULT));
      check0(sqlNode, sqlWriterConfig, converter, expected);
    }

    protected SqlNode parseStmtAndHandleEx(SqlTestFactory factory,
        String sql, Consumer<SqlParser> parserChecker) {
      final SqlParser parser = factory.createParser(sql);
      final SqlNode sqlNode;
      try {
        sqlNode = parser.parseStmt();
        parserChecker.accept(parser);
      } catch (SqlParseException e) {
        throw new RuntimeException("Error while parsing SQL: " + sql, e);
      }
      return sqlNode;
    }

    /** Parses a list of statements. */
    protected SqlNodeList parseStmtsAndHandleEx(SqlTestFactory factory,
        String sql) {
      final SqlParser parser = factory.createParser(sql);
      final SqlNodeList sqlNodeList;
      try {
        sqlNodeList = parser.parseStmtList();
      } catch (SqlParseException e) {
        throw new RuntimeException("Error while parsing SQL: " + sql, e);
      }
      return sqlNodeList;
    }

    @Override public void checkExp(SqlTestFactory factory, StringAndPos sap,
        UnaryOperator<String> converter, String expected,
        Consumer<SqlParser> parserChecker) {
      final SqlNode sqlNode =
          parseExpressionAndHandleEx(factory, sap.sql, parserChecker);
      final String actual = sqlNode.toSqlString(null, true).getSql();
      TestUtil.assertEqualsVerbose(expected, converter.apply(actual));
    }

    protected SqlNode parseExpressionAndHandleEx(SqlTestFactory factory,
        String sql, Consumer<SqlParser> parserChecker) {
      final SqlNode sqlNode;
      try {
        final SqlParser parser = factory.createParser(sql);
        sqlNode = parser.parseExpression();
        parserChecker.accept(parser);
      } catch (SqlParseException e) {
        throw new RuntimeException("Error while parsing expression: " + sql, e);
      }
      return sqlNode;
    }

    @Override public void checkFails(SqlTestFactory factory,
        StringAndPos sap, boolean list, String expectedMsgPattern) {
      Throwable thrown = null;
      try {
        final SqlParser parser = factory.createParser(sap.sql);
        final SqlNode sqlNode;
        if (list) {
          sqlNode = parser.parseStmtList();
        } else {
          sqlNode = parser.parseStmt();
        }
        Util.discard(sqlNode);
      } catch (Throwable ex) {
        thrown = ex;
      }

      checkEx(expectedMsgPattern, sap, thrown);
    }

    @Override public void checkNode(SqlTestFactory factory, StringAndPos sap,
        Matcher<SqlNode> matcher) {
      try {
        final SqlParser parser = factory.createParser(sap.sql);
        final SqlNode sqlNode = parser.parseStmt();
        assertThat(sqlNode, matcher);
      } catch (SqlParseException e) {
        throw TestUtil.rethrow(e);
      }
    }

    @Override public void checkExpFails(SqlTestFactory factory,
        StringAndPos sap, String expectedMsgPattern) {
      Throwable thrown = null;
      try {
        final SqlParser parser = factory.createParser(sap.sql);
        final SqlNode sqlNode = parser.parseExpression();
        Util.discard(sqlNode);
      } catch (Throwable ex) {
        thrown = ex;
      }

      checkEx(expectedMsgPattern, sap, thrown);
    }

    protected void checkEx(String expectedMsgPattern, StringAndPos sap,
        @Nullable Throwable thrown) {
      SqlTests.checkEx(thrown, expectedMsgPattern, sap,
          SqlTests.Stage.VALIDATE);
    }
  }

  /**
   * Implementation of {@link Tester} which makes sure that the results of
   * unparsing a query are consistent with the original query.
   */
  public static class UnparsingTesterImpl extends TesterImpl {
    @Override public boolean isUnparserTest() {
      return true;
    }

    static UnaryOperator<SqlWriterConfig> simple() {
      return c -> c.withSelectListItemsOnSeparateLines(false)
          .withUpdateSetListNewline(false)
          .withIndentation(0)
          .withFromFolding(SqlWriterConfig.LineFolding.TALL);
    }

    static SqlWriterConfig simpleWithParens(SqlWriterConfig c) {
      return simple().andThen(UnparsingTesterImpl::withParens).apply(c);
    }

    static SqlWriterConfig simpleWithParensAnsi(SqlWriterConfig c) {
      return withAnsi(simpleWithParens(c));
    }

    static SqlWriterConfig withParens(SqlWriterConfig c) {
      return c.withAlwaysUseParentheses(true);
    }

    static SqlWriterConfig withAnsi(SqlWriterConfig c) {
      return c.withDialect(AnsiSqlDialect.DEFAULT);
    }

    static UnaryOperator<SqlWriterConfig> randomize(Random random) {
      return c -> c.withFoldLength(random.nextInt(5) * 20 + 3)
          .withHavingFolding(nextLineFolding(random))
          .withWhereFolding(nextLineFolding(random))
          .withSelectFolding(nextLineFolding(random))
          .withFromFolding(nextLineFolding(random))
          .withGroupByFolding(nextLineFolding(random))
          .withClauseStartsLine(random.nextBoolean())
          .withClauseEndsLine(random.nextBoolean());
    }

    static String toSqlString(SqlNodeList sqlNodeList,
        UnaryOperator<SqlWriterConfig> transform) {
      return sqlNodeList.stream()
          .map(node -> node.toSqlString(transform).getSql())
          .collect(Collectors.joining(";"));
    }

    static SqlWriterConfig.LineFolding nextLineFolding(Random random) {
      return nextEnum(random, SqlWriterConfig.LineFolding.class);
    }

    static <E extends Enum<E>> E nextEnum(Random random, Class<E> enumClass) {
      final E[] constants = enumClass.getEnumConstants();
      return constants[random.nextInt(constants.length)];
    }

    static void checkList(SqlNodeList sqlNodeList,
        UnaryOperator<String> converter, List<String> expected) {
      assertThat(sqlNodeList, hasSize(expected.size()));

      for (int i = 0; i < sqlNodeList.size(); i++) {
        SqlNode sqlNode = sqlNodeList.get(i);
        // Unparse with no dialect, always parenthesize.
        final String actual =
            sqlNode.toSqlString(UnparsingTesterImpl::simpleWithParensAnsi)
                .getSql();
        assertEquals(expected.get(i), converter.apply(actual));
      }
    }

    @Override public void checkList(SqlTestFactory factory, StringAndPos sap,
        @Nullable SqlDialect dialect, UnaryOperator<String> converter,
        List<String> expected) {
      SqlNodeList sqlNodeList = parseStmtsAndHandleEx(factory, sap.sql);

      checkList(sqlNodeList, converter, expected);

      // Unparse again in Calcite dialect (which we can parse), and
      // minimal parentheses.
      final String sql1 = toSqlString(sqlNodeList, simple());

      // Parse and unparse again.
      SqlNodeList sqlNodeList2 =
          parseStmtsAndHandleEx(
              factory.withParserConfig(c ->
                  c.withQuoting(Quoting.DOUBLE_QUOTE)), sql1);
      final String sql2 = toSqlString(sqlNodeList2, simple());

      // Should be the same as we started with.
      assertEquals(sql1, sql2);

      // Now unparse again in the null dialect.
      // If the unparser is not including sufficient parens to override
      // precedence, the problem will show up here.
      checkList(sqlNodeList2, converter, expected);

      final Random random = new Random();
      final String sql3 = toSqlString(sqlNodeList, randomize(random));
      assertThat(sql3, notNullValue());
    }

    @Override public void check(SqlTestFactory factory, StringAndPos sap,
        @Nullable SqlDialect dialect, UnaryOperator<String> converter,
        String expected, Consumer<SqlParser> parserChecker) {
      SqlNode sqlNode = parseStmtAndHandleEx(factory, sap.sql, parserChecker);

      // Unparse with the given dialect, always parenthesize.
      final SqlDialect dialect2 = Util.first(dialect, AnsiSqlDialect.DEFAULT);
      final UnaryOperator<SqlWriterConfig> writerTransform =
          c -> simpleWithParens(c)
              .withDialect(dialect2);
      final String actual = sqlNode.toSqlString(writerTransform).getSql();
      assertEquals(expected, converter.apply(actual));

      // Unparse again in Calcite dialect (which we can parse), and
      // minimal parentheses.
      final String sql1 = sqlNode.toSqlString(simple()).getSql();

      // Parse and unparse again.
      SqlTestFactory factory2 =
          factory.withParserConfig(c -> c.withQuoting(Quoting.DOUBLE_QUOTE));
      SqlNode sqlNode2 =
          parseStmtAndHandleEx(factory2, sql1, parser -> { });
      final String sql2 = sqlNode2.toSqlString(simple()).getSql();

      // Should be the same as we started with.
      assertEquals(sql1, sql2);

      // Now unparse again in the given dialect.
      // If the unparser is not including sufficient parens to override
      // precedence, the problem will show up here.
      final String actual2 = sqlNode.toSqlString(writerTransform).getSql();
      assertEquals(expected, converter.apply(actual2));

      // Now unparse with a randomly configured SqlPrettyWriter.
      // (This is a much a test for SqlPrettyWriter as for the parser.)
      final Random random = new Random();
      final String sql3 = sqlNode.toSqlString(randomize(random)).getSql();
      assertThat(sql3, notNullValue());
      SqlNode sqlNode4 =
          parseStmtAndHandleEx(factory2, sql1, parser -> { });
      final String sql4 = sqlNode4.toSqlString(simple()).getSql();
      assertEquals(sql1, sql4);
    }

    @Override public void checkExp(SqlTestFactory factory, StringAndPos sap,
        UnaryOperator<String> converter, String expected,
        Consumer<SqlParser> parserChecker) {
      SqlNode sqlNode =
          parseExpressionAndHandleEx(factory, sap.sql, parserChecker);

      // Unparse with no dialect, always parenthesize.
      final UnaryOperator<SqlWriterConfig> writerTransform =
          c -> simpleWithParens(c)
              .withDialect(AnsiSqlDialect.DEFAULT);
      final String actual = sqlNode.toSqlString(writerTransform).getSql();
      assertEquals(expected, converter.apply(actual));

      // Unparse again in Calcite dialect (which we can parse), and
      // minimal parentheses.
      final String sql1 =
          sqlNode.toSqlString(UnaryOperator.identity()).getSql();

      // Parse and unparse again.
      // (Turn off parser checking, and use double-quotes.)
      final Consumer<SqlParser> nullChecker = parser -> { };
      final SqlTestFactory dqFactory =
          factory.withParserConfig(c -> c.withQuoting(Quoting.DOUBLE_QUOTE));
      SqlNode sqlNode2 =
          parseExpressionAndHandleEx(dqFactory, sql1, nullChecker);
      final String sql2 =
          sqlNode2.toSqlString(UnaryOperator.identity()).getSql();

      // Should be the same as we started with.
      assertEquals(sql1, sql2);

      // Now unparse again in the null dialect.
      // If the unparser is not including sufficient parens to override
      // precedence, the problem will show up here.
      final String actual2 = sqlNode2.toSqlString(null, true).getSql();
      assertEquals(expected, converter.apply(actual2));
    }

    @Override public void checkFails(SqlTestFactory factory,
        StringAndPos sap, boolean list, String expectedMsgPattern) {
      // Do nothing. We're not interested in unparsing invalid SQL
    }

    @Override public void checkExpFails(SqlTestFactory factory,
        StringAndPos sap, String expectedMsgPattern) {
      // Do nothing. We're not interested in unparsing invalid SQL
    }
  }

  /** Runs tests on period operators such as OVERLAPS, IMMEDIATELY PRECEDES. */
  private class Checker {
    final String op;
    final String period;

    Checker(String op, String period) {
      this.op = op;
      this.period = period;
    }

    public void checkExp(String sql, String expected) {
      expr(sql.replace("$op", op).replace("$p", period))
          .ok(expected.replace("$op", op.toUpperCase(Locale.ROOT)));
    }

    public void checkExpFails(String sql, String expected) {
      expr(sql.replace("$op", op).replace("$p", period))
          .fails(expected.replace("$op", op));
    }
  }
}
