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

com.redhat.lightblue.assoc.RewriteQuery Maven / Gradle / Ivy

There is a newer version: 2.18.0
Show newest version
/*
 Copyright 2013 Red Hat, Inc. and/or its affiliates.

 This file is part of lightblue.

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU 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 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see .
 */
package com.redhat.lightblue.assoc;

import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;

import com.redhat.lightblue.metadata.CompositeMetadata;
import com.redhat.lightblue.metadata.ResolvedReferenceField;
import com.redhat.lightblue.query.*;

import com.redhat.lightblue.util.Path;
import com.redhat.lightblue.util.Error;
import com.redhat.lightblue.util.CopyOnWriteIterator;

/**
 * Rewrite queries so that any clause referring to a field outside the current
 * entity is removed from the query, and any clause comparing a field in this
 * entity with a field in another entity is bound to a slot.
 *
 * It is not possible to rewrite a query clause referring to a field in another
 * entity. For instance a query of the form x=1 would become meaningless, as 'x'
 * has no value while evaluating this entity. So the clause is removed from the
 * query, and replaced with a placeholder.
 */
public class RewriteQuery extends QueryIterator {

    /**
     * The entity for which the query is being rewritten
     */
    private final CompositeMetadata currentEntity;

    /**
     * Placeholder that always evaluates to true
     */
    public static final class TruePH extends QueryExpression {
        @Override
        public JsonNode toJson() {
            return JsonNodeFactory.instance.booleanNode(true);
        }
    }

    /**
     * Placeholder that always evaluates to false
     */
    public static final class FalsePH extends QueryExpression {
        @Override
        public JsonNode toJson() {
            return JsonNodeFactory.instance.booleanNode(false);
        }
    }

    public static final class RewriteQueryResult {
        public final QueryExpression query;
        public final List bindings;

        public RewriteQueryResult(QueryExpression query, List bindings) {
            this.query = query;
            this.bindings = bindings;
        }
    }

    /**
     * Construct a class to rewrite queries to retrieve an entity, potentially
     * evaluated at another entity
     *
     * @param root The root entity
     * @param currentEntity The query will be rewritten relative to this entity
     *
     * The root entity is the entity that will be retrieved ultimately. That
     * entity may contain associations to other entities, and the association
     * queries need to be rewritten based on the query plan node they are
     * evaluated. The currentEntity gives that node relative to which the
     * queries will be rewritten.
     */
    public RewriteQuery(CompositeMetadata root,
                        CompositeMetadata currentEntity) {
        this.currentEntity = currentEntity;
    }

    /**
     * Rewrites a query using field information obtained from AnalyzeQuery
     *
     * @return Returns a result object containing the new rewritten query, and
     * the field bindings that will be set to values from already retrieved
     * documents.
     */
    public RewriteQueryResult rewriteQuery(QueryExpression q, List fieldInfo) {
        RewriteQueryImpl w = new RewriteQueryImpl(fieldInfo);
        QueryExpression newq = w.iterate(q);
        return new RewriteQueryResult(newq, w.bindings);
    }

    private class RewriteQueryImpl extends QueryIterator {

        // Set by rewriteQuery before calling
        private final List fieldInfo;
        // Set by rewriteQuery before calling
        private final List bindings = new ArrayList<>(16);

        // This is prefixed to all field names
        private Path nestedFieldPrefix = Path.EMPTY;
        // This is the field info the the field of the current array context
        private QueryFieldInfo contextField=null;

        /**
         * Rewrites a query for a query plan node
         *
         * @param root The root entity metadata
         * @param currentEntity The entity for the query plan node for which the
         * query is being rewritten
         * @param fieldInfo Query field
         */
        public RewriteQueryImpl(List fieldInfo) {
            this.fieldInfo = fieldInfo;
        }

        /**
         * Searches for the field info for a field in the given clase. The
         * fieldInfo is looked up in fieldInfo list. Object reference
         * equivalence is used to compare query clauses to find out the field
         * information, so the same query clauses that used to build the
         * fieldInfo list must be used here.
         */
        private QueryFieldInfo findFieldInfo(Path field, QueryExpression clause) {
            for (QueryFieldInfo fi : fieldInfo) {
                if (fi.getClause() == clause) {
                    if (fi.getFieldNameInClause().equals(field)) {
                        return fi;
                    }
                }
            }
            throw Error.get(AssocConstants.ERR_REWRITE, field.toString() + "@" + clause.toString());
        }

