grakn.core.graql.reasoner.query.ReasonerQueryImpl.java Source code

Java tutorial

Introduction

Here is the source code for grakn.core.graql.reasoner.query.ReasonerQueryImpl.java

Source

/*
 * GRAKN.AI - THE KNOWLEDGE GRAPH
 * Copyright (C) 2018 Grakn Labs Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package grakn.core.graql.reasoner.query;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import grakn.core.concept.Concept;
import grakn.core.concept.ConceptId;
import grakn.core.concept.Label;
import grakn.core.concept.answer.ConceptMap;
import grakn.core.concept.type.Type;
import grakn.core.graql.exception.GraqlCheckedException;
import grakn.core.graql.exception.GraqlQueryException;
import grakn.core.graql.reasoner.ResolutionIterator;
import grakn.core.graql.reasoner.atom.Atom;
import grakn.core.graql.reasoner.atom.Atomic;
import grakn.core.graql.reasoner.atom.AtomicBase;
import grakn.core.graql.reasoner.atom.AtomicFactory;
import grakn.core.graql.reasoner.atom.binary.AttributeAtom;
import grakn.core.graql.reasoner.atom.binary.IsaAtom;
import grakn.core.graql.reasoner.atom.binary.IsaAtomBase;
import grakn.core.graql.reasoner.atom.binary.RelationAtom;
import grakn.core.graql.reasoner.atom.predicate.IdPredicate;
import grakn.core.graql.reasoner.atom.predicate.NeqPredicate;
import grakn.core.graql.reasoner.cache.Index;
import grakn.core.graql.reasoner.explanation.JoinExplanation;
import grakn.core.graql.reasoner.explanation.LookupExplanation;
import grakn.core.graql.reasoner.plan.GraqlTraversalPlanner;
import grakn.core.graql.reasoner.plan.ResolutionPlan;
import grakn.core.graql.reasoner.plan.ResolutionQueryPlan;
import grakn.core.graql.reasoner.rule.InferenceRule;
import grakn.core.graql.reasoner.rule.RuleUtils;
import grakn.core.graql.reasoner.state.AnswerState;
import grakn.core.graql.reasoner.state.ConjunctiveState;
import grakn.core.graql.reasoner.state.CumulativeState;
import grakn.core.graql.reasoner.state.NeqComplementState;
import grakn.core.graql.reasoner.state.QueryStateBase;
import grakn.core.graql.reasoner.state.ResolutionState;
import grakn.core.graql.reasoner.unifier.MultiUnifier;
import grakn.core.graql.reasoner.unifier.Unifier;
import grakn.core.graql.reasoner.unifier.UnifierType;
import grakn.core.graql.reasoner.utils.Pair;
import grakn.core.server.kb.Schema;
import grakn.core.server.kb.concept.ConceptUtils;
import grakn.core.server.session.TransactionOLTP;
import graql.lang.Graql;
import graql.lang.pattern.Conjunction;
import graql.lang.pattern.Pattern;
import graql.lang.statement.Statement;
import graql.lang.statement.Variable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;

/**
 *
 * Base reasoner query providing resolution and atom handling facilities for conjunctive graql queries.
 *
 */
public class ReasonerQueryImpl implements ResolvableQuery {

    private final TransactionOLTP tx;
    private final ImmutableSet<Atomic> atomSet;
    private ConceptMap substitution = null;
    private ImmutableSetMultimap<Variable, Type> varTypeMap = null;
    private ResolutionPlan resolutionPlan = null;
    private Conjunction<Pattern> pattern = null;
    private Set<Variable> varNames = null;

    ReasonerQueryImpl(Conjunction<Statement> pattern, TransactionOLTP tx) {
        this.tx = tx;
        this.atomSet = ImmutableSet.<Atomic>builder().addAll(AtomicFactory.createAtoms(pattern, this).iterator())
                .build();
    }

    ReasonerQueryImpl(Set<Atomic> atoms, TransactionOLTP tx) {
        this.tx = tx;
        this.atomSet = ImmutableSet.<Atomic>builder().addAll(atoms.stream().map(at -> at.copy(this)).iterator())
                .build();
    }

