/**
 * Abstracts Hibernate OGM from Neo4j.
 *
 * <p>A {@link Tuple} is saved as a {@link Node} where the columns are converted into properties of
 * the node.<br>
 * An {@link Association} is converted into a {@link Relationship} identified by the {@link
 * AssociationKey} and the {@link RowKey}. The type of the relationship is the value returned by
 * {@link AssociationKeyMetadata#getCollectionRole()}.
 *
 * <p>If the value of a property is set to null the property will be removed (Neo4j does not allow
 * to store null values).
 *
 * @author Davide D'Alto &lt;[email protected]&gt;
 */
public class EmbeddedNeo4jDialect extends BaseNeo4jDialect {

  private static final Log log = LoggerFactory.getLogger();

  private final GraphDatabaseService dataBase;

  private final EmbeddedNeo4jSequenceGenerator sequenceGenerator;

  private Map<EntityKeyMetadata, EmbeddedNeo4jEntityQueries> entityQueries;

  private Map<AssociationKeyMetadata, EmbeddedNeo4jAssociationQueries> associationQueries;

  public EmbeddedNeo4jDialect(EmbeddedNeo4jDatastoreProvider provider) {
    super(EmbeddedNeo4jTypeConverter.INSTANCE);
    this.dataBase = provider.getDatabase();
    this.sequenceGenerator = provider.getSequenceGenerator();
  }

  @Override
  public void sessionFactoryCreated(SessionFactoryImplementor sessionFactoryImplementor) {
    this.associationQueries =
        Collections.unmodifiableMap(initializeAssociationQueries(sessionFactoryImplementor));
    this.entityQueries =
        Collections.unmodifiableMap(
            initializeEntityQueries(sessionFactoryImplementor, associationQueries));
  }

  private Map<EntityKeyMetadata, EmbeddedNeo4jEntityQueries> initializeEntityQueries(
      SessionFactoryImplementor sessionFactoryImplementor,
      Map<AssociationKeyMetadata, EmbeddedNeo4jAssociationQueries> associationQueries) {
    Map<EntityKeyMetadata, EmbeddedNeo4jEntityQueries> entityQueries =
        initializeEntityQueries(sessionFactoryImplementor);
    for (AssociationKeyMetadata associationKeyMetadata : associationQueries.keySet()) {
      EntityKeyMetadata entityKeyMetadata =
          associationKeyMetadata.getAssociatedEntityKeyMetadata().getEntityKeyMetadata();
      if (!entityQueries.containsKey(entityKeyMetadata)) {
        // Embeddables metadata
        entityQueries.put(entityKeyMetadata, new EmbeddedNeo4jEntityQueries(entityKeyMetadata));
      }
    }
    return entityQueries;
  }

  private Map<EntityKeyMetadata, EmbeddedNeo4jEntityQueries> initializeEntityQueries(
      SessionFactoryImplementor sessionFactoryImplementor) {
    Map<EntityKeyMetadata, EmbeddedNeo4jEntityQueries> queryMap =
        new HashMap<EntityKeyMetadata, EmbeddedNeo4jEntityQueries>();
    Collection<EntityPersister> entityPersisters =
        sessionFactoryImplementor.getEntityPersisters().values();
    for (EntityPersister entityPersister : entityPersisters) {
      if (entityPersister instanceof OgmEntityPersister) {
        OgmEntityPersister ogmEntityPersister = (OgmEntityPersister) entityPersister;
        queryMap.put(
            ogmEntityPersister.getEntityKeyMetadata(),
            new EmbeddedNeo4jEntityQueries(ogmEntityPersister.getEntityKeyMetadata()));
      }
    }
    return queryMap;
  }

  private Map<AssociationKeyMetadata, EmbeddedNeo4jAssociationQueries> initializeAssociationQueries(
      SessionFactoryImplementor sessionFactoryImplementor) {
    Map<AssociationKeyMetadata, EmbeddedNeo4jAssociationQueries> queryMap =
        new HashMap<AssociationKeyMetadata, EmbeddedNeo4jAssociationQueries>();
    Collection<CollectionPersister> collectionPersisters =
        sessionFactoryImplementor.getCollectionPersisters().values();
    for (CollectionPersister collectionPersister : collectionPersisters) {
      if (collectionPersister instanceof OgmCollectionPersister) {
        OgmCollectionPersister ogmCollectionPersister =
            (OgmCollectionPersister) collectionPersister;
        EntityKeyMetadata ownerEntityKeyMetadata =
            ((OgmEntityPersister) (ogmCollectionPersister.getOwnerEntityPersister()))
                .getEntityKeyMetadata();
        AssociationKeyMetadata associationKeyMetadata =
            ogmCollectionPersister.getAssociationKeyMetadata();
        queryMap.put(
            associationKeyMetadata,
            new EmbeddedNeo4jAssociationQueries(ownerEntityKeyMetadata, associationKeyMetadata));
      }
    }
    return queryMap;
  }

  @Override
  public Tuple getTuple(EntityKey key, OperationContext context) {
    Node entityNode =
        entityQueries.get(key.getMetadata()).findEntity(dataBase, key.getColumnValues());
    if (entityNode == null) {
      return null;
    }

    return new Tuple(
        EmbeddedNeo4jTupleSnapshot.fromNode(
            entityNode,
            context.getTupleTypeContext().getAllAssociatedEntityKeyMetadata(),
            context.getTupleTypeContext().getAllRoles(),
            key.getMetadata()),
        SnapshotType.UPDATE);
  }

  @Override
  public List<Tuple> getTuples(EntityKey[] keys, TupleContext tupleContext) {
    if (keys.length == 0) {
      return Collections.emptyList();
    }

    // We only supports one metadata for now
    EntityKeyMetadata metadata = keys[0].getMetadata();
    // The result returned by the query might not be in the same order as the keys.
    ResourceIterator<Node> nodes = entityQueries.get(metadata).findEntities(dataBase, keys);
    try {
      return tuplesResult(keys, tupleContext, nodes);
    } finally {
      nodes.close();
    }
  }

  /*
   * This method assumes that the nodes might not be in the same order as the keys and some keys might not have a
   * matching result in the db.
   */
  private List<Tuple> tuplesResult(
      EntityKey[] keys, TupleContext tupleContext, ResourceIterator<Node> nodes) {
    // The list is initialized with null because some keys might not have a corresponding node
    Tuple[] tuples = new Tuple[keys.length];
    while (nodes.hasNext()) {
      Node node = nodes.next();
      for (int i = 0; i < keys.length; i++) {
        if (matches(node, keys[i].getColumnNames(), keys[i].getColumnValues())) {
          tuples[i] =
              new Tuple(
                  EmbeddedNeo4jTupleSnapshot.fromNode(
                      node,
                      tupleContext.getTupleTypeContext().getAllAssociatedEntityKeyMetadata(),
                      tupleContext.getTupleTypeContext().getAllRoles(),
                      keys[i].getMetadata()),
                  SnapshotType.UPDATE);
          // We assume there are no duplicated keys
          break;
        }
      }
    }
    return Arrays.asList(tuples);
  }

