blob: 1cb1e03d5983973e6735a37a0f72ecbfc1dfd160 [file] [log] [blame]
/*
* 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.ignite.internal.sql.engine;
import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
import org.apache.ignite.internal.sql.engine.hint.IgniteHint;
import org.apache.ignite.internal.sql.engine.util.HintUtils;
import org.apache.ignite.internal.sql.engine.util.MetadataMatcher;
import org.apache.ignite.internal.sql.engine.util.QueryChecker;
import org.apache.ignite.sql.ColumnType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Integration test for set op (EXCEPT, INTERSECT).
*/
public class ItSetOpTest extends BaseSqlIntegrationTest {
/**
* Before all.
*/
@BeforeAll
static void initTestData() {
createTable("EMP1");
createTable("EMP2");
int idx = 0;
insertData("PUBLIC.EMP1", List.of("ID", "NAME", "SALARY"), new Object[][]{
{idx++, "Igor", 10d},
{idx++, "Igor", 11d},
{idx++, "Igor", 12d},
{idx++, "Igor1", 13d},
{idx++, "Igor1", 13d},
{idx++, "Igor1", 13d},
{idx, "Roman", 14d}
});
idx = 0;
insertData("PUBLIC.EMP2", List.of("ID", "NAME", "SALARY"), new Object[][]{
{idx++, "Roman", 10d},
{idx++, "Roman", 11d},
{idx++, "Roman", 12d},
{idx++, "Roman", 13d},
{idx++, "Igor1", 13d},
{idx, "Igor1", 13d}
});
// Creating tables with different numeric types for the "val" column.
{
sql("CREATE TABLE t1(id INTEGER PRIMARY KEY, val INTEGER)");
sql("INSERT INTO t1 VALUES(1, 1)");
sql("INSERT INTO t1 VALUES(2, 2)");
sql("INSERT INTO t1 VALUES(3, 3)");
sql("INSERT INTO t1 VALUES(4, 4)");
sql("CREATE TABLE t2(id INTEGER PRIMARY KEY, val DECIMAL(4,2))");
sql("INSERT INTO t2 VALUES(2, 2)");
sql("INSERT INTO t2 VALUES(4, 4)");
sql("CREATE TABLE t3(id INTEGER PRIMARY KEY, val NUMERIC(4,2))");
sql("INSERT INTO t3 VALUES(2, 2)");
sql("INSERT INTO t3 VALUES(3, 3)");
}
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testExcept(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name FROM emp1 EXCEPT SELECT name FROM emp2");
assertEquals(1, rows.size());
assertEquals("Igor", rows.get(0).get(0));
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testExceptFromEmpty(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name FROM emp1 WHERE salary < 0 EXCEPT SELECT name FROM emp2");
assertEquals(0, rows.size());
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testExceptSeveralColumns(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name, salary FROM emp1 EXCEPT SELECT name, salary FROM emp2");
assertEquals(4, rows.size());
assertEquals(3, countIf(rows, r -> r.get(0).equals("Igor")));
assertEquals(1, countIf(rows, r -> r.get(0).equals("Roman")));
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testExceptAll(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name FROM emp1 EXCEPT ALL SELECT name FROM emp2");
assertEquals(4, rows.size());
assertEquals(3, countIf(rows, r -> r.get(0).equals("Igor")));
assertEquals(1, countIf(rows, r -> r.get(0).equals("Igor1")));
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testExceptNested(SetOpVariant setOp) {
var rows =
sql(setOp, "SELECT name FROM emp1 EXCEPT (SELECT name FROM emp1 EXCEPT SELECT name FROM emp2)");
assertEquals(2, rows.size());
assertEquals(1, countIf(rows, r -> r.get(0).equals("Roman")));
assertEquals(1, countIf(rows, r -> r.get(0).equals("Igor1")));
}
@Test
@Disabled("https://issues.apache.org/jira/browse/IGNITE-18475")
public void testSetOpBigBatch() {
sql("CREATE TABLE big_table1(key INT PRIMARY KEY, val INT)");
sql("CREATE TABLE big_table2(key INT PRIMARY KEY, val INT)");
int key = 0;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < ((i == 0) ? 1 : (1 << (i * 4 - 1))); j++) {
// Cache1 keys count: 1 of "0", 8 of "1", 128 of "2", 2048 of "3", 32768 of "4".
sql("INSERT INTO big_table1 VALUES (?, ?)", key++, i);
// Cache2 keys count: 1 of "5", 128 of "3", 32768 of "1".
if ((i & 1) == 0) {
sql("INSERT INTO big_table2 VALUES (?, ?)", key++, 5 - i);
}
}
}
// Check 2 partitioned caches.
var rows = sql("SELECT val FROM BIG_TABLE1 EXCEPT SELECT val FROM BIG_TABLE2");
assertEquals(3, rows.size());
assertEquals(1, countIf(rows, r -> r.get(0).equals(0)));
assertEquals(1, countIf(rows, r -> r.get(0).equals(2)));
assertEquals(1, countIf(rows, r -> r.get(0).equals(4)));
rows = sql("SELECT val FROM BIG_TABLE1 EXCEPT ALL SELECT val FROM BIG_TABLE2");
assertEquals(34817, rows.size());
assertEquals(1, countIf(rows, r -> r.get(0).equals(0)));
assertEquals(128, countIf(rows, r -> r.get(0).equals(2)));
assertEquals(1920, countIf(rows, r -> r.get(0).equals(3)));
assertEquals(32768, countIf(rows, r -> r.get(0).equals(4)));
rows = sql("SELECT val FROM BIG_TABLE1 INTERSECT SELECT val FROM BIG_TABLE2");
assertEquals(2, rows.size());
assertEquals(1, countIf(rows, r -> r.get(0).equals(1)));
assertEquals(1, countIf(rows, r -> r.get(0).equals(3)));
rows = sql("SELECT val FROM BIG_TABLE1 INTERSECT ALL SELECT val FROM BIG_TABLE2");
assertEquals(136, rows.size());
assertEquals(8, countIf(rows, r -> r.get(0).equals(1)));
assertEquals(128, countIf(rows, r -> r.get(0).equals(3)));
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testIntersect(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name FROM emp1 INTERSECT SELECT name FROM emp2");
assertEquals(2, rows.size());
assertEquals(1, countIf(rows, r -> r.get(0).equals("Igor1")));
assertEquals(1, countIf(rows, r -> r.get(0).equals("Roman")));
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testIntersectAll(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name FROM emp1 INTERSECT ALL SELECT name FROM emp2");
assertEquals(3, rows.size());
assertEquals(2, countIf(rows, r -> r.get(0).equals("Igor1")));
assertEquals(1, countIf(rows, r -> r.get(0).equals("Roman")));
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testIntersectEmpty(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name FROM emp1 WHERE salary < 0 INTERSECT SELECT name FROM emp2");
assertEquals(0, rows.size());
}
@ParameterizedTest
@EnumSource(SetOpVariant.class)
public void testIntersectSeveralColumns(SetOpVariant setOp) {
var rows = sql(setOp, "SELECT name, salary FROM emp1 INTERSECT ALL SELECT name, salary FROM emp2");
assertEquals(2, rows.size());
assertEquals(2, countIf(rows, r -> r.get(0).equals("Igor1")));
}
@Disabled("https://issues.apache.org/jira/browse/IGNITE-18426")
@Test
public void testSetOpColocated() {
sql("CREATE TABLE emp(empid INTEGER, deptid INTEGER, name VARCHAR, PRIMARY KEY(empid, deptid)) COLOCATE BY (deptid)");
sql("CREATE TABLE dept(deptid INTEGER, name VARCHAR, PRIMARY KEY(deptid))");
sql("INSERT INTO emp VALUES (0, 0, 'test0'), (1, 0, 'test1'), (2, 1, 'test2')");
sql("INSERT INTO dept VALUES (0, 'test0'), (1, 'test1'), (2, 'test2')");
assertQuery("SELECT deptid, name FROM emp EXCEPT SELECT deptid, name FROM dept")
.matches(QueryChecker.matches(".*IgniteExchange.*IgniteColocatedMinus.*"))
.returns(0, "test1")
.returns(1, "test2")
.check();
assertQuery("SELECT deptid, name FROM dept EXCEPT SELECT deptid, name FROM emp")
.matches(QueryChecker.matches(".*IgniteExchange.*IgniteColocatedMinus.*"))
.returns(1, "test1")
.returns(2, "test2")
.check();
assertQuery("SELECT deptid FROM dept EXCEPT SELECT deptid FROM emp")
.matches(QueryChecker.matches(".*IgniteExchange.*IgniteColocatedMinus.*"))
.returns(2)
.check();
assertQuery("SELECT deptid FROM dept INTERSECT SELECT deptid FROM emp")
.matches(QueryChecker.matches(".*IgniteExchange.*IgniteColocatedIntersect.*"))
.returns(0)
.returns(1)
.check();
}
@ParameterizedTest(name = "{0}")
@CsvSource({"EXCEPT,1,1.00", "INTERSECT,2,2.00"})
public void testSetOpDifferentNumericTypes(String setOp, int expectId, String expectVal) {
String query = "SELECT id, val FROM t1 "
+ setOp
+ " SELECT id, val FROM t2 "
+ setOp
+ " SELECT id, val FROM t3 ";
assertQuery(query)
.returns(expectId, new BigDecimal(expectVal))
.columnMetadata(
new MetadataMatcher().nullable(false).type(ColumnType.INT32),
new MetadataMatcher().nullable(true).type(ColumnType.DECIMAL)
)
.ordered()
.check();
}
@Test
public void testUnionDifferentNumericTypes() {
String query = ""
+ "SELECT id, val FROM t1 "
+ "UNION "
+ "SELECT id, val FROM t2";
assertQuery(query)
.returns(1, new BigDecimal("1.00"))
.returns(2, new BigDecimal("2.00"))
.returns(3, new BigDecimal("3.00"))
.returns(4, new BigDecimal("4.00"))
.columnMetadata(
new MetadataMatcher().type(ColumnType.INT32),
new MetadataMatcher().type(ColumnType.DECIMAL)
)
.check();
}
/**
* Test that set op node can be rewinded.
*/
@ParameterizedTest
@MethodSource("rewindSetOpVariants")
public void testSetOpRewindability(SetOpVariant setOp, int tableNum) {
sql(format("CREATE TABLE test_{}(id int PRIMARY KEY, i INTEGER)", tableNum));
sql(format("INSERT INTO test_{} VALUES (1, 1), (2, 2)", tableNum));
String query = format("SELECT {} (SELECT i FROM test_{} EXCEPT SELECT test_{}.i) FROM test_{}",
setOp.hint(), tableNum, tableNum, tableNum, tableNum
);
assertQuery(query)
.returns(1)
.returns(2)
.check();
}
private static Stream<Arguments> rewindSetOpVariants() {
List<Arguments> arguments = new ArrayList<>();
SetOpVariant[] ops = SetOpVariant.values();
for (int i = 0; i < ops.length; i++) {
arguments.add(Arguments.of(ops[i], i));
}
return arguments.stream();
}
@Test
public void testUnionAll() {
var rows = sql("SELECT name, salary FROM emp1 "
+ "UNION ALL "
+ "SELECT name, salary FROM emp2 "
+ "UNION ALL "
+ "SELECT name, salary FROM emp1 WHERE salary > 13 ");
assertEquals(14, rows.size());
}
@Test
public void testUnion() {
var rows = sql("SELECT name, salary FROM emp1 "
+ "UNION "
+ "SELECT name, salary FROM emp2 "
+ "UNION "
+ "SELECT name, salary FROM emp1 WHERE salary > 13 ");
assertEquals(9, rows.size());
}
@Test
public void testUnionWithDistinct() {
var rows = sql(
"SELECT distinct(name) FROM emp1 UNION SELECT name from emp2");
assertEquals(3, rows.size());
}
private static void createTable(String tableName) {
sql("CREATE TABLE " + tableName + "(id INT PRIMARY KEY, name VARCHAR, salary DOUBLE)");
}
private <T> long countIf(Iterable<T> it, Predicate<T> pred) {
return StreamSupport.stream(it.spliterator(), false).filter(pred).count();
}
private static List<List<Object>> sql(SetOpVariant setOp, String sql) {
// Wrap set query into SELECT because calcite does not allow to specify hints on set operation nodes (SqlCall nodes).
return sql(format("SELECT {} * FROM (" + sql + ")", setOp.hint()));
}
/**
* Set operation variant.
*/
public enum SetOpVariant {
COLOCATED("MapReduceMinusConverterRule"),
MAP_REDUCE("ColocatedMinusConverterRule");
final String[] disabledRules;
SetOpVariant(String... disabledRules) {
this.disabledRules = disabledRules;
}
String hint() {
return HintUtils.toHint(IgniteHint.DISABLE_RULE, disabledRules);
}
}
}