content: /*
   * 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.test.enumerable;
  
  import org.apache.calcite.adapter.enumerable.EnumerableRules;
  import org.apache.calcite.adapter.java.ReflectiveSchema;
  import org.apache.calcite.config.CalciteConnectionProperty;
  import org.apache.calcite.config.Lex;
  import org.apache.calcite.jdbc.JavaCollation;
  import org.apache.calcite.plan.RelOptPlanner;
  import org.apache.calcite.plan.RelOptUtil;
  import org.apache.calcite.plan.hep.HepPlanner;
  import org.apache.calcite.plan.hep.HepProgram;
  import org.apache.calcite.plan.hep.HepProgramBuilder;
  import org.apache.calcite.rel.RelNode;
  import org.apache.calcite.rel.core.JoinRelType;
  import org.apache.calcite.rel.rules.CoreRules;
  import org.apache.calcite.rel.type.RelDataType;
  import org.apache.calcite.rex.RexBuilder;
  import org.apache.calcite.runtime.Hook;
  import org.apache.calcite.sql.SqlCollation;
  import org.apache.calcite.sql.SqlOperator;
  import org.apache.calcite.sql.type.SqlTypeName;
  import org.apache.calcite.test.CalciteAssert;
  import org.apache.calcite.test.RelBuilderTest;
  import org.apache.calcite.test.schemata.hr.HrSchema;
  import org.apache.calcite.tools.RelBuilder;
  import org.apache.calcite.util.Util;
  
  import org.junit.jupiter.api.Test;
  
  import java.text.Collator;
  import java.util.Collections;
  import java.util.Locale;
  import java.util.function.Consumer;
  
  import static org.apache.calcite.sql.fun.SqlStdOperatorTable.EQUALS;
  import static org.apache.calcite.sql.fun.SqlStdOperatorTable.GREATER_THAN;
  import static org.apache.calcite.sql.fun.SqlStdOperatorTable.LESS_THAN;
  import static org.apache.calcite.sql.fun.SqlStdOperatorTable.NOT_EQUALS;
  import static org.apache.calcite.test.Matchers.isLinux;
  
  import static org.hamcrest.MatcherAssert.assertThat;
  
  /**
   * Test cases for
   * <a href="https://issues.apache.org/jira/browse/CALCITE-3951">[CALCITE-3951]
   * Support different string comparison based on SqlCollation</a>.
   */
  class EnumerableStringComparisonTest {
  
    private static final SqlCollation SPECIAL_COLLATION_PRIMARY =
        new JavaCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
            Util.getDefaultCharset(), Collator.PRIMARY);
  
    private static final SqlCollation SPECIAL_COLLATION_SECONDARY =
        new JavaCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
            Util.getDefaultCharset(), Collator.SECONDARY);
  
    private static final SqlCollation SPECIAL_COLLATION_TERTIARY =
        new JavaCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
            Util.getDefaultCharset(), Collator.TERTIARY);
  
    private static final SqlCollation SPECIAL_COLLATION_IDENTICAL =
        new JavaCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
            Util.getDefaultCharset(), Collator.IDENTICAL);
  
    private RelDataType createRecordVarcharSpecialCollation(RelBuilder builder) {
      return builder.getTypeFactory().builder()
          .add(
              "name",
              builder.getTypeFactory().createTypeWithCharsetAndCollation(
                  builder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR),
                  builder.getTypeFactory().getDefaultCharset(),
                  SPECIAL_COLLATION_TERTIARY))
          .build();
    }
  
    private RelDataType createVarcharSpecialCollation(RelBuilder builder, SqlCollation collation) {
      return builder.getTypeFactory().createTypeWithCharsetAndCollation(
          builder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR),
          builder.getTypeFactory().getDefaultCharset(),
          collation);
    }
  
    @Test void testSortStringDefault() {
      tester()
          .withRel(builder -> builder
              .values(
                  builder.getTypeFactory().builder()
                      .add("name",
                          builder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR)).build(),
                  "Legal", "presales", "hr", "Administration", "MARKETING")
              .sort(
                  builder.field(1, 0, "name"))
              .build())
          .explainHookMatches(""
              + "EnumerableSort(sort0=[$0], dir0=[ASC])\n"
              + "  EnumerableValues(tuples=[[{ 'Legal' }, { 'presales' }, { 'hr' }, { 'Administration' }, { 'MARKETING' }]])\n")
          .returnsOrdered("name=Administration",
              "name=Legal",
              "name=MARKETING",
              "name=hr",
              "name=presales");
    }
  
    @Test void testSortStringSpecialCollation() {
      tester()
          .withRel(builder -> builder
              .values(
                  createRecordVarcharSpecialCollation(builder),
                  "Legal", "presales", "hr", "Administration", "MARKETING")
              .sort(
                  builder.field(1, 0, "name"))
              .build())
          .explainHookMatches(""
              + "EnumerableSort(sort0=[$0], dir0=[ASC])\n"
              + "  EnumerableValues(tuples=[[{ 'Legal' }, { 'presales' }, { 'hr' }, { 'Administration' }, { 'MARKETING' }]])\n")
          .returnsOrdered("name=Administration",
              "name=hr",
              "name=Legal",
              "name=MARKETING",
              "name=presales");
    }
  
    /** Test case for
     * <a href="https://issues.apache.org/jira/browse/CALCITE-5967">[CALCITE-5967]
     * UnsupportedOperationException while implementing a call that requires a special collator</a>.
     */
    @Test void testFilterStringSpecialCollation() {
      tester()
          .withRel(builder -> builder
              .values(
                  createRecordVarcharSpecialCollation(builder),
                  "Legal", "presales", "hr", "Administration", "MARKETING")
              // Filter on a field with special collation:
              // a special comparator needs to be used inside the eq operation
              .filter(
                  builder.equals(
                      builder.field(1, 0, "name"),
                      builder.literal("MARKETING")))
              .build())
          .explainHookMatches(""
              + "EnumerableCalc(expr#0=[{inputs}], expr#1=['MARKETING'], expr#2=[=($t0, $t1)], name=[$t0], $condition=[$t2])\n"
              + "  EnumerableValues(tuples=[[{ 'Legal' }, { 'presales' }, { 'hr' }, { 'Administration' }, { 'MARKETING' }]])\n")
          .returnsUnordered("name=MARKETING");
    }
  
    @Test void testMergeJoinOnStringSpecialCollation() {
      tester()
          .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
            planner.addRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
            planner.removeRule(EnumerableRules.ENUMERABLE_JOIN_RULE);
          })
          .withRel(builder -> builder
                .values(createRecordVarcharSpecialCollation(builder),
                    "Legal", "presales", "HR", "Administration", "Marketing").as("v1")
                .values(createRecordVarcharSpecialCollation(builder),
                    "Marketing", "bureaucracy", "Sales", "HR").as("v2")
                .join(JoinRelType.INNER,
                    builder.equals(
                        builder.field(2, 0, "name"),
                        builder.field(2, 1, "name")))
                .project(
                    builder.field("v1", "name"),
                    builder.field("v2", "name"))
                .build())
          .explainHookMatches("" // It is important that we have MergeJoin in the plan
              + "EnumerableMergeJoin(condition=[=($0, $1)], joinType=[inner])\n"
              + "  EnumerableSort(sort0=[$0], dir0=[ASC])\n"
              + "    EnumerableValues(tuples=[[{ 'Legal' }, { 'presales' }, { 'HR' }, { 'Administration' }, { 'Marketing' }]])\n"
              + "  EnumerableSort(sort0=[$0], dir0=[ASC])\n"
              + "    EnumerableValues(tuples=[[{ 'Marketing' }, { 'bureaucracy' }, { 'Sales' }, { 'HR' }]])\n")
          .returnsOrdered("name=HR; name0=HR",
              "name=Marketing; name0=Marketing");
    }
  
    /** Test case for
     * <a href="https://issues.apache.org/jira/browse/CALCITE-5003">[CALCITE-5003]
     * MergeUnion on types with different collators produces wrong result</a>. */
    @Test void testMergeUnionOnStringDifferentCollation() {
      tester()
          .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner ->
              planner.removeRule(EnumerableRules.ENUMERABLE_UNION_RULE))
          .withRel(b -> {
            final RelBuilder builder = b.transform(c -> c.withSimplifyValues(false));
            return builder
                .values(builder.getTypeFactory().builder()
                        .add("name",
                            builder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR)).build(),
                    "facilities", "HR", "administration", "Marketing")
                .values(createRecordVarcharSpecialCollation(builder),
                    "Marketing", "administration", "presales", "HR")
                .union(false)
                .sort(0)
                .build();
          })
          .explainHookMatches("" // It is important that we have MergeUnion in the plan
              + "EnumerableMergeUnion(all=[false])\n"
              + "  EnumerableSort(sort0=[$0], dir0=[ASC])\n"
              + "    EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):VARCHAR COLLATE \"ISO-8859-1$en_US$tertiary$JAVA_COLLATOR\" NOT NULL], name=[$t1])\n"
              + "      EnumerableValues(tuples=[[{ 'facilities' }, { 'HR' }, { 'administration' }, { 'Marketing' }]])\n"
              + "  EnumerableSort(sort0=[$0], dir0=[ASC])\n"
              + "    EnumerableValues(tuples=[[{ 'Marketing' }, { 'administration' }, { 'presales' }, { 'HR' }]])\n")
          .returnsOrdered("name=administration",
              "name=facilities",
              "name=HR",
              "name=Marketing",
              "name=presales");
    }
  
    /** Test case for
     * <a href="https://issues.apache.org/jira/browse/CALCITE-4195">[CALCITE-4195]
     * Cast between types with different collators must be evaluated as not monotonic</a>. */
    @Test void testCastDifferentCollationShouldNotApplySortProjectTranspose() {
      final RelBuilder relBuilder = RelBuilder.create(RelBuilderTest.config().build());
      final RelNode relNode = relBuilder
          .values(
              createRecordVarcharSpecialCollation(relBuilder),
              "Legal", "presales", "hr", "Administration", "MARKETING")
          .project(
              relBuilder.cast(relBuilder.field("name"), SqlTypeName.VARCHAR))
          .sort(
              relBuilder.field(1, 0, 0))
          .build();
  
      // Cast to a type with a different collation, and then sort;
      // in this scenario SORT_PROJECT_TRANSPOSE must not be applied.
      final HepProgram program = new HepProgramBuilder()
          .addRuleInstance(CoreRules.SORT_PROJECT_TRANSPOSE)
          .build();
      final HepPlanner hepPlanner = new HepPlanner(program);
      hepPlanner.setRoot(relNode);
      final RelNode output = hepPlanner.findBestExp();
      final String planBefore = RelOptUtil.toString(relNode);
      final String planAfter = RelOptUtil.toString(output);
      final String expected =
          "LogicalSort(sort0=[$0], dir0=[ASC])\n"
          + "  LogicalProject(name=[CAST($0):VARCHAR NOT NULL])\n"
          + "    LogicalValues(tuples=[[{ 'Legal' }, { 'presales' }, { 'hr' }, { 'Administration' }, { 'MARKETING' }]])\n";
      assertThat(planBefore, isLinux(expected));
      assertThat(planAfter, isLinux(expected));
    }
  
    @Test void testStringComparison() {
      testStringComparison("a", "A", LESS_THAN, true);
      testStringComparison("a", "A", GREATER_THAN, false);
      testStringComparison("A", "a", LESS_THAN, false);
      testStringComparison("A", "a", GREATER_THAN, true);
  
      testStringComparison("aaa", "AAA", EQUALS, false);
      testStringComparison("aaa", "AAA", NOT_EQUALS, true);
      testStringComparison("AAA", "AAA", EQUALS, true);
      testStringComparison("AAA", "AAA", NOT_EQUALS, false);
      testStringComparison("AAA", "BBB", EQUALS, false);
      testStringComparison("AAA", "BBB", NOT_EQUALS, true);
  
      testStringComparison("a", "b", LESS_THAN, true);
      testStringComparison("A", "B", LESS_THAN, true);
      testStringComparison("a", "B", LESS_THAN, true);
      testStringComparison("A", "b", LESS_THAN, true);
      testStringComparison("a", "b", GREATER_THAN, false);
      testStringComparison("A", "B", GREATER_THAN, false);
      testStringComparison("a", "B", GREATER_THAN, false);
      testStringComparison("A", "b", GREATER_THAN, false);
  
      testStringComparison("b", "a", GREATER_THAN, true);
      testStringComparison("B", "A", GREATER_THAN, true);
      testStringComparison("B", "a", GREATER_THAN, true);
      testStringComparison("b", "A", GREATER_THAN, true);
      testStringComparison("b", "a", LESS_THAN, false);
      testStringComparison("B", "A", LESS_THAN, false);
      testStringComparison("B", "a", LESS_THAN, false);
      testStringComparison("b", "A", LESS_THAN, false);
  
      // Check differences regarding strength:
  
      testStringComparison("ABC", "ABC", EQUALS, SPECIAL_COLLATION_PRIMARY, true);
      testStringComparison("ABC", "ABC", EQUALS, SPECIAL_COLLATION_SECONDARY, true);
      testStringComparison("ABC", "ABC", EQUALS, SPECIAL_COLLATION_TERTIARY, true);
      testStringComparison("ABC", "ABC", EQUALS, SPECIAL_COLLATION_IDENTICAL, true);
  
      testStringComparison("abc", "ÀBC", EQUALS, SPECIAL_COLLATION_PRIMARY, true);
      testStringComparison("abc", "ÀBC", EQUALS, SPECIAL_COLLATION_SECONDARY, false);
      testStringComparison("abc", "ÀBC", EQUALS, SPECIAL_COLLATION_TERTIARY, false);
      testStringComparison("abc", "ÀBC", EQUALS, SPECIAL_COLLATION_IDENTICAL, false);
  
      testStringComparison("abc", "ABC", EQUALS, SPECIAL_COLLATION_PRIMARY, true);
      testStringComparison("abc", "ABC", EQUALS, SPECIAL_COLLATION_SECONDARY, true);
      testStringComparison("abc", "ABC", EQUALS, SPECIAL_COLLATION_TERTIARY, false);
      testStringComparison("abc", "ABC", EQUALS, SPECIAL_COLLATION_IDENTICAL, false);
  
      testStringComparison("\u0001", "\u0002", EQUALS, SPECIAL_COLLATION_PRIMARY, true);
      testStringComparison("\u0001", "\u0002", EQUALS, SPECIAL_COLLATION_SECONDARY, true);
      testStringComparison("\u0001", "\u0002", EQUALS, SPECIAL_COLLATION_TERTIARY, true);
      testStringComparison("\u0001", "\u0002", EQUALS, SPECIAL_COLLATION_IDENTICAL, false);
    }
  
    private void testStringComparison(String str1, String str2,
                                      SqlOperator operator, boolean expectedResult) {
      testStringComparison(str1, str2, operator, SPECIAL_COLLATION_TERTIARY, expectedResult);
    }
  
    private void testStringComparison(String str1, String str2,
                                      SqlOperator operator, SqlCollation col,
                                      boolean expectedResult) {
      tester()
          .withRel(builder -> {
            final RexBuilder rexBuilder = builder.getRexBuilder();
            final RelDataType varcharSpecialCollation = createVarcharSpecialCollation(builder, col);
            return builder
                .values(new String[]{"aux"}, false)
                .project(
                    Collections.singletonList(
                        builder.call(
                            operator,
                            rexBuilder.makeCast(varcharSpecialCollation, builder.literal(str1)),
                            rexBuilder.makeCast(varcharSpecialCollation, builder.literal(str2)))),
                    Collections.singletonList("result"))
                .build();
          })
          .returnsUnordered("result=" + expectedResult);
    }
  
    private CalciteAssert.AssertThat tester() {
      return CalciteAssert.that()
          .with(CalciteConnectionProperty.LEX, Lex.JAVA)
          .with(CalciteConnectionProperty.FORCE_DECORRELATE, false)
          .withSchema("s", new ReflectiveSchema(new HrSchema()));
    }
  }
  