All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.unibz.inf.ontop.iq.node.impl.UnionNodeImpl Maven / Gradle / Ivy

package it.unibz.inf.ontop.iq.node.impl;

import com.google.common.collect.*;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import it.unibz.inf.ontop.exception.MinorOntopInternalBugException;
import it.unibz.inf.ontop.injection.IntermediateQueryFactory;
import it.unibz.inf.ontop.iq.exception.InvalidIntermediateQueryException;
import it.unibz.inf.ontop.iq.exception.QueryNodeSubstitutionException;
import it.unibz.inf.ontop.iq.exception.QueryNodeTransformationException;
import it.unibz.inf.ontop.iq.impl.IQTreeTools;
import it.unibz.inf.ontop.iq.node.*;
import it.unibz.inf.ontop.iq.node.normalization.NotRequiredVariableRemover;
import it.unibz.inf.ontop.iq.request.FunctionalDependencies;
import it.unibz.inf.ontop.iq.request.VariableNonRequirement;
import it.unibz.inf.ontop.iq.transform.IQTreeExtendedTransformer;
import it.unibz.inf.ontop.iq.transform.IQTreeVisitingTransformer;
import it.unibz.inf.ontop.iq.visit.IQVisitor;
import it.unibz.inf.ontop.model.term.*;
import it.unibz.inf.ontop.model.term.functionsymbol.FunctionSymbol;
import it.unibz.inf.ontop.substitution.*;
import it.unibz.inf.ontop.iq.*;
import it.unibz.inf.ontop.iq.transform.node.HomogeneousQueryNodeTransformer;
import it.unibz.inf.ontop.utils.CoreUtilsFactory;
import it.unibz.inf.ontop.utils.ImmutableCollectors;
import it.unibz.inf.ontop.utils.VariableGenerator;