    ReasonerQueryImpl(List<Atom> atoms, TransactionOLTP tx) {
        this.tx = tx;
        this.atomSet = ImmutableSet.<Atomic>builder()
                .addAll(atoms.stream().flatMap(at -> Stream.concat(Stream.of(at), at.getNonSelectableConstraints()))
                        .map(at -> at.copy(this)).iterator())
                .build();
    }

    ReasonerQueryImpl(Atom atom) {
        this(Collections.singletonList(atom), atom.getParentQuery().tx());
    }

    ReasonerQueryImpl(ReasonerQueryImpl q) {
        this.tx = q.tx;
        this.atomSet = ImmutableSet.<Atomic>builder()
                .addAll(q.getAtoms().stream().map(at -> at.copy(this)).iterator()).build();
    }

    @Override
    public ReasonerQuery conjunction(ReasonerQuery q) {
        return new ReasonerQueryImpl(Sets.union(getAtoms(), q.getAtoms()), this.tx());
    }

    @Override
    public CompositeQuery asComposite() {
        return new CompositeQuery(getPattern(), tx());
    }

    @Override
    public ReasonerQueryImpl withSubstitution(ConceptMap sub) {
        return new ReasonerQueryImpl(Sets.union(this.getAtoms(), AtomicFactory.answerToPredicates(sub, this)),
                this.tx());
    }

    @Override
    public ReasonerQueryImpl inferTypes() {
        return new ReasonerQueryImpl(getAtoms().stream().map(Atomic::inferTypes).collect(Collectors.toSet()), tx());
    }

    @Override
    public ReasonerQueryImpl neqPositive() {
        return ReasonerQueries.create(getAtoms().stream().map(Atomic::neqPositive)
                .filter(at -> !(at instanceof NeqPredicate)).collect(Collectors.toSet()), tx());
    }

    /**
     * @return true if the query doesn't contain any NeqPredicates
     */
    boolean isNeqPositive() {
        return !getAtoms(NeqPredicate.class).findFirst().isPresent()
                && getAtoms(AttributeAtom.class).flatMap(at -> at.getMultiPredicate().stream())
                        .noneMatch(p -> p.getPredicate().comparator().equals(Graql.Token.Comparator.NEQV));
    }

    /**
     * @param transform map defining id transform: var -> new id
     * @return new query with id predicates transformed according to the transform
     */
    public ReasonerQueryImpl transformIds(Map<Variable, ConceptId> transform) {
        Set<Atomic> atoms = this.getAtoms(IdPredicate.class).map(p -> {
            ConceptId conceptId = transform.get(p.getVarName());
            if (conceptId != null)
                return IdPredicate.create(p.getVarName(), conceptId, p.getParentQuery());
            return p;
        }).collect(Collectors.toSet());
        getAtoms().stream().filter(at -> !(at instanceof IdPredicate)).forEach(atoms::add);
        return new ReasonerQueryImpl(atoms, tx());
    }

    @Override
    public String toString() {
        return "{\n\t" + getAtoms(Atomic.class).map(Atomic::toString).collect(Collectors.joining(";\n\t")) + "\n}";
    }

    @Override
    public ReasonerQueryImpl copy() {
        return new ReasonerQueryImpl(this);
    }

    //alpha-equivalence equality
    @Override
    public boolean equals(Object obj) {
        if (obj == null || this.getClass() != obj.getClass())
            return false;
        if (obj == this)
            return true;
        ReasonerQueryImpl q2 = (ReasonerQueryImpl) obj;
        return this.isEquivalent(q2);
    }

    @Override
    public int hashCode() {
        return ReasonerQueryEquivalence.AlphaEquivalence.hash(this);
    }

    @Override
    public TransactionOLTP tx() {
        return tx;
    }

    @Override
    public void checkValid() {
        getAtoms().forEach(Atomic::checkValid);
    }