  private boolean matches(Node node, String[] properties, Object[] values) {
    for (int i = 0; i < properties.length; i++) {
      if (node.hasProperty(properties[i]) && !node.getProperty(properties[i]).equals(values[i])) {
        return false;
      } else if (!node.hasProperty(properties[i]) && values[i] != null) {
        return false;
      }
    }
    return true;
  }

  @Override
  public Tuple createTuple(EntityKey key, OperationContext tupleContext) {
    return new Tuple(
        EmbeddedNeo4jTupleSnapshot.emptySnapshot(key.getMetadata()), SnapshotType.INSERT);
  }

  @Override
  public void insertOrUpdateTuple(
      EntityKey key, TuplePointer tuplePointer, TupleContext tupleContext) {
    Tuple tuple = tuplePointer.getTuple();
    EmbeddedNeo4jTupleSnapshot snapshot = (EmbeddedNeo4jTupleSnapshot) tuple.getSnapshot();

    // insert
    if (snapshot.isNew()) {
      Node node = insertTuple(key, tuple);
      snapshot.setNode(node);
      applyTupleOperations(key, tuple, node, tuple.getOperations(), tupleContext);
      GraphLogger.log("Inserted node: %1$s", node);
    }
    // update
    else {
      Node node = snapshot.getNode();
      applyTupleOperations(key, tuple, node, tuple.getOperations(), tupleContext);
      GraphLogger.log("Updated node: %1$s", node);
    }
  }

  private Node insertTuple(EntityKey key, Tuple tuple) {
    try {
      return entityQueries.get(key.getMetadata()).insertEntity(dataBase, key.getColumnValues());
    } catch (QueryExecutionException qee) {
      if (CONSTRAINT_VIOLATION_CODE.equals(qee.getStatusCode())) {
        Throwable cause = findRecognizableCause(qee);
        if (cause instanceof UniquePropertyConstraintViolationKernelException) {
          throw new TupleAlreadyExistsException(key, qee);
        }
      }
      throw qee;
    }
  }

  private Throwable findRecognizableCause(QueryExecutionException qee) {
    Throwable cause = qee.getCause();
    while (cause.getCause() != null) {
      cause = cause.getCause();
    }
    return cause;
  }

  @Override
  public void removeTuple(EntityKey key, TupleContext tupleContext) {
    entityQueries.get(key.getMetadata()).removeEntity(dataBase, key.getColumnValues());
  }

  /**
   * When dealing with some scenarios like, for example, a bidirectional association, OGM calls this
   * method twice:
   *
   * <p>the first time with the information related to the owner of the association and the {@link
   * RowKey}, the second time using the same {@link RowKey} but with the {@link AssociationKey}
   * referring to the other side of the association.
   *
   * @param associatedEntityKeyMetadata
   */
  private Relationship createRelationship(
      AssociationKey associationKey,
      Tuple associationRow,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    switch (associationKey.getMetadata().getAssociationKind()) {
      case EMBEDDED_COLLECTION:
        return createRelationshipWithEmbeddedNode(
            associationKey, associationRow, associatedEntityKeyMetadata);
      case ASSOCIATION:
        return findOrCreateRelationshipWithEntityNode(
            associationKey, associationRow, associatedEntityKeyMetadata);
      default:
        throw new AssertionFailure(
            "Unrecognized associationKind: " + associationKey.getMetadata().getAssociationKind());
    }
  }

  private Relationship createRelationshipWithEmbeddedNode(
      AssociationKey associationKey,
      Tuple associationRow,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    EntityKey embeddedKey = getEntityKey(associationRow, associatedEntityKeyMetadata);
    Relationship relationship =
        associationQueries
            .get(associationKey.getMetadata())
            .createRelationshipForEmbeddedAssociation(dataBase, associationKey, embeddedKey);
    applyProperties(associationKey, associationRow, relationship);
    return relationship;
  }

  private Relationship findOrCreateRelationshipWithEntityNode(
      AssociationKey associationKey,
      Tuple associationRow,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    EntityKey targetEntityKey = getEntityKey(associationRow, associatedEntityKeyMetadata);
    Node targetNode =
        entityQueries
            .get(targetEntityKey.getMetadata())
            .findEntity(dataBase, targetEntityKey.getColumnValues());
    return createRelationshipWithTargetNode(associationKey, associationRow, targetNode);
  }

  /**
   * The only properties added to a relationship are the columns representing the index of the
   * association.
   */
  private void applyProperties(
      AssociationKey associationKey, Tuple associationRow, Relationship relationship) {
    String[] indexColumns = associationKey.getMetadata().getRowKeyIndexColumnNames();
    for (int i = 0; i < indexColumns.length; i++) {
      String propertyName = indexColumns[i];
      Object propertyValue = associationRow.get(propertyName);
      relationship.setProperty(propertyName, propertyValue);
    }
  }

  private Relationship createRelationshipWithTargetNode(
      AssociationKey associationKey, Tuple associationRow, Node targetNode) {
    EntityKey entityKey = associationKey.getEntityKey();
    Node ownerNode =
        entityQueries
            .get(entityKey.getMetadata())
            .findEntity(dataBase, entityKey.getColumnValues());
    Relationship relationship =
        ownerNode.createRelationshipTo(
            targetNode, withName(associationKey.getMetadata().getCollectionRole()));
    applyProperties(associationKey, associationRow, relationship);
    return relationship;
  }

  @Override
  public Association getAssociation(
      AssociationKey associationKey, AssociationContext associationContext) {
    EntityKey entityKey = associationKey.getEntityKey();
    Node entityNode =
        entityQueries
            .get(entityKey.getMetadata())
            .findEntity(dataBase, entityKey.getColumnValues());
    GraphLogger.log("Found owner node: %1$s", entityNode);
    if (entityNode == null) {
      return null;
    }

    Map<RowKey, Tuple> tuples = createAssociationMap(associationKey, associationContext, entityKey);
    return new Association(new EmbeddedNeo4jAssociationSnapshot(tuples));
  }

