Show:
/**
  @module ember-flexberry-data
*/

import RSVP from 'rsvp';
import EmberMap from '@ember/map';
import { getOwner } from '@ember/application';
import { warn } from '@ember/debug';
import FilterOperator from './filter-operator';
import {
  SimplePredicate,
  ComplexPredicate,
  StringPredicate,
  DetailPredicate,
  DatePredicate,
  GeographyPredicate,
  GeometryPredicate,
  TruePredicate,
  FalsePredicate
} from './predicate';
import { ConstParam, AttributeParam } from './parameter';
import BaseAdapter from './base-adapter';
import JSAdapter from 'ember-flexberry-data/query/js-adapter';
import Information from '../utils/information';
import getSerializedDateValue from '../utils/get-serialized-date-value';
import Dexie from 'npm:dexie';
import Queue from '../utils/queue';

/**
  Class of query language adapter that allows to load data from IndexedDB.

  @class IndexedDBAdapter
  @extends Query.BaseAdapter
*/
export default class extends BaseAdapter {
  /**
    @param {Dexie} db Dexie database instance.
    @class IndexedDBAdapter
    @constructor
  */
  constructor(db) {
    super();

    if (!db) {
      throw new Error('Database must be.');
    }

    this._db = db;
  }

  /**
    Loads data from IndexedDB.

    @method query
    @param {DS.Store or subclass} store Store instance to the adapter.
    @param {QueryObject} query QueryObject instance to the adapter.
    @return {Promise} Promise with loaded data.
  */
  query(store, query) {
    return new RSVP.Promise((resolve, reject) => {
      let _this = this;
      let jsAdapter;
      let datePredicates = [];

      if (query.predicate instanceof ComplexPredicate) {
        datePredicates = query.predicate.predicates.filter(predicate => predicate instanceof DatePredicate);
      }

      if (query.predicate instanceof DatePredicate || datePredicates.length > 0) {
        let moment = getOwner(store).lookup('service:moment');
        jsAdapter = new JSAdapter(moment);
      } else {
        jsAdapter = new JSAdapter();
      }

      let table = _this._db.table(query.modelName);
      let complexQuery = containsRelationships(query);

      let sortData = function(data, sortField) {
        // Sorting array by `sortField` and asc.
        let singleSort = function(a, b) {
          let aVal = a[sortField];
          let bVal = b[sortField];
          return (!aVal && bVal) || (aVal < bVal) ? -1 : (aVal && !bVal) || (aVal > bVal) ? 1 : 0;
        };

        data.sort(singleSort);
      };

      let getDetailsHashMap = function(data, primaryKeyName) {
        let ret = EmberMap.create();
        let dataLength = data.length;
        for (let i = 0; i < dataLength; i++) {
          let obj = data[i];
          let key = obj[primaryKeyName];
          ret.set(key, obj);
        }

        return ret;
      };

      let joinSortedDataArrays = function(data, masterFieldName, masterData, masterPrimaryKeyName, masterTypeName, dataTypeName) {
        // Joining array `data` on field `masterField` with array `masterData` of objects `masterTypeName`. Array `data` must be ordered by `masterField`, array `masterData` must be ordered by id. Function do not use recursive calls.
        let masterIndex = 0;
        let dataLength = data.length;
        let masterDataLength;
        if (!masterData) {
          // TODO: May be return?
          masterDataLength = 0;
        } else {
          masterDataLength = masterData.length;
        }

        for (let dataIndex = 0; dataIndex < dataLength; dataIndex++) {
          let masterKey = data[dataIndex][masterFieldName];
          if (!masterKey) {
            continue;
          }

          let moveMastersForvard = true;
          while (moveMastersForvard) {
            let masterDataValue = masterData[masterIndex];

            if (!masterDataValue || !masterDataValue.hasOwnProperty(masterPrimaryKeyName)) {
              warn(
                `Metadata consistance error. Not found property '${masterPrimaryKeyName}' in type '${masterTypeName}'.`,
                false,
                { id: 'ember-flexberry-data-debug.offline.indexeddb-inconsistent-database' }
              );

              break;
            }

            if (masterKey > masterDataValue[masterPrimaryKeyName] && masterIndex < masterDataLength) {
              masterIndex++;
            } else if (masterKey < masterDataValue[masterPrimaryKeyName] || masterIndex >= masterDataLength) {
              warn(
                `Data constraint error. Not found object type '${masterTypeName}' with id '${masterKey}'. ` +
                `It used in object of type '${dataTypeName}' with id '${data[dataIndex].id}'.`,
                false,
                { id: 'ember-flexberry-data-debug.offline.indexeddb-inconsistent-database' }
              );

              break;
            }

            if (masterKey === masterDataValue[masterPrimaryKeyName]) {
              data[dataIndex][masterFieldName] = masterDataValue;
              moveMastersForvard = false;
              continue;
            }
          }
        }
      };

      let joinHasManyData = function(data, detailFieldName, detailsData, detailsTypeName, dataTypeName) {
        // Joining array `data` on field `masterField` with hash map `detailsData` of objects `detailsTypeName`. Function do not use recursive calls.
        let dataLength = data.length;
        for (let dataIndex = 0; dataIndex < dataLength; dataIndex++) {
          let detailsKeys = data[dataIndex][detailFieldName];
          if (!detailsKeys) {
            continue;
          }

          let detailsKeysLength = detailsKeys.length;
          let detailsObjects = [];
          for (let i = 0; i < detailsKeysLength; i++) {
            let detailKey = detailsKeys[i];
            let detailObj = detailsData.get(detailKey);

            if (!detailObj) {
              warn(
                `Data constraint error. Not found object type '${detailsTypeName}' with id ${detailKey}. ` +
                `It used in object of type '${dataTypeName}' with id '${data[dataIndex].id}'.`,
                false,
                { id: 'ember-flexberry-data-debug.offline.indexeddb-inconsistent-database' }
              );

              continue;
            }

            detailsObjects.push(detailObj);
          }

          data[dataIndex][detailFieldName] = detailsObjects;
        }
      };

      let buildJoinTree = function(joinTree) {
        let currentQueryTreeDeepLevel = 0;

        if (!complexQuery && (!query.expand || Object.keys(query.expand).length === 0) && query.select.length === 1 && query.select[0] === 'id') {
          return currentQueryTreeDeepLevel;
        }

        if (query.expand || query.extend) {
          let buildJoinPlan = function(exp, parent, deepLevel) {
            if (!exp) {
              return;
            }

            let masterPropNames = Object.keys(exp);
            let length = masterPropNames.length;
            let masterDeepLevel = deepLevel + 1;
            for (let i = 0; i < length; i++) {
              let masterPropName = masterPropNames[i];

              // Performing joining only if relationship is not async and embedded.
              if (exp[masterPropName].relationship.async || !exp[masterPropName].relationship.isEmbedded) {
                continue;
              }

              if (!parent.expand) {
                parent.expand = {};
              }

              if (!parent.expand[masterPropName]) {
                parent.expand[masterPropName] = {
                  propNameInParent: masterPropName,
                  modelName: exp[masterPropName].modelName,
                  primaryKeyName: exp[masterPropName].primaryKeyName,
                  data: null,
                  sorting: null,
                  deepLevel: masterDeepLevel,
                  expand: null,
                  parent: parent,
                  relationType: exp[masterPropName].relationship.type
                };
              }

              buildJoinPlan(exp[masterPropName].expand, parent.expand[masterPropName], masterDeepLevel);
              if (masterDeepLevel > currentQueryTreeDeepLevel) {
                currentQueryTreeDeepLevel = masterDeepLevel;
              }
            }
          };

          if (query.expand) {
            buildJoinPlan(query.expand, joinTree, 0);
          }

          if (query.extend.expand) {
            buildJoinPlan(query.extend.expand, joinTree, 0);
          }
        }

        return currentQueryTreeDeepLevel;
      };

      let joinTree = {
        modelName: query.modelName,
        primaryKeyName: query.primaryKeyName,
        data: null,
        sorting: null,
        deepLevel: 0,
        expand: null
      };

      let currentQueryTreeDeepLevel = buildJoinTree(joinTree);

      let joinDataByJoinTree = function(joinTree, applyFilter, applyOrder, applyTopSkip, applyProjection, count) {

        // Sort data and merge join data level by level.
        let scanDeepLevel = function(node, deepLevel) {
          return new RSVP.Promise((resolve, reject) => {
            if (node.deepLevel === deepLevel) {
              let processData = () => {
                if (node.relationType === 'belongsTo') {
                  if (node.parent.sorting !== node.propNameInParent) {
                    sortData(node.parent.data, node.propNameInParent);
                    node.parent.sorting = node.propNameInParent;
                  }

                  if (node.sorting !== node.primaryKeyName) {
                    sortData(node.data, node.primaryKeyName);
                    node.sorting = node.primaryKeyName;
                  }

                  joinSortedDataArrays(node.parent.data, node.propNameInParent, node.data, node.primaryKeyName, node.modelName, node.parent.modelName);

                } else {
                  joinHasManyData(node.parent.data, node.propNameInParent,
                    getDetailsHashMap(node.data, node.primaryKeyName),
                    node.modelName, node.parent.modelName);
                }

                // Remove node from parent.expand.
                let masters = Object.keys(node.parent.expand);
                let mastersCount = masters.length;
                let masterName;
                let masterFound = false;
                for (let i = 0; i < mastersCount; i++) {
                  masterName = masters[i];
                  if (node.parent.expand[masterName].propNameInParent === node.propNameInParent) {
                    masterFound = true;
                    break;
                  }
                }

                if (masterFound) {
                  delete node.parent.expand[masterName];
                }
              };

              // Load and join data.
              let loadPromises = [];

              // If parent data is one record and relationship is belongsTo then apply filter by master id from parent data.
              let filterById;

              if (!node.parent.data) {
                // Load parent data.
                let nodeTable = _this._db.table(node.parent.modelName);
                let loadPromise = new RSVP.Promise((loadResolve, loadReject) => {
                  nodeTable.toArray().then((data) => {
                    node.parent.data = data;
                    node.parent.sorting = node.parent.primaryKeyName;
                    loadResolve();
                  }, loadReject);});
                loadPromises.push(loadPromise);
              } else if (node.parent.data.length === 1 && node.relationType === 'belongsTo') {
                filterById = node.parent.data[0][node.propNameInParent];
              }

              if (!node.data) {
                // Load data.
                let nodeTable = _this._db.table(node.modelName);
                if (filterById) {
                  nodeTable = nodeTable.where(node.primaryKeyName).equals(filterById);
                }

                let loadPromise = new RSVP.Promise((loadResolve, loadReject) => {nodeTable.toArray().then((data) => {
                  node.data = data;
                  node.sorting = node.primaryKeyName;
                  loadResolve();
                }, loadReject);});
                loadPromises.push(loadPromise);
              }

              RSVP.all(loadPromises).then(() => {
                processData();
                resolve();
              }, reject);

            } else {
              if (node.expand) {
                let joinRelationsQueue = Queue.create();
                joinRelationsQueue.set('continueOnError', false);

                let masters = Object.keys(node.expand);
                let mastersCount = masters.length;
                let attachScanDeepLevelToRelationsQueue = (masterName) => {
                  joinRelationsQueue.attach((queryItemResolve, queryItemReject) => {
                    // load data for optimal performance.
                    let expandedMaster = node.expand[masterName];
                    let loadPromise;
                    let skipScan = false;
                    if (node.data && !expandedMaster.data) {
                      let nodeDataLength = node.data.length;
                      if (nodeDataLength === 0) {
                        skipScan = true;
                      } else if (nodeDataLength <= 50) { // Magical constant which depend on list form max rows.
                        let anyOfKeys = [];

                        for (let i = 0; i < nodeDataLength; i++) {
                          let relationKeyValue = node.data[i][masterName];

                          if (relationKeyValue) {
                            if (expandedMaster.relationType === 'belongsTo') {
                              anyOfKeys.push(relationKeyValue);
                            } else {
                              anyOfKeys = anyOfKeys.concat(relationKeyValue);
                            }
                          }
                        }

                        if (anyOfKeys.length === 0) {
                          skipScan = true;
                        } else {
                          loadPromise = new RSVP.Promise((loadResolve, loadReject) => {
                            _this._db.table(expandedMaster.modelName)
                            .where(expandedMaster.primaryKeyName)
                            .anyOf(anyOfKeys)
                            .toArray()
                            .then((loadedData) => {
                              expandedMaster.data = loadedData;
                              expandedMaster.sorting = expandedMaster.primaryKeyName;
                              loadResolve();
                            }, loadReject);
                          });
                        }
                      }
                    }

                    RSVP.all([loadPromise]).then(() => {
                      if (!skipScan) {
                        scanDeepLevel(expandedMaster, deepLevel).then(queryItemResolve, queryItemReject);
                      } else {
                        queryItemResolve();
                      }
                    });
                  });
                };

                for (let i = 0; i < mastersCount; i++) {
                  let masterName = masters[i];
                  attachScanDeepLevelToRelationsQueue(masterName);
                }

                joinRelationsQueue.attach((queueItemResolve) => {
                  resolve();
                  queueItemResolve();
                });
              } else {
                resolve();
              }
            }
          });
        };

        let joinQueue = Queue.create();
        joinQueue.set('continueOnError', false);

        let attachScanDeepLevelToQueue = (i) => {
          joinQueue.attach((queryItemResolve, queryItemReject) => scanDeepLevel(joinTree, i).then(queryItemResolve, queryItemReject));
        };

        // Scan query tree from leafs to root.
        for (let i = currentQueryTreeDeepLevel; i > 0; i--) {
          attachScanDeepLevelToQueue(i);
        }

        joinQueue.attach((queueItemResolve) => {
          let applyFilterOrderTopSkipProjection = function(data, applyFilter, applyOrder, applyTopSkip, applyProjection, count) {
            if (applyFilter) {
              let filter = query.predicate ? jsAdapter.buildFilter(query.predicate, { booleanAsString: true }) : (dataForFilter) => dataForFilter;
              data = filter(data);
            }

            if (applyOrder) {
              let order = jsAdapter.buildOrder(query);
              data = order(data);
            }

            let length = count ? count : data.length;

            if (applyTopSkip) {
              let topskip = jsAdapter.buildTopSkip(query);
              data = topskip(data);
            }

            if (applyProjection) {
              let jsProjection = jsAdapter.buildProjection(query);
              data = jsProjection(data);
            }

            let response = { meta: {}, data: data };
            if (query.count) {
              response.meta.count = length;
            }

            resolve(response);
          };

          applyFilterOrderTopSkipProjection(joinTree.data, applyFilter, applyOrder, applyTopSkip, applyProjection, count);
          queueItemResolve();
        });
      };

      if (!complexQuery) {
        let offset = query.skip;
        let limit = query.top;

        table = updateWhereClause(store, table, query);

        if (table instanceof _this._db.Table && !query.order) {
          // Go this way if filter is empty and no sorting.
          if (offset) {
            table = table.offset(offset);
          }

          if (limit) {
            table = table.limit(limit);
          }

          table.toArray().then((data) => {
            let length = data.length;
            let countPromise;

            if (query.count && (offset || limit)) {
              let fullTable = updateWhereClause(store, _this._db.table(query.modelName), query);
              countPromise = fullTable.count().then((count) => {
                length = count;
              }, reject);
            }

            let promises;
            if (countPromise) {
              promises = [countPromise];
            }

            Dexie.Promise.all(promises).then(() => {
              joinTree.data = data;
              joinDataByJoinTree(joinTree, false, false, false, true, length);
            });
          }, reject);
        } else {
          // Go this way if used simple filter.
          if (table instanceof this._db.Table) {
            table = table.toCollection();
          }

          let skipTopApplyed = false;

          if (query.order) {
            let sortFunc = function(a) {
                let len = query.order.length;

                let singleSort = function(a, b, i) {
                  if (i === undefined) {
                    i = 0;
                  }

                  if (i >= len) {
                    return 0;
                  }

                  let attrName = query.order.attribute(i).name;
                  let direction = query.order.attribute(i).direction;
                  i = i + 1;

                  let aVal = a[attrName];
                  let bVal = b[attrName];
                  if (!direction || direction === 'asc') {
                    return (!aVal && bVal) || (aVal < bVal) ? -1 : (aVal && !bVal) || (aVal > bVal) ? 1 : singleSort(a, b, i);
                  } else {
                    return (aVal && !bVal) || (aVal > bVal) ? -1 : (!aVal && bVal) || (aVal < bVal) ? 1 : singleSort(a, b, i);
                  }
                };

                return a.sort(singleSort);
              };

            table = table.toArray(sortFunc);
          } else {
            if (offset) {
              table = table.offset(offset);
            }

            if (limit) {
              table = table.limit(limit);
            }

            skipTopApplyed = true;
            table = table.toArray();
          }

          table.then(data => {
            let length = data.length;
            let countPromise;

            // if this is result of sortBy() Promise need apply top-skip.
            if (!skipTopApplyed) {
              let topskip = jsAdapter.buildTopSkip(query);
              data = topskip(data);
            } else {
              if (query.count && (offset || limit)) {
                let fullTable = updateWhereClause(store, _this._db.table(query.modelName), query);
                countPromise = fullTable.count().then((count) => {
                  length = count;
                }, reject);
              }
            }

            let promises;
            if (countPromise) {
              promises = [countPromise];
            }

            Dexie.Promise.all(promises).then(() => {
              joinTree.data = data;
              joinDataByJoinTree(joinTree, false, true, false, true, length);
            }, reject);
          }, reject);
        }
      } else {
        table.toArray().then((data) => {
          joinTree.data = data;
          joinDataByJoinTree(joinTree, true, true, true, true);
        }, reject);
      }
    });
  }
}

