/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.storage.impl.local.paginated.atomicoperations;

import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.exception.OStorageException;
import com.orientechnologies.orient.core.storage.cache.OCacheEntry;
import com.orientechnologies.orient.core.storage.cache.OCacheEntryImpl;
import com.orientechnologies.orient.core.storage.cache.OCachePointer;
import com.orientechnologies.orient.core.storage.cache.OReadCache;
import com.orientechnologies.orient.core.storage.cache.OWriteCache;
import com.orientechnologies.orient.core.storage.impl.local.paginated.base.ODurablePage;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OFileCreatedWALRecord;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OFileDeletedWALRecord;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OLogSequenceNumber;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OOperationUnitId;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OUpdatePageRecord;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OWriteAheadLog;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * Note: all atomic operations methods are designed in context that all operations on single files will be wrapped in shared lock.
 *
 * @author Andrey Lomakin (a.lomakin-at-orientdb.com)
 * @since 12/3/13
 */
public final class OAtomicOperation {

  private final int                storageId;
  private final OLogSequenceNumber startLSN;
  private final OOperationUnitId   operationUnitId;

  private int     startCounter;
  private boolean rollback;

  private final Set<String>            lockedObjects        = new HashSet<>();
  private final Map<Long, FileChanges> fileChanges          = new HashMap<>();
  private final Map<String, Long>      newFileNamesId       = new HashMap<>();
  private final Set<Long>              deletedFiles         = new HashSet<>();
  private final Map<String, Long>      deletedFileNameIdMap = new HashMap<>();

  private final OReadCache  readCache;
  private final OWriteCache writeCache;

  private final Map<String, OAtomicOperationMetadata<?>> metadata = new LinkedHashMap<>();

  public OAtomicOperation(final OLogSequenceNumber startLSN, final OOperationUnitId operationUnitId, final OReadCache readCache,
      final OWriteCache writeCache, final int storageId) {
    this.storageId = storageId;
    this.startLSN = startLSN;
    this.operationUnitId = operationUnitId;

    startCounter = 1;
    this.readCache = readCache;
    this.writeCache = writeCache;
  }

  public OOperationUnitId getOperationUnitId() {
    return operationUnitId;
  }

  public OCacheEntry loadPageForWrite(long fileId, final long pageIndex, final boolean checkPinnedPages, final int pageCount,
      final boolean verifyChecksum) throws IOException {
    assert pageCount > 0;

    fileId = checkFileIdCompatibility(fileId, storageId);

    if (deletedFiles.contains(fileId)) {
      throw new OStorageException("File with id " + fileId + " is deleted.");
    }

    final FileChanges changesContainer = fileChanges.computeIfAbsent(fileId, k -> new FileChanges());

    if (changesContainer.isNew) {
      if (pageIndex <= changesContainer.maxNewPageIndex) {
        final OCacheEntryChanges pageChange = changesContainer.pageChangesMap.get(pageIndex);
        return pageChange;
      } else {
        return null;
      }
    } else {
      OCacheEntryChanges pageChangesContainer = changesContainer.pageChangesMap.get(pageIndex);

      if (checkChangesFilledUpTo(changesContainer, pageIndex)) {
        if (pageChangesContainer == null) {
          final OCacheEntry delegate = readCache.loadForRead(fileId, pageIndex, checkPinnedPages, writeCache, pageCount, true);
          if (delegate != null) {
            pageChangesContainer = new OCacheEntryChanges(verifyChecksum);
            changesContainer.pageChangesMap.put(pageIndex, pageChangesContainer);
            pageChangesContainer.delegate = delegate;
            return pageChangesContainer;
          }
        } else {
          if (pageChangesContainer.isNew) {
            return pageChangesContainer;
          } else {
            // Need to load the page again from cache for locking reasons
            final OCacheEntry delegate = readCache
                .loadForRead(fileId, pageIndex, checkPinnedPages, writeCache, pageCount, verifyChecksum);
            pageChangesContainer.delegate = delegate;
            return pageChangesContainer;
          }
        }
      }
    }
    return null;
  }

  public OCacheEntry loadPageForRead(long fileId, final long pageIndex, final boolean checkPinnedPages, final int pageCount)
      throws IOException {
    assert pageCount > 0;

    fileId = checkFileIdCompatibility(fileId, storageId);

    if (deletedFiles.contains(fileId)) {
      throw new OStorageException("File with id " + fileId + " is deleted.");
    }

    final FileChanges changesContainer = fileChanges.get(fileId);
    if (changesContainer == null) {
      return readCache.loadForRead(fileId, pageIndex, checkPinnedPages, writeCache, pageCount, true);
    }

    if (changesContainer.isNew) {
      if (pageIndex <= changesContainer.maxNewPageIndex) {
        final OCacheEntryChanges pageChange = changesContainer.pageChangesMap.get(pageIndex);
        return pageChange;
      } else {
        return null;
      }
    } else {
      final OCacheEntryChanges pageChangesContainer = changesContainer.pageChangesMap.get(pageIndex);

      if (checkChangesFilledUpTo(changesContainer, pageIndex)) {
        if (pageChangesContainer == null) {
          return readCache.loadForRead(fileId, pageIndex, checkPinnedPages, writeCache, pageCount, true);
        } else {
          if (pageChangesContainer.isNew) {
            return pageChangesContainer;
          } else {
            // Need to load the page again from cache for locking reasons
            final OCacheEntry delegate = readCache.loadForRead(fileId, pageIndex, checkPinnedPages, writeCache, pageCount, true);
            pageChangesContainer.delegate = delegate;
            return pageChangesContainer;
          }
        }
      }
    }
    return null;
  }

  /**
   * Add metadata with given key inside of atomic operation. If metadata with the same key insist inside of atomic operation it will
   * be overwritten.
   *
   * @param metadata Metadata to add.
   *
   * @see OAtomicOperationMetadata
   */
  public void addMetadata(final OAtomicOperationMetadata<?> metadata) {
    this.metadata.put(metadata.getKey(), metadata);
  }

  /**
   * @param key Key of metadata which is looking for.
   *
   * @return Metadata by associated key or null if such metadata is absent.
   */
  public OAtomicOperationMetadata<?> getMetadata(final String key) {
    return metadata.get(key);
  }

  /**
   * @return All keys and associated metadata contained inside of atomic operation
   */
  private Map<String, OAtomicOperationMetadata<?>> getMetadata() {
    return Collections.unmodifiableMap(metadata);
  }

  public void pinPage(final OCacheEntry cacheEntry) {
    if (deletedFiles.contains(cacheEntry.getFileId())) {
      throw new OStorageException("File with id " + cacheEntry.getFileId() + " is deleted.");
    }

    final FileChanges changesContainer = fileChanges.get(cacheEntry.getFileId());
    assert changesContainer != null;

    final OCacheEntryChanges pageChangesContainer = changesContainer.pageChangesMap.get(cacheEntry.getPageIndex());
    assert pageChangesContainer != null;

    pageChangesContainer.pinPage = true;
  }

  public OCacheEntry addPage(long fileId) {
    fileId = checkFileIdCompatibility(fileId, storageId);

    if (deletedFiles.contains(fileId)) {
      throw new OStorageException("File with id " + fileId + " is deleted.");
    }

    final FileChanges changesContainer = fileChanges.get(fileId);
    assert changesContainer != null;

    final long filledUpTo = internalFilledUpTo(fileId, changesContainer);

    OCacheEntryChanges pageChangesContainer = changesContainer.pageChangesMap.get(filledUpTo);
    assert pageChangesContainer == null;

    pageChangesContainer = new OCacheEntryChanges(false);
    pageChangesContainer.isNew = true;

    changesContainer.pageChangesMap.put(filledUpTo, pageChangesContainer);
    changesContainer.maxNewPageIndex = filledUpTo;
    final OCacheEntry delegate = new OCacheEntryImpl(fileId, filledUpTo, new OCachePointer(null, null, fileId, filledUpTo));
    pageChangesContainer.delegate = delegate;
    return pageChangesContainer;
  }

  public void releasePageFromRead(final OCacheEntry cacheEntry) {
    if (cacheEntry instanceof OCacheEntryChanges) {
      releasePageFromWrite(cacheEntry);
    } else {
      readCache.releaseFromRead(cacheEntry, writeCache);
    }
  }

  public void releasePageFromWrite(final OCacheEntry cacheEntry) {
    final OCacheEntryChanges real = (OCacheEntryChanges) cacheEntry;
    if (deletedFiles.contains(cacheEntry.getFileId())) {
      throw new OStorageException("File with id " + cacheEntry.getFileId() + " is deleted.");
    }

    if (cacheEntry.getCachePointer().getBuffer() != null) {
      readCache.releaseFromRead(real.getDelegate(), writeCache);
    } else {
      assert real.isNew || !cacheEntry.isLockAcquiredByCurrentThread();
    }
  }

  public long filledUpTo(long fileId) {
    fileId = checkFileIdCompatibility(fileId, storageId);

    if (deletedFiles.contains(fileId)) {
      throw new OStorageException("File with id " + fileId + " is deleted.");
    }

    final FileChanges changesContainer = fileChanges.get(fileId);

    return internalFilledUpTo(fileId, changesContainer);
  }

  private long internalFilledUpTo(final long fileId, FileChanges changesContainer) {
    if (changesContainer == null) {
      changesContainer = new FileChanges();
      fileChanges.put(fileId, changesContainer);
    } else if (changesContainer.isNew || changesContainer.maxNewPageIndex > -2) {
      return changesContainer.maxNewPageIndex + 1;
    } else if (changesContainer.truncate) {
      return 0;
    }

    return writeCache.getFilledUpTo(fileId);
  }

  /**
   * This check if a file was trimmed or trunked in the current atomic operation.
   *
   * @param changesContainer changes container to check
   * @param pageIndex        limit to check against the changes
   *
   * @return true if there are no changes or pageIndex still fit, false if the pageIndex do not fit anymore
   */
  private static boolean checkChangesFilledUpTo(final FileChanges changesContainer, final long pageIndex) {
    if (changesContainer == null) {
      return true;
    } else if (changesContainer.isNew || changesContainer.maxNewPageIndex > -2) {
      return pageIndex < changesContainer.maxNewPageIndex + 1;
    } else
      return !changesContainer.truncate;
  }

  public long addFile(final String fileName) {
    if (newFileNamesId.containsKey(fileName)) {
      throw new OStorageException("File with name " + fileName + " already exists.");
    }

    final long fileId;
    final boolean isNew;

    if (deletedFileNameIdMap.containsKey(fileName)) {
      fileId = deletedFileNameIdMap.remove(fileName);
      deletedFiles.remove(fileId);

      isNew = false;
    } else {
      fileId = writeCache.bookFileId(fileName);
      isNew = true;
    }

    newFileNamesId.put(fileName, fileId);

    final FileChanges fileChanges = new FileChanges();
    fileChanges.isNew = isNew;
    fileChanges.fileName = fileName;
    fileChanges.maxNewPageIndex = -1;

    this.fileChanges.put(fileId, fileChanges);

    return fileId;
  }

  public long loadFile(final String fileName) throws IOException {
    Long fileId = newFileNamesId.get(fileName);

    if (fileId == null) {
      fileId = writeCache.loadFile(fileName);
    }

    this.fileChanges.computeIfAbsent(fileId, k -> new FileChanges());

    return fileId;
  }

  public void deleteFile(long fileId) {
    fileId = checkFileIdCompatibility(fileId, storageId);

    final FileChanges fileChanges = this.fileChanges.remove(fileId);
    if (fileChanges != null && fileChanges.fileName != null) {
      newFileNamesId.remove(fileChanges.fileName);
    } else {
      deletedFiles.add(fileId);
      final String f = writeCache.fileNameById(fileId);
      if (f != null) {
        deletedFileNameIdMap.put(f, fileId);
      }
    }
  }

  public boolean isFileExists(final String fileName) {
    if (newFileNamesId.containsKey(fileName)) {
      return true;
    }

    if (deletedFileNameIdMap.containsKey(fileName)) {
      return false;
    }

    return writeCache.exists(fileName);
  }

  public String fileNameById(long fileId) {
    fileId = checkFileIdCompatibility(fileId, storageId);

    final FileChanges fileChanges = this.fileChanges.get(fileId);

    if (fileChanges != null && fileChanges.fileName != null) {
      return fileChanges.fileName;
    }

    if (deletedFiles.contains(fileId)) {
      throw new OStorageException("File with id " + fileId + " was deleted.");
    }

    return writeCache.fileNameById(fileId);
  }

  public void truncateFile(long fileId) {
    fileId = checkFileIdCompatibility(fileId, storageId);

    final FileChanges fileChanges = this.fileChanges.computeIfAbsent(fileId, k -> new FileChanges());

    fileChanges.pageChangesMap.clear();
    fileChanges.maxNewPageIndex = -1;

    if (fileChanges.isNew) {
      return;
    }

    fileChanges.truncate = true;
  }

  OLogSequenceNumber commitChanges(final OWriteAheadLog writeAheadLog) throws IOException {
    OLogSequenceNumber txEndLsn = null;
    if (writeAheadLog != null) {
      final OLogSequenceNumber startLSN = writeAheadLog.end();

      for (final long deletedFileId : deletedFiles) {
        writeAheadLog.log(new OFileDeletedWALRecord(operationUnitId, deletedFileId));
      }

      for (final Map.Entry<Long, FileChanges> fileChangesEntry : fileChanges.entrySet()) {
        final FileChanges fileChanges = fileChangesEntry.getValue();
        final long fileId = fileChangesEntry.getKey();

        if (fileChanges.isNew) {
          writeAheadLog.log(new OFileCreatedWALRecord(operationUnitId, fileChanges.fileName, fileId));
        } else if (fileChanges.truncate) {
          OLogManager.instance().warn(this,
              "You performing truncate operation which is considered unsafe because can not be rolled back, "
                  + "as result data can be incorrectly restored after crash, this operation is not recommended to be used");
        }

        final Iterator<Map.Entry<Long, OCacheEntryChanges>> filePageChangesIterator = fileChanges.pageChangesMap.entrySet()
            .iterator();
        while (filePageChangesIterator.hasNext()) {
          final Map.Entry<Long, OCacheEntryChanges> filePageChangesEntry = filePageChangesIterator.next();

          if (filePageChangesEntry.getValue().changes.hasChanges()) {
            final long pageIndex = filePageChangesEntry.getKey();
            final OCacheEntryChanges filePageChanges = filePageChangesEntry.getValue();

            final OLogSequenceNumber changesLSN = writeAheadLog
                .log(new OUpdatePageRecord(pageIndex, fileId, operationUnitId, filePageChanges.changes));
            filePageChanges.setChangeLSN(changesLSN);
          } else {
            filePageChangesIterator.remove();
          }
        }
      }

      txEndLsn = writeAheadLog.logAtomicOperationEndRecord(getOperationUnitId(), rollback, this.startLSN, getMetadata());

      for (final long deletedFileId : deletedFiles) {
        readCache.deleteFile(deletedFileId, writeCache);
      }

      for (final Map.Entry<Long, FileChanges> fileChangesEntry : fileChanges.entrySet()) {
        final FileChanges fileChanges = fileChangesEntry.getValue();
        final long fileId = fileChangesEntry.getKey();

        if (fileChanges.isNew) {
          readCache.addFile(fileChanges.fileName, newFileNamesId.get(fileChanges.fileName), writeCache);
        } else if (fileChanges.truncate) {
          OLogManager.instance().warn(this,
              "You performing truncate operation which is considered unsafe because can not be rolled back, "
                  + "as result data can be incorrectly restored after crash, this operation is not recommended to be used");
          readCache.truncateFile(fileId, writeCache);
        }

        final Iterator<Map.Entry<Long, OCacheEntryChanges>> filePageChangesIterator = fileChanges.pageChangesMap.entrySet()
            .iterator();
        while (filePageChangesIterator.hasNext()) {
          final Map.Entry<Long, OCacheEntryChanges> filePageChangesEntry = filePageChangesIterator.next();

          if (filePageChangesEntry.getValue().changes.hasChanges()) {
            final long pageIndex = filePageChangesEntry.getKey();
            final OCacheEntryChanges filePageChanges = filePageChangesEntry.getValue();

            OCacheEntry cacheEntry = readCache
                .loadForWrite(fileId, pageIndex, true, writeCache, 1, filePageChanges.verifyCheckSum, startLSN);
            if (cacheEntry == null) {
              assert filePageChanges.isNew;
              do {
                if (cacheEntry != null) {
                  readCache.releaseFromWrite(cacheEntry, writeCache);
                }

                cacheEntry = readCache.allocateNewPage(fileId, writeCache, startLSN);
              } while (cacheEntry.getPageIndex() != pageIndex);
            }

            try {
              final ODurablePage durablePage = new ODurablePage(cacheEntry);
              cacheEntry.setEndLSN(txEndLsn);

              durablePage.restoreChanges(filePageChanges.changes);
              durablePage.setLsn(filePageChanges.getChangeLSN());

              if (filePageChanges.pinPage) {
                readCache.pinPage(cacheEntry, writeCache);
              }

            } finally {
              readCache.releaseFromWrite(cacheEntry, writeCache);
            }
          } else {
            filePageChangesIterator.remove();
          }
        }
      }
    } else {
      for (final long deletedFileId : deletedFiles) {
        readCache.deleteFile(deletedFileId, writeCache);
      }

      for (final Map.Entry<Long, FileChanges> fileChangesEntry : fileChanges.entrySet()) {
        final FileChanges fileChanges = fileChangesEntry.getValue();
        final long fileId = fileChangesEntry.getKey();

        if (fileChanges.isNew) {
          readCache.addFile(fileChanges.fileName, newFileNamesId.get(fileChanges.fileName), writeCache);
        } else if (fileChanges.truncate) {
          readCache.truncateFile(fileId, writeCache);
        }

        for (final Map.Entry<Long, OCacheEntryChanges> filePageChangesEntry : fileChanges.pageChangesMap.entrySet()) {
          final OCacheEntryChanges filePageChanges = filePageChangesEntry.getValue();
          if (!filePageChanges.changes.hasChanges()) {
            continue;
          }
          final long pageIndex = filePageChangesEntry.getKey();

          OCacheEntry cacheEntry = readCache.loadForWrite(fileId, pageIndex, true, writeCache, 1, true, null);
          if (cacheEntry == null) {
            assert filePageChanges.isNew;
            do {
              if (cacheEntry != null) {
                readCache.releaseFromWrite(cacheEntry, writeCache);
              }

              cacheEntry = readCache.allocateNewPage(fileId, writeCache, null);
            } while (cacheEntry.getPageIndex() != pageIndex);
          }

          try {
            final ODurablePage durablePage = new ODurablePage(cacheEntry);
            durablePage.restoreChanges(filePageChanges.changes);

            if (filePageChanges.pinPage) {
              readCache.pinPage(cacheEntry, writeCache);
            }

          } finally {
            readCache.releaseFromWrite(cacheEntry, writeCache);
          }
        }
      }
    }

    return txEndLsn;
  }

  void incrementCounter() {
    startCounter++;
  }

  void decrementCounter() {
    startCounter--;
  }

  public int getCounter() {
    return startCounter;
  }

  void rollback() {
    rollback = true;
  }

  boolean isRollback() {
    return rollback;
  }

  void addLockedObject(final String lockedObject) {
    lockedObjects.add(lockedObject);
  }

  boolean containsInLockedObjects(final String objectToLock) {
    return lockedObjects.contains(objectToLock);
  }

  Iterable<String> lockedObjects() {
    return lockedObjects;
  }

  @Override
  public boolean equals(final Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    final OAtomicOperation operation = (OAtomicOperation) o;

    return operationUnitId.equals(operation.operationUnitId);
  }

  @Override
  public int hashCode() {
    return operationUnitId.hashCode();
  }

  private static final class FileChanges {
    private final Map<Long, OCacheEntryChanges> pageChangesMap  = new HashMap<>();
    private       long                          maxNewPageIndex = -2;
    private       boolean                       isNew;
    private       boolean                       truncate;
    private       String                        fileName;
  }

  private static int storageId(final long fileId) {
    return (int) (fileId >>> 32);
  }

  private static long composeFileId(final long fileId, final int storageId) {
    return (((long) storageId) << 32) | fileId;
  }

  private static long checkFileIdCompatibility(final long fileId, final int storageId) {
    // indicates that storage has no it's own id.
    if (storageId == -1) {
      return fileId;
    }

    if (storageId(fileId) == 0) {
      return composeFileId(fileId, storageId);
    }

    return fileId;
  }
}


package com.orientechnologies.orient.core.metadata.schema;

import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OSchemaException;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.record.impl.ODocument;

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

/**
 * Created by tglman on 14/06/17.
 */
public class OClassRemote extends OClassImpl {
  protected OClassRemote(OSchemaShared iOwner, String iName) {
    super(iOwner, iName);
  }

  protected OClassRemote(OSchemaShared iOwner, String iName, int[] iClusterIds) {
    super(iOwner, iName, iClusterIds);
  }

  protected OClassRemote(OSchemaShared iOwner, ODocument iDocument, String iName) {
    super(iOwner, iDocument, iName);
  }

  protected OProperty addProperty(final String propertyName, final OType type, final OType linkedType, final OClass linkedClass,
      final boolean unsafe) {
    if (type == null)
      throw new OSchemaException("Property type not defined.");

    if (propertyName == null || propertyName.length() == 0)
      throw new OSchemaException("Property name is null or empty");

    final ODatabaseDocumentInternal database = getDatabase();
    validatePropertyName(propertyName);
    if (database.getTransaction().isActive()) {
      throw new OSchemaException("Cannot create property '" + propertyName + "' inside a transaction");
    }

    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    if (linkedType != null)
      OPropertyImpl.checkLinkTypeSupport(type);

    if (linkedClass != null)
      OPropertyImpl.checkSupportLinkedClass(type);

    acquireSchemaWriteLock();
    try {
      final StringBuilder cmd = new StringBuilder("create property ");
      // CLASS.PROPERTY NAME
      cmd.append('`');
      cmd.append(name);
      cmd.append('`');
      cmd.append('.');
      cmd.append('`');
      cmd.append(propertyName);
      cmd.append('`');

      // TYPE
      cmd.append(' ');
      cmd.append(type.name);

      if (linkedType != null) {
        // TYPE
        cmd.append(' ');
        cmd.append(linkedType.name);

      } else if (linkedClass != null) {
        // TYPE
        cmd.append(' ');
        cmd.append('`');
        cmd.append(linkedClass.getName());
        cmd.append('`');
      }

      if (unsafe)
        cmd.append(" unsafe ");

      database.command(cmd.toString()).close();
      getOwner().reload(database);

      return getProperty(propertyName);
    } finally {
      releaseSchemaWriteLock();
    }
  }

  public OClassImpl setEncryption(final String iValue) {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    acquireSchemaWriteLock();
    try {
      final ODatabaseDocumentInternal database = getDatabase();
      final String cmd = String.format("alter class `%s` encryption %s", name, iValue);
      database.command(cmd);
    } finally {
      releaseSchemaWriteLock();
    }
    return this;
  }

  @Override
  public OClass setClusterSelection(final String value) {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    acquireSchemaWriteLock();
    try {
      final ODatabaseDocumentInternal database = getDatabase();
      final String cmd = String.format("alter class `%s` clusterselection '%s'", name, value);
      database.command(cmd).close();
      return this;
    } finally {
      releaseSchemaWriteLock();
    }
  }

  public OClassImpl setCustom(final String name, final String value) {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    acquireSchemaWriteLock();
    try {
      final ODatabaseDocumentInternal database = getDatabase();
      final String cmd = String.format("alter class `%s` custom %s = ?", getName(), name);
      database.command(cmd, value).close();
      return this;
    } finally {
      releaseSchemaWriteLock();
    }
  }

  public void clearCustom() {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    acquireSchemaWriteLock();
    try {
      final ODatabaseDocumentInternal database = getDatabase();
      final String cmd = String.format("alter class `%s` custom clear", getName());
      database.command(cmd).close();
    } finally {
      releaseSchemaWriteLock();
    }
  }