    @Override
    public Conjunction<Pattern> getPattern() {
        if (pattern == null) {
            pattern = Graql.and(getAtoms().stream().map(Atomic::getCombinedPattern)
                    .flatMap(p -> p.statements().stream()).collect(Collectors.toSet()));
        }
        return pattern;
    }

    @Override
    public Set<String> validateOntologically(Label ruleLabel) {
        return getAtoms().stream().flatMap(at -> at.validateAsRuleBody(ruleLabel).stream())
                .collect(Collectors.toSet());
    }

    @Override
    public boolean isRuleResolvable() {
        return selectAtoms().anyMatch(Atom::isRuleResolvable);
    }

    /**
     * @return true if this query is atomic
     */
    @Override
    public boolean isAtomic() {
        return atomSet.stream().filter(Atomic::isSelectable).count() == 1;
    }

    /**
     * @param typedVar variable of interest
     * @param parentType to be checked
     * @return true if typing the typeVar with type is compatible with role configuration of this query
     */
    @Override
    public boolean isTypeRoleCompatible(Variable typedVar, Type parentType) {
        if (parentType == null || Schema.MetaSchema.isMetaLabel(parentType.label()))
            return true;

        Set<Type> parentTypes = parentType.subs().collect(Collectors.toSet());
        return getAtoms(RelationAtom.class).filter(ra -> ra.getVarNames().contains(typedVar))
                .noneMatch(ra -> ra.getRoleVarMap().entries().stream()
                        //get roles this type needs to play
                        .filter(e -> e.getValue().equals(typedVar))
                        .filter(e -> !Schema.MetaSchema.isMetaLabel(e.getKey().label()))
                        //check if it can play it
                        .anyMatch(e -> e.getKey().players().noneMatch(parentTypes::contains)));
    }

    @Override
    public boolean isEquivalent(ResolvableQuery q) {
        return ReasonerQueryEquivalence.AlphaEquivalence.equivalent(this, q);
    }

    /**
     * @return true if this query is a ground query
     */
    public boolean isGround() {
        return getSubstitution().vars().containsAll(getVarNames());
    }

    /**
     * @return true if this query contains disconnected atoms that are unbounded
     */
    public boolean isBoundlesslyDisconnected() {
        return !isAtomic() && selectAtoms().filter(at -> !at.isBounded()).anyMatch(Atom::isDisconnected);
    }

    /**
     * @return true if the query requires direct schema lookups
     */
    public boolean requiresSchema() {
        return selectAtoms().anyMatch(Atom::requiresSchema);
    }

    @Override
    public Set<Atomic> getAtoms() {
        return atomSet;
    }

    @Override
    public Set<Variable> getVarNames() {
        if (varNames == null) {
            Set<Variable> vars = new HashSet<>();
            getAtoms().forEach(atom -> vars.addAll(atom.getVarNames()));
            varNames = vars;
        }
        return varNames;
    }

    @Override
    public MultiUnifier getMultiUnifier(ReasonerQuery parent) {
        return getMultiUnifier(parent, UnifierType.EXACT);
    }

    /**
     * @param parent query we want to unify this query with
     * @param unifierType unifier type
     * @return corresponding multiunifier
     */
    public MultiUnifier getMultiUnifier(ReasonerQuery parent, UnifierType unifierType) {
        throw GraqlQueryException.getUnifierOfNonAtomicQuery();
    }

    private Stream<IsaAtom> inferEntityTypes(ConceptMap sub) {
        Set<Variable> typedVars = getAtoms(IsaAtomBase.class).map(AtomicBase::getVarName)
                .collect(Collectors.toSet());
        return Stream
                .concat(getAtoms(IdPredicate.class),
                        AtomicFactory.answerToPredicates(sub, this).stream().map(IdPredicate.class::cast))
                .filter(p -> !typedVars.contains(p.getVarName()))
                .map(p -> new Pair<>(p, tx().<Concept>getConcept(p.getPredicate())))
                .filter(p -> Objects.nonNull(p.getValue())).filter(p -> p.getValue().isEntity())
                .map(p -> IsaAtom.create(p.getKey().getVarName(), new Variable(), p.getValue().asEntity().type(),
                        false, this));
    }

    private Multimap<Variable, Type> getVarTypeMap(Stream<IsaAtomBase> isas) {
        HashMultimap<Variable, Type> map = HashMultimap.create();
        isas.map(at -> new Pair<>(at.getVarName(), at.getSchemaConcept()))
                .filter(p -> Objects.nonNull(p.getValue())).filter(p -> p.getValue().isType()).forEach(p -> {
                    Variable var = p.getKey();
                    Type newType = p.getValue().asType();
                    Set<Type> types = map.get(var);

                    if (types.isEmpty())
                        map.put(var, newType);
                    else {
                        boolean isSubType = newType.sups().anyMatch(types::contains);
                        boolean isSuperType = newType.subs().anyMatch(types::contains);

                        //if it's a supertype of existing type, put most specific type
                        if (isSubType) {
                            map.removeAll(var);
                            ConceptUtils.bottom(Sets.union(types, Sets.newHashSet(newType)))
                                    .forEach(t -> map.put(var, t));
                        }
                        if (!isSubType && !isSuperType)
                            map.put(var, newType);
                    }
                });
        return map;
    }

    @Override
    public ImmutableSetMultimap<Variable, Type> getVarTypeMap(boolean inferTypes) {
        if (!inferTypes)
            return ImmutableSetMultimap.copyOf(getVarTypeMap(getAtoms(IsaAtomBase.class)));
        return getVarTypeMap();
    }

    @Override
    public ImmutableSetMultimap<Variable, Type> getVarTypeMap() {
        if (varTypeMap == null) {
            this.varTypeMap = getVarTypeMap(new ConceptMap());
        }
        return varTypeMap;
    }

    @Override
    public ImmutableSetMultimap<Variable, Type> getVarTypeMap(ConceptMap sub) {
        return ImmutableSetMultimap
                .copyOf(getVarTypeMap(Stream.concat(getAtoms(IsaAtomBase.class), inferEntityTypes(sub))));
    }

    @Override
    public Type getUnambiguousType(Variable var, boolean inferTypes) {
        ImmutableSet<Type> types = getVarTypeMap(inferTypes).get(var);
        Type type = null;
        if (types.isEmpty())
            return type;

        try {
            type = Iterables.getOnlyElement(types);
        } catch (IllegalArgumentException e) {
            throw GraqlQueryException.ambiguousType(var, types);
        }
        return type;
    }

    /**
     * @return the resolution plan for this query
     */
    public ResolutionPlan resolutionPlan() {
        if (resolutionPlan == null) {
            resolutionPlan = new ResolutionPlan(this);
        }
        return resolutionPlan;
    }

    /**
     * @param var variable name
     * @return id predicate for the specified var name if any
     */
    @Nullable
    private IdPredicate getIdPredicate(Variable var) {
        return getAtoms(IdPredicate.class).filter(sub -> sub.getVarName().equals(var)).findFirst().orElse(null);
    }

    /**
     * returns id transform that would convert this query to a query alpha-equivalent to the query,
     * provided they are structurally equivalent
     * @param query for which the transform is to be constructed
     * @param unifier between this query and provided query
     * @return id transform
     */
    public Map<Variable, ConceptId> idTransform(ReasonerQueryImpl query, Unifier unifier) {
        Map<Variable, ConceptId> transform = new HashMap<>();
        this.getAtoms(IdPredicate.class).forEach(thisP -> {
            Collection<Variable> vars = unifier.get(thisP.getVarName());
            Variable var = !vars.isEmpty() ? Iterators.getOnlyElement(vars.iterator()) : thisP.getVarName();
            IdPredicate p2 = query.getIdPredicate(var);
            if (p2 != null)
                transform.put(thisP.getVarName(), p2.getPredicate());
        });
        return transform;
    }