/**
  Builds Dexie `WhereClause` for filtering data.
  Filtering only with Dexie can applied only for simple cases (for `SimplePredicate`).
  For complex cases all logic implemened programmatically.

  @param {DS.Store or subclass} store Store instance.
  @param {Dexie.Table} table Table instance for loading objects.
  @param {Query} query Query language instance for loading data.
  @returns {Dexie.Collection|Dexie.Table} Table or collection that can be used with `toArray` method.
*/
function updateWhereClause(store, table, query) {
  let predicate = query.predicate;

  if (query.id) {
    if (!predicate) {
      predicate = new SimplePredicate('id', FilterOperator.Eq, query.id);
    } else {
      predicate = predicate.and(new SimplePredicate('id', FilterOperator.Eq, query.id));
    }
  }

  if (predicate instanceof GeographyPredicate) {
    warn('GeographyPredicate is not supported in indexedDB-adapter',
    false,
    { id: 'ember-flexberry-data-debug.offline.geography-predicate-is-not-supported' });
    return table;
  }

  if (predicate instanceof GeometryPredicate) {
    warn('GeometryPredicate is not supported in indexedDB-adapter',
    false,
    { id: 'ember-flexberry-data-debug.offline.geometry-predicate-is-not-supported' });
    return table;
  }

  if (!predicate) {
    return table;
  }


  if (predicate instanceof SimplePredicate || predicate instanceof DatePredicate) {
    // predicate.attributePath - attribute or AttributeParam or ConstParam.
    // predicate.value - const or AttributeParam or ConstParam.

    let information = new Information(store);
    let firstParameter = predicate.attributePath;
    let secondParameter = predicate.value;
    let firstObject = null;
    let secondObject = null;
    let isFirstParameterAttribute = !(firstParameter instanceof ConstParam);
    let isSecondParameterAttribute = secondParameter instanceof AttributeParam;

    let processAttributeParam = function(attributePathParameter) {
      let realAttributePath = attributePathParameter instanceof AttributeParam
                            ? attributePathParameter.attributePath
                            : attributePathParameter;

      return {
        attributePath: realAttributePath
      };
    };

    let processConstParam = function(valueParameter, attrType, timeless, store) {
      let realPredicateValue = valueParameter instanceof ConstParam
                              ? valueParameter.constValue
                              : valueParameter;

      let realAttrType = attrType === null ? typeof realPredicateValue : attrType;
      let resultValue =  realAttrType === 'boolean'
                          ? (typeof realPredicateValue === 'boolean' ? `${realPredicateValue}` : realPredicateValue)
                          : (realAttrType === 'date' || (realAttrType === 'object' && realPredicateValue instanceof Date)
                              ? getSerializedDateValue.call(store, realPredicateValue, timeless)
                              : realPredicateValue);

      let nextValue;
      if (predicate.timeless) {
        let moment = getOwner(store).lookup('service:moment');
        nextValue = moment.moment(resultValue, 'YYYY-MM-DD').add(1, 'd').format('YYYY-MM-DD');
      }

      return {
        value: resultValue,
        nextValue: nextValue
      };
    }

    if (isFirstParameterAttribute) {
      firstObject = processAttributeParam(firstParameter);
    }

    if (isSecondParameterAttribute) {
      secondObject = processAttributeParam(secondParameter);
    }

    if (!isFirstParameterAttribute) {
      firstObject = processConstParam(
        firstParameter,
        isSecondParameterAttribute ? information.getType(query.modelName, secondObject.attributePath) : null,
        predicate.timeless,
        store);
    }

    if (!isSecondParameterAttribute) {
      secondObject = processConstParam(
        secondParameter,
        isFirstParameterAttribute ? information.getType(query.modelName, firstObject.attributePath) : null,
        predicate.timeless,
        store);
    }

    if ((!isFirstParameterAttribute && firstObject.value === null)
        || (!isSecondParameterAttribute && secondObject.value === null)
        || (isFirstParameterAttribute && isSecondParameterAttribute)
        || (!isFirstParameterAttribute && !isSecondParameterAttribute)) {
      // IndexedDB (and Dexie) doesn't support null - use JS filter instead.
      // https://github.com/dfahlander/Dexie.js/issues/153

      // Also use JS filter for two consts or two attributes.
      let jsAdapter = predicate instanceof DatePredicate ? new JSAdapter(getOwner(store).lookup('service:moment')) : new JSAdapter();

      return table.filter(jsAdapter.getAttributeFilterFunction(predicate));
    }

    let isProperVariant = isFirstParameterAttribute && !isSecondParameterAttribute;
    let realAttributePath = isProperVariant
                            ? firstObject.attributePath
                            : secondObject.attributePath;
    let realValue = isProperVariant
                    ? secondObject.value
                    : firstObject.value;
    let realNextValue = isProperVariant
                        ? secondObject.nextValue
                        : firstObject.nextValue;

    switch (predicate.operator) {
      case FilterOperator.Eq:
        return predicate.timeless ?
          table.where(realAttributePath).between(realValue, realNextValue, false) :
          table.where(realAttributePath).equals(realValue);

      case FilterOperator.Neq:
        return predicate.timeless ?
          table.where(realAttributePath).below(realValue).or(realAttributePath).aboveOrEqual(realNextValue) :
          table.where(realAttributePath).notEqual(realValue);

      case FilterOperator.Le:
        return isProperVariant ?
          table.where(realAttributePath).below(realValue) :
          (predicate.timeless ?
                table.where(realAttributePath).aboveOrEqual(realNextValue) :
                table.where(realAttributePath).above(realValue));

      case FilterOperator.Leq:
        return isProperVariant ?
        (predicate.timeless ?
                table.where(realAttributePath).below(realNextValue) :
                table.where(realAttributePath).belowOrEqual(realValue)):
        table.where(realAttributePath).aboveOrEqual(realValue);

      case FilterOperator.Ge:
        return isProperVariant ?
        (predicate.timeless ?
                table.where(realAttributePath).aboveOrEqual(realNextValue) :
                table.where(realAttributePath).above(realValue)):
        table.where(realAttributePath).below(realValue);

      case FilterOperator.Geq:
        return isProperVariant ?
        table.where(realAttributePath).aboveOrEqual(realValue) :
        (predicate.timeless ?
                table.where(realAttributePath).below(realNextValue) :
                table.where(realAttributePath).belowOrEqual(realValue));

      default:
        throw new Error('Unknown operator');
    }
  }

  if (predicate instanceof StringPredicate) {
    let jsAdapter = new JSAdapter();
    return table.filter(jsAdapter.getAttributeFilterFunction(predicate, { booleanAsString: true }));
  }

  if (predicate instanceof TruePredicate) {
    return table;
  }

  if (predicate instanceof FalsePredicate) {
    return table.limit(0);
  }

  if (predicate instanceof ComplexPredicate) {
    let datePredicates = predicate.predicates.filter(pred => pred instanceof DatePredicate);
    let jsAdapter = datePredicates.length > 0 ? new JSAdapter(getOwner(store).lookup('service:moment')) : new JSAdapter();

    return table.filter(jsAdapter.getAttributeFilterFunction(predicate, { booleanAsString: true }));
  }

  throw new Error(`Unsupported predicate '${predicate}'`);
}

