TestLogicalRowExpressions.java

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.facebook.presto.sql.relational;

import com.facebook.presto.common.function.OperatorType;
import com.facebook.presto.expressions.LogicalRowExpressions;
import com.facebook.presto.metadata.FunctionAndTypeManager;
import com.facebook.presto.spi.relation.CallExpression;
import com.facebook.presto.spi.relation.RowExpression;
import com.facebook.presto.spi.relation.SpecialFormExpression;
import com.facebook.presto.spi.relation.VariableReferenceExpression;
import com.google.common.collect.ImmutableList;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static com.facebook.presto.common.function.OperatorType.EQUAL;
import static com.facebook.presto.common.function.OperatorType.GREATER_THAN;
import static com.facebook.presto.common.function.OperatorType.GREATER_THAN_OR_EQUAL;
import static com.facebook.presto.common.function.OperatorType.LESS_THAN;
import static com.facebook.presto.common.function.OperatorType.LESS_THAN_OR_EQUAL;
import static com.facebook.presto.common.function.OperatorType.NOT_EQUAL;
import static com.facebook.presto.common.type.BooleanType.BOOLEAN;
import static com.facebook.presto.common.type.DoubleType.DOUBLE;
import static com.facebook.presto.common.type.IntegerType.INTEGER;
import static com.facebook.presto.common.type.VarcharType.VARCHAR;
import static com.facebook.presto.expressions.LogicalRowExpressions.FALSE_CONSTANT;
import static com.facebook.presto.expressions.LogicalRowExpressions.TRUE_CONSTANT;
import static com.facebook.presto.expressions.LogicalRowExpressions.extractPredicates;
import static com.facebook.presto.metadata.FunctionAndTypeManager.createTestFunctionAndTypeManager;
import static com.facebook.presto.spi.relation.SpecialFormExpression.Form.AND;
import static com.facebook.presto.spi.relation.SpecialFormExpression.Form.IS_NULL;
import static com.facebook.presto.spi.relation.SpecialFormExpression.Form.OR;
import static com.facebook.presto.sql.analyzer.TypeSignatureProvider.fromTypes;
import static com.facebook.presto.sql.relational.Expressions.call;
import static com.facebook.presto.sql.relational.Expressions.constant;
import static org.testng.Assert.assertEquals;

public class TestLogicalRowExpressions
{
    private FunctionAndTypeManager functionAndTypeManager;
    private LogicalRowExpressions logicalRowExpressions;
    private static final RowExpression a = name("a");
    private static final RowExpression b = name("b");
    private static final RowExpression c = name("c");
    private static final RowExpression d = name("d");
    private static final RowExpression e = name("e");
    private static final RowExpression f = name("f");
    private static final RowExpression g = name("g");
    private static final RowExpression h = name("h");
    private static final VariableReferenceExpression V_0 = variable("v0");
    private static final VariableReferenceExpression V_1 = variable("v1");
    private static final VariableReferenceExpression V_2 = variable("v2");

    @BeforeClass
    public void setup()
    {
        functionAndTypeManager = createTestFunctionAndTypeManager();
        logicalRowExpressions = new LogicalRowExpressions(new RowExpressionDeterminismEvaluator(functionAndTypeManager), new FunctionResolution(functionAndTypeManager.getFunctionAndTypeResolver()), functionAndTypeManager);
    }

    @Test
    public void testAnd()
    {
        assertEquals(
                LogicalRowExpressions.and(a, b, c, d, e),
                and(and(and(a, b), and(c, d)), e));

        assertEquals(
                LogicalRowExpressions.and(),
                constant(true, BOOLEAN));

        assertEquals(
                logicalRowExpressions.combinePredicates(AND, a, b, a, c, d, c, e),
                and(and(and(a, b), and(c, d)), e));

        assertEquals(
                logicalRowExpressions.combineConjuncts(a, b, constant(false, BOOLEAN)),
                constant(false, BOOLEAN));

        assertEquals(
                extractPredicates(and(and(and(a, b), and(c, d)), e)),
                ImmutableList.of(a, b, c, d, e));
    }

    @Test
    public void testAndWithSubclassOfRowExpression()
    {
        assertEquals(
                LogicalRowExpressions.and(V_0, V_1, V_2),
                and(and(V_0, V_1), V_2));

        assertEquals(
                LogicalRowExpressions.and(ImmutableList.of(V_0, V_1, V_2)),
                and(and(V_0, V_1), V_2));
    }