    /** Does id predicates -> answer conversion
     * @return substitution obtained from all id predicates (including internal) in the query
     */
    public ConceptMap getSubstitution() {
        if (substitution == null) {
            Set<Variable> varNames = getVarNames();
            Set<IdPredicate> predicates = getAtoms(IsaAtomBase.class).map(IsaAtomBase::getTypePredicate)
                    .filter(Objects::nonNull).filter(p -> varNames.contains(p.getVarName()))
                    .collect(Collectors.toSet());
            getAtoms(IdPredicate.class).forEach(predicates::add);

            HashMap<Variable, Concept> answerMap = new HashMap<>();
            predicates.forEach(p -> {
                Concept concept = tx().getConcept(p.getPredicate());
                if (concept == null)
                    throw GraqlCheckedException.idNotFound(p.getPredicate());
                answerMap.put(p.getVarName(), concept);
            });
            substitution = new ConceptMap(answerMap);
        }
        return substitution;
    }

    public ConceptMap getRoleSubstitution() {
        Map<Variable, Concept> roleSub = new HashMap<>();
        getAtoms(RelationAtom.class).flatMap(RelationAtom::getRolePredicates).forEach(p -> {
            Concept concept = tx().getConcept(p.getPredicate());
            if (concept == null)
                throw GraqlCheckedException.idNotFound(p.getPredicate());
            roleSub.put(p.getVarName(), concept);
        });
        return new ConceptMap(roleSub);
    }

    /**
     * @return selected atoms
     */
    @Override
    public Stream<Atom> selectAtoms() {
        return getAtoms(Atom.class).filter(Atomic::isSelectable);
    }

    @Override
    public boolean requiresDecomposition() {
        return this.selectAtoms().anyMatch(Atom::requiresDecomposition);
    }

    /**
     * @return rewritten (decomposed) version of the query
     */
    @Override
    public ReasonerQueryImpl rewrite() {
        if (!requiresDecomposition())
            return this;
        return new ReasonerQueryImpl(
                this.selectAtoms().flatMap(at -> at.rewriteToAtoms().stream()).collect(Collectors.toList()), tx());
    }

    private static final String PLACEHOLDER_ID = "placeholder_id";

    /**
     * @return true if this query has complete entries in the cache
     */
    public boolean isCacheComplete() {
        if (selectAtoms().count() == 0)
            return false;
        if (isAtomic())
            return tx.queryCache().isComplete(ReasonerQueries.atomic(selectAtoms().iterator().next()));
        List<ReasonerAtomicQuery> queries = resolutionPlan().plan().stream().map(ReasonerQueries::atomic)
                .collect(Collectors.toList());
        Set<IdPredicate> subs = new HashSet<>();
        Map<ReasonerAtomicQuery, ReasonerAtomicQuery> queryMap = new HashMap<>();
        for (ReasonerAtomicQuery query : queries) {
            Set<Variable> vars = query.getVarNames();
            Conjunction<Statement> conjunction = Graql.and(GraqlTraversalPlanner
                    .atomsToPattern(query.getAtoms(Atom.class).collect(Collectors.toList()),
                            Sets.union(query.getAtoms(IdPredicate.class).collect(Collectors.toSet()), subs.stream()
                                    .filter(sub -> vars.contains(sub.getVarName())).collect(Collectors.toSet())))
                    .statements());
            queryMap.put(query, ReasonerQueries.atomic(conjunction, tx()));
            query.getVarNames().stream().filter(v -> subs.stream().noneMatch(s -> s.getVarName().equals(v)))
                    .map(v -> IdPredicate.create(v, ConceptId.of(PLACEHOLDER_ID), query)).forEach(subs::add);
        }
        return queryMap.entrySet().stream().filter(e -> e.getKey().isRuleResolvable())
                .allMatch(e -> Objects.nonNull(e.getKey().getAtom().getSchemaConcept())
                        && tx().queryCache().isComplete(e.getValue()));
    }