import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class UnionNodeImpl extends CompositeQueryNodeImpl implements UnionNode {

    private static final String UNION_NODE_STR = "UNION";

    private final ImmutableSet projectedVariables;

    private final IQTreeTools iqTreeTools;
    private final CoreUtilsFactory coreUtilsFactory;
    private final NotRequiredVariableRemover notRequiredVariableRemover;

    @AssistedInject
    private UnionNodeImpl(@Assisted ImmutableSet projectedVariables,
                          IntermediateQueryFactory iqFactory,
                          SubstitutionFactory substitutionFactory, TermFactory termFactory,
                          CoreUtilsFactory coreUtilsFactory, IQTreeTools iqTreeTools,
                          NotRequiredVariableRemover notRequiredVariableRemover) {
        super(substitutionFactory, termFactory, iqFactory, iqTreeTools);
        this.projectedVariables = projectedVariables;
        this.iqTreeTools = iqTreeTools;
        this.coreUtilsFactory = coreUtilsFactory;
        this.notRequiredVariableRemover = notRequiredVariableRemover;
    }

    @Override
    public void acceptVisitor(QueryNodeVisitor visitor) {
        visitor.visit(this);
    }

    @Override
    public UnionNode acceptNodeTransformer(HomogeneousQueryNodeTransformer transformer)
            throws QueryNodeTransformationException {
        return transformer.transform(this);
    }

    @Override
    public ImmutableSet> getPossibleVariableDefinitions(ImmutableList children) {
        return children.stream()
                .flatMap(c -> c.getPossibleVariableDefinitions().stream())
                .map(s -> s.restrictDomainTo(projectedVariables))
                .collect(ImmutableCollectors.toSet());
    }

    @Override
    public boolean hasAChildWithLiftableDefinition(Variable variable, ImmutableList children) {
        return children.stream()
                .anyMatch(c -> (c.getRootNode() instanceof ConstructionNode)
                        && ((ConstructionNode) c.getRootNode()).getSubstitution().isDefining(variable));
    }

    @Override
    public VariableNullability getVariableNullability(ImmutableList children) {
        ImmutableSet variableNullabilities = children.stream()
                .map(IQTree::getVariableNullability)
                .collect(ImmutableCollectors.toSet());

        ImmutableMultimap> multimap = variableNullabilities.stream()
                .flatMap(vn -> vn.getNullableGroups().stream())
                .flatMap(g -> g.stream()
                        .map(v -> Maps.immutableEntry(v, g)))
                .collect(ImmutableCollectors.toMultimap());

        ImmutableMap> preselectedGroupMap = multimap.asMap().entrySet().stream()
                .collect(ImmutableCollectors.toMap(
                        Map.Entry::getKey,
                        e -> intersectionOfAll(e.getValue())));

        ImmutableSet> nullableGroups = preselectedGroupMap.keySet().stream()
                .map(v -> computeNullableGroup(v, preselectedGroupMap, variableNullabilities))
                .collect(ImmutableCollectors.toSet());

        ImmutableSet scope = iqTreeTools.getChildrenVariables(children);
        return coreUtilsFactory.createVariableNullability(nullableGroups, scope);
    }

    private ImmutableSet computeNullableGroup(Variable mainVariable,
                                                        ImmutableMap> preselectedGroupMap,
                                                        ImmutableSet variableNullabilities) {
        return preselectedGroupMap.get(mainVariable).stream()
                .filter(v -> mainVariable.equals(v)
                        || preselectedGroupMap.get(v).contains(mainVariable)
                            && variableNullabilities.stream().allMatch(vn -> vn.isPossiblyNullable(mainVariable) == vn.isPossiblyNullable(v)))
                .collect(ImmutableCollectors.toSet());
    }

    private static ImmutableSet intersectionOfAll(Collection> groups) {
        return groups.stream()
                .reduce((g1, g2) -> Sets.intersection(g1, g2).immutableCopy())
                .orElseThrow(() -> new IllegalArgumentException("groups must not be empty"));
    }

    @Override
    public boolean isConstructed(Variable variable, ImmutableList children) {
        return children.stream()
                .anyMatch(c -> c.isConstructed(variable));
    }

    @Override
    public boolean isDistinct(IQTree tree, ImmutableList children) {
        if (children.stream().anyMatch(c -> !c.isDistinct()))
            return false;

        return IntStream.range(0, children.size())
                .allMatch(i -> children.subList(i+1, children.size()).stream()
                        .allMatch(o -> areDisjoint(children.get(i), o)));
    }

    /**
     * Returns true if we are sure the two children can only return different tuples
     */
    private boolean areDisjoint(IQTree child1, IQTree child2) {
        return areDisjoint(child1, child2, projectedVariables);
    }

    private boolean areDisjoint(IQTree child1, IQTree child2, ImmutableSet variables) {
        VariableNullability variableNullability1 = child1.getVariableNullability();
        VariableNullability variableNullability2 = child2.getVariableNullability();

        ImmutableSet> possibleDefs1 = child1.getPossibleVariableDefinitions();
        ImmutableSet> possibleDefs2 = child2.getPossibleVariableDefinitions();

        return variables.stream()
                // We don't consider variables nullable on both side
                .filter(v -> !(variableNullability1.isPossiblyNullable(v) && variableNullability2.isPossiblyNullable(v)))
                .anyMatch(v -> areDisjointWhenNonNull(extractDefs(possibleDefs1, v), extractDefs(possibleDefs2, v), variableNullability1));
    }

    @Override
    public IQTree makeDistinct(ImmutableList children) {
        ImmutableMap> compatibilityMap = extractCompatibilityMap(children);

        if (areGroupDisjoint(compatibilityMap)) {
            // NB: multiple occurrences of the same child are automatically eliminated
            ImmutableList newChildren = ImmutableSet.copyOf(compatibilityMap.values()).stream()
                    .map(this::makeDistinctGroup)
                    .collect(ImmutableCollectors.toList());

            return makeDistinctGroupTree(newChildren);
        }
        /*
         * Fail-back: in the presence of non-disjoint groups of children,
         * puts the DISTINCT above.
         *
         * TODO: could be improved
         */
        else {
            return makeDistinctGroup(ImmutableSet.copyOf(children));
        }
    }

    private ImmutableMap> extractCompatibilityMap(ImmutableList children) {
        return IntStream.range(0, children.size())
                .boxed()
                // Compare to itself
                .flatMap(i -> IntStream.range(i, children.size())
                        .boxed()
                        .flatMap(j -> (i.equals(j) || (!areDisjoint(children.get(i), children.get(j))))
                                ? Stream.of(
                                Maps.immutableEntry(children.get(i), children.get(j)),
                                Maps.immutableEntry(children.get(j), children.get(i)))
                                : Stream.empty()))
                .collect(ImmutableCollectors.toMultimap())
                .asMap().entrySet().stream()
                .collect(ImmutableCollectors.toMap(
                        Map.Entry::getKey,
                        e -> ImmutableSet.copyOf(e.getValue())));
    }

    private IQTree makeDistinctGroup(ImmutableSet childGroup) {
        return iqFactory.createUnaryIQTree(iqFactory.createDistinctNode(),
                makeDistinctGroupTree(ImmutableList.copyOf(childGroup)));
    }

    private IQTree makeDistinctGroupTree(ImmutableList list) {
        switch (list.size()) {
            case 0:
                throw new MinorOntopInternalBugException("Unexpected empty child group");
            case 1:
                return list.get(0);
            default:
                return iqFactory.createNaryIQTree(this, list);
        }
    }

    private boolean areGroupDisjoint(ImmutableMap> compatibilityMap) {
        return compatibilityMap.values().stream()
                .allMatch(g -> g.stream()
                        .allMatch(t -> compatibilityMap.get(t).equals(g)));
    }

    private ImmutableSet extractDefs(ImmutableSet> possibleDefs,
                                                           Variable v) {
        if (possibleDefs.isEmpty())
            return ImmutableSet.of(v);

        return possibleDefs.stream()
                .map(s -> s.apply(v))
                .collect(ImmutableCollectors.toSet());
    }

    private boolean areDisjointWhenNonNull(ImmutableSet defs1, ImmutableSet defs2,
                                           VariableNullability variableNullability) {
        return defs1.stream()
                .allMatch(d1 -> defs2.stream()
                        .allMatch(d2 -> areDisjointWhenNonNull(d1, d2, variableNullability)));
    }

    private boolean areDisjointWhenNonNull(ImmutableTerm t1, ImmutableTerm t2, VariableNullability variableNullability) {
        IncrementalEvaluation evaluation = t1.evaluateStrictEq(t2, variableNullability);
        switch (evaluation.getStatus()) {
            case SIMPLIFIED_EXPRESSION:
                return evaluation.getNewExpression()
                        .orElseThrow(() -> new MinorOntopInternalBugException("An expression was expected"))
                        .evaluate2VL(variableNullability)
                        .isEffectiveFalse();
            case IS_NULL:
            case IS_FALSE:
                return true;
            case SAME_EXPRESSION:
            case IS_TRUE:
            default:
                return false;
        }
    }


    /**
     * TODO: make it compatible definitions together (requires a VariableGenerator so as to lift bindings)
     */
    @Override
    public IQTree liftIncompatibleDefinitions(Variable variable, ImmutableList children, VariableGenerator variableGenerator) {
        ImmutableList liftedChildren = children.stream()
                .map(c -> c.liftIncompatibleDefinitions(variable, variableGenerator))
                .collect(ImmutableCollectors.toList());
        
        return iqFactory.createNaryIQTree(this, liftedChildren);
    }

    @Override
    public IQTree propagateDownConstraint(ImmutableExpression constraint, ImmutableList children,
                                          VariableGenerator variableGenerator) {
        return iqFactory.createNaryIQTree(this,
                children.stream()
                        .map(c -> c.propagateDownConstraint(constraint, variableGenerator))
                        .collect(ImmutableCollectors.toList()));
    }

    @Override
    public IQTree acceptTransformer(IQTree tree, IQTreeVisitingTransformer transformer, ImmutableList children) {
        return transformer.transformUnion(tree,this, children);
    }

    @Override
    public  IQTree acceptTransformer(IQTree tree, IQTreeExtendedTransformer transformer,
                                    ImmutableList children, T context) {
        return transformer.transformUnion(tree,this, children, context);
    }

    @Override
    public  T acceptVisitor(IQVisitor visitor, ImmutableList children) {
        return visitor.visitUnion(this, children);
    }

    @Override
    public void validateNode(ImmutableList children) throws InvalidIntermediateQueryException {
        if (children.size() < 2) {
            throw new InvalidIntermediateQueryException("UNION node " + this
                    +" does not have at least 2 children node.");
        }

        ImmutableSet unionVariables = getVariables();
        for (IQTree child : children) {
            if (!child.getVariables().equals(unionVariables)) {
                throw new InvalidIntermediateQueryException("This child " + child
                        + " does not project exactly all the variables " +
                        "of the UNION node (" + unionVariables + ")\n" + this);
            }
        }
    }

    @Override
    public IQTree removeDistincts(ImmutableList children, IQTreeCache treeCache) {
        ImmutableList newChildren = children.stream()
                .map(IQTree::removeDistincts)
                .collect(ImmutableCollectors.toList());

        IQTreeCache newTreeCache = treeCache.declareDistinctRemoval(newChildren.equals(children));
        return iqFactory.createNaryIQTree(this, newChildren, newTreeCache);
    }

    /**
     * TODO: generalize it and merge it with the isDistinct() method implementation
     *
     * We could infer more constraints by not only checking for equal unique constraints, but also checking if
     * some unique constraint is a superset of all unique constraints.
     */
    @Override
    public ImmutableSet> inferUniqueConstraints(ImmutableList children) {
        int childrenCount = children.size();
        if (childrenCount < 2)
            throw new InvalidIntermediateQueryException("At least 2 children are expected for a union");

        IQTree firstChild = children.get(0);
        // Partitions the unique constraints based on if they are fully disjoint or not. Guaranteed to have two entries: true and false (but they may be empty)
        ImmutableMap>> ucsPartitionedByDisjointness = firstChild.inferUniqueConstraints().stream()
                .filter(uc -> children.stream()
                        .skip(1)
                        .allMatch(c -> c.inferUniqueConstraints().contains(uc)))
                .collect(ImmutableCollectors.partitioningBy(uc -> areDisjoint(children, uc)));

        if (ucsPartitionedByDisjointness.get(false).isEmpty())
            return ImmutableSet.copyOf(ucsPartitionedByDisjointness.get(true));

        // By definition not parts of the non-disjoint UCs
        var disjointVariables = firstChild.getVariables().stream()
                .filter(v -> areDisjoint(children, ImmutableSet.of(v)))
                .filter(v -> ucsPartitionedByDisjointness.get(true).stream().noneMatch(set -> set.size() == 1 && set.stream().findFirst().get().equals(v)))
                .collect(ImmutableCollectors.toSet());

        return Stream.concat(
                ucsPartitionedByDisjointness.get(true).stream(),
                ucsPartitionedByDisjointness.get(false).stream()
                        .flatMap(uc -> disjointVariables.stream()
                                        .map(v -> Sets.union(uc, ImmutableSet.of(v)).immutableCopy()))
                ).collect(ImmutableCollectors.toSet());
    }

    @Override
    public FunctionalDependencies inferFunctionalDependencies(ImmutableList children, ImmutableSet> uniqueConstraints, ImmutableSet variables) {
        int childrenCount = children.size();
        if (childrenCount < 2)
            throw new InvalidIntermediateQueryException("At least 2 children are expected for a union");

        IQTree firstChild = children.get(0);

        var mergedDependencies = children.stream()
                .skip(1)
                .reduce(firstChild.inferFunctionalDependencies(), (fd, c) -> fd.merge(c.inferFunctionalDependencies()), (fd1, fd2) -> fd1.merge(fd2));

        // Partitions the fds based on if they are fully disjoint or not. Guaranteed to have two entries: true and false (but they may be empty)
        ImmutableMap, ImmutableSet>>> fdsPartitionedByDisjointness = mergedDependencies.stream()
                .collect(ImmutableCollectors.partitioningBy(fd -> areDisjoint(children, fd.getKey())));

        if (fdsPartitionedByDisjointness.get(false).isEmpty())
            return fdsPartitionedByDisjointness.get(true)
                    .stream()
                    .collect(FunctionalDependencies.toFunctionalDependencies());

        // By definition not parts of the non-disjoint UCs
        var disjointVariables = firstChild.getVariables().stream()
                .filter(v -> areDisjoint(children, ImmutableSet.of(v)))
                .filter(v -> fdsPartitionedByDisjointness.get(true).stream().noneMatch(entry -> entry.getKey().size() == 1 && entry.getKey().stream().findFirst().get().equals(v)))
                .collect(ImmutableCollectors.toSet());

        return Stream.concat(
                fdsPartitionedByDisjointness.get(true).stream(),
                fdsPartitionedByDisjointness.get(false).stream()
                        .flatMap(fd -> disjointVariables.stream()
                                .map(v -> appendDeterminant(fd, v)))
        ).collect(FunctionalDependencies.toFunctionalDependencies());
    }

    private static Map.Entry, ImmutableSet> appendDeterminant(Map.Entry, ImmutableSet> fd, Variable v) {
        ImmutableSet vSet = ImmutableSet.of(v);
        return Maps.immutableEntry(
                Sets.union(fd.getKey(), vSet).immutableCopy(),
                Sets.difference(fd.getValue(), vSet).immutableCopy());
    }


    private boolean areDisjoint(ImmutableList children, ImmutableSet uc) {
        int childrenCount = children.size();
        return IntStream.range(0, childrenCount)
                .allMatch(i -> IntStream.range(i + 1, childrenCount)
                        .allMatch(j -> areDisjoint(children.get(i), children.get(j), uc)));
    }

    /**
     * All the variables of a union could be projected out
     */
    @Override
    public VariableNonRequirement computeVariableNonRequirement(ImmutableList children) {
        return VariableNonRequirement.of(getVariables());
    }

    @Override
    public ImmutableSet getVariables() {
        return projectedVariables;
    }

    @Override
    public ImmutableSet getLocalVariables() {
        return projectedVariables;
    }

    @Override
    public String toString() {
        return UNION_NODE_STR + " " + projectedVariables;
    }

    @Override
    public ImmutableSet getLocallyRequiredVariables() {
        return projectedVariables;
    }

    @Override
    public ImmutableSet getLocallyDefinedVariables() {
        return ImmutableSet.of();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof UnionNodeImpl) {
            UnionNodeImpl that = (UnionNodeImpl) o;
            return projectedVariables.equals(that.projectedVariables);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(projectedVariables);
    }

    /**
     * TODO: refactor
     */
    @Override
    public IQTree normalizeForOptimization(ImmutableList children, VariableGenerator variableGenerator, IQTreeCache treeCache) {
        ImmutableList liftedChildren = children.stream()
                .map(c -> c.normalizeForOptimization(variableGenerator))
                .filter(c -> !c.isDeclaredAsEmpty())
                .map(c -> notRequiredVariableRemover.optimize(c, projectedVariables, variableGenerator))
                .collect(ImmutableCollectors.toList());

        switch (liftedChildren.size()) {
            case 0:
                return iqFactory.createEmptyNode(projectedVariables);
            case 1:
                return liftedChildren.get(0);
            default:
                return tryToMergeSomeChildrenInAValuesNode(
                        liftBindingFromLiftedChildrenAndFlatten(liftedChildren, variableGenerator, treeCache),
                        variableGenerator, treeCache);
        }
    }

    @Override
    public IQTree applyDescendingSubstitution(Substitution descendingSubstitution,
                                              Optional constraint, ImmutableList children,
                                              VariableGenerator variableGenerator) {

        ImmutableList updatedChildren = children.stream()
                .map(c -> c.applyDescendingSubstitution(descendingSubstitution, constraint, variableGenerator))
                .filter(c -> !c.isDeclaredAsEmpty())
                .collect(ImmutableCollectors.toList());

        switch (updatedChildren.size()) {
            case 0:
                return iqFactory.createEmptyNode(iqTreeTools.computeNewProjectedVariables(descendingSubstitution, projectedVariables));
            case 1:
                return updatedChildren.get(0);
            default:
                UnionNode newRootNode = iqFactory.createUnionNode(iqTreeTools.computeNewProjectedVariables(descendingSubstitution, projectedVariables));
                return iqFactory.createNaryIQTree(newRootNode, updatedChildren);
        }
    }

    @Override
    public IQTree applyDescendingSubstitutionWithoutOptimizing(
            Substitution descendingSubstitution, ImmutableList children,
            VariableGenerator variableGenerator) {
        ImmutableSet updatedProjectedVariables = iqTreeTools.computeNewProjectedVariables(descendingSubstitution, projectedVariables);

        ImmutableList updatedChildren = children.stream()
                .map(c -> c.applyDescendingSubstitutionWithoutOptimizing(descendingSubstitution, variableGenerator))
                .collect(ImmutableCollectors.toList());

        UnionNode newRootNode = iqFactory.createUnionNode(updatedProjectedVariables);
        return iqFactory.createNaryIQTree(newRootNode, updatedChildren);
    }

    @Override
    public IQTree applyFreshRenaming(InjectiveSubstitution renamingSubstitution,
                                     ImmutableList children, IQTreeCache treeCache) {
        ImmutableList newChildren = children.stream()
                .map(c -> c.applyFreshRenaming(renamingSubstitution))
                .collect(ImmutableCollectors.toList());

        UnionNode newUnionNode = iqFactory.createUnionNode(substitutionFactory.apply(renamingSubstitution, getVariables()));

        IQTreeCache newTreeCache = treeCache.applyFreshRenaming(renamingSubstitution);
        return iqFactory.createNaryIQTree(newUnionNode, newChildren, newTreeCache);
    }


    /**
     * Has at least two children.
     * The returned tree may be opportunistically marked as "normalized" in case no further optimization is applied in this class.
     * Such a flag won't be taken seriously until leaving this class.
     *
     */
    private IQTree liftBindingFromLiftedChildrenAndFlatten(ImmutableList liftedChildren, VariableGenerator variableGenerator,
                                                           IQTreeCache treeCache) {
        /*
         * Cannot lift anything if some children do not have a construction node
         */
        if (liftedChildren.stream()
                .anyMatch(c -> !(c.getRootNode() instanceof ConstructionNode)))
            // Opportunistically flagged as normalized. May be discarded later on
            return iqFactory.createNaryIQTree(this, flattenChildren(liftedChildren), treeCache.declareAsNormalizedForOptimizationWithEffect());

        ImmutableList> tmpNormalizedChildSubstitutions = liftedChildren.stream()
                .map(c -> (ConstructionNode) c.getRootNode())
                .map(ConstructionNode::getSubstitution)
                .map(s -> s.transform(this::normalizeNullAndRDFConstants))
                .collect(ImmutableCollectors.toList());

        Substitution mergedSubstitution = projectedVariables.stream()
                .map(v -> mergeDefinitions(v, tmpNormalizedChildSubstitutions, variableGenerator)
                        .map(d -> Maps.immutableEntry(v, d)))
                .flatMap(Optional::stream)
                .collect(substitutionFactory.toSubstitution());

        if (mergedSubstitution.isEmpty()) {
            // Opportunistically flagged as normalized. May be discarded later on
            return iqFactory.createNaryIQTree(this, flattenChildren(liftedChildren), treeCache.declareAsNormalizedForOptimizationWithEffect());
        }
        ConstructionNode newRootNode = iqFactory.createConstructionNode(projectedVariables,
                // Cleans up the temporary "normalization"
                mergedSubstitution.transform(ImmutableTerm::simplify));

        ImmutableSet unionVariables = newRootNode.getChildVariables();
        UnionNode newUnionNode = iqFactory.createUnionNode(unionVariables);

        NaryIQTree unionIQ = iqFactory.createNaryIQTree(newUnionNode,
                IntStream.range(0, liftedChildren.size())
                        .mapToObj(i -> updateChild((UnaryIQTree) liftedChildren.get(i), mergedSubstitution,
                                tmpNormalizedChildSubstitutions.get(i), unionVariables, variableGenerator))
                        .flatMap(this::flattenChild)
                        .map(c -> iqTreeTools.createConstructionNodeTreeIfNontrivial(c, unionVariables))
                        .collect(ImmutableCollectors.toList()));

        return iqFactory.createUnaryIQTree(newRootNode, unionIQ)
                // TODO: see if needed or if we could opportunistically mark the tree as normalized
                .normalizeForOptimization(variableGenerator);
    }

    private ImmutableList flattenChildren(ImmutableList children) {
        ImmutableList flattenedChildren = children.stream()
                .flatMap(this::flattenChild)
                .collect(ImmutableCollectors.toList());
        return (children.size() == flattenedChildren.size())
                ? children
                : flattenedChildren;
    }

    private Stream flattenChild(IQTree child) {
        return (child.getRootNode() instanceof UnionNode)
                ? child.getChildren().stream()
                : Stream.of(child);
    }

    /**
     * RDF constants are transformed into RDF ground terms
     * Trick: NULL --> RDF(NULL,NULL)
     *
     * This "normalization" is temporary --> it will be "cleaned" but simplify the terms afterwards
     *
     */
    private ImmutableTerm normalizeNullAndRDFConstants(ImmutableTerm definition) {
        if (definition instanceof RDFConstant) {
            RDFConstant constant = (RDFConstant) definition;
            return termFactory.getRDFFunctionalTerm(
                    termFactory.getDBStringConstant(constant.getValue()),
                    termFactory.getRDFTermTypeConstant(constant.getType()));
        }
        else if (definition.isNull())
            return termFactory.getRDFFunctionalTerm(
                    termFactory.getNullConstant(), termFactory.getNullConstant());
        else
            return definition;
    }

    private Optional mergeDefinitions(
            Variable variable,
            ImmutableCollection> childSubstitutions,
            VariableGenerator variableGenerator) {

        if (childSubstitutions.stream()
                .anyMatch(s -> !s.isDefining(variable)))
            return Optional.empty();

        return childSubstitutions.stream()
                .map(s -> s.get(variable))
                .map(this::normalizeNullAndRDFConstants)
                .map(Optional::of)
                .reduce((od1, od2) -> od1
                        .flatMap(d1 -> od2
                                .flatMap(d2 -> combineDefinitions(d1, d2, variableGenerator, true))))
                .flatMap(t -> t);
    }

    /**
     * Compare and combine the bindings, returning only the compatible (partial) values.
     *
     */
    private Optional combineDefinitions(ImmutableTerm d1, ImmutableTerm d2,
                                                       VariableGenerator variableGenerator,
                                                       boolean topLevel) {
        if (d1.equals(d2)) {
            return Optional.of(
                    // Top-level var-to-var must not be renamed since they are about projected variables
                    (d1.isGround() || (topLevel && (d1 instanceof Variable)))
                            ? d1
                            : replaceVariablesByFreshOnes((NonGroundTerm)d1, variableGenerator));
        }
        else if (d1 instanceof Variable)  {
            return topLevel
                    ? Optional.empty()
                    : Optional.of(variableGenerator.generateNewVariableFromVar((Variable) d1));
        }
        else if (d2 instanceof Variable)  {
            return topLevel
                    ? Optional.empty()
                    : Optional.of(variableGenerator.generateNewVariableFromVar((Variable) d2));
        }
        else if ((d1 instanceof ImmutableFunctionalTerm) && (d2 instanceof ImmutableFunctionalTerm)) {
            ImmutableFunctionalTerm functionalTerm1 = (ImmutableFunctionalTerm) d1;
            ImmutableFunctionalTerm functionalTerm2 = (ImmutableFunctionalTerm) d2;

            FunctionSymbol firstFunctionSymbol = functionalTerm1.getFunctionSymbol();

            /*
             * Different function symbols: stops the common part here.
             *
             * Same for functions that do not strongly type their arguments (unsafe to decompose). Example: STRICT_EQ
             */
            if ((!firstFunctionSymbol.equals(functionalTerm2.getFunctionSymbol()))
                    || (!firstFunctionSymbol.shouldBeDecomposedInUnion())) {
                return topLevel
                        ? Optional.empty()
                        : Optional.of(variableGenerator.generateNewVariable());
            }
            else {
                ImmutableList arguments1 = functionalTerm1.getTerms();
                ImmutableList arguments2 = functionalTerm2.getTerms();
                if (arguments1.size() != arguments2.size()) {
                    throw new IllegalStateException("Functions have different arities, they cannot be combined");
                }

                ImmutableList newArguments = IntStream.range(0, arguments1.size())
                        // RECURSIVE
                        .mapToObj(i -> combineDefinitions(arguments1.get(i), arguments2.get(i), variableGenerator, false)
                                .orElseGet(variableGenerator::generateNewVariable))
                        .collect(ImmutableCollectors.toList());

                return Optional.of(termFactory.getImmutableFunctionalTerm(firstFunctionSymbol, newArguments));
            }
        }
        else {
            return Optional.empty();
        }
    }

    private NonGroundTerm replaceVariablesByFreshOnes(NonGroundTerm term, VariableGenerator variableGenerator) {
        if (term instanceof Variable)
            return variableGenerator.generateNewVariableFromVar((Variable) term);

        NonGroundFunctionalTerm functionalTerm = (NonGroundFunctionalTerm) term;

        return termFactory.getNonGroundFunctionalTerm(functionalTerm.getFunctionSymbol(),
                    functionalTerm.getTerms().stream()
                        .map(a -> a instanceof NonGroundTerm
                                // RECURSIVE
                                ? replaceVariablesByFreshOnes((NonGroundTerm) a, variableGenerator)
                                : a)
                        .collect(ImmutableCollectors.toList()));
    }

    /**
     * TODO: find a better name
     */
    private IQTree updateChild(UnaryIQTree liftedChildTree, Substitution mergedSubstitution,
                               Substitution tmpNormalizedSubstitution,
                               ImmutableSet projectedVariables, VariableGenerator variableGenerator) {

        ConstructionNode constructionNode = (ConstructionNode) liftedChildTree.getRootNode();

        ImmutableSet formerV = constructionNode.getVariables();

        Substitution normalizedEta = substitutionFactory.onImmutableTerms().unifierBuilder(tmpNormalizedSubstitution)
                .unify(mergedSubstitution.stream(), Map.Entry::getKey, Map.Entry::getValue)
                .build()
                /*
                 * Normalizes eta so as to avoid projected variables to be substituted by non-projected variables.
                 *
                 * This normalization can be understood as a way to select a MGU (eta) among a set of equivalent MGUs.
                 * Such a "selection" is done a posteriori.
                 *
                 * Due to the current implementation of MGUS, the normalization should have no effect
                 * (already in a normal form). Here for safety.
                 */
                .map(eta -> substitutionFactory.getPrioritizingRenaming(eta, projectedVariables).compose(eta))
                .orElseThrow(() -> new QueryNodeSubstitutionException("The descending substitution " + mergedSubstitution
                        + " is incompatible with " + tmpNormalizedSubstitution));

        Substitution newTheta = normalizedEta.builder()
                .restrictDomainTo(projectedVariables)
                // Cleans up the temporary "normalization", in particular non-lifted RDF(NULL,NULL)
                .transform(ImmutableTerm::simplify)
                .build();

        Substitution descendingSubstitution = normalizedEta.builder()
                .removeFromDomain(tmpNormalizedSubstitution.getDomain())
                .removeFromDomain(Sets.difference(newTheta.getDomain(), formerV))
                // NB: this is expected to be ok given that the expected compatibility of the merged substitution with
                // this construction node
                .transform(t -> (VariableOrGroundTerm)t)
                .build();

        IQTree newChild = liftedChildTree.getChild()
                .applyDescendingSubstitution(descendingSubstitution, Optional.empty(), variableGenerator);

        return iqTreeTools.createConstructionNodeTreeIfNontrivial(newChild, newTheta, () -> projectedVariables);
    }

    private IQTree tryToMergeSomeChildrenInAValuesNode(IQTree tree, VariableGenerator variableGenerator, IQTreeCache treeCache) {
        QueryNode rootNode = tree.getRootNode();

        if (rootNode instanceof ConstructionNode) {
            IQTree subTree = tree.getChildren().get(0);
            IQTree newSubTree = tryToMergeSomeChildrenInAValuesNode(subTree, variableGenerator, treeCache, false);
            return (subTree == newSubTree)
                    ? tree
                    : iqFactory.createUnaryIQTree((ConstructionNode) rootNode, newSubTree,
                    treeCache.declareAsNormalizedForOptimizationWithEffect());
        }
        else
            return tryToMergeSomeChildrenInAValuesNode(tree, variableGenerator, treeCache, true);
    }


    private IQTree tryToMergeSomeChildrenInAValuesNode(IQTree tree, VariableGenerator variableGenerator, IQTreeCache treeCache,
                                                       boolean isRoot) {
        QueryNode rootNode = tree.getRootNode();
        if (!(rootNode instanceof UnionNode))
            return tree;

        UnionNode unionNode = (UnionNode) rootNode;

        ImmutableList children = tree.getChildren();

        ImmutableList nonMergedChildren = children.stream()
                .filter(t -> !isMergeableInValuesNode(t))
                .collect(ImmutableCollectors.toList());

        // At least 2 mergeable children are needed
        if (nonMergedChildren.size() >= children.size() - 1)
            return tree;

        // Tries to reuse the ordered value list of a values node
        ImmutableList valuesVariables = children.stream()
                .map(IQTree::getRootNode)
                .filter(n -> n instanceof ValuesNode)
                .map(n -> ((ValuesNode) n).getOrderedVariables())
                .findAny()
                // Otherwise creates an arbitrary order
                .orElseGet(() -> ImmutableList.copyOf(unionNode.getVariables()));

        ImmutableList> values = children.stream()
                .filter(this::isMergeableInValuesNode)
                .flatMap(c -> extractValues(c, valuesVariables))
                .collect(ImmutableCollectors.toList());

        IQTree mergedSubTree = iqFactory.createValuesNode(valuesVariables, values)
                // NB: some columns may be extracted and put into a construction node
                .normalizeForOptimization(variableGenerator);

        if (nonMergedChildren.isEmpty())
            return mergedSubTree;

        ImmutableList newChildren = Stream.concat(
                        Stream.of(mergedSubTree),
                        nonMergedChildren.stream())
                .collect(ImmutableCollectors.toList());

        return isRoot
                // Merging values nodes cannot trigger new binding lift opportunities
                ? iqFactory.createNaryIQTree(unionNode, newChildren,
                        treeCache.declareAsNormalizedForOptimizationWithEffect())
                : iqFactory.createNaryIQTree(unionNode, newChildren);
    }

    /**
     * TODO: relax these constraints once we are sure non-DB constants in values nodes
     * are lifted or transformed properly in the rest of the code
     */
    private boolean isMergeableInValuesNode(IQTree tree) {
        QueryNode rootNode = tree.getRootNode();
        if ((rootNode instanceof ValuesNode) || (rootNode instanceof TrueNode))
            return true;

        if (!(rootNode instanceof ConstructionNode))
            return false;

        IQTree child = tree.getChildren().get(0);

        if (!((child instanceof TrueNode) || (child instanceof ValuesNode)))
            return false;

        ConstructionNode constructionNode = (ConstructionNode) rootNode;

        //NB: RDF constants are already expected to be decomposed
        return constructionNode.getSubstitution().rangeAllMatch(v -> (v instanceof DBConstant) || v.isNull());
    }

    private Stream> extractValues(IQTree tree, ImmutableList outputOrderedVariables) {
        if (tree instanceof ValuesNode)
            return extractValuesFromValuesNode((ValuesNode) tree, outputOrderedVariables);

        if (tree instanceof TrueNode)
            return Stream.of(ImmutableList.of());

        QueryNode rootNode = tree.getRootNode();
        if (!(rootNode instanceof ConstructionNode))
            throw new MinorOntopInternalBugException("Was expecting either a ValuesNode, a TrueNode or a ConstructionNode");

        Substitution substitution = ((ConstructionNode) rootNode).getSubstitution();
        IQTree child = tree.getChildren().get(0);

        if (child instanceof TrueNode)
            return Stream.of(
                    outputOrderedVariables.stream()
                        .map(substitution::get)
                        .map(t -> (Constant) t)
                        .collect(ImmutableCollectors.toList()));

        if (child instanceof ValuesNode)
            return extractValuesFromValuesNode((ValuesNode) child, outputOrderedVariables, substitution);

        throw new MinorOntopInternalBugException("Unexpected child: " + child);
    }

    private Stream> extractValuesFromValuesNode(ValuesNode valuesNode, ImmutableList outputOrderedVariables) {
        ImmutableList nodeOrderedVariables = valuesNode.getOrderedVariables();
        if (nodeOrderedVariables.equals(outputOrderedVariables))
            return valuesNode.getValues().stream();

        ImmutableList indexes = outputOrderedVariables.stream()
                .map(nodeOrderedVariables::indexOf)
                .collect(ImmutableCollectors.toList());

        return valuesNode.getValues().stream()
                .map(vs -> indexes.stream()
                        .map(vs::get)
                        .collect(ImmutableCollectors.toList()));
    }

    private Stream> extractValuesFromValuesNode(ValuesNode valuesNode,
                                                                        ImmutableList outputOrderedVariables,
                                                                        Substitution substitution) {
        ImmutableList nodeOrderedVariables = valuesNode.getOrderedVariables();
        ImmutableMap indexMap = outputOrderedVariables.stream()
                .collect(ImmutableCollectors.toMap(
                        v -> v,
                        nodeOrderedVariables::indexOf));

        return valuesNode.getValues().stream()
                .map(vs -> outputOrderedVariables.stream()
                        .map(v -> {
                            int index = indexMap.get(v);
                            return index == -1
                                    ? (Constant) substitution.get(v)
                                    : vs.get(index);
                        })
                        .collect(ImmutableCollectors.toList()));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy