// Copyright 2017 JanusGraph Authors
//
// 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 org.janusgraph.graphdb.query;
import com.google.common.base.Preconditions;
import com.google.common.collect.*;
import org.janusgraph.core.*;
import org.janusgraph.core.attribute.Cmp;
import org.janusgraph.core.attribute.Contain;
import org.janusgraph.graphdb.internal.InternalRelationType;
import org.janusgraph.graphdb.query.condition.*;
import org.janusgraph.graphdb.transaction.StandardJanusGraphTx;
import java.util.*;
/**
* Utility methods used in query optimization and processing.
*
* @author Matthias Broecheler (me@matthiasb.com)
*/
public class QueryUtil {
public static int adjustLimitForTxModifications(StandardJanusGraphTx tx, int uncoveredAndConditions, int limit) {
assert limit > 0 && limit <= 1000000000; //To make sure limit computation does not overflow
assert uncoveredAndConditions >= 0;
if (uncoveredAndConditions > 0) {
int maxMultiplier = Integer.MAX_VALUE / limit;
limit = limit * Math.min(maxMultiplier, (int) Math.pow(2, uncoveredAndConditions)); //(limit*3)/2+1;
}
if (tx.hasModifications())
limit += Math.min(Integer.MAX_VALUE - limit, 5);
return limit;
}
public static int convertLimit(long limit) {
assert limit>=0;
if (limit>=Integer.MAX_VALUE) return Integer.MAX_VALUE;
else return (int)limit;
}
public static int mergeLimits(int limit1, int limit2) {
assert limit1>=0 && limit2>=0;
return Math.min(limit1,limit2);
}
public static InternalRelationType getType(StandardJanusGraphTx tx, String typeName) {
RelationType t = tx.getRelationType(typeName);
if (t == null && !tx.getConfiguration().getAutoSchemaMaker().ignoreUndefinedQueryTypes()) {
throw new IllegalArgumentException("Undefined type used in query: " + typeName);
}
return (InternalRelationType) t;
}
public static Iterable<JanusGraphVertex> getVertices(StandardJanusGraphTx tx,
PropertyKey key, Object equalityCondition) {
return tx.query().has(key,Cmp.EQUAL,equalityCondition).vertices();
}
public static Iterable<JanusGraphVertex> getVertices(StandardJanusGraphTx tx,
String key, Object equalityCondition) {
return tx.query().has(key,Cmp.EQUAL,equalityCondition).vertices();
}
public static Iterable<JanusGraphEdge> getEdges(StandardJanusGraphTx tx,
PropertyKey key, Object equalityCondition) {
return tx.query().has(key,Cmp.EQUAL,equalityCondition).edges();
}
public static Iterable<JanusGraphEdge> getEdges(StandardJanusGraphTx tx,
String key, Object equalityCondition) {
return tx.query().has(key,Cmp.EQUAL,equalityCondition).edges();
}
/**
* Query-normal-form (QNF) for JanusGraph is a variant of CNF (conjunctive normal form) with negation inlined where possible
*
* @param condition
* @return
*/
public static boolean isQueryNormalForm(Condition<?> condition) {
if (isQNFLiteralOrNot(condition)) return true;
else if (condition instanceof And) {
for (Condition<?> child : ((And<?>) condition).getChildren()) {
if (isQNFLiteralOrNot(child)) continue;
else if (child instanceof Or) {
for (Condition<?> child2 : ((Or<?>) child).getChildren()) {
if (!isQNFLiteralOrNot(child2)) return false;
}
} else return false;
}
return true;
} else return false;
}
private static final boolean isQNFLiteralOrNot(Condition<?> condition) {
if (condition instanceof Not) {
Condition child = ((Not) condition).getChild();
if (!isQNFLiteral(child)) return false;
else if (child instanceof PredicateCondition) {
return !((PredicateCondition) child).getPredicate().hasNegation();
} else return true;
} else return isQNFLiteral(condition);
}
private static final boolean isQNFLiteral(Condition<?> condition) {
if (condition.getType() != Condition.Type.LITERAL) return false;
if (condition instanceof PredicateCondition) {
return ((PredicateCondition) condition).getPredicate().isQNF();
} else return true;
}
public static final <E extends JanusGraphElement> Condition<E> simplifyQNF(Condition<E> condition) {
Preconditions.checkArgument(isQueryNormalForm(condition));
if (condition.numChildren() == 1) {
Condition<E> child = ((And) condition).get(0);
if (child.getType() == Condition.Type.LITERAL) return child;
}
return condition;
}
public static boolean isEmpty(Condition<?> condition) {
return condition.getType() != Condition.Type.LITERAL && condition.numChildren() == 0;
}
/**
* Prepares the constraints from the query builder into a QNF compliant condition.
* If the condition is invalid or trivially false, it returns null.
*
* @param tx
* @param constraints
* @param <E>
* @return
* @see #isQueryNormalForm(org.janusgraph.graphdb.query.condition.Condition)
*/
public static <E extends JanusGraphElement> And<E> constraints2QNF(StandardJanusGraphTx tx, List<PredicateCondition<String, E>> constraints) {
And<E> conditions = new And<E>(constraints.size() + 4);
for (PredicateCondition<String, E> atom : constraints) {
RelationType type = getType(tx, atom.getKey());
if (type == null) {
if (atom.getPredicate() == Cmp.EQUAL && atom.getValue() == null ||
(atom.getPredicate() == Cmp.NOT_EQUAL && atom.getValue() != null))
continue; //Ignore condition, its trivially satisfied
return null;
}
Object value = atom.getValue();
JanusGraphPredicate predicate = atom.getPredicate();
if (type.isPropertyKey()) {
PropertyKey key = (PropertyKey) type;
assert predicate.isValidCondition(value);
Preconditions.checkArgument(key.dataType()==Object.class || predicate.isValidValueType(key.dataType()), "Data type of key is not compatible with condition");
} else { //its a label
Preconditions.checkArgument(((EdgeLabel) type).isUnidirected());
Preconditions.checkArgument(predicate.isValidValueType(JanusGraphVertex.class), "Data type of key is not compatible with condition");
}
if (predicate instanceof Contain) {
//Rewrite contains conditions
Collection values = (Collection) value;
if (predicate == Contain.NOT_IN) {
if (values.isEmpty()) continue; //Simply ignore since trivially satisfied
for (Object invalue : values)
addConstraint(type, Cmp.NOT_EQUAL, invalue, conditions, tx);
} else {
Preconditions.checkArgument(predicate == Contain.IN);
if (values.isEmpty()) {
return null; //Cannot be satisfied
} if (values.size() == 1) {
addConstraint(type, Cmp.EQUAL, values.iterator().next(), conditions, tx);
} else {
Or<E> nested = new Or<E>(values.size());
for (Object invalue : values)
addConstraint(type, Cmp.EQUAL, invalue, nested, tx);
conditions.add(nested);
}
}
} else {
addConstraint(type, predicate, value, conditions, tx);
}
}
return conditions;
}
private static <E extends JanusGraphElement> void addConstraint(RelationType type, JanusGraphPredicate predicate,
Object value, MultiCondition<E> conditions, StandardJanusGraphTx tx) {
if (type.isPropertyKey()) {
if (value != null)
value = tx.verifyAttribute((PropertyKey) type, value);
} else { //t.isEdgeLabel()
Preconditions.checkArgument(value instanceof JanusGraphVertex);
}
PredicateCondition<RelationType, E> pc = new PredicateCondition<RelationType, E>(type, predicate, value);
if (!conditions.contains(pc)) conditions.add(pc);
}
public static Map.Entry<RelationType,Collection> extractOrCondition(Or<JanusGraphRelation> condition) {
RelationType masterType = null;
List<Object> values = new ArrayList<Object>();
for (Condition c : condition.getChildren()) {
if (!(c instanceof PredicateCondition)) return null;
PredicateCondition<RelationType, JanusGraphRelation> atom = (PredicateCondition)c;
if (atom.getPredicate()!=Cmp.EQUAL) return null;
Object value = atom.getValue();
if (value==null) return null;
RelationType type = atom.getKey();
if (masterType==null) masterType=type;
else if (!masterType.equals(type)) return null;
values.add(value);
}
if (masterType==null) return null;
assert !values.isEmpty();
return new AbstractMap.SimpleImmutableEntry(masterType,values);
}
public static <R> List<R> processIntersectingRetrievals(List<IndexCall<R>> retrievals, final int limit) {
Preconditions.checkArgument(!retrievals.isEmpty());
Preconditions.checkArgument(limit >= 0, "Invalid limit: %s", limit);
List<R> results;
/*
* Iterate over the clauses in the and collection
* query.getCondition().getChildren(), taking the intersection
* of current results with cumulative results on each iteration.
*/
//TODO: smarter limit estimation
int multiplier = Math.min(16, (int) Math.pow(2, retrievals.size() - 1));
int sublimit = Integer.MAX_VALUE;
if (Integer.MAX_VALUE / multiplier >= limit) sublimit = limit * multiplier;
boolean exhaustedResults;
do {
exhaustedResults = true;
results = null;
for (IndexCall<R> call : retrievals) {
Collection<R> subresult;
try {
subresult = call.call(sublimit);
} catch (Exception e) {
throw new JanusGraphException("Could not process individual retrieval call ", e);
}
if (subresult.size() >= sublimit) exhaustedResults = false;
if (results == null) {
results = Lists.newArrayList(subresult);
} else {
Set<R> subresultset = ImmutableSet.copyOf(subresult);
Iterator riter = results.iterator();
while (riter.hasNext()) {
if (!subresultset.contains(riter.next())) riter.remove();
}
}
}
sublimit = (int) Math.min(Integer.MAX_VALUE - 1, Math.max(Math.pow(sublimit, 1.5),(sublimit+1)*2));
} while (results.size() < limit && !exhaustedResults);
return results;
}
public interface IndexCall<R> {
public Collection<R> call(int limit);
}
}