    @Override
    public boolean requiresReiteration() {
        if (isCacheComplete())
            return false;
        Set<InferenceRule> dependentRules = RuleUtils.getDependentRules(this);
        return RuleUtils.subGraphIsCyclical(dependentRules)
                || RuleUtils.subGraphHasRulesWithHeadSatisfyingBody(dependentRules);
    }

    @Override
    public Stream<ConceptMap> resolve() {
        return resolve(new HashSet<>(), this.requiresReiteration());
    }

    @Override
    public Stream<ConceptMap> resolve(Set<ReasonerAtomicQuery> subGoals, boolean reiterate) {
        return new ResolutionIterator(this, subGoals, reiterate).hasStream();
    }

    @Override
    public ResolutionState subGoal(ConceptMap sub, Unifier u, QueryStateBase parent,
            Set<ReasonerAtomicQuery> subGoals) {
        return isNeqPositive() ? new ConjunctiveState(this, sub, u, parent, subGoals)
                : new NeqComplementState(this, sub, u, parent, subGoals);
    }

    /**
     * @param sub partial substitution
     * @param u unifier with parent state
     * @param parent parent state
     * @param subGoals set of visited sub goals
     * @return resolution subGoals formed from this query obtained by expanding the inferred types contained in the query
     */
    public Stream<ResolutionState> subGoals(ConceptMap sub, Unifier u, QueryStateBase parent,
            Set<ReasonerAtomicQuery> subGoals) {
        return getQueryStream(sub).map(q -> q.subGoal(sub, u, parent, subGoals));
    }

    /**
     * @return stream of queries obtained by inserting all inferred possible types (if ambiguous)
     */
    Stream<ReasonerQueryImpl> getQueryStream(ConceptMap sub) {
        return Stream.of(this);
    }

    private List<ConceptMap> splitToPartialAnswers(ConceptMap mergedAnswer) {
        return this.selectAtoms().map(at -> at.inferTypes(mergedAnswer.project(at.getVarNames())))
                .map(ReasonerQueries::atomic)
                .map(aq -> mergedAnswer.project(aq.getVarNames()).explain(new LookupExplanation(aq.getPattern())))
                .collect(Collectors.toList());
    }

    /**
     * @param parent parent state
     * @param subGoals set of visited sub goals
     * @return query state iterator (db iter + unifier + state iter) for this query
     */
    public Iterator<ResolutionState> queryStateIterator(QueryStateBase parent, Set<ReasonerAtomicQuery> subGoals) {
        Iterator<AnswerState> dbIterator;
        Iterator<QueryStateBase> subGoalIterator;

        if (!this.isRuleResolvable()) {
            Set<Type> queryTypes = new HashSet<>(this.getVarTypeMap().values());
            boolean fruitless = tx.ruleCache().absentTypes(queryTypes);
            if (fruitless)
                dbIterator = Collections.emptyIterator();
            else {
                dbIterator = tx.stream(getQuery(), false).map(
                        ans -> ans.explain(new JoinExplanation(this.getPattern(), this.splitToPartialAnswers(ans))))
                        .map(ans -> new AnswerState(ans, parent.getUnifier(), parent)).iterator();
            }
            subGoalIterator = Collections.emptyIterator();
        } else {
            dbIterator = Collections.emptyIterator();

            ResolutionQueryPlan queryPlan = new ResolutionQueryPlan(this);
            subGoalIterator = Iterators.singletonIterator(new CumulativeState(queryPlan.queries(), new ConceptMap(),
                    parent.getUnifier(), parent, subGoals));
        }
        return Iterators.concat(dbIterator, subGoalIterator);
    }

    /**
     * @return set o variables containing a matching substitution
     */
    private Set<Variable> subbedVars() {
        return getAtoms(IdPredicate.class).map(Atomic::getVarName).collect(Collectors.toSet());
    }

    /**
     * @return answer index corresponding to corresponding partial substitution
     */
    public ConceptMap getAnswerIndex() {
        return getSubstitution().project(subbedVars());
    }

    /**
     * @return var index consisting of variables with a substitution
     */
    public Index index() {
        return Index.of(subbedVars());
    }
}