/*
* (C) Copyright 2014-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.storage;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import org.nuxeo.ecm.core.api.impl.FacetFilter;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.ecm.core.query.sql.model.Expression;
import org.nuxeo.ecm.core.query.sql.model.FromClause;
import org.nuxeo.ecm.core.query.sql.model.FromList;
import org.nuxeo.ecm.core.query.sql.model.Literal;
import org.nuxeo.ecm.core.query.sql.model.LiteralList;
import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
import org.nuxeo.ecm.core.query.sql.model.Operand;
import org.nuxeo.ecm.core.query.sql.model.Operator;
import org.nuxeo.ecm.core.query.sql.model.Reference;
import org.nuxeo.ecm.core.query.sql.model.SQLQuery;
import org.nuxeo.ecm.core.query.sql.model.StringLiteral;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.types.Type;
import org.nuxeo.runtime.api.Framework;
/**
* Generic optimizer for a NXQL query.
*
* @since 5.9.4
*/
public class QueryOptimizer {
public static final String TYPE_ROOT = "Root";
public static final String TYPE_DOCUMENT = "Document";
public static final String TYPE_RELATION = "Relation";
protected final SchemaManager schemaManager;
protected final Set<String> neverPerInstanceMixins;
protected final LinkedList<Operand> toplevelOperands;
/** Do we match only relations? */
protected boolean onlyRelations;
public QueryOptimizer() {
schemaManager = Framework.getLocalService(SchemaManager.class);
neverPerInstanceMixins = new HashSet<>(schemaManager.getNoPerDocumentQueryFacets());
toplevelOperands = new LinkedList<>();
}
public MultiExpression getOptimizedQuery(SQLQuery query, FacetFilter facetFilter) {
if (facetFilter != null) {
addFacetFilterClauses(facetFilter);
}
visitFromClause(query.from); // for primary types
if (query.where != null) {
analyzeToplevelOperands(query.where.predicate);
}
simplifyToplevelOperands();
return new MultiExpression(Operator.AND, toplevelOperands);
}
protected void addFacetFilterClauses(FacetFilter facetFilter) {
for (String mixin : facetFilter.required) {
// every facet is required, not just any of them,
// so do them one by one
Expression expr = new Expression(new Reference(NXQL.ECM_MIXINTYPE), Operator.EQ, new StringLiteral(mixin));
toplevelOperands.add(expr);
}
if (!facetFilter.excluded.isEmpty()) {
LiteralList list = new LiteralList();
for (String mixin : facetFilter.excluded) {
list.add(new StringLiteral(mixin));
}
Expression expr = new Expression(new Reference(NXQL.ECM_MIXINTYPE), Operator.NOTIN, list);
toplevelOperands.add(expr);
}
}
/**
* Finds all the types to take into account (all concrete types being a subtype of the passed types) based on the
* FROM list.
* <p>
* Adds them as a ecm:primaryType match in the toplevel operands.
*/
protected void visitFromClause(FromClause node) {
onlyRelations = true;
Set<String> fromTypes = new HashSet<>();
FromList elements = node.elements;
for (String typeName : elements.values()) {
if (TYPE_DOCUMENT.equalsIgnoreCase(typeName)) {
typeName = TYPE_DOCUMENT;
}
Set<String> subTypes = schemaManager.getDocumentTypeNamesExtending(typeName);
if (subTypes == null) {
throw new RuntimeException("Unknown type: " + typeName);
}
fromTypes.addAll(subTypes);
boolean isRelation = false;
do {
if (TYPE_RELATION.equals(typeName)) {
isRelation = true;
break;
}
Type t = schemaManager.getDocumentType(typeName);
if (t != null) {
t = t.getSuperType();
}
typeName = t == null ? null : t.getName();
} while (typeName != null);
onlyRelations = onlyRelations && isRelation;
}
fromTypes.remove(TYPE_ROOT);
LiteralList list = new LiteralList();
for (String type : fromTypes) {
list.add(new StringLiteral(type));
}
toplevelOperands.add(new Expression(new Reference(NXQL.ECM_PRIMARYTYPE), Operator.IN, list));
}
/**
* Expand toplevel ANDed operands into simple list.
*/
protected void analyzeToplevelOperands(Operand node) {
if (node instanceof Expression) {
Expression expr = (Expression) node;
Operator op = expr.operator;
if (op == Operator.AND) {
analyzeToplevelOperands(expr.lvalue);
analyzeToplevelOperands(expr.rvalue);
return;
}
}
toplevelOperands.add(node);
}
/**
* Simplify ecm:primaryType positive references, and non-per-instance mixin types.
*/
protected void simplifyToplevelOperands() {
Set<String> primaryTypes = null; // if defined, required
for (Iterator<Operand> it = toplevelOperands.iterator(); it.hasNext();) {
// whenever we don't know how to optimize the expression,
// we just continue the loop
Operand node = it.next();
if (!(node instanceof Expression)) {
continue;
}
Expression expr = (Expression) node;
if (!(expr.lvalue instanceof Reference)) {
continue;
}
String name = ((Reference) expr.lvalue).name;
Operator op = expr.operator;
Operand rvalue = expr.rvalue;
if (NXQL.ECM_PRIMARYTYPE.equals(name)) {
if (op != Operator.EQ && op != Operator.IN) {
continue;
}
Set<String> set;
if (op == Operator.EQ) {
if (!(rvalue instanceof StringLiteral)) {
continue;
}
String primaryType = ((StringLiteral) rvalue).value;
set = new HashSet<>(Collections.singleton(primaryType));
} else { // Operator.IN
if (!(rvalue instanceof LiteralList)) {
continue;
}
set = getStringLiterals((LiteralList) rvalue);
}
if (primaryTypes == null) {
primaryTypes = set;
} else {
primaryTypes.retainAll(set);
}
it.remove(); // expression simplified into primaryTypes set
} else if (NXQL.ECM_MIXINTYPE.equals(name)) {
if (op != Operator.EQ && op != Operator.NOTEQ) {
continue;
}
if (!(rvalue instanceof StringLiteral)) {
continue;
}
String mixin = ((StringLiteral) rvalue).value;
if (!neverPerInstanceMixins.contains(mixin)) {
// mixin per instance -> primary type checks not enough
continue;
}
Set<String> set = schemaManager.getDocumentTypeNamesForFacet(mixin);
if (set == null) {
// unknown mixin
set = Collections.emptySet();
}
if (primaryTypes == null) {
if (op == Operator.EQ) {
primaryTypes = new HashSet<>(set); // copy
} else {
continue; // unknown positive, no optimization
}
} else {
if (op == Operator.EQ) {
primaryTypes.retainAll(set);
} else {
primaryTypes.removeAll(set);
}
}
it.remove(); // expression simplified into primaryTypes set
}
}
// readd the simplified primary types constraints
if (primaryTypes != null) {
if (primaryTypes.isEmpty()) {
// TODO better removal
primaryTypes.add("__NOSUCHTYPE__");
}
Expression expr;
if (primaryTypes.size() == 1) {
String pt = primaryTypes.iterator().next();
expr = new Expression(new Reference(NXQL.ECM_PRIMARYTYPE), Operator.EQ, new StringLiteral(pt));
} else { // primaryTypes.size() > 1
LiteralList list = new LiteralList();
for (String pt : primaryTypes) {
list.add(new StringLiteral(pt));
}
expr = new Expression(new Reference(NXQL.ECM_PRIMARYTYPE), Operator.IN, list);
}
toplevelOperands.addFirst(expr);
}
}
protected static Set<String> getStringLiterals(LiteralList list) {
Set<String> set = new HashSet<>();
for (Literal literal : list) {
if (!(literal instanceof StringLiteral)) {
throw new RuntimeException("requires string literals");
}
set.add(((StringLiteral) literal).value);
}
return set;
}
}