  @Override
  public OClass setSuperClasses(final List<? extends OClass> classes) {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);
    if (classes != null) {
      List<OClass> toCheck = new ArrayList<OClass>(classes);
      toCheck.add(this);
      checkParametersConflict(toCheck);
    }
    acquireSchemaWriteLock();
    try {
      final ODatabaseDocumentInternal database = getDatabase();
      final StringBuilder sb = new StringBuilder();
      if (classes != null && classes.size() > 0) {
        for (OClass superClass : classes) {
          sb.append('`').append(superClass.getName()).append("`,");
        }
        sb.deleteCharAt(sb.length() - 1);
      } else
        sb.append("null");

      final String cmd = String.format("alter class `%s` superclasses %s", name, sb);
      database.command(cmd).close();
    } finally {
      releaseSchemaWriteLock();
    }
    return this;
  }

  @Override
  public OClass addSuperClass(final OClass superClass) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);
    checkParametersConflict(superClass);
    acquireSchemaWriteLock();
    try {

      final String cmd = String.format("alter class `%s` superclass +`%s`", name, superClass != null ? superClass.getName() : null);
      database.command(cmd).close();

    } finally {
      releaseSchemaWriteLock();
    }
    return this;
  }

  @Override
  public OClass removeSuperClass(OClass superClass) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);
    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` superclass -`%s`", name, superClass != null ? superClass.getName() : null);
      database.command(cmd).close();
    } finally {
      releaseSchemaWriteLock();
    }
    return this;
  }

  public OClass setName(final String name) {
    if (getName().equals(name))
      return this;
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);
    final Character wrongCharacter = OSchemaShared.checkClassNameIfValid(name);
    OClass oClass = database.getMetadata().getSchema().getClass(name);
    if (oClass != null) {
      String error = String.format("Cannot rename class %s to %s. A Class with name %s exists", this.name, name, name);
      throw new OSchemaException(error);
    }
    if (wrongCharacter != null)
      throw new OSchemaException(
          "Invalid class name found. Character '" + wrongCharacter + "' cannot be used in class name '" + name + "'");
    acquireSchemaWriteLock();
    try {

      final String cmd = String.format("alter class `%s` name `%s`", this.name, name);
      database.command(cmd);

    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  public OClass setShortName(String shortName) {
    if (shortName != null) {
      shortName = shortName.trim();
      if (shortName.isEmpty())
        shortName = null;
    }
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` shortname `%s`", name, shortName);
      database.command(cmd);
    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  protected OPropertyImpl createPropertyInstance(ODocument p) {
    return new OPropertyRemote(this, p);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public OClass truncateCluster(String clusterName) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.CLASS, ORole.PERMISSION_DELETE, name);
    acquireSchemaReadLock();
    try {

      final String cmd = String.format("truncate cluster %s", clusterName);
      database.command(cmd).close();
    } finally {
      releaseSchemaReadLock();
    }

    return this;
  }

  public OClass setStrictMode(final boolean isStrict) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);
    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` strictmode %s", name, isStrict);
      database.command(cmd);
    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  public OClass setDescription(String iDescription) {
    if (iDescription != null) {
      iDescription = iDescription.trim();
      if (iDescription.isEmpty())
        iDescription = null;
    }
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` description ?", name);
      database.command(cmd, iDescription).close();
    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  public OClass addClusterId(final int clusterId) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    if (isAbstract()) {
      throw new OSchemaException("Impossible to associate a cluster to an abstract class class");
    }
    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` addcluster %d", name, clusterId);
      database.command(cmd).close();

    } finally {
      releaseSchemaWriteLock();
    }
    return this;
  }

  public OClass removeClusterId(final int clusterId) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    if (clusterIds.length == 1 && clusterId == clusterIds[0])
      throw new ODatabaseException(" Impossible to remove the last cluster of class '" + getName() + "' drop the class instead");

    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` removecluster %d", name, clusterId);
      database.command(cmd).close();
    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  public void dropProperty(final String propertyName) {
    final ODatabaseDocumentInternal database = getDatabase();
    if (database.getTransaction().isActive())
      throw new IllegalStateException("Cannot drop a property inside a transaction");

    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_DELETE);

    acquireSchemaWriteLock();
    try {
      if (!properties.containsKey(propertyName))
        throw new OSchemaException("Property '" + propertyName + "' not found in class " + name + "'");

      database.command("drop property " + name + '.' + propertyName).close();

    } finally {
      releaseSchemaWriteLock();
    }
  }

  @Override
  public OClass addCluster(final String clusterNameOrId) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    if (isAbstract()) {
      throw new OSchemaException("Impossible to associate a cluster to an abstract class class");
    }

    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` addcluster `%s`", name, clusterNameOrId);
      database.command(cmd).close();

    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  public OClass setOverSize(final float overSize) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);
    acquireSchemaWriteLock();
    try {
      // FORMAT FLOAT LOCALE AGNOSTIC
      final String cmd = String.format("alter class `%s` oversize %s", name, new Float(overSize).toString());
      database.command(cmd).close();
    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  public OClass setAbstract(boolean isAbstract) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_UPDATE);

    acquireSchemaWriteLock();
    try {
      final String cmd = String.format("alter class `%s` abstract %s", name, isAbstract);
      database.command(cmd).close();
    } finally {
      releaseSchemaWriteLock();
    }

    return this;
  }

  protected OClass removeBaseClassInternal(final OClass baseClass) {
    acquireSchemaWriteLock();
    try {
      checkEmbedded();

      if (subclasses == null)
        return this;

      if (subclasses.remove(baseClass))
        removePolymorphicClusterIds((OClassImpl) baseClass);

      return this;
    } finally {
      releaseSchemaWriteLock();
    }
  }

  protected void setSuperClassesInternal(final List<? extends OClass> classes) {
    List<OClassImpl> newSuperClasses = new ArrayList<OClassImpl>();
    OClassImpl cls;
    for (OClass superClass : classes) {
      if (superClass instanceof OClassAbstractDelegate)
        cls = (OClassImpl) ((OClassAbstractDelegate) superClass).delegate;
      else
        cls = (OClassImpl) superClass;

      if (newSuperClasses.contains(cls)) {
        throw new OSchemaException("Duplicated superclass '" + cls.getName() + "'");
      }

      newSuperClasses.add(cls);
    }

    List<OClassImpl> toAddList = new ArrayList<OClassImpl>(newSuperClasses);
    toAddList.removeAll(superClasses);
    List<OClassImpl> toRemoveList = new ArrayList<OClassImpl>(superClasses);
    toRemoveList.removeAll(newSuperClasses);

    for (OClassImpl toRemove : toRemoveList) {
      toRemove.removeBaseClassInternal(this);
    }
    for (OClassImpl addTo : toAddList) {
      addTo.addBaseClass(this);
    }
    superClasses.clear();
    superClasses.addAll(newSuperClasses);
  }

  public void setDefaultClusterId(final int defaultClusterId) {
    final ODatabaseDocumentInternal database = getDatabase();
    String clusterName = database.getClusterNameById(defaultClusterId);
    if (clusterName == null) {
      throw new OSchemaException("Cluster with id '" + defaultClusterId + "' does not exist");
    }
    final String cmd = String.format("alter class `%s` DEFAULTCLUSTER `%s`", this.name, clusterName);
    database.command(cmd).close();
  }

}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.sql;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.orient.core.command.OCommandContext.TIMEOUT_STRATEGY;
import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandExecutorAbstract;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestAbstract;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.metadata.OMetadataInternal;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OClassImpl;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.sql.parser.OStatement;
import com.orientechnologies.orient.core.sql.parser.OStatementCache;

import java.util.*;

/**
 * SQL abstract Command Executor implementation.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 *
 */
public abstract class OCommandExecutorSQLAbstract extends OCommandExecutorAbstract {

  public static final String KEYWORD_FROM             = "FROM";
  public static final String KEYWORD_LET              = "LET";
  public static final String KEYWORD_WHERE            = "WHERE";
  public static final String KEYWORD_LIMIT            = "LIMIT";
  public static final String KEYWORD_SKIP             = "SKIP";
  public static final String KEYWORD_OFFSET           = "OFFSET";
  public static final String KEYWORD_TIMEOUT          = "TIMEOUT";
  public static final String KEYWORD_LOCK             = "LOCK";
  public static final String KEYWORD_RETURN           = "RETURN";
  public static final String KEYWORD_KEY              = "key";
  public static final String KEYWORD_RID              = "rid";
  public static final String CLUSTER_PREFIX           = "CLUSTER:";
  public static final String CLASS_PREFIX             = "CLASS:";
  public static final String INDEX_PREFIX             = "INDEX:";
  public static final String KEYWORD_UNSAFE           = "UNSAFE";

  public static final String INDEX_VALUES_PREFIX      = "INDEXVALUES:";
  public static final String INDEX_VALUES_ASC_PREFIX  = "INDEXVALUESASC:";
  public static final String INDEX_VALUES_DESC_PREFIX = "INDEXVALUESDESC:";

  public static final String DICTIONARY_PREFIX        = "DICTIONARY:";
  public static final String METADATA_PREFIX          = "METADATA:";
  public static final String METADATA_SCHEMA          = "SCHEMA";
  public static final String METADATA_INDEXMGR        = "INDEXMANAGER";
  public static final String METADATA_STORAGE         = "STORAGE";
  public static final String METADATA_DATABASE        = "DATABASE";

  public static final String DEFAULT_PARAM_USER       = "$user";

  protected long             timeoutMs                = OGlobalConfiguration.COMMAND_TIMEOUT.getValueAsLong();
  protected TIMEOUT_STRATEGY timeoutStrategy          = TIMEOUT_STRATEGY.EXCEPTION;
  protected OStatement       preParsedStatement;

  /**
   * The command is replicated
   *
   * @return
   */
  public OCommandDistributedReplicateRequest.DISTRIBUTED_EXECUTION_MODE getDistributedExecutionMode() {
    return OCommandDistributedReplicateRequest.DISTRIBUTED_EXECUTION_MODE.REPLICATE;
  }

  public boolean isIdempotent() {
    return false;
  }

  protected void throwSyntaxErrorException(final String iText) {
    throw new OCommandSQLParsingException(iText + ". Use " + getSyntax(), parserText, parserGetPreviousPosition());
  }

  protected void throwParsingException(final String iText) {
    throw new OCommandSQLParsingException(iText, parserText, parserGetPreviousPosition());
  }

  protected void throwParsingException(final String iText, Exception e) {
    throw OException.wrapException(new OCommandSQLParsingException(iText, parserText, parserGetPreviousPosition()), e);
  }

  /**
   * Parses the timeout keyword if found.
   */
  protected boolean parseTimeout(final String w) throws OCommandSQLParsingException {
    if (!w.equals(KEYWORD_TIMEOUT))
      return false;

    String word = parserNextWord(true);

    try {
      timeoutMs = Long.parseLong(word);
    } catch (NumberFormatException ignore) {
      throwParsingException("Invalid " + KEYWORD_TIMEOUT + " value set to '" + word + "' but it should be a valid long. Example: "
          + KEYWORD_TIMEOUT + " 3000");
    }

    if (timeoutMs < 0)
      throwParsingException("Invalid " + KEYWORD_TIMEOUT + ": value set minor than ZERO. Example: " + KEYWORD_TIMEOUT + " 10000");

    word = parserNextWord(true);

    if (word != null)
      if (word.equals(TIMEOUT_STRATEGY.EXCEPTION.toString()))
        timeoutStrategy = TIMEOUT_STRATEGY.EXCEPTION;
      else if (word.equals(TIMEOUT_STRATEGY.RETURN.toString()))
        timeoutStrategy = TIMEOUT_STRATEGY.RETURN;
      else
        parserGoBack();

    return true;
  }

  /**
   * Parses the lock keyword if found.
   */
  protected String parseLock() throws OCommandSQLParsingException {
    final String lockStrategy = parserNextWord(true);

    if (!lockStrategy.equalsIgnoreCase("DEFAULT") && !lockStrategy.equalsIgnoreCase("NONE")
        && !lockStrategy.equalsIgnoreCase("RECORD"))
      throwParsingException("Invalid " + KEYWORD_LOCK + " value set to '" + lockStrategy
          + "' but it should be NONE (default) or RECORD. Example: " + KEYWORD_LOCK + " RECORD");

    return lockStrategy;
  }

  protected Set<String> getInvolvedClustersOfClasses(final Collection<String> iClassNames) {
    final ODatabaseDocument db = getDatabase();

    final Set<String> clusters = new HashSet<String>();

    for (String clazz : iClassNames) {
      final OClass cls = ((OMetadataInternal) db.getMetadata()).getImmutableSchemaSnapshot().getClass(clazz);
      if (cls != null)
        for (int clId : cls.getPolymorphicClusterIds()) {
          // FILTER THE CLUSTER WHERE THE USER HAS THE RIGHT ACCESS
          if (clId > -1 && checkClusterAccess(db, db.getClusterNameById(clId)))
            clusters.add(db.getClusterNameById(clId).toLowerCase(Locale.ENGLISH));
        }
    }

    return clusters;
  }

  protected Set<String> getInvolvedClustersOfClusters(final Collection<String> iClusterNames) {
    final ODatabaseDocument db = getDatabase();

    final Set<String> clusters = new HashSet<String>();

    for (String cluster : iClusterNames) {
      final String c = cluster.toLowerCase(Locale.ENGLISH);
      // FILTER THE CLUSTER WHERE THE USER HAS THE RIGHT ACCESS
      if (checkClusterAccess(db, c))
        clusters.add(c);
    }

    return clusters;
  }

  protected Set<String> getInvolvedClustersOfIndex(final String iIndexName) {
    final ODatabaseDocumentInternal db = getDatabase();

    final Set<String> clusters = new HashSet<String>();

    final OMetadataInternal metadata = (OMetadataInternal) db.getMetadata();
    final OIndex<?> idx = metadata.getIndexManager().getIndex(iIndexName);
    if (idx != null && idx.getDefinition() != null) {
      final String clazz = idx.getDefinition().getClassName();

      if (clazz != null) {
        final OClass cls = metadata.getImmutableSchemaSnapshot().getClass(clazz);
        if (cls != null)
          for (int clId : cls.getClusterIds()) {
            final String clName = db.getClusterNameById(clId);
            if (clName != null)
              clusters.add(clName.toLowerCase(Locale.ENGLISH));
          }
      }
    }

    return clusters;
  }

  protected boolean checkClusterAccess(final ODatabaseDocument db, final String iClusterName) {
    return db.getUser() == null
        || db.getUser().checkIfAllowed(ORule.ResourceGeneric.CLUSTER, iClusterName, getSecurityOperationType()) != null;
  }

  protected void bindDefaultContextVariables() {
    if (context != null) {
      if (getDatabase() != null && getDatabase().getUser() != null) {
        context.setVariable(DEFAULT_PARAM_USER, getDatabase().getUser().getIdentity());
      }
    }
  }

  protected String preParse(final String queryText, final OCommandRequest iRequest) {
    final boolean strict = getDatabase().getStorage().getConfiguration().isStrictSql();

    if (strict) {
      try {
        final OStatement result = OStatementCache.get(queryText, getDatabase());
        preParsedStatement = result;

        if (iRequest instanceof OCommandRequestAbstract) {
          final Map<Object, Object> params = ((OCommandRequestAbstract) iRequest).getParameters();
          StringBuilder builder = new StringBuilder();
          result.toString(params, builder);
          return builder.toString();
        }
        return result.toString();
      } catch (OCommandSQLParsingException sqlx) {
        throw sqlx;
      } catch (Exception e) {
        throwParsingException("Error parsing query: \n" + queryText + "\n" + e.getMessage(), e);
      }
      OClass clazz = getDatabase().getMetadata().getSchema().getClass("Foo");
      clazz.setCustom("schemaVersion", "1");
      String version = clazz.getCustom("schemaVersion");
    }
    return queryText;
  }

  protected String decodeClassName(String s) {
    return OClassImpl.decodeClassName(s);
  }

}


/**
 * Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *

 * 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.
 *

 * For more information: http://www.orientdb.com
 */
package com.orientechnologies.spatial.strategy;

import com.orientechnologies.spatial.engine.OLuceneSpatialIndexContainer;
import com.orientechnologies.spatial.query.OSpatialQueryContext;
import com.orientechnologies.spatial.shape.OShapeBuilder;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.locationtech.spatial4j.shape.Shape;

import java.util.Map;

/**
 * Created by Enrico Risa on 11/08/15.
 */
public class SpatialQueryBuilderOverlap extends SpatialQueryBuilderAbstract {

  public static final String NAME = "&&";

  public SpatialQueryBuilderOverlap(OLuceneSpatialIndexContainer manager, OShapeBuilder factory) {
    super(manager, factory);
  }

  // TODO check PGIS
  @Override
  public OSpatialQueryContext build(Map<String, Object> query) throws Exception {
    Shape shape = parseShape(query);
    SpatialStrategy strategy = manager.strategy();
    SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects, shape.getBoundingBox());
    Query filterQuery = strategy.makeQuery(args);
    BooleanQuery q = new BooleanQuery.Builder().add(filterQuery, BooleanClause.Occur.MUST)
        .add(new MatchAllDocsQuery(), BooleanClause.Occur.SHOULD).build();

    return new OSpatialQueryContext(null, manager.searcher(), q);
  }

  @Override
  public String getName() {
    return NAME;
  }
}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.sql;

import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ridbag.ORidBag;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.intent.OIntentMassiveInsert;
import com.orientechnologies.orient.core.record.impl.ODocument;

import java.util.Iterator;
import java.util.Map;

/**
 * SQL ALTER DATABASE command: Changes an attribute of the current database.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
@SuppressWarnings("unchecked")
public class OCommandExecutorSQLOptimizeDatabase extends OCommandExecutorSQLAbstract
    implements OCommandDistributedReplicateRequest {
  public static final String KEYWORD_OPTIMIZE  = "OPTIMIZE";
  public static final String KEYWORD_DATABASE  = "DATABASE";
  public static final String KEYWORD_EDGE      = "-LWEDGES";
  public static final String KEYWORD_NOVERBOSE = "-NOVERBOSE";

  private boolean optimizeEdges = false;
  private boolean verbose       = true;
  private int     batch         = 1000;

  public OCommandExecutorSQLOptimizeDatabase parse(final OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

    String queryText = textRequest.getText();
    String originalQuery = queryText;
    try {
      queryText = preParse(queryText, iRequest);
      textRequest.setText(queryText);
      init((OCommandRequestText) iRequest);

      StringBuilder word = new StringBuilder();

      int oldPos = 0;
      int pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_OPTIMIZE))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_OPTIMIZE + " not found. Use " + getSyntax(), parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_DATABASE))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_DATABASE + " not found. Use " + getSyntax(), parserText, oldPos);

      while (!parserIsEnded() && word.length() > 0) {
        oldPos = pos;
        pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
        if (word.toString().equals(KEYWORD_EDGE))
          optimizeEdges = true;
        else if (word.toString().equals(KEYWORD_NOVERBOSE))
          verbose = false;
      }
    } finally {
      textRequest.setText(originalQuery);
    }

    return this;
  }

  /**
   * Execute the ALTER DATABASE.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    final StringBuilder result = new StringBuilder();

    if (optimizeEdges)
      result.append(optimizeEdges());

    return result.toString();
  }

  private String optimizeEdges() {
    final ODatabaseDocumentInternal db = getDatabase();

    db.declareIntent(new OIntentMassiveInsert());
    try {
      long transformed = 0;
      if (db.getTransaction().isActive())
        db.commit();

      db.begin();

      try {

        final long totalEdges = db.countClass("E");
        long browsedEdges = 0;
        long lastLapBrowsed = 0;
        long lastLapTime = System.currentTimeMillis();

        for (ODocument doc : db.browseClass("E")) {
          if (Thread.currentThread().isInterrupted())
            break;

          browsedEdges++;

          if (doc != null) {
            if (doc.fields() == 2) {
              final ORID edgeIdentity = doc.getIdentity();

              final ODocument outV = doc.field("out");
              final ODocument inV = doc.field("in");

              // OUTGOING
              final Object outField = outV.field("out_" + doc.getClassName());
              if (outField instanceof ORidBag) {
                final Iterator<OIdentifiable> it = ((ORidBag) outField).iterator();
                while (it.hasNext()) {
                  OIdentifiable v = it.next();
                  if (edgeIdentity.equals(v)) {
                    // REPLACE EDGE RID WITH IN-VERTEX RID
                    it.remove();
                    ((ORidBag) outField).add(inV.getIdentity());
                    break;
                  }
                }
              }

              outV.save();

              // INCOMING
              final Object inField = inV.field("in_" + doc.getClassName());
              if (outField instanceof ORidBag) {
                final Iterator<OIdentifiable> it = ((ORidBag) inField).iterator();
                while (it.hasNext()) {
                  OIdentifiable v = it.next();
                  if (edgeIdentity.equals(v)) {
                    // REPLACE EDGE RID WITH IN-VERTEX RID
                    it.remove();
                    ((ORidBag) inField).add(outV.getIdentity());
                    break;
                  }
                }
              }

              inV.save();

              doc.delete();

              if (++transformed % batch == 0) {
                db.commit();
                db.begin();
              }

              final long now = System.currentTimeMillis();

              if (verbose && (now - lastLapTime > 2000)) {
                final long elapsed = now - lastLapTime;

                OLogManager.instance().info(this, "Browsed %,d of %,d edges, transformed %,d so far (%,d edges/sec)",
                    browsedEdges, totalEdges,
                    transformed,
                    (((browsedEdges - lastLapBrowsed) * 1000 / elapsed)));

                lastLapTime = System.currentTimeMillis();
                lastLapBrowsed = browsedEdges;
              }
            }
          }
        }

        // LAST COMMIT
        db.commit();

      } finally {
        if (db.getTransaction().isActive())
          db.rollback();
      }
      return "Transformed " + transformed + " regular edges in lightweight edges";

    } finally {
      db.declareIntent(null);
    }
  }

  @Override
  public QUORUM_TYPE getQuorumType() {
    return QUORUM_TYPE.ALL;
  }

  public String getSyntax() {
    return "OPTIMIZE DATABASE [-lwedges]";
  }
}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.graph.sql;

import com.orientechnologies.common.types.OModifiableBoolean;
import com.orientechnologies.orient.core.command.*;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OCommandExecutorSQLSetAware;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.sql.filter.OSQLFilter;
import com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery;
import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Edge;
import com.tinkerpop.blueprints.impls.orient.*;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

/**
 * SQL DELETE EDGE command.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OCommandExecutorSQLDeleteEdge extends OCommandExecutorSQLSetAware
    implements OCommandDistributedReplicateRequest, OCommandResultListener {
  public static final String               NAME          = "DELETE EDGE";
  private static final String              KEYWORD_BATCH = "BATCH";
  private List<ORecordId>                  rids;
  private String                           fromExpr;
  private String                           toExpr;
  private int                              removed       = 0;
  private OCommandRequest                  query;
  private OSQLFilter                       compiledFilter;
  private AtomicReference<OrientBaseGraph> currentGraph  = new AtomicReference<OrientBaseGraph>();
  private String                           label;
  private OModifiableBoolean               shutdownFlag  = new OModifiableBoolean();
  private boolean                          txAlreadyBegun;
  private int                              batch         = 100;

  @SuppressWarnings("unchecked")
  public OCommandExecutorSQLDeleteEdge parse(final OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

    String queryText = textRequest.getText();
    String originalQuery = queryText;

    try {
      // System.out.println("NEW PARSER FROM: " + queryText);
      queryText = preParse(queryText, iRequest);
      // System.out.println("NEW PARSER TO: " + queryText);
      textRequest.setText(queryText);

      init((OCommandRequestText) iRequest);

      parserRequiredKeyword("DELETE");
      parserRequiredKeyword("EDGE");

      OClass clazz = null;
      String where = null;

      String temp = parseOptionalWord(true);
      String originalTemp = null;

      int limit = -1;

      if (temp != null && !parserIsEnded())
        originalTemp = parserText.substring(parserGetPreviousPosition(), parserGetCurrentPosition()).trim();

      final OModifiableBoolean shutdownFlag = new OModifiableBoolean();
      ODatabaseDocumentInternal curDb = ODatabaseRecordThreadLocal.instance().get();
      final OrientGraph graph = OGraphCommandExecutorSQLFactory.getGraph(false, shutdownFlag);
      try {
        while (temp != null) {

          if (temp.equals("FROM")) {
            fromExpr = parserRequiredWord(false, "Syntax error", " =><,\r\n");
            if (rids != null)
              throwSyntaxErrorException("FROM '" + fromExpr + "' is not allowed when specify a RIDs (" + rids + ")");

          } else if (temp.equals("TO")) {
            toExpr = parserRequiredWord(false, "Syntax error", " =><,\r\n");
            if (rids != null)
              throwSyntaxErrorException("TO '" + toExpr + "' is not allowed when specify a RID (" + rids + ")");

          } else if (temp.startsWith("#")) {
            rids = new ArrayList<ORecordId>();
            rids.add(new ORecordId(temp));
            if (fromExpr != null || toExpr != null)
              throwSyntaxErrorException("Specifying the RID " + rids + " is not allowed with FROM/TO");

          } else if (temp.startsWith("[") && temp.endsWith("]")) {
            temp = temp.substring(1, temp.length() - 1);
            rids = new ArrayList<ORecordId>();
            for (String rid : temp.split(",")) {
              rid = rid.trim();
              if (!rid.startsWith("#")) {
                throwSyntaxErrorException("Not a valid RID: " + rid);
              }
              rids.add(new ORecordId(rid));
            }
          } else if (temp.equals(KEYWORD_WHERE)) {
            if (clazz == null)
              // ASSIGN DEFAULT CLASS
              clazz = graph.getEdgeType(OrientEdgeType.CLASS_NAME);

            where = parserGetCurrentPosition() > -1 ? " " + parserText.substring(parserGetCurrentPosition()) : "";

            compiledFilter = OSQLEngine.getInstance().parseCondition(where, getContext(), KEYWORD_WHERE);
            break;

          } else if (temp.equals(KEYWORD_BATCH)) {
            temp = parserNextWord(true);
            if (temp != null)
              batch = Integer.parseInt(temp);

          } else if (temp.equals(KEYWORD_LIMIT)) {
            temp = parserNextWord(true);
            if (temp != null)
              limit = Integer.parseInt(temp);

          } else if (temp.length() > 0) {
            // GET/CHECK CLASS NAME
            label = originalTemp;
            clazz = graph.getEdgeType(temp);
            if (clazz == null)
              throw new OCommandSQLParsingException("Class '" + temp + "' was not found");
          }

          temp = parseOptionalWord(true);
          if (parserIsEnded())
            break;
        }

        if (where == null)
          if (limit > -1) {
            where = " LIMIT " + limit;
          } else {
            where = "";
          }
        else
          where = " WHERE " + where;

        if (fromExpr == null && toExpr == null && rids == null)
          if (clazz == null)
            // DELETE ALL THE EDGES
            query = graph.getRawGraph().command(new OSQLAsynchQuery<ODocument>("select from E" + where, this));
          else
            // DELETE EDGES OF CLASS X
            query = graph.getRawGraph().command(new OSQLAsynchQuery<ODocument>("select from " + clazz.getName() + where, this));

        return this;
      } finally {
        if (shutdownFlag.getValue())
          graph.shutdown(false, false);
        ODatabaseRecordThreadLocal.instance().set(curDb);
      }
    } finally {
      textRequest.setText(originalQuery);
    }

  }

  /**
   * Execute the command and return the ODocument object created.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (fromExpr == null && toExpr == null && rids == null && query == null && compiledFilter == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");

    txAlreadyBegun = getDatabase().getTransaction().isActive();

    if (rids != null) {
      // REMOVE PUNCTUAL RID
      OGraphCommandExecutorSQLFactory.runInConfiguredTxMode(new OGraphCommandExecutorSQLFactory.GraphCallBack<Object>() {
        @Override
        public Object call(OrientBaseGraph graph) {
          for (ORecordId rid : rids) {
            final OrientEdge e = graph.getEdge(rid);
            if (e != null) {
              e.remove();
              removed++;
            }
          }
          return null;
        }
      });
      // CLOSE PENDING TX
      end();

    } else {
      // MULTIPLE EDGES
      final Set<OrientEdge> edges = new HashSet<OrientEdge>();
      if (query == null) {
        OGraphCommandExecutorSQLFactory.runInConfiguredTxMode(new OGraphCommandExecutorSQLFactory.GraphCallBack<Object>() {

          @Override
          public Object call(OrientBaseGraph graph) {
            Set<OIdentifiable> fromIds = null;
            if (fromExpr != null)
              fromIds = OSQLEngine.getInstance().parseRIDTarget(graph.getRawGraph(), fromExpr, context, iArgs);
            Set<OIdentifiable> toIds = null;
            if (toExpr != null)
              toIds = OSQLEngine.getInstance().parseRIDTarget(graph.getRawGraph(), toExpr, context, iArgs);
            if(label == null )
              label = OrientEdgeType.CLASS_NAME;

            if (fromIds != null && toIds != null) {
              int fromCount = 0;
              int toCount = 0;
              for (OIdentifiable fromId : fromIds) {
                final OrientVertex v = graph.getVertex(fromId);
                if (v != null)
                  fromCount += v.countEdges(Direction.OUT, label);
              }
              for (OIdentifiable toId : toIds) {
                final OrientVertex v = graph.getVertex(toId);
                if (v != null)
                  toCount += v.countEdges(Direction.IN, label);
              }
              if (fromCount <= toCount) {
                // REMOVE ALL THE EDGES BETWEEN VERTICES
                for (OIdentifiable fromId : fromIds) {
                  final OrientVertex v = graph.getVertex(fromId);
                  if (v != null)
                    for (Edge e : v.getEdges(Direction.OUT, label)) {
                      final OIdentifiable inV = ((OrientEdge) e).getInVertex();
                      if (inV != null && toIds.contains(inV.getIdentity()))
                        edges.add((OrientEdge) e);
                    }
                }
              } else {
                for (OIdentifiable toId : toIds) {
                  final OrientVertex v = graph.getVertex(toId);
                  if (v != null)
                    for (Edge e : v.getEdges(Direction.IN, label)) {
                      final OIdentifiable outV = ((OrientEdge) e).getOutVertex();
                      if (outV != null && fromIds.contains(outV.getIdentity()))
                        edges.add((OrientEdge) e);
                    }
                }
              }
            } else if (fromIds != null) {
              // REMOVE ALL THE EDGES THAT START FROM A VERTEXES
              for (OIdentifiable fromId : fromIds) {

                final OrientVertex v = graph.getVertex(fromId);
                if (v != null) {
                  for (Edge e : v.getEdges(Direction.OUT, label)) {
                    edges.add((OrientEdge) e);
                  }
                }
              }
            } else if (toIds != null) {
              // REMOVE ALL THE EDGES THAT ARRIVE TO A VERTEXES
              for (OIdentifiable toId : toIds) {
                final OrientVertex v = graph.getVertex(toId);
                if (v != null) {
                  for (Edge e : v.getEdges(Direction.IN, label)) {
                    edges.add((OrientEdge) e);
                  }
                }
              }
            } else
              throw new OCommandExecutionException("Invalid target: " + toIds);

            if (compiledFilter != null) {
              // ADDITIONAL FILTERING
              for (Iterator<OrientEdge> it = edges.iterator(); it.hasNext();) {
                final OrientEdge edge = it.next();
                if (!(Boolean) compiledFilter.evaluate(edge.getRecord(), null, context))
                  it.remove();
              }
            }

            // DELETE THE FOUND EDGES
            removed = edges.size();
            for (OrientEdge edge : edges)
              edge.remove();

            return null;
          }
        });

        // CLOSE PENDING TX
        end();

      } else {
        OGraphCommandExecutorSQLFactory.runInConfiguredTxMode(new OGraphCommandExecutorSQLFactory.GraphCallBack<OrientGraph>() {
          @Override
          public OrientGraph call(final OrientBaseGraph iGraph) {
            // TARGET IS A CLASS + OPTIONAL CONDITION
            currentGraph.set(iGraph);
            query.setContext(getContext());
            query.execute(iArgs);
            return null;
          }
        });
      }
    }

    return removed;
  }

  /**
   * Delete the current edge.
   */
  public boolean result(final Object iRecord) {
    final OIdentifiable id = (OIdentifiable) iRecord;

    if (compiledFilter != null) {
      // ADDITIONAL FILTERING
      if (!(Boolean) compiledFilter.evaluate(id.getRecord(), null, context))
        return true;
    }

    if (id.getIdentity().isValid()) {
      final OrientBaseGraph g = currentGraph.get();

      final OrientEdge e = g.getEdge(id);

      if (e != null) {
        e.remove();

        if (!txAlreadyBegun && batch > 0 && (removed + 1) % batch == 0) {
          if (g instanceof OrientGraph) {
            g.commit();
            ((OrientGraph) g).begin();
          }
        }

        removed++;
      }
    }

    return true;
  }

  @Override
  public String getSyntax() {
    return "DELETE EDGE <rid>|FROM <rid>|TO <rid>|<[<class>] [WHERE <conditions>]> [BATCH <batch-size>]";
  }

  @Override
  public void end() {
    final OrientBaseGraph g = currentGraph.get();
    if (g != null) {
      if (!txAlreadyBegun) {
        g.commit();

        if (shutdownFlag.getValue())
          g.shutdown(false, false);
      }
    }
  }

  @Override
  public int getSecurityOperationType() {
    return ORole.PERMISSION_DELETE;
  }

  @Override
  public QUORUM_TYPE getQuorumType() {
    return QUORUM_TYPE.WRITE;
  }

  public DISTRIBUTED_RESULT_MGMT getDistributedResultManagement() {
    return getDistributedExecutionMode() == DISTRIBUTED_EXECUTION_MODE.LOCAL ? DISTRIBUTED_RESULT_MGMT.CHECK_FOR_EQUALS
        : DISTRIBUTED_RESULT_MGMT.MERGE;
  }

  @Override
  public Object getResult() {
    return null;
  }

  public DISTRIBUTED_EXECUTION_MODE getDistributedExecutionMode() {
    return query != null && !getDatabase().getTransaction().isActive() ? DISTRIBUTED_EXECUTION_MODE.REPLICATE
        : DISTRIBUTED_EXECUTION_MODE.LOCAL;
  }

  @Override
  public Set<String> getInvolvedClusters() {
    final HashSet<String> result = new HashSet<String>();
    if (rids != null) {
      final ODatabaseDocumentInternal database = getDatabase();
      for (ORecordId rid : rids) {
        result.add(database.getClusterNameById(rid.getClusterId()));
      }
    } else if (query != null) {
      final OCommandExecutor executor = OCommandManager.instance().getExecutor((OCommandRequestInternal) query);
      // COPY THE CONTEXT FROM THE REQUEST
      executor.setContext(context);
      executor.parse(query);
      return executor.getInvolvedClusters();
    }
    return result;
  }

  /**
   * setLimit() for DELETE EDGE is ignored. Please use LIMIT keyword in the SQL statement
   */
  public <RET extends OCommandExecutor> RET setLimit(final int iLimit) {
    // do nothing
    return (RET) this;
  }
}


/*
  *
  *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
  *  *
  *  *  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.
  *  *
  *  * For more information: http://orientdb.com
  *
  */

package com.orientechnologies.orient.core.sql;

import com.orientechnologies.orient.core.sql.filter.OSQLFilterItemField;
import com.orientechnologies.orient.core.sql.operator.*;

import java.util.*;

/**
 * Presents query subset in form of field1 = "field1 value" AND field2 = "field2 value" ... AND fieldN anyOpetator "fieldN value"
 *

 * Where pairs (field1, value1) ... (fieldn-1, valuen-1) are stored in {@link #fieldValuePairs} map but last pair is stored in
 * {@link #lastField} {@link #lastValue} properties and their operator will be stored in {@link #lastOperator} property.
 *

 * Such data structure is used because from composite index point of view any "field and value" pairs can be reordered to match keys
 * order that is used in index in case all fields and values are related to each other using equals operator, but position of field
 * - value pair that uses non equals operator cannot be changed. Actually only one non-equals operator can be used for composite
 * index search and filed - value pair that uses this index should always be placed at last position.
 */
public class OIndexSearchResult {
  public final Map<String, Object>            fieldValuePairs = new HashMap<String, Object>(8);
  public final OQueryOperator                 lastOperator;
  public final OSQLFilterItemField.FieldChain lastField;
  public final Object                         lastValue;
  boolean                                     containsNullValues;

  public OIndexSearchResult(final OQueryOperator lastOperator, final OSQLFilterItemField.FieldChain field, final Object value) {
    this.lastOperator = lastOperator;
    lastField = field;
    lastValue = value;

    containsNullValues = value == null;
  }

  public static boolean isIndexEqualityOperator(OQueryOperator queryOperator) {
    return queryOperator instanceof OQueryOperatorEquals || queryOperator instanceof OQueryOperatorContains
        || queryOperator instanceof OQueryOperatorContainsKey || queryOperator instanceof OQueryOperatorContainsValue;
  }

  /**
   * Combines two queries subset into one. This operation will be valid only if {@link #canBeMerged(OIndexSearchResult)} method will
   * return true for the same passed in parameter.
   *
   * @param searchResult
   *          Query subset to merge.
   * @return New instance that presents merged query.
   */
  public OIndexSearchResult merge(final OIndexSearchResult searchResult) {
    // if (searchResult.lastOperator instanceof OQueryOperatorEquals) {
    if (searchResult.lastOperator instanceof OQueryOperatorEquals) {
      return mergeFields(this, searchResult);
    }
    if (lastOperator instanceof OQueryOperatorEquals) {
      return mergeFields(searchResult, this);
    }
    if (isIndexEqualityOperator(searchResult.lastOperator)) {
      return mergeFields(this, searchResult);
    }
    return mergeFields(searchResult, this);
  }

  private OIndexSearchResult mergeFields(OIndexSearchResult mainSearchResult, OIndexSearchResult searchResult) {
    OIndexSearchResult result = new OIndexSearchResult(mainSearchResult.lastOperator, mainSearchResult.lastField,
        mainSearchResult.lastValue);
    result.fieldValuePairs.putAll(searchResult.fieldValuePairs);
    result.fieldValuePairs.putAll(mainSearchResult.fieldValuePairs);
    result.fieldValuePairs.put(searchResult.lastField.getItemName(0), searchResult.lastValue);
    result.containsNullValues = searchResult.containsNullValues || this.containsNullValues;
    return result;
  }

  /**
   * @param searchResult
   *          Query subset is going to be merged with given one.
   * @return true if two query subsets can be merged.
   */
  boolean canBeMerged(final OIndexSearchResult searchResult) {
    if (lastField.isLong() || searchResult.lastField.isLong()) {
      return false;
    }
    if (!lastOperator.canBeMerged() || !searchResult.lastOperator.canBeMerged()) {
      return false;
    }
    return isIndexEqualityOperator(lastOperator) || isIndexEqualityOperator(searchResult.lastOperator);
  }

  public List<String> fields() {
    final List<String> result = new ArrayList<String>(fieldValuePairs.size() + 1);
    result.addAll(fieldValuePairs.keySet());
    result.add(lastField.getItemName(0));
    return result;
  }

  int getFieldCount() {
    return fieldValuePairs.size() + 1;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    OIndexSearchResult that = (OIndexSearchResult) o;

    if (containsNullValues != that.containsNullValues) {
      return false;
    }
    for (Map.Entry<String, Object> entry : fieldValuePairs.entrySet()) {
      if (!that.fieldValuePairs.get(entry.getKey()).equals(entry.getValue())) {
        return false;
      }
    }

    if (!lastField.equals(that.lastField)) {
      return false;
    }
    if (!lastOperator.equals(that.lastOperator)) {
      return false;
    }
    if (!lastValue.equals(that.lastValue)) {
      return false;
    }

    return true;
  }

  @Override
  public int hashCode() {
    int result = lastOperator.hashCode();

    for (Map.Entry<String, Object> entry : fieldValuePairs.entrySet()) {
      if (entry.getKey() != null) {
        result = 31 * result + entry.getKey().hashCode();
      }
      if (entry.getValue() != null) {
        result = 31 * result + entry.getValue().hashCode();
      }
    }

    if (lastField != null) {
      result = 31 * result + lastField.hashCode();
    }
    if (lastValue != null) {
      result = 31 * result + lastValue.hashCode();
    }

    result = 31 * result + (containsNullValues ? 1 : 0);
    return result;
  }

}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.sql;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.orient.core.cache.OCommandCache;
import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.metadata.schema.OClass;

import java.io.IOException;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;

/**
 * SQL TRUNCATE CLASS command: Truncates an entire class deleting all configured clusters where the class relies on.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OCommandExecutorSQLTruncateClass extends OCommandExecutorSQLAbstract implements OCommandDistributedReplicateRequest {
  public static final String KEYWORD_TRUNCATE    = "TRUNCATE";
  public static final String KEYWORD_CLASS       = "CLASS";
  public static final String KEYWORD_POLYMORPHIC = "POLYMORPHIC";
  private OClass schemaClass;
  private boolean unsafe = false;
  private boolean deep   = false;

  @SuppressWarnings("unchecked") public OCommandExecutorSQLTruncateClass parse(final OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

    String queryText = textRequest.getText();
    String originalQuery = queryText;
    try {
      queryText = preParse(queryText, iRequest);
      textRequest.setText(queryText);

      final ODatabaseDocument database = getDatabase();

      init((OCommandRequestText) iRequest);

      StringBuilder word = new StringBuilder();

      int oldPos = 0;
      int pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_TRUNCATE))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_TRUNCATE + " not found. Use " + getSyntax(), parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_CLASS))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_CLASS + " not found. Use " + getSyntax(), parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserText, oldPos, word, true);
      if (pos == -1)
        throw new OCommandSQLParsingException("Expected class name. Use " + getSyntax(), parserText, oldPos);

      final String className = word.toString();
      schemaClass = database.getMetadata().getSchema().getClass(className);

      if (schemaClass == null)
        throw new OCommandSQLParsingException("Class '" + className + "' not found", parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserText, oldPos, word, true);

      while (pos > 0) {
        String nextWord = word.toString();
        if (nextWord.toUpperCase(Locale.ENGLISH).equals(KEYWORD_UNSAFE)) {
          unsafe = true;
        } else if (nextWord.toUpperCase(Locale.ENGLISH).equals(KEYWORD_POLYMORPHIC)) {
          deep = true;
        }
        oldPos = pos;
        pos = nextWord(parserText, parserText, oldPos, word, true);
      }
    } finally {
      textRequest.setText(originalQuery);
    }

    return this;
  }

  /**
   * Execute the command.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (schemaClass == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");

    final long recs = schemaClass.count(deep);
    if (recs > 0 && !unsafe) {
      if (schemaClass.isSubClassOf("V")) {
        throw new OCommandExecutionException(
            "'TRUNCATE CLASS' command cannot be used on not empty vertex classes. Apply the 'UNSAFE' keyword to force it (at your own risk)");
      } else if (schemaClass.isSubClassOf("E")) {
        throw new OCommandExecutionException(
            "'TRUNCATE CLASS' command cannot be used on not empty edge classes. Apply the 'UNSAFE' keyword to force it (at your own risk)");
      }
    }

    Collection<OClass> subclasses = schemaClass.getAllSubclasses();
    if (deep && !unsafe) {// for multiple inheritance
      for (OClass subclass : subclasses) {
        long subclassRecs = schemaClass.count();
        if (subclassRecs > 0) {
          if (subclass.isSubClassOf("V")) {
            throw new OCommandExecutionException(
                "'TRUNCATE CLASS' command cannot be used on not empty vertex classes (" + subclass.getName()
                    + "). Apply the 'UNSAFE' keyword to force it (at your own risk)");
          } else if (subclass.isSubClassOf("E")) {
            throw new OCommandExecutionException(
                "'TRUNCATE CLASS' command cannot be used on not empty edge classes (" + subclass.getName()
                    + "). Apply the 'UNSAFE' keyword to force it (at your own risk)");
          }
        }
      }
    }

    try {
      schemaClass.truncate();
      invalidateCommandCache(schemaClass);
      if (deep) {
        for (OClass subclass : subclasses) {
          subclass.truncate();
          invalidateCommandCache(subclass);
        }
      }
    } catch (IOException e) {
      throw OException.wrapException(new OCommandExecutionException("Error on executing command"), e);
    }

    return recs;
  }

  private void invalidateCommandCache(OClass clazz) {
    if (clazz == null) {
      return;
    }
    OCommandCache commandCache = getDatabase().getMetadata().getCommandCache();
    if (commandCache != null && commandCache.isEnabled()) {
      int[] clusterIds = clazz.getClusterIds();
      if (clusterIds != null) {
        for (int i : clusterIds) {
          String clusterName = getDatabase().getClusterNameById(i);
          if (clusterName != null) {
            commandCache.invalidateResultsOfCluster(clusterName);
          }
        }
      }
    }
  }

  @Override public long getDistributedTimeout() {
    return getDatabase().getConfiguration().getValueAsLong(OGlobalConfiguration.DISTRIBUTED_COMMAND_TASK_SYNCH_TIMEOUT);
  }

  @Override public String getSyntax() {
    return "TRUNCATE CLASS <class-name>";
  }

  @Override public QUORUM_TYPE getQuorumType() {
    return QUORUM_TYPE.WRITE;
  }
}