        private Path addPrefix(Path fieldName) {
            if (nestedFieldPrefix.isEmpty()) {
                return fieldName;
            } else {
                return new Path(nestedFieldPrefix, fieldName);
            }
        }

        /**
         * Remove the context prefix from the field. If the field is under an array elemMatch, and if the field
         * shares the same prefix as the array field, remove that prefix. This is only useful if the field
         * and the array are in the same entity
         */
        private Path removeContext(CompositeMetadata fieldEntity,Path fieldName) {
            if(contextField!=null&&fieldEntity==contextField.getFieldEntity()) {
                Path prefix=new Path(contextField.getEntityRelativeFieldName(),Path.ANYPATH);
                if(fieldName.numSegments()>=prefix.numSegments()) {
                    Path fieldPrefix=fieldName.prefix(prefix.numSegments()); // Also include the *
                    if(fieldPrefix.equals(prefix)) {
                        // Remove fieldPrefix
                        Path relativeFieldName=fieldName.suffix(-prefix.numSegments());
                        if(relativeFieldName.isEmpty())
                            relativeFieldName=new Path(Path.THIS);
                        return relativeFieldName;
                    }
                }
            } 
            return fieldName;
        }

        @Override
        protected QueryExpression itrValueComparisonExpression(ValueComparisonExpression q, Path context) {
            QueryFieldInfo qfi = findFieldInfo(q.getField(), q);
            if (qfi.getFieldEntity() != currentEntity) {
                return new TruePH();
            } else if (qfi.getFieldNameInClause().equals(qfi.getEntityRelativeFieldName()) && nestedFieldPrefix.isEmpty()) {
                return q;
            } else {
                return new ValueComparisonExpression(addPrefix(qfi.getEntityRelativeFieldName()), q.getOp(), q.getRvalue());
            }
        }

        @Override
        protected QueryExpression itrRegexMatchExpression(RegexMatchExpression q, Path context) {
            QueryFieldInfo qfi = findFieldInfo(q.getField(), q);
            if (qfi.getFieldEntity() != currentEntity) {
                return new TruePH();
            } else if (qfi.getFieldNameInClause().equals(qfi.getEntityRelativeFieldName()) && nestedFieldPrefix.isEmpty()) {
                return q;
            } else {
                return new RegexMatchExpression(addPrefix(qfi.getEntityRelativeFieldName()),
                        q.getRegex(),
                        q.isCaseInsensitive(),
                        q.isMultiline(),
                        q.isExtended(),
                        q.isDotAll());
            }
        }

        @Override
        protected QueryExpression itrNaryValueRelationalExpression(NaryValueRelationalExpression q, Path context) {
            QueryFieldInfo qfi = findFieldInfo(q.getField(), q);
            if (qfi.getFieldEntity() != currentEntity) {
                return new TruePH();
            } else if (qfi.getFieldNameInClause().equals(qfi.getEntityRelativeFieldName()) && nestedFieldPrefix.isEmpty()) {
                return q;
            } else {
                return new NaryValueRelationalExpression(addPrefix(qfi.getEntityRelativeFieldName()), q.getOp(), q.getValues());
            }
        }

        @Override
        protected QueryExpression itrArrayContainsExpression(ArrayContainsExpression q, Path context) {
            QueryFieldInfo qfi = findFieldInfo(q.getArray(), q);
            if (qfi.getFieldEntity() != currentEntity) {
                return new TruePH();
            } else if (qfi.getFieldNameInClause().equals(qfi.getEntityRelativeFieldName()) && nestedFieldPrefix.isEmpty()) {
                return q;
            } else {
                return new ArrayContainsExpression(addPrefix(qfi.getEntityRelativeFieldName()), q.getOp(), q.getValues());
            }
        }

        @Override
        public QueryExpression iterate(QueryExpression q, Path context) {
            // Don't send classes the base iterator doesn't recognize
            if (q instanceof TruePH || q instanceof FalsePH) {
                return q;
            } else {
                return super.iterate(q, context);
            }
        }

        /**
         * Binds the given field to a value.
         */
        private Value bind(QueryFieldInfo fi) {
            BoundValue b = new BoundValue(fi);
            bindings.add(b);
            return b;
        }

        /**
         * Binds the given field to a list of values
         */
        private List bindList(QueryFieldInfo fi) {
            BoundList b = new BoundList(fi);
            bindings.add(b);
            return b;
        }