/**
  Checks query on contains restrictions by relationships.

  @method containsRelationships
  @param {QueryObject} query
  @return {Boolean}
*/
function containsRelationships(query) {
  let contains = false;

  if (query.predicate instanceof SimplePredicate || query.predicate instanceof StringPredicate || query.predicate instanceof DatePredicate) {
    // predicate.attributePath - attribute or AttributeParam or ConstParam.
    // predicate.value - const or AttributeParam or ConstParam.

    let firstParameter = query.predicate.attributePath;
    let secondParameter = query.predicate.value;
    let isFirstParameterAttribute = !(firstParameter instanceof ConstParam);
    let isSecondParameterAttribute = secondParameter instanceof AttributeParam;

    if (isFirstParameterAttribute) {
      let realAttributePath = firstParameter instanceof AttributeParam
                            ? firstParameter.attributePath
                            : firstParameter;
      contains = contains || Information.parseAttributePath(realAttributePath).length > 1;
    }

    if (isSecondParameterAttribute) {
      let realAttributePath = secondParameter instanceof AttributeParam
                            ? secondParameter.attributePath
                            : secondParameter;
      contains = contains || Information.parseAttributePath(realAttributePath).length > 1;
    }
  }

  if (query.predicate instanceof DetailPredicate) {
    return true;
  }

  if (query.predicate instanceof ComplexPredicate) {
    query.predicate.predicates.forEach((predicate) => {
      if (containsRelationships({ predicate })) {
        contains = true;
      }
    });
  }

  if (query.order) {
    for (let i = 0; i < query.order.length; i++) {
      let attributePath = query.order.attribute(i).name;
      if (Information.parseAttributePath(attributePath).length > 1) {
        contains = true;
      }
    }
  }

  return contains;
}