  private Map<RowKey, Tuple> createAssociationMap(
      AssociationKey associationKey, AssociationContext associationContext, EntityKey entityKey) {
    String relationshipType = associationContext.getAssociationTypeContext().getRoleOnMainSide();
    ResourceIterator<Relationship> relationships =
        entityQueries
            .get(entityKey.getMetadata())
            .findAssociation(dataBase, entityKey.getColumnValues(), relationshipType);

    Map<RowKey, Tuple> tuples = new HashMap<RowKey, Tuple>();
    try {
      while (relationships.hasNext()) {
        Relationship relationship = relationships.next();
        AssociatedEntityKeyMetadata associatedEntityKeyMetadata =
            associationContext.getAssociationTypeContext().getAssociatedEntityKeyMetadata();
        EmbeddedNeo4jTupleAssociationSnapshot snapshot =
            new EmbeddedNeo4jTupleAssociationSnapshot(
                relationship, associationKey, associatedEntityKeyMetadata);
        RowKey rowKey = convert(associationKey, snapshot);
        tuples.put(rowKey, new Tuple(snapshot, SnapshotType.UPDATE));
      }
      return tuples;
    } finally {
      relationships.close();
    }
  }

  @Override
  public void insertOrUpdateAssociation(
      AssociationKey key, Association association, AssociationContext associationContext) {
    // If this is the inverse side of a bi-directional association, we don't create a relationship
    // for this; this
    // will happen when updating the main side
    if (key.getMetadata().isInverse()) {
      return;
    }

    for (AssociationOperation action : association.getOperations()) {
      applyAssociationOperation(association, key, action, associationContext);
    }
  }

  @Override
  public void removeAssociation(AssociationKey key, AssociationContext associationContext) {
    // If this is the inverse side of a bi-directional association, we don't manage the relationship
    // from this side
    if (key.getMetadata().isInverse()) {
      return;
    }

    associationQueries.get(key.getMetadata()).removeAssociation(dataBase, key);
  }

  private void applyAssociationOperation(
      Association association,
      AssociationKey key,
      AssociationOperation operation,
      AssociationContext associationContext) {
    switch (operation.getType()) {
      case CLEAR:
        removeAssociation(key, associationContext);
        break;
      case PUT:
        putAssociationOperation(
            association,
            key,
            operation,
            associationContext.getAssociationTypeContext().getAssociatedEntityKeyMetadata());
        break;
      case REMOVE:
        removeAssociationOperation(
            association,
            key,
            operation,
            associationContext.getAssociationTypeContext().getAssociatedEntityKeyMetadata());
        break;
    }
  }

  private void putAssociationOperation(
      Association association,
      AssociationKey associationKey,
      AssociationOperation action,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    Relationship relationship =
        associationQueries
            .get(associationKey.getMetadata())
            .findRelationship(dataBase, associationKey, action.getKey());

    if (relationship != null) {
      for (String relationshipProperty : associationKey.getMetadata().getRowKeyIndexColumnNames()) {
        relationship.setProperty(relationshipProperty, action.getValue().get(relationshipProperty));
      }

      for (String column :
          associationKey
              .getMetadata()
              .getColumnsWithoutKeyColumns(action.getValue().getColumnNames())) {
        if (!isRowKeyColumn(associationKey.getMetadata(), column)) {
          relationship.getEndNode().setProperty(column, action.getValue().get(column));
        }
      }

      GraphLogger.log("Updated relationship: %1$s", relationship);
    } else {
      relationship =
          createRelationship(associationKey, action.getValue(), associatedEntityKeyMetadata);
      GraphLogger.log("Created relationship: %1$s", relationship);
    }
  }

  // TODO replace with method provided by OGM-1035
  private boolean isRowKeyColumn(AssociationKeyMetadata metadata, String column) {
    for (String rowKeyColumn : metadata.getRowKeyColumnNames()) {
      if (rowKeyColumn.equals(column)) {
        return true;
      }
    }

    return false;
  }

  private void removeAssociationOperation(
      Association association,
      AssociationKey associationKey,
      AssociationOperation action,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    associationQueries
        .get(associationKey.getMetadata())
        .removeAssociationRow(dataBase, associationKey, action.getKey());
  }

  private void applyTupleOperations(
      EntityKey entityKey,
      Tuple tuple,
      Node node,
      Set<TupleOperation> operations,
      TupleContext tupleContext) {
    Set<String> processedAssociationRoles = new HashSet<String>();

    for (TupleOperation operation : operations) {
      applyOperation(entityKey, tuple, node, operation, tupleContext, processedAssociationRoles);
    }
  }

  private void applyOperation(
      EntityKey entityKey,
      Tuple tuple,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    switch (operation.getType()) {
      case PUT:
        putTupleOperation(
            entityKey, tuple, node, operation, tupleContext, processedAssociationRoles);
        break;
      case PUT_NULL:
      case REMOVE:
        removeTupleOperation(entityKey, node, operation, tupleContext, processedAssociationRoles);
        break;
    }
  }

  private void removeTupleOperation(
      EntityKey entityKey,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    if (!tupleContext.getTupleTypeContext().isPartOfAssociation(operation.getColumn())) {
      if (isPartOfRegularEmbedded(entityKey.getColumnNames(), operation.getColumn())) {
        // Embedded node
        String[] split = split(operation.getColumn());
        removePropertyForEmbedded(node, split, 0);
      } else if (node.hasProperty(operation.getColumn())) {
        node.removeProperty(operation.getColumn());
      }
    }
    // if the column represents a to-one association, remove the relationship
    else {
      String associationRole = tupleContext.getTupleTypeContext().getRole(operation.getColumn());
      if (!processedAssociationRoles.contains(associationRole)) {

        Iterator<Relationship> relationships =
            node.getRelationships(withName(associationRole)).iterator();

        if (relationships.hasNext()) {
          relationships.next().delete();
        }
      }
    }
  }

  /*
   * It will remove a property from an embedded node if it exists.
   * After deleting the property, if the node does not have any more properties and relationships (except for an incoming one),
   * it will delete the embedded node as well.
   */
  private void removePropertyForEmbedded(Node embeddedNode, String[] embeddedColumnSplit, int i) {
    if (i == embeddedColumnSplit.length - 1) {
      // Property
      String property = embeddedColumnSplit[embeddedColumnSplit.length - 1];
      if (embeddedNode.hasProperty(property)) {
        embeddedNode.removeProperty(property);
      }
    } else {
      Iterator<Relationship> iterator =
          embeddedNode
              .getRelationships(Direction.OUTGOING, withName(embeddedColumnSplit[i]))
              .iterator();
      if (iterator.hasNext()) {
        removePropertyForEmbedded(iterator.next().getEndNode(), embeddedColumnSplit, i + 1);
      }
    }
    if (!embeddedNode.getPropertyKeys().iterator().hasNext()) {
      // Node without properties
      Iterator<Relationship> iterator = embeddedNode.getRelationships().iterator();
      if (iterator.hasNext()) {
        Relationship relationship = iterator.next();
        if (!iterator.hasNext()) {
          // Node with only one relationship and no properties,
          // we can remove it:
          // It means we have removed all the properties from the embedded node
          // and it is NOT an intermediate node like
          // (entity) --> (embedded1) --> (embedded2)
          relationship.delete();
          embeddedNode.delete();
        }
      }
    }
  }

  private void putTupleOperation(
      EntityKey entityKey,
      Tuple tuple,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    if (tupleContext.getTupleTypeContext().isPartOfAssociation(operation.getColumn())) {
      // the column represents a to-one association, map it as relationship
      putOneToOneAssociation(tuple, node, operation, tupleContext, processedAssociationRoles);
    } else if (isPartOfRegularEmbedded(
        entityKey.getMetadata().getColumnNames(), operation.getColumn())) {
      entityQueries
          .get(entityKey.getMetadata())
          .updateEmbeddedColumn(
              dataBase, entityKey.getColumnValues(), operation.getColumn(), operation.getValue());
    } else {
      putProperty(entityKey, node, operation);
    }
  }

  private void putProperty(EntityKey entityKey, Node node, TupleOperation operation) {
    try {
      node.setProperty(operation.getColumn(), operation.getValue());
    } catch (ConstraintViolationException e) {
      String message = e.getMessage();
      if (message.contains("already exists")) {
        throw log.mustNotInsertSameEntityTwice(String.valueOf(operation), e);
      } else {
        throw log.constraintViolation(entityKey, String.valueOf(operation), e);
      }
    }
  }

  private void putOneToOneAssociation(
      Tuple tuple,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    String associationRole = tupleContext.getTupleTypeContext().getRole(operation.getColumn());

    if (!processedAssociationRoles.contains(associationRole)) {
      processedAssociationRoles.add(associationRole);

      EntityKey targetKey =
          getEntityKey(
              tuple,
              tupleContext
                  .getTupleTypeContext()
                  .getAssociatedEntityKeyMetadata(operation.getColumn()));

      // delete the previous relationship if there is one; for a to-one association, the
      // relationship won't have any
      // properties, so the type is uniquely identifying it
      Iterator<Relationship> relationships =
          node.getRelationships(withName(associationRole)).iterator();
      if (relationships.hasNext()) {
        relationships.next().delete();
      }

      // create a new relationship
      Node targetNode =
          entityQueries
              .get(targetKey.getMetadata())
              .findEntity(dataBase, targetKey.getColumnValues());
      node.createRelationshipTo(targetNode, withName(associationRole));
    }
  }

  @Override
  public void forEachTuple(
      ModelConsumer consumer,
      TupleTypeContext tupleTypeContext,
      EntityKeyMetadata entityKeyMetadata) {
    ResourceIterator<Node> queryNodes = entityQueries.get(entityKeyMetadata).findEntities(dataBase);
    try {
      while (queryNodes.hasNext()) {
        Node next = queryNodes.next();
        Tuple tuple =
            new Tuple(
                EmbeddedNeo4jTupleSnapshot.fromNode(
                    next,
                    tupleTypeContext.getAllAssociatedEntityKeyMetadata(),
                    tupleTypeContext.getAllRoles(),
                    entityKeyMetadata),
                SnapshotType.UPDATE);
        consumer.consume(tuple);
      }
    } finally {
      queryNodes.close();
    }
  }

  @Override
  public Number nextValue(NextValueRequest request) {
    return sequenceGenerator.nextValue(request);
  }

  @Override
  public ClosableIterator<Tuple> executeBackendQuery(
      BackendQuery<String> backendQuery,
      QueryParameters queryParameters,
      TupleContext tupleContext) {
    Map<String, Object> parameters = getParameters(queryParameters);
    String nativeQuery = buildNativeQuery(backendQuery, queryParameters);
    try {
      Result result = dataBase.execute(nativeQuery, parameters);

      EntityMetadataInformation entityMetadataInformation =
          backendQuery.getSingleEntityMetadataInformationOrNull();
      if (entityMetadataInformation != null) {
        return new EmbeddedNeo4jNodesTupleIterator(
            result, entityMetadataInformation.getEntityKeyMetadata(), tupleContext);
      }
      return new EmbeddedNeo4jMapsTupleIterator(result);
    } catch (QueryExecutionException qe) {
      throw log.nativeQueryException(qe.getStatusCode(), qe.getMessage(), qe);
    }
  }

  @Override
  public int executeBackendUpdateQuery(
      BackendQuery<String> backendQuery,
      QueryParameters queryParameters,
      TupleContext tupleContext) {
    Map<String, Object> parameters = getParameters(queryParameters);
    String nativeQuery = buildNativeQuery(backendQuery, queryParameters);
    try {
      Result result = dataBase.execute(nativeQuery, parameters);
      return summaryUpdates(result);
    } catch (QueryExecutionException qe) {
      throw log.nativeQueryException(qe.getStatusCode(), qe.getMessage(), qe);
    }
  }

  private int summaryUpdates(Result result) {
    int updates = 0;
    updates += result.getQueryStatistics().getConstraintsAdded();
    updates += result.getQueryStatistics().getConstraintsRemoved();
    updates += result.getQueryStatistics().getNodesCreated();
    updates += result.getQueryStatistics().getNodesDeleted();
    updates += result.getQueryStatistics().getRelationshipsCreated();
    updates += result.getQueryStatistics().getRelationshipsDeleted();
    updates += result.getQueryStatistics().getLabelsAdded();
    updates += result.getQueryStatistics().getLabelsRemoved();
    updates += result.getQueryStatistics().getPropertiesSet();
    return updates;
  }

  @Override
  public String parseNativeQuery(String nativeQuery) {
    // We return given Cypher queries as they are; Currently there is no API for validating Cypher
    // queries without
    // actually executing them (see https://github.com/neo4j/neo4j/issues/2766)
    return nativeQuery;
  }
}
Example #2
0
/**
 * Abstracts Hibernate OGM from Neo4j.
 *
 * <p>A {@link Tuple} is saved as a {@link Node} where the columns are converted into properties of
 * the node.<br>
 * An {@link Association} is converted into a {@link Relationship} identified by the {@link
 * AssociationKey} and the {@link RowKey}. The type of the relationship is the value returned by
 * {@link AssociationKeyMetadata#getCollectionRole()}.
 *
 * <p>If the value of a property is set to null the property will be removed (Neo4j does not allow
 * to store null values).
 *
 * @author Davide D'Alto &lt;[email protected]&gt;
 */
public class Neo4jDialect extends BaseGridDialect
    implements MultigetGridDialect,
        QueryableGridDialect<String>,
        SessionFactoryLifecycleAwareDialect {

  public static final String CONSTRAINT_VIOLATION_CODE =
      "Neo.ClientError.Schema.ConstraintViolation";

  private static final Log log = LoggerFactory.getLogger();

  private final Neo4jSequenceGenerator neo4jSequenceGenerator;

  private final GraphDatabaseService dataBase;

  private Map<EntityKeyMetadata, Neo4jEntityQueries> entityQueries;

  private Map<AssociationKeyMetadata, Neo4jAssociationQueries> associationQueries;

  public Neo4jDialect(Neo4jDatastoreProvider provider) {
    dataBase = provider.getDataBase();
    this.neo4jSequenceGenerator = provider.getSequenceGenerator();
  }

  @Override
  public void sessionFactoryCreated(SessionFactoryImplementor sessionFactoryImplementor) {
    this.associationQueries =
        Collections.unmodifiableMap(initializeAssociationQueries(sessionFactoryImplementor));
    this.entityQueries =
        Collections.unmodifiableMap(
            initializeEntityQueries(sessionFactoryImplementor, associationQueries));
  }

  private Map<EntityKeyMetadata, Neo4jEntityQueries> initializeEntityQueries(
      SessionFactoryImplementor sessionFactoryImplementor,
      Map<AssociationKeyMetadata, Neo4jAssociationQueries> associationQueries) {
    Map<EntityKeyMetadata, Neo4jEntityQueries> entityQueries =
        initializeEntityQueries(sessionFactoryImplementor);
    for (AssociationKeyMetadata associationKeyMetadata : associationQueries.keySet()) {
      EntityKeyMetadata entityKeyMetadata =
          associationKeyMetadata.getAssociatedEntityKeyMetadata().getEntityKeyMetadata();
      if (!entityQueries.containsKey(entityKeyMetadata)) {
        // Embeddables metadata
        entityQueries.put(entityKeyMetadata, new Neo4jEntityQueries(entityKeyMetadata));
      }
    }
    return entityQueries;
  }

  private Map<EntityKeyMetadata, Neo4jEntityQueries> initializeEntityQueries(
      SessionFactoryImplementor sessionFactoryImplementor) {
    Map<EntityKeyMetadata, Neo4jEntityQueries> queryMap =
        new HashMap<EntityKeyMetadata, Neo4jEntityQueries>();
    Collection<EntityPersister> entityPersisters =
        sessionFactoryImplementor.getEntityPersisters().values();
    for (EntityPersister entityPersister : entityPersisters) {
      if (entityPersister instanceof OgmEntityPersister) {
        OgmEntityPersister ogmEntityPersister = (OgmEntityPersister) entityPersister;
        queryMap.put(
            ogmEntityPersister.getEntityKeyMetadata(),
            new Neo4jEntityQueries(ogmEntityPersister.getEntityKeyMetadata()));
      }
    }
    return queryMap;
  }

  private Map<AssociationKeyMetadata, Neo4jAssociationQueries> initializeAssociationQueries(
      SessionFactoryImplementor sessionFactoryImplementor) {
    Map<AssociationKeyMetadata, Neo4jAssociationQueries> queryMap =
        new HashMap<AssociationKeyMetadata, Neo4jAssociationQueries>();
    Collection<CollectionPersister> collectionPersisters =
        sessionFactoryImplementor.getCollectionPersisters().values();
    for (CollectionPersister collectionPersister : collectionPersisters) {
      if (collectionPersister instanceof OgmCollectionPersister) {
        OgmCollectionPersister ogmCollectionPersister =
            (OgmCollectionPersister) collectionPersister;
        EntityKeyMetadata ownerEntityKeyMetadata =
            ((OgmEntityPersister) (ogmCollectionPersister.getOwnerEntityPersister()))
                .getEntityKeyMetadata();
        AssociationKeyMetadata associationKeyMetadata =
            ogmCollectionPersister.getAssociationKeyMetadata();
        queryMap.put(
            associationKeyMetadata,
            new Neo4jAssociationQueries(ownerEntityKeyMetadata, associationKeyMetadata));
      }
    }
    return queryMap;
  }

  @Override
  public Tuple getTuple(EntityKey key, TupleContext context) {
    Node entityNode =
        entityQueries.get(key.getMetadata()).findEntity(dataBase, key.getColumnValues());
    if (entityNode == null) {
      return null;
    }

    return new Tuple(
        new Neo4jTupleSnapshot(
            entityNode,
            context.getAllAssociatedEntityKeyMetadata(),
            context.getAllRoles(),
            key.getMetadata()));
  }

  @Override
  public List<Tuple> getTuples(EntityKey[] keys, TupleContext tupleContext) {
    if (keys.length == 0) {
      return Collections.emptyList();
    }

    // We only supports one metadata for now
    EntityKeyMetadata metadata = keys[0].getMetadata();
    // The result returned by the query might not be in the same order as the keys.
    ResourceIterator<Node> nodes = entityQueries.get(metadata).findEntities(dataBase, keys);
    try {
      return tuplesResult(keys, tupleContext, nodes);
    } finally {
      nodes.close();
    }
  }

  /*
   * This method assumes that the nodes might not be in the same order as the keys and some keys might not have a
   * matching result in the db.
   */
  private List<Tuple> tuplesResult(
      EntityKey[] keys, TupleContext tupleContext, ResourceIterator<Node> nodes) {
    // The list is initialized with null because some keys might not have a corresponding node
    Tuple[] tuples = new Tuple[keys.length];
    while (nodes.hasNext()) {
      Node node = nodes.next();
      for (int i = 0; i < keys.length; i++) {
        if (matches(node, keys[i].getColumnNames(), keys[i].getColumnValues())) {
          tuples[i] =
              new Tuple(
                  new Neo4jTupleSnapshot(
                      node,
                      tupleContext.getAllAssociatedEntityKeyMetadata(),
                      tupleContext.getAllRoles(),
                      keys[i].getMetadata()));
          // We assume there are no duplicated keys
          break;
        }
      }
    }
    return Arrays.asList(tuples);
  }

  private boolean matches(Node node, String[] properties, Object[] values) {
    for (int i = 0; i < properties.length; i++) {
      if (node.hasProperty(properties[i]) && !node.getProperty(properties[i]).equals(values[i])) {
        return false;
      } else if (!node.hasProperty(properties[i]) && values[i] != null) {
        return false;
      }
    }
    return true;
  }

  @Override
  public Tuple createTuple(EntityKey key, TupleContext tupleContext) {
    return new Tuple(new Neo4jTupleSnapshot(key.getMetadata()));
  }

  @Override
  public void insertOrUpdateTuple(EntityKey key, Tuple tuple, TupleContext tupleContext) {
    Neo4jTupleSnapshot snapshot = (Neo4jTupleSnapshot) tuple.getSnapshot();

    // insert
    if (snapshot.isNew()) {
      Node node = insertTuple(key, tuple);
      snapshot.setNode(node);
      applyTupleOperations(key, tuple, node, tuple.getOperations(), tupleContext);
      GraphLogger.log("Inserted node: %1$s", node);
    }
    // update
    else {
      Node node = snapshot.getNode();
      applyTupleOperations(key, tuple, node, tuple.getOperations(), tupleContext);
      GraphLogger.log("Updated node: %1$s", node);
    }
  }

  private Node insertTuple(EntityKey key, Tuple tuple) {
    try {
      return entityQueries.get(key.getMetadata()).insertEntity(dataBase, key.getColumnValues());
    } catch (QueryExecutionException qee) {
      if (CONSTRAINT_VIOLATION_CODE.equals(qee.getStatusCode())) {
        Throwable cause = findRecognizableCause(qee);
        if (cause instanceof UniquePropertyConstraintViolationKernelException) {
          throw new TupleAlreadyExistsException(key.getMetadata(), tuple, qee);
        }
      }
      throw qee;
    }
  }

  private Throwable findRecognizableCause(QueryExecutionException qee) {
    Throwable cause = qee.getCause();
    while (cause.getCause() != null) {
      cause = cause.getCause();
    }
    return cause;
  }

  @Override
  public void removeTuple(EntityKey key, TupleContext tupleContext) {
    entityQueries.get(key.getMetadata()).removeEntity(dataBase, key.getColumnValues());
  }

  /**
   * When dealing with some scenarios like, for example, a bidirectional association, OGM calls this
   * method twice:
   *
   * <p>the first time with the information related to the owner of the association and the {@link
   * RowKey}, the second time using the same {@link RowKey} but with the {@link AssociationKey}
   * referring to the other side of the association.
   *
   * @param associatedEntityKeyMetadata
   */
  private Relationship createRelationship(
      AssociationKey associationKey,
      Tuple associationRow,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    switch (associationKey.getMetadata().getAssociationKind()) {
      case EMBEDDED_COLLECTION:
        return createRelationshipWithEmbeddedNode(
            associationKey, associationRow, associatedEntityKeyMetadata);
      case ASSOCIATION:
        return findOrCreateRelationshipWithEntityNode(
            associationKey, associationRow, associatedEntityKeyMetadata);
      default:
        throw new AssertionFailure(
            "Unrecognized associationKind: " + associationKey.getMetadata().getAssociationKind());
    }
  }

  private Relationship createRelationshipWithEmbeddedNode(
      AssociationKey associationKey,
      Tuple associationRow,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    EntityKey embeddedKey = getEntityKey(associationRow, associatedEntityKeyMetadata);
    Relationship relationship =
        associationQueries
            .get(associationKey.getMetadata())
            .createRelationshipForEmbeddedAssociation(dataBase, associationKey, embeddedKey);
    applyProperties(associationKey, associationRow, relationship);
    return relationship;
  }

  private Relationship findOrCreateRelationshipWithEntityNode(
      AssociationKey associationKey,
      Tuple associationRow,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    EntityKey targetEntityKey = getEntityKey(associationRow, associatedEntityKeyMetadata);
    Node targetNode =
        entityQueries
            .get(targetEntityKey.getMetadata())
            .findEntity(dataBase, targetEntityKey.getColumnValues());
    return createRelationshipWithTargetNode(associationKey, associationRow, targetNode);
  }

  /**
   * The only properties added to a relationship are the columns representing the index of the
   * association.
   */
  private void applyProperties(
      AssociationKey associationKey, Tuple associationRow, Relationship relationship) {
    String[] indexColumns = associationKey.getMetadata().getRowKeyIndexColumnNames();
    for (int i = 0; i < indexColumns.length; i++) {
      String propertyName = indexColumns[i];
      Object propertyValue = associationRow.get(propertyName);
      relationship.setProperty(propertyName, propertyValue);
    }
  }

  private Relationship createRelationshipWithTargetNode(
      AssociationKey associationKey, Tuple associationRow, Node targetNode) {
    EntityKey entityKey = associationKey.getEntityKey();
    Node ownerNode =
        entityQueries
            .get(entityKey.getMetadata())
            .findEntity(dataBase, entityKey.getColumnValues());
    Relationship relationship =
        ownerNode.createRelationshipTo(
            targetNode, withName(associationKey.getMetadata().getCollectionRole()));
    applyProperties(associationKey, associationRow, relationship);
    return relationship;
  }

  @Override
  public Association getAssociation(
      AssociationKey associationKey, AssociationContext associationContext) {
    EntityKey entityKey = associationKey.getEntityKey();
    Node entityNode =
        entityQueries
            .get(entityKey.getMetadata())
            .findEntity(dataBase, entityKey.getColumnValues());
    GraphLogger.log("Found owner node: %1$s", entityNode);
    if (entityNode == null) {
      return null;
    }

    Map<RowKey, Tuple> tuples = createAssociationMap(associationKey, associationContext, entityKey);
    return new Association(new Neo4jAssociationSnapshot(tuples));
  }

  private Map<RowKey, Tuple> createAssociationMap(
      AssociationKey associationKey, AssociationContext associationContext, EntityKey entityKey) {
    String relationshipType = associationContext.getAssociationTypeContext().getRoleOnMainSide();
    ResourceIterator<Relationship> relationships =
        entityQueries
            .get(entityKey.getMetadata())
            .findAssociation(dataBase, entityKey.getColumnValues(), relationshipType);

    Map<RowKey, Tuple> tuples = new HashMap<RowKey, Tuple>();
    try {
      while (relationships.hasNext()) {
        Relationship relationship = relationships.next();
        AssociatedEntityKeyMetadata associatedEntityKeyMetadata =
            associationContext.getAssociationTypeContext().getAssociatedEntityKeyMetadata();
        Neo4jTupleAssociationSnapshot snapshot =
            new Neo4jTupleAssociationSnapshot(
                relationship, associationKey, associatedEntityKeyMetadata);
        RowKey rowKey = convert(associationKey, snapshot);
        tuples.put(rowKey, new Tuple(snapshot));
      }
      return tuples;
    } finally {
      relationships.close();
    }
  }

  private RowKey convert(AssociationKey associationKey, Neo4jTupleAssociationSnapshot snapshot) {
    String[] columnNames = associationKey.getMetadata().getRowKeyColumnNames();
    Object[] values = new Object[columnNames.length];

    for (int i = 0; i < columnNames.length; i++) {
      values[i] = snapshot.get(columnNames[i]);
    }

    return new RowKey(columnNames, values);
  }

  @Override
  public Association createAssociation(
      AssociationKey associationKey, AssociationContext associationContext) {
    return new Association();
  }

  @Override
  public void insertOrUpdateAssociation(
      AssociationKey key, Association association, AssociationContext associationContext) {
    // If this is the inverse side of a bi-directional association, we don't create a relationship
    // for this; this
    // will happen when updating the main side
    if (key.getMetadata().isInverse()) {
      return;
    }

    for (AssociationOperation action : association.getOperations()) {
      applyAssociationOperation(association, key, action, associationContext);
    }
  }

  @Override
  public boolean isStoredInEntityStructure(
      AssociationKeyMetadata associationKeyMetadata,
      AssociationTypeContext associationTypeContext) {
    return false;
  }

  @Override
  public Number nextValue(NextValueRequest request) {
    return neo4jSequenceGenerator.nextValue(
        request.getKey(), request.getIncrement(), request.getInitialValue());
  }

  @Override
  public boolean supportsSequences() {
    return true;
  }

  @Override
  public GridType overrideType(Type type) {
    return Neo4jTypeConverter.INSTANCE.convert(type);
  }

  @Override
  public void removeAssociation(AssociationKey key, AssociationContext associationContext) {
    // If this is the inverse side of a bi-directional association, we don't manage the relationship
    // from this side
    if (key.getMetadata().isInverse()) {
      return;
    }

    associationQueries.get(key.getMetadata()).removeAssociation(dataBase, key);
  }

  private void applyAssociationOperation(
      Association association,
      AssociationKey key,
      AssociationOperation operation,
      AssociationContext associationContext) {
    switch (operation.getType()) {
      case CLEAR:
        removeAssociation(key, associationContext);
        break;
      case PUT:
        putAssociationOperation(
            association,
            key,
            operation,
            associationContext.getAssociationTypeContext().getAssociatedEntityKeyMetadata());
        break;
      case REMOVE:
        removeAssociationOperation(
            association,
            key,
            operation,
            associationContext.getAssociationTypeContext().getAssociatedEntityKeyMetadata());
        break;
    }
  }

  private void putAssociationOperation(
      Association association,
      AssociationKey associationKey,
      AssociationOperation action,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    Relationship relationship =
        associationQueries
            .get(associationKey.getMetadata())
            .findRelationship(dataBase, associationKey, action.getKey());

    if (relationship != null) {
      for (String relationshipProperty : associationKey.getMetadata().getRowKeyIndexColumnNames()) {
        relationship.setProperty(relationshipProperty, action.getValue().get(relationshipProperty));
      }
      GraphLogger.log("Updated relationship: %1$s", relationship);
    } else {
      relationship =
          createRelationship(associationKey, action.getValue(), associatedEntityKeyMetadata);
      GraphLogger.log("Created relationship: %1$s", relationship);
    }
  }

  private void removeAssociationOperation(
      Association association,
      AssociationKey associationKey,
      AssociationOperation action,
      AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    associationQueries
        .get(associationKey.getMetadata())
        .removeAssociationRow(dataBase, associationKey, action.getKey());
  }

  private void applyTupleOperations(
      EntityKey entityKey,
      Tuple tuple,
      Node node,
      Set<TupleOperation> operations,
      TupleContext tupleContext) {
    Set<String> processedAssociationRoles = new HashSet<String>();

    for (TupleOperation operation : operations) {
      applyOperation(entityKey, tuple, node, operation, tupleContext, processedAssociationRoles);
    }
  }

  private void applyOperation(
      EntityKey entityKey,
      Tuple tuple,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    switch (operation.getType()) {
      case PUT:
        putTupleOperation(
            entityKey, tuple, node, operation, tupleContext, processedAssociationRoles);
        break;
      case PUT_NULL:
      case REMOVE:
        removeTupleOperation(entityKey, node, operation, tupleContext, processedAssociationRoles);
        break;
    }
  }

  private void removeTupleOperation(
      EntityKey entityKey,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    if (!tupleContext.isPartOfAssociation(operation.getColumn())) {
      if (isPartOfRegularEmbedded(entityKey.getColumnNames(), operation.getColumn())) {
        // Embedded node
        String[] split = split(operation.getColumn());
        removePropertyForEmbedded(node, split, 0);
      } else if (node.hasProperty(operation.getColumn())) {
        node.removeProperty(operation.getColumn());
      }
    }
    // if the column represents a to-one association, remove the relationship
    else {
      String associationRole = tupleContext.getRole(operation.getColumn());
      if (!processedAssociationRoles.contains(associationRole)) {

        Iterator<Relationship> relationships =
            node.getRelationships(withName(associationRole)).iterator();

        if (relationships.hasNext()) {
          relationships.next().delete();
        }
      }
    }
  }

  /*
   * It will remove a property from an embedded node if it exists.
   * After deleting the property, if the node does not have any more properties and relationships (except for an incoming one),
   * it will delete the embedded node as well.
   */
  private void removePropertyForEmbedded(Node embeddedNode, String[] embeddedColumnSplit, int i) {
    if (i == embeddedColumnSplit.length - 1) {
      // Property
      String property = embeddedColumnSplit[embeddedColumnSplit.length - 1];
      if (embeddedNode.hasProperty(property)) {
        embeddedNode.removeProperty(property);
      }
    } else {
      Iterator<Relationship> iterator =
          embeddedNode
              .getRelationships(Direction.OUTGOING, withName(embeddedColumnSplit[i]))
              .iterator();
      if (iterator.hasNext()) {
        removePropertyForEmbedded(iterator.next().getEndNode(), embeddedColumnSplit, i + 1);
      }
    }
    if (!embeddedNode.getPropertyKeys().iterator().hasNext()) {
      // Node without properties
      Iterator<Relationship> iterator = embeddedNode.getRelationships().iterator();
      if (iterator.hasNext()) {
        Relationship relationship = iterator.next();
        if (!iterator.hasNext()) {
          // Node with only one relationship and no properties,
          // we can remove it:
          // It means we have removed all the properties from the embedded node
          // and it is NOT an intermediate node like
          // (entity) --> (embedded1) --> (embedded2)
          relationship.delete();
          embeddedNode.delete();
        }
      }
    }
  }

  private void putTupleOperation(
      EntityKey entityKey,
      Tuple tuple,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    if (tupleContext.isPartOfAssociation(operation.getColumn())) {
      // the column represents a to-one association, map it as relationship
      putOneToOneAssociation(tuple, node, operation, tupleContext, processedAssociationRoles);
    } else if (isPartOfRegularEmbedded(
        entityKey.getMetadata().getColumnNames(), operation.getColumn())) {
      entityQueries
          .get(entityKey.getMetadata())
          .updateEmbeddedColumn(
              dataBase, entityKey.getColumnValues(), operation.getColumn(), operation.getValue());
    } else {
      putProperty(entityKey, node, operation);
    }
  }

  /**
   * A regular embedded is an element that it is embedded but it is not a key or a collection.
   *
   * @param keyColumnNames the column names representing the identifier of the entity
   * @param column the column we want to check
   * @return {@code true} if the column represent an attribute of a regular embedded element, {@code
   *     false} otherwise
   */
  public static boolean isPartOfRegularEmbedded(String[] keyColumnNames, String column) {
    return isPartOfEmbedded(column) && !ArrayHelper.contains(keyColumnNames, column);
  }

  private void putProperty(EntityKey entityKey, Node node, TupleOperation operation) {
    try {
      node.setProperty(operation.getColumn(), operation.getValue());
    } catch (ConstraintViolationException e) {
      throw log.constraintViolation(entityKey, operation, e);
    }
  }

  private void putOneToOneAssociation(
      Tuple tuple,
      Node node,
      TupleOperation operation,
      TupleContext tupleContext,
      Set<String> processedAssociationRoles) {
    String associationRole = tupleContext.getRole(operation.getColumn());

    if (!processedAssociationRoles.contains(associationRole)) {
      processedAssociationRoles.add(associationRole);

      EntityKey targetKey =
          getEntityKey(tuple, tupleContext.getAssociatedEntityKeyMetadata(operation.getColumn()));

      // delete the previous relationship if there is one; for a to-one association, the
      // relationship won't have any
      // properties, so the type is uniquely identifying it
      Iterator<Relationship> relationships =
          node.getRelationships(withName(associationRole)).iterator();
      if (relationships.hasNext()) {
        relationships.next().delete();
      }

      // create a new relationship
      Node targetNode =
          entityQueries
              .get(targetKey.getMetadata())
              .findEntity(dataBase, targetKey.getColumnValues());
      node.createRelationshipTo(targetNode, withName(associationRole));
    }
  }

  @Override
  public void forEachTuple(ModelConsumer consumer, EntityKeyMetadata... entityKeyMetadatas) {
    for (EntityKeyMetadata entityKeyMetadata : entityKeyMetadatas) {
      ResourceIterator<Node> queryNodes =
          entityQueries.get(entityKeyMetadata).findEntities(dataBase);
      try {
        while (queryNodes.hasNext()) {
          Node next = queryNodes.next();
          Tuple tuple = new Tuple(new Neo4jTupleSnapshot(next, entityKeyMetadata));
          consumer.consume(tuple);
        }
      } finally {
        queryNodes.close();
      }
    }
  }

  @Override
  public ClosableIterator<Tuple> executeBackendQuery(
      BackendQuery<String> backendQuery, QueryParameters queryParameters) {
    Map<String, Object> parameters = getNamedParameterValuesConvertedByGridType(queryParameters);
    String nativeQuery = buildNativeQuery(backendQuery, queryParameters);
    Result result = dataBase.execute(nativeQuery, parameters);

    if (backendQuery.getSingleEntityKeyMetadataOrNull() != null) {
      return new NodesTupleIterator(result, backendQuery.getSingleEntityKeyMetadataOrNull());
    }
    return new MapsTupleIterator(result);
  }

  @Override
  public int executeBackendUpdateQuery(
      BackendQuery<String> query, QueryParameters queryParameters) {
    // TODO implement.
    // org.hibernate.ogm.datastore.mongodb.MongoDBDialect.executeBackendUpdateQuery(BackendQuery<MongoDBQueryDescriptor>, QueryParameters) might be helpful as a reference.
    throw new UnsupportedOperationException("Not yet implemented.");
  }

  @Override
  public String parseNativeQuery(String nativeQuery) {
    // We return given Cypher queries as they are; Currently there is no API for validating Cypher
    // queries without
    // actually executing them (see https://github.com/neo4j/neo4j/issues/2766)
    return nativeQuery;
  }

  private String buildNativeQuery(
      BackendQuery<String> customQuery, QueryParameters queryParameters) {
    StringBuilder nativeQuery = new StringBuilder(customQuery.getQuery());
    applyFirstRow(queryParameters, nativeQuery);
    applyMaxRows(queryParameters, nativeQuery);
    return nativeQuery.toString();
  }

  private void applyFirstRow(QueryParameters queryParameters, StringBuilder nativeQuery) {
    Integer firstRow = queryParameters.getRowSelection().getFirstRow();
    if (firstRow != null) {
      skip(nativeQuery, firstRow);
    }
  }

  private void applyMaxRows(QueryParameters queryParameters, StringBuilder nativeQuery) {
    Integer maxRows = queryParameters.getRowSelection().getMaxRows();
    if (maxRows != null) {
      limit(nativeQuery, maxRows);
    }
  }

  /**
   * Returns a map with the named parameter values from the given parameters object, converted by
   * the {@link GridType} corresponding to each parameter type.
   */
  private Map<String, Object> getNamedParameterValuesConvertedByGridType(
      QueryParameters queryParameters) {
    Map<String, Object> parameterValues =
        new HashMap<String, Object>(queryParameters.getNamedParameters().size());
    Tuple dummy = new Tuple();

    for (Entry<String, TypedGridValue> parameter :
        queryParameters.getNamedParameters().entrySet()) {
      parameter
          .getValue()
          .getType()
          .nullSafeSet(
              dummy, parameter.getValue().getValue(), new String[] {parameter.getKey()}, null);
      parameterValues.put(parameter.getKey(), dummy.get(parameter.getKey()));
    }

    return parameterValues;
  }

  @Override
  public ParameterMetadataBuilder getParameterMetadataBuilder() {
    return new Neo4jParameterMetadataBuilder();
  }

  /**
   * Returns the key of the entity targeted by the represented association, retrieved from the given
   * tuple.
   *
   * @param tuple the tuple from which to retrieve the referenced entity key
   * @return the key of the entity targeted by the represented association
   */
  private EntityKey getEntityKey(
      Tuple tuple, AssociatedEntityKeyMetadata associatedEntityKeyMetadata) {
    Object[] columnValues =
        new Object[associatedEntityKeyMetadata.getAssociationKeyColumns().length];
    int i = 0;

    for (String associationKeyColumn : associatedEntityKeyMetadata.getAssociationKeyColumns()) {
      columnValues[i] = tuple.get(associationKeyColumn);
      i++;
    }

    return new EntityKey(associatedEntityKeyMetadata.getEntityKeyMetadata(), columnValues);
  }

  @Override
  public DuplicateInsertPreventionStrategy getDuplicateInsertPreventionStrategy(
      EntityKeyMetadata entityKeyMetadata) {
    // Only for non-composite keys (= one column) Neo4j supports unique key constraints; Hence an
    // explicit look-up
    // is required to detect duplicate insertions when using composite keys
    return entityKeyMetadata.getColumnNames().length == 1
        ? DuplicateInsertPreventionStrategy.NATIVE
        : DuplicateInsertPreventionStrategy.LOOK_UP;
  }
}