    @Test
    public void testOr()
    {
        assertEquals(
                LogicalRowExpressions.or(a, b, c, d, e),
                or(or(or(a, b), or(c, d)), e));

        assertEquals(
                LogicalRowExpressions.or(),
                constant(false, BOOLEAN));

        assertEquals(
                logicalRowExpressions.combinePredicates(OR, a, b, constant(true, BOOLEAN)),
                constant(true, BOOLEAN));

        assertEquals(
                extractPredicates(or(or(or(a, b), or(c, d)), e)),
                ImmutableList.of(a, b, c, d, e));
    }

    @Test
    public void testOrWithSubclassOfRowExpression()
    {
        assertEquals(
                LogicalRowExpressions.or(V_0, V_1, V_2),
                or(or(V_0, V_1), V_2));

        assertEquals(
                LogicalRowExpressions.or(ImmutableList.of(V_0, V_1, V_2)),
                or(or(V_0, V_1), V_2));
    }

    @Test
    public void testDeterminism()
    {
        RowExpression nondeterministic = call("random", functionAndTypeManager.lookupFunction("random", fromTypes()), DOUBLE);
        RowExpression deterministic = call("length", functionAndTypeManager.lookupFunction("length", fromTypes(VARCHAR)), INTEGER);

        RowExpression expression = and(and(a, or(b, nondeterministic)), deterministic);

        assertEquals(logicalRowExpressions.filterDeterministicConjuncts(expression), and(a, deterministic));
        assertEquals(logicalRowExpressions.filterNonDeterministicConjuncts(expression), or(b, nondeterministic));
    }

    @Test
    public void testPushNegationToLeaves()
    {
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(and(a, b))), or(not(a), not(b)));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(a, b))), and(not(a), not(b)));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(not(a), not(b)))), and(a, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(not(a), not(not(not(b)))))), and(a, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(not(and(a, b)), or(b, not(and(c, d)))))), and(and(a, b), and(not(b), and(c, d))));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(not(and(a, b)), not(b)))), and(and(a, b), b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(not(and(a, b)), not(and(b, c))))), and(and(a, b), and(b, c)));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(not(and(a, b)), not(or(b, c))))), and(and(a, b), or(b, c)));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(or(not(and(a, b)), not(or(b, not(and(c, d))))))), and(and(a, b), or(b, or(not(c), not(d)))));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(or(not(and(a, b)), not(or(b, not(and(c, d)))))), or(or(not(a), not(b)), and(not(b), and(c, d))));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(and(and(a, b), c)), and(and(a, b), c));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, EQUAL, b))), compare(a, NOT_EQUAL, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, NOT_EQUAL, b))), compare(a, EQUAL, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, GREATER_THAN, b))), compare(a, LESS_THAN_OR_EQUAL, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, GREATER_THAN_OR_EQUAL, b))), compare(a, LESS_THAN, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, GREATER_THAN_OR_EQUAL, not(not(b))))), compare(a, LESS_THAN, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, LESS_THAN, b))), compare(a, GREATER_THAN_OR_EQUAL, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, LESS_THAN_OR_EQUAL, b))), compare(a, GREATER_THAN, b));
        assertEquals(logicalRowExpressions.pushNegationToLeaves(not(compare(a, LESS_THAN_OR_EQUAL, not(not(b))))), compare(a, GREATER_THAN, b));
    }

    @Test
    public void testEliminateConstant()
    {
        // Testing eliminate constant
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(TRUE_CONSTANT, a), and(FALSE_CONSTANT, b))),
                a);
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(or(and(TRUE_CONSTANT, a), and(FALSE_CONSTANT, b))),
                a);

        // Testing eliminate constant in nested tree
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(a, and(b, or(c, and(FALSE_CONSTANT, d))))),
                and(and(a, b), c));
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(a, and(b, or(c, and(e, or(f, and(FALSE_CONSTANT, d))))))),
                and(and(a, b), or(c, and(e, f))));
    }

    @Test
    public void testDuplicateIsNullExpressions()
    {
        SpecialFormExpression isNullExpression = new SpecialFormExpression(IS_NULL, BOOLEAN, a);
        List<RowExpression> arguments = Arrays.asList(new SpecialFormExpression[]{isNullExpression, isNullExpression});
        SpecialFormExpression duplicateIsNullExpression = new SpecialFormExpression(OR, BOOLEAN, arguments);
        logicalRowExpressions.minimalNormalForm(duplicateIsNullExpression);
    }

    @Test
    public void testEliminateDuplicate()
    {
        RowExpression nd = call("random", functionAndTypeManager.lookupFunction("random", fromTypes()), DOUBLE);

        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(TRUE_CONSTANT, a), and(b, b))),
                or(a, b));

        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(a, b), and(a, b))),
                and(a, b));
        // we will prefer most simplified expression than correct conjunctive/disjunctive form
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(or(and(a, b), and(a, b))),
                and(a, b));

        // eliminate duplicated items with different order, prefers the ones appears first.
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(b, a), and(a, b))),
                and(b, a));
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(or(and(b, a), and(a, b))),
                and(b, a));

        // (b && a) || a
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(b, a), a)),
                a);
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(or(and(b, a), a)),
                a);
        // (b || a) && a
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(a, or(b, a))),
                a);
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(a, or(b, a))),
                a);

        // (b && a) || (a && b && c) -> b && a (should keep b && a instead of a && b as it appears first)
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(b, a), and(and(a, b), c))),
                and(b, a));
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(or(and(b, a), and(and(a, b), c))),
                and(b, a));

        // (b || a) && (a || b) && (a || b || c || d) || (a || b || c) -> b || a (should keep b || a instead of a || b as it appears first)
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(or(b, a), and(or(a, b), and(or(or(c, d), or(a, b)), or(a, or(b, c)))))),
                or(b, a));

        // (b || a)  && (a || b || c) && (a || b) && (a || b || nd) && e
        // we cannot eliminate nd because it is non-deterministic
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(and(or(b, a), and(or(a, or(b, c)), and(or(a, b), or(or(a, b), nd)))), e)),
                and(and(or(b, a), or(or(a, b), nd)), e));
        // we cannot convert to disjunctive form because nd is non-deterministic
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(and(or(b, a), and(or(a, or(b, c)), and(or(a, b), or(or(a, b), nd)))), e)),
                and(and(or(b, a), or(or(a, b), nd)), e));

        // (b || a)  && (a || b || c) && (a || b) && (a || b || d) && e
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(and(or(b, a), and(or(a, or(b, c)), and(or(a, b), or(or(a, b), d)))), e)),
                and(or(b, a), e));
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(and(or(b, a), and(or(a, or(b, c)), and(or(a, b), or(or(a, b), d)))), e)),
                or(and(b, e), and(a, e)));

        // (b || a || c) && (a || b || d) && (a || b || e) && (a || b || f)
        // already conjunctive form
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(or(or(b, a), c), and(or(d, or(a, b)), and(or(or(a, b), e), or(or(a, b), f))))),
                and(and(or(or(b, a), c), or(or(b, a), d)), and(or(or(b, a), e), or(or(b, a), f))));
        // (b || a || c) && (a || b || d) && (a || b || f) -> b || a || (c && d && e && f)
        // can be simplified by extract common predicates
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(or(b, a), c), and(or(d, or(a, b)), and(or(or(a, b), e), or(or(a, b), f))))),
                or(or(b, a), and(and(c, d), and(e, f))));

        // de-duplicate nested expression
        // ((a && b) || (a && c) || (a && d)) && ((b && c) || (c && a) || (c && d)) -> a && c
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(or(and(a, b), and(a, c), and(a, d)), or(and(c, b), and(c, a), and(c, d)))),
                and(a, c));
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(and(a, b), and(a, c), and(a, d)), or(and(c, b), and(c, a), and(c, d)))),
                and(a, c));
    }

    @Test
    public void testConvertToCNF()
    {
        // When considering where we want to swap forms (i.e. an AND expression for DNF or OR expression for CNF), there are 3 cases for each argument
        // 1) it's a 1-term clause (literal) (neither AND nor OR)
        // 2) it's a multi-term single clause (ex. for CNF: (A OR B), DNF: (A AND B))
        // 3) it's a multi-clause branch (ex. for CNF: (A OR B) AND (C OR D), DNF: (A AND B) OR (C AND D))

        // Consider the following unique argument pairs and specify what the expected transformation should be.
        // left = 1, right = 1 --> (2) termJoiner(left, right)
        // left = 1, right = 2 --> (2) termJoiner(left, right)
        // left = 1, right = 3 --> (3) distribute left into right
        // left = 2, right = 2 --> (2) distribute left into right
        // left = 2, right = 3 --> (3) right but for each arg of right's arguments : arg = OR (arg, left);
        // left = 3, right = 3 --> (3) expand and distribute both

        // Now let us test each of these permutations
        // 1, 1
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(and(a, b)),
                and(a, b),
                "Failed 1,1");

        // Should not change shape
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(a, b)),
                or(a, b),
                "Failed to keep same form if cannot convert");

        // 1, 1 with not pushdown
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(not(and(a, b))),
                or(not(a), not(b)),
                "Failed 1,1 with NOT pushdown");

        // 1, 2
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(or(a, b), c)),
                or(or(a, b), c),
                "Failed 1,2");

        // 1, 3
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(a, b), c)),
                and(or(a, c), or(b, c)),
                "Failed 1,3");

        // 1, 3 with multiple clauses
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(a, and(b, or(e, g))), c)),
                and(and(or(a, c), or(b, c)), or(or(e, g), c)),
                "Failed 1,3 with multiple clauses");

        // 2, 2
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(or(a, b), or(c, d))),
                or(or(a, b), or(c, d)),
                "Failed 2,2");

        // 2, 3
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(or(a, b), and(or(e, f), or(g, h)))),
                and(or(or(a, b), or(e, f)), or(or(a, b), or(g, h))),
                "Failed 2,3");

        // 3, 3
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(or(a, b), or(c, d)), and(or(e, f), or(g, h)))),
                and(and(or(or(a, b), or(e, f)), or(or(a, b), or(g, h))), and(or(or(c, d), or(e, f)), or(or(c, d), or(g, h)))),
                "Failed 3,3");

        // Testing symmetry
        // 2, 1
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(c, or(a, b))),
                or(or(c, a), b),
                "Failed 2,1");

        // 3, 1
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(c, and(a, b))),
                and(or(c, a), or(c, b)),
                "Failed 3,1");

        // 3, 2
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(or(e, f), or(g, h)), or(a, b))),
                and(or(or(e, f), or(a, b)), or(or(g, h), or(a, b))),
                "Failed 3,2");

        // 3, 3 large with NOT push down
        // (a && b && (e || g)) || !(a && b) || !(b || !(c && d)) ==> (a || !a || !b) && (b || !a || !b) && ((e || g) || !a || !b)
        // notice that a || !a cannot be easily optimized away in SQL since we have to handle if a is unknown (null).
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(a, and(b, or(e, g))), or(not(and(a, b)), not(or(b, not(and(c, d))))))),
                and(and(or(or(a, not(a)), not(b)), or(or(b, not(a)), not(b))), or(or(e, g), or(not(a), not(b)))),
                "Failed 3,3 large with NOT pushdown");

        // a || b || c || d || (d && e) || (e && f) ==> (a || b || c || d || e) && (a || b || c || d || f)
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(
                or(a, or(b, or(c, or(d, or(and(d, e), and(e, f))))))),
                and(or(or(or(a, b), or(c, d)), e), or(or(or(a, b), or(c, d)), f)));

        // (a && b && c) || (d && e) can increase size significantly so do not expand.
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(and(a, and(b, c)), and(and(d, e), f))),
                or(and(and(a, b), c), and(and(d, e), f)));
    }

    @Test
    public void testBigExpressions()
    {
        // Do not expand big list (a0 && b0) || (a1 && b1) || ....
        RowExpression bigExpression = or(IntStream.range(0, 1000)
                .boxed()
                .map(i -> and(name("a" + i), name("b" + i)))
                .toArray(RowExpression[]::new));
        assertEquals(logicalRowExpressions.convertToConjunctiveNormalForm(bigExpression), bigExpression);

        // extract common predicates on (a && b0) || (a && b1) || ....
        RowExpression bigExpressionWithCommonPredicate = or(IntStream.range(0, 10001)
                .boxed()
                .map(i -> and(name("a"), name("b" + i)))
                .toArray(RowExpression[]::new));
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(bigExpressionWithCommonPredicate),
                or(IntStream.range(0, 10001).boxed().map(i -> and(name("a"), name("b" + i))).toArray(RowExpression[]::new)));
        // a || (a && b0) || (a && b1) || ... can be simplified to a but if conjunctive is very large, we will skip reduction.
        assertEquals(
                logicalRowExpressions.convertToConjunctiveNormalForm(or(a, bigExpressionWithCommonPredicate)),
                or(Stream.concat(Stream.of(name("a")), IntStream.range(0, 10001).boxed().map(i -> and(name("a"), name("b" + i)))).toArray(RowExpression[]::new)));
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(or(a, bigExpressionWithCommonPredicate)),
                or(Stream.concat(Stream.of(name("a")), IntStream.range(0, 10001).boxed().map(i -> and(name("a"), name("b" + i)))).toArray(RowExpression[]::new)));
    }

    @Test
    public void testConvertToDNF()
    {
        // 1, 1
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(or(a, b)),
                or(a, b),
                "Failed 1,1");

        // Should not change shape
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(a, b)),
                and(a, b),
                "Failed to keep same form if cannot convert");

        // 1, 1 with not pushdown
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(not(or(a, b))),
                and(not(a), not(b)),
                "Failed 1,1 with NOT pushdown");

        // 1, 2
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(and(a, b), c)),
                and(and(a, b), c),
                "Failed 1,2");

        // 1, 3
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(a, b), c)),
                or(and(a, c), and(b, c)),
                "Failed 1,3");

        // 1, 3 with multiple clauses
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(a, or(b, and(e, g))), c)),
                or(or(and(a, c), and(b, c)), and(and(e, g), c)),
                "Failed 1,3 with multiple clauses");

        // 2, 2
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(and(a, b), and(c, d))),
                and(and(a, b), and(c, d)),
                "Failed 2,2");

        // 2, 3
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(and(a, b), or(and(e, f), and(g, h)))),
                or(and(and(a, b), and(e, f)), and(and(a, b), and(g, h))),
                "Failed 2,3");

        // 3, 3
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(and(a, b), and(c, d)), or(and(e, f), and(g, h)))),
                or(or(and(and(a, b), and(e, f)), and(and(a, b), and(g, h))), or(and(and(c, d), and(e, f)), and(and(c, d), and(g, h)))),
                "Failed 3,3");

        // Testing symmetry
        // 2, 1
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(c, and(a, b))),
                and(and(c, a), b),
                "Failed 2,1");

        // 3, 1
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(c, or(a, b))),
                or(and(c, a), and(c, b)),
                "Failed 3,1");

        // 3, 2
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(and(e, f), and(g, h)), and(a, b))),
                or(and(and(e, f), and(a, b)), and(and(g, h), and(a, b))),
                "Failed 3,2");

        // 3, 3 large with NOT pushdown
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(a, or(b, and(e, g))), and(not(or(a, b)), not(and(b, not(or(c, d))))))),
                or(or(and(and(a, not(a)), not(b)), and(and(b, not(a)), not(b))), and(and(e, g), and(not(a), not(b)))),
                "Failed 3,3 large with NOT pushdown");

        // (a || b || c) && ( d || e) will expand to too big if we convert to disjunctive form.
        assertEquals(
                logicalRowExpressions.convertToDisjunctiveNormalForm(and(or(a, or(b, c)), or(or(d, e), f))),
                and(or(or(a, b), c), or(or(d, e), f)));
    }

    private static RowExpression name(String name)
    {
        return new VariableReferenceExpression(Optional.empty(), name, BOOLEAN);
    }

    private static VariableReferenceExpression variable(String name)
    {
        return new VariableReferenceExpression(Optional.empty(), name, BOOLEAN);
    }

    private RowExpression compare(RowExpression left, OperatorType operator, RowExpression right)
    {
        return call(
                operator.getOperator(),
                new FunctionResolution(functionAndTypeManager.getFunctionAndTypeResolver()).comparisonFunction(operator, left.getType(), right.getType()),
                BOOLEAN,
                left,
                right);
    }

    private RowExpression and(RowExpression left, RowExpression right)
    {
        return new SpecialFormExpression(AND, BOOLEAN, left, right);
    }

    private RowExpression or(RowExpression... expressions)
    {
        return logicalRowExpressions.or(expressions);
    }

    private RowExpression or(RowExpression left, RowExpression right)
    {
        return new SpecialFormExpression(OR, BOOLEAN, left, right);
    }

    private RowExpression not(RowExpression expression)
    {
        return new CallExpression("not", new FunctionResolution(functionAndTypeManager.getFunctionAndTypeResolver()).notFunction(), BOOLEAN, ImmutableList.of(expression));
    }
}