        /**
         * Rewrites a field comparison
         *
         * A field comparison is of the form:
         * 
         *  { field: f1, op:'=', rfield: f2 }
         * 
* * There are four possible ways this can be rewritten: * * If both f1 and f2 are in the current entity, the query is returned * unmodified. * * If f1 is in current entity, but f2 is in a different entity, then the * query is rewritten as: *
         *    { field: f1, op:'=', rvalue:  }
         * 
and f2 is bound. * * If f2 is in current entity, but f1 is in a different entity, then the * query is rewritten as: *
         *   { field: f2, op: '=', rvalue:  }
         * 
and f1 is bound. Also, the operator is inverted (i.e. If >, it * is converted to <, etc.). * * If both f1 and f2 are not in the current entity, a placeholder for * TRUE is returned. * */ @Override protected QueryExpression itrFieldComparisonExpression(FieldComparisonExpression q, Path context) { QueryFieldInfo lfi = findFieldInfo(q.getField(), q); QueryFieldInfo rfi = findFieldInfo(q.getRfield(), q); if (lfi.getFieldEntity() == currentEntity) { if (rfi.getFieldEntity() != currentEntity) { Value value = bind(rfi); return new ValueComparisonExpression(removeContext(lfi.getFieldEntity(),addPrefix(lfi.getEntityRelativeFieldName())), q.getOp(), value); } else if (nestedFieldPrefix.isEmpty()) { return q; } else { return new FieldComparisonExpression(addPrefix(q.getField()), q.getOp(), addPrefix(q.getRfield())); } } else if (rfi.getFieldEntity() == currentEntity) { Value value = bind(lfi); return new ValueComparisonExpression(removeContext(rfi.getFieldEntity(),addPrefix(rfi.getEntityRelativeFieldName())), q.getOp().invert(), value); } else { return new TruePH(); } } /** * Rewrites an nary- field comparison * * An nary- field comparison is of the form: *
         *  { field: f1, op:'$in', rfield: f2 }
         * 
* * There are four possible ways this can be rewritten: * * If both f1 and f2 are in the current entity, the query is returned * unmodified. * * If f1 is in current entity, but f2 is in a different entity, then the * query is rewritten as: *
         *    { field: f1, op:'$in', rvalue:  }
         * 
and f2 is bound to a list. * * If f2 is in current entity, but f1 is in a different entity, then the * query is rewritten as: *
         *   { field: f2, op: '$all', rvalue: [] }
         * 
and f1 is bound. * * If both f1 and f2 are not in the current entity, a placeholder for * TRUE is returned. * */ @Override protected QueryExpression itrNaryFieldRelationalExpression(NaryFieldRelationalExpression q, Path context) { QueryFieldInfo lfi = findFieldInfo(q.getField(), q); QueryFieldInfo rfi = findFieldInfo(q.getRfield(), q); if (lfi.getFieldEntity() == currentEntity) { if (rfi.getFieldEntity() != currentEntity) { List value = bindList(rfi); return new NaryValueRelationalExpression(removeContext(lfi.getFieldEntity(),addPrefix(lfi.getEntityRelativeFieldName())), q.getOp(), value); } else if (nestedFieldPrefix.isEmpty()) { return q; } else { return new NaryFieldRelationalExpression(removeContext(lfi.getFieldEntity(),addPrefix(q.getField())), q.getOp(), removeContext(rfi.getFieldEntity(),addPrefix(q.getRfield()))); } } else if (rfi.getFieldEntity() == currentEntity) { Value value = bind(lfi); List list = new ArrayList<>(1); list.add(value); return new ArrayContainsExpression(removeContext(rfi.getFieldEntity(),addPrefix(rfi.getEntityRelativeFieldName())), q.getOp() == NaryRelationalOperator._in ? ContainsOperator._all : ContainsOperator._none, list); } else { return new TruePH(); } } /** * If the enclosed query is a placeholder (TruePH or FalsePH), it * negates the placeholder, otherwise, the query remains as is */ @Override protected QueryExpression itrUnaryLogicalExpression(UnaryLogicalExpression q, Path context) { UnaryLogicalExpression nq = (UnaryLogicalExpression) super.itrUnaryLogicalExpression(q, context); if (nq.getQuery() instanceof TruePH) { return new FalsePH(); } else if (nq.getQuery() instanceof FalsePH) { return new TruePH(); } else { return nq; } } /** * All TruePH placeholders are removed from an AND expression. * * All FalsePH placeholders are removed from an OR expression. * * If an AND expression contains a FalsePH, the expression is replaced * with a FalsePH * * If an OR expression contains a TruePH, the expression is replaced * with a TruePH */ @Override protected QueryExpression itrNaryLogicalExpression(NaryLogicalExpression q, Path context) { NaryLogicalExpression nq = (NaryLogicalExpression) super.itrNaryLogicalExpression(q, context); CopyOnWriteIterator itr = new CopyOnWriteIterator(nq.getQueries()); while (itr.hasNext()) { QueryExpression nestedq = itr.next(); if (q.getOp() == NaryLogicalOperator._and) { if (nestedq instanceof TruePH) { itr.remove(); } else if (nestedq instanceof FalsePH) { return new FalsePH(); } } else if (nestedq instanceof TruePH) { return new TruePH(); } else if (nestedq instanceof FalsePH) { itr.remove(); } } QueryExpression ret; if (itr.isCopied()) { List newList = itr.getCopiedList(); if (newList.size() == 0) { ret=new TruePH(); } else if (newList.size() == 1) { ret=newList.get(0); } else { ret=new NaryLogicalExpression(q.getOp(), newList); } } else { ret=nq; } return ret; } /** * Several possibilities exist when rewriting an array query. * * The array field itself may be pointing to a different entity than the * currentEntity. This includes the case where the array is the * reference field itself. If this is the case, this is no longer an * array elem match expression. The nested query is moved outside and * rewritten. * * The array field itself may be pointing to an array in currentEntity. * In this case, the array field is rewritten as the local field, and * the nested query is rewritten recursively. * * If the nested query contains a placeholder, the query is replaced * with that. Otherwise, the query remains as is */ @Override protected QueryExpression itrArrayMatchExpression(ArrayMatchExpression q, Path context) { Path newContext=context.equals(Path.EMPTY)?new Path(q.getArray(),Path.ANYPATH): new Path(context,new Path(q.getArray(),Path.ANYPATH)); QueryFieldInfo oldContextField=contextField; QueryFieldInfo qfi = findFieldInfo(q.getArray(), q); try { if (qfi.getFieldEntity() == currentEntity) { if (qfi.getFieldMd() instanceof ResolvedReferenceField) { // The array is pointing to a reference field // This could be a simple reference, like {array:ref, elemMatch: Q } // In that case, the return value is Q // Example: in: { array: ref, elemMatch:{ field:_id, op:$eq, rfield: $parent.ref_id} } // out: { field: ref_id, op: $eq, rvalue: <_id value> } // // This could also be a nested array reference, like {array: x.*.ref, elemMatch: Q } // Example: in: { array: x.*.obj.ref, elemMatch: { field: _id, op:$eq, rfield: $parent.ref_id } } // out: { array: x, elemMatch: { field: obj.ref_id, op: $eq, rvalue: < _id value> } } // Find the closest array containing the reference Path arrName = qfi.getEntityRelativeFieldName(); int lastAny = -1; int n = arrName.numSegments(); for (int i = 0; i < n; i++) { if (arrName.tail(i).equals(Path.ANY)) { lastAny = i; break; } } if (lastAny == -1) { // Reference field is not in an array. QueryExpression em = iterate(q.getElemMatch(), newContext); return em; } else { // If arrName is x.*.ref, lastAny will be 1, so a prefix of -2 will be x Path closestArray = arrName.prefix(-(lastAny + 1)); // Any remaining part between the closest array and reference should be a prefix of all the nested fields // For instance, if arrName is x.*.obj.ref, all fields should be prefixed by obj Path oldPrefix = nestedFieldPrefix; nestedFieldPrefix = arrName.suffix(-(closestArray.numSegments() + 1)).prefix(-1); QueryExpression em = iterate(q.getElemMatch(), newContext); nestedFieldPrefix = oldPrefix; if (em instanceof TruePH || em instanceof FalsePH) { return em; } else { // Update context only if we're writing a new arraymatch expression contextField=qfi; return new ArrayMatchExpression(addPrefix(closestArray), em); } } } else if (currentEntity.getParent() != null) { // The array is pointing to a field in current entity, and current entity is not the root // Remove any parts of the array field before the reference Path relative = qfi.getEntityRelativeFieldName(); // Update context only if we're writing a new arraymatch expression contextField=qfi; return new ArrayMatchExpression(relative, iterate(q.getElemMatch(), newContext)); } else { QueryExpression em = iterate(q.getElemMatch(), newContext); if (em instanceof TruePH || em instanceof FalsePH) { return em; } else { // Update context only if we're writing a new arraymatch expression contextField=qfi; return new ArrayMatchExpression(addPrefix(q.getArray()), em); } } } else { QueryExpression em = iterate(q.getElemMatch(), newContext); return em; } } finally { contextField=oldContextField; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy