Skip to content
Browse files

Added the possibility to create nodes from an iterable of maps

  • Loading branch information...
1 parent 5040b69 commit 62f9ce87ac46f67a89128e840c21b6149f1abb18 @systay committed May 3, 2012
View
5 cypher/CHANGES.txt
@@ -1,3 +1,8 @@
+1.8.M02 (not yet released)
+--------------------
+o Added the possibility to create nodes from an iterable of maps
+o Fixes issue #479
+
1.8.M01 (2012-04-27)
--------------------
o Added the possibility to return all graph elements using RETURN *
View
22 cypher/src/docs/dev/ql/create/index.txt
@@ -33,4 +33,26 @@ component=neo4j-cypher
source=org/neo4j/cypher/javacompat/JavaExecutionEngineTests.java
tag=create_node_from_map
classifier=test-sources
+----
+
+[[create-create-multiple-nodes-from-map]]
+== Create multiple nodes from maps ==
+By providing an iterable of maps (Iterable<Map<String,Object>>), Cypher will create a node for each map
+in the iterable. When you do this, you can't create anything else in the same create statement.
+
+_Query_
+
+[source,cypher]
+----
+create node {props}
+----
+
+This query can be used in the following fashion:
+
+[snippet,java]
+----
+component=neo4j-cypher
+source=org/neo4j/cypher/javacompat/JavaExecutionEngineTests.java
+tag=create_multiple_nodes_from_map
+classifier=test-sources
----
View
26 cypher/src/main/scala/org/neo4j/cypher/internal/commands/InIterable.scala
@@ -22,15 +22,16 @@ package org.neo4j.cypher.internal.commands
import collection.Seq
import org.neo4j.cypher.internal.symbols.{Identifier, AnyIterableType}
import collection.Map
+import java.lang.{Iterable => JavaIterable}
+import java.util.{Map => JavaMap}
-abstract class InIterable(expression: Expression, symbol: String, closure: Predicate) extends Predicate {
+import collection.JavaConverters._
+
+abstract class InIterable(expression: Expression, symbol: String, closure: Predicate) extends Predicate with IterableSupport {
def seqMethod[U](f: Seq[U]): ((U) => Boolean) => Boolean
def isMatch(m: Map[String, Any]): Boolean = {
- val seq = expression(m) match {
- case x:Seq[_] => x
- case x:Array[_] => x.toSeq
- }
+ val seq = makeTraversable(expression(m)).toSeq
seqMethod(seq)(item => {
val innerMap = m ++ Map(symbol -> item)
@@ -70,3 +71,18 @@ case class SingleInIterable(iterable: Expression, symbolName: String, inner: Pre
def name = "single"
def rewrite(f: (Expression) => Expression) = SingleInIterable(iterable.rewrite(f), symbolName, inner.rewrite(f))
}
+
+trait IterableSupport {
+ def makeTraversable(z:Any):Traversable[Any] = z match {
+ case x:Seq[_] => x
+ case x:Map[_,_] => Stream(x)
+ case x:JavaMap[_,_] => Stream(x.asScala)
+ case x:Iterable[_] => x.toStream
+ case x:JavaIterable[_] => x.asScala.map {
+ case y:JavaMap[_,_] => y.asScala
+ case y => y
+ }
+ case x:Array[_] => x.toStream
+ case x => Stream(x)
+ }
+}
View
74 cypher/src/main/scala/org/neo4j/cypher/internal/mutation/UpdateAction.scala
@@ -23,20 +23,19 @@ import org.neo4j.cypher.CypherTypeException
import org.neo4j.cypher.internal.symbols._
import org.neo4j.graphdb.{RelationshipType => KernelRelType, _}
import org.neo4j.cypher.internal.pipes.{QueryState, ExecutionContext}
-import org.neo4j.cypher.internal.commands.{Property, Expression}
import java.util.{Map => JavaMap}
import scala.collection.JavaConverters._
-
+import collection.Map
+import org.neo4j.cypher.internal.commands.{Literal, IterableSupport, Property, Expression}
abstract class UpdateAction {
- def exec(context: ExecutionContext, state: QueryState)
+ def exec(context: ExecutionContext, state: QueryState): Traversable[ExecutionContext]
def dependencies: Seq[Identifier]
def influenceSymbolTable(symbols: SymbolTable): SymbolTable
-
def makeValueNeoSafe(a: Any): Any = if (a.isInstanceOf[Traversable[_]]) {
transformTraversableToArray(a)
} else {
@@ -64,11 +63,9 @@ abstract class UpdateAction {
} catch {
case e: ClassCastException => throw new CypherTypeException("Collections containing mixed types can not be stored in properties.", e)
}
-
}
}
-
trait GraphElementPropertyFunctions extends UpdateAction {
def setProps(pc: PropertyContainer, props: Map[String, Expression], context: ExecutionContext, state: QueryState) {
props.foreach {
@@ -93,26 +90,38 @@ trait GraphElementPropertyFunctions extends UpdateAction {
state.propertySet.increase()
}
}
-
}
private def setSingleValue(expression: Expression, context: ExecutionContext, pc: PropertyContainer, key: String, state: QueryState) {
val value = makeValueNeoSafe(expression(context))
pc.setProperty(key, value)
state.propertySet.increase()
-
}
-
}
case class CreateNodeAction(key: String, props: Map[String, Expression], db: GraphDatabaseService)
- extends UpdateAction with GraphElementPropertyFunctions {
- def exec(context: ExecutionContext, state: QueryState) {
- val node = db.createNode()
- state.createdNodes.increase()
- setProps(node, props, context, state)
- context.put(key, node)
- }
+ extends UpdateAction
+ with GraphElementPropertyFunctions
+ with IterableSupport {
+ def exec(context: ExecutionContext, state: QueryState) =
+ if (props.size == 1 && props.head._1 == "*") {
+ makeTraversable(props.head._2(context)).map(x => {
+ val m: Map[String, Expression] = x.asInstanceOf[Map[String, Any]].map {
+ case (k, v) => (k -> Literal(v))
+ }
+ val node = db.createNode()
+ state.createdNodes.increase()
+ setProps(node, m, context, state)
+ context.copy(m = context.m ++ Map(key -> node))
+ })
+ } else {
+ val node = db.createNode()
+ state.createdNodes.increase()
+ setProps(node, props, context, state)
+ context.put(key, node)
+
+ Stream(context)
+ }
def dependencies = propDependencies(props)
@@ -131,6 +140,7 @@ case class CreateRelationshipAction(key: String, from: Expression, to: Expressio
state.createdRelationships.increase()
setProps(relationship, props, context, state)
context.put(key, relationship)
+ Stream(context)
}
def dependencies = from.dependencies(NodeType()) ++ to.dependencies(NodeType()) ++ propDependencies(props)
@@ -142,7 +152,7 @@ case class CreateRelationshipAction(key: String, from: Expression, to: Expressio
case class DeleteEntityAction(elementToDelete: Expression)
extends UpdateAction {
- def exec(context: ExecutionContext, state: QueryState) {
+ def exec(context: ExecutionContext, state: QueryState) = {
elementToDelete(context) match {
case n: Node => {
state.deletedNodes.increase()
@@ -154,6 +164,8 @@ case class DeleteEntityAction(elementToDelete: Expression)
}
case x => throw new CypherTypeException("Expression `" + elementToDelete.toString() + "` yielded `" + x.toString + "`. Don't know how to delete that.")
}
+
+ Stream(context)
}
def dependencies = elementToDelete.dependencies(MapType())
@@ -164,12 +176,14 @@ case class DeleteEntityAction(elementToDelete: Expression)
case class DeletePropertyAction(element: Expression, property: String)
extends UpdateAction {
- def exec(context: ExecutionContext, state: QueryState) {
+ def exec(context: ExecutionContext, state: QueryState) = {
val entity = element(context).asInstanceOf[PropertyContainer]
if (entity.hasProperty(property)) {
entity.removeProperty(property)
state.propertySet.increase()
}
+
+ Stream(context)
}
def dependencies = element.dependencies(MapType())
@@ -183,7 +197,7 @@ case class PropertySetAction(prop: Property, e: Expression)
def dependencies = e.dependencies(AnyType())
- def exec(context: ExecutionContext, state: QueryState) {
+ def exec(context: ExecutionContext, state: QueryState) = {
val value = makeValueNeoSafe(e(context))
val entity = context(entityKey).asInstanceOf[PropertyContainer]
@@ -193,38 +207,34 @@ case class PropertySetAction(prop: Property, e: Expression)
}
state.propertySet.increase()
+
+ Stream(context)
}
def influenceSymbolTable(symbols: SymbolTable) = symbols
}
case class ForeachAction(iterable: Expression, symbol: String, actions: Seq[UpdateAction])
- extends UpdateAction {
+ extends UpdateAction
+ with IterableSupport {
def dependencies = iterable.dependencies(AnyIterableType()) ++ actions.flatMap(_.dependencies).filterNot(_.name == symbol)
- private def getSeq(context: ExecutionContext): Seq[Any] = {
- iterable(context) match {
- case x: Seq[_] => x
- case x: Array[_] => x.toSeq
- case x => Seq(x)
- }
- }
-
- def exec(context: ExecutionContext, state: QueryState) {
+ def exec(context: ExecutionContext, state: QueryState) = {
val before = context.get(symbol)
- val seq = getSeq(context)
+ val seq = makeTraversable(iterable(context))
seq.foreach(element => {
context.put(symbol, element)
- actions.foreach( action => action.exec(context, state) )
+ actions.foreach(action => action.exec(context, state))
})
before match {
case None => context.remove(symbol)
case Some(old) => context.put(symbol, old)
}
- }
+ Stream(context)
+ }
def influenceSymbolTable(symbols: SymbolTable) = symbols
}
View
27 cypher/src/main/scala/org/neo4j/cypher/internal/pipes/ExecuteUpdateCommandsPipe.scala
@@ -19,10 +19,10 @@
*/
package org.neo4j.cypher.internal.pipes
-import org.neo4j.cypher.InternalException
import collection.mutable.{HashSet => MutableHashSet}
import org.neo4j.cypher.internal.mutation.{DeleteEntityAction, UpdateAction}
import org.neo4j.graphdb.{Relationship, Node, GraphDatabaseService, NotInTransactionException}
+import org.neo4j.cypher.{ParameterWrongTypeException, InternalException}
class ExecuteUpdateCommandsPipe(source: Pipe, db: GraphDatabaseService, commands: Seq[UpdateAction]) extends PipeWithSource(source) {
@@ -31,32 +31,38 @@ class ExecuteUpdateCommandsPipe(source: Pipe, db: GraphDatabaseService, commands
val deletedNodes = MutableHashSet[Long]()
val deletedRelationships = MutableHashSet[Long]()
- source.createResults(state).map {
- case ctx => {
- executeMutationCommands(ctx, state, deletedNodes, deletedRelationships)
- ctx
+ if (commands.size == 1) {
+ source.createResults(state).flatMap {
+ case ctx => executeMutationCommands(ctx, state, deletedNodes, deletedRelationships).flatten
+ }
+ } else {
+ source.createResults(state).flatMap {
+ case ctx => {
+ val commands = executeMutationCommands(ctx, state, deletedNodes, deletedRelationships)
+ commands.foreach(x => if (x.size > 1) throw new ParameterWrongTypeException("If you create multiple elements, you can only create one of each."))
+ commands.last
+ }
}
}
}
// TODO: Make it better
- private def executeMutationCommands(ctx: ExecutionContext, state: QueryState, deletedNodes: MutableHashSet[Long], deletedRelationships: MutableHashSet[Long]) {
-
+ private def executeMutationCommands(ctx: ExecutionContext, state: QueryState, deletedNodes: MutableHashSet[Long], deletedRelationships: MutableHashSet[Long]): Traversable[Traversable[ExecutionContext]] =
try {
- commands.foreach {
+ commands.map {
case cmd@DeleteEntityAction(expression) => {
expression(ctx) match {
case n: Node => {
if (!deletedNodes.contains(n.getId)) {
deletedNodes.add(n.getId)
cmd.exec(ctx, state)
- }
+ } else Stream()
}
case r: Relationship => {
if (!deletedRelationships.contains(r.getId)) {
deletedRelationships.add(r.getId)
cmd.exec(ctx, state)
- }
+ } else Stream()
}
case _ => cmd.exec(ctx, state)
}
@@ -66,7 +72,6 @@ class ExecuteUpdateCommandsPipe(source: Pipe, db: GraphDatabaseService, commands
} catch {
case e: NotInTransactionException => throw new InternalException("Expected to be in a transaction at this point", e)
}
- }
def executionPlan() = source.executionPlan() + "\nUpdateGraph(" + commands.mkString + ")"
View
55 cypher/src/test/java/org/neo4j/cypher/javacompat/JavaExecutionEngineTests.java
@@ -19,26 +19,6 @@
*/
package org.neo4j.cypher.javacompat;
-import static java.util.Arrays.asList;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import static org.junit.matchers.JUnitMatchers.containsString;
-import static org.junit.matchers.JUnitMatchers.hasItem;
-import static org.junit.matchers.JUnitMatchers.hasItems;
-import static org.neo4j.cypher.javacompat.RegularExpressionMatcher.matchesPattern;
-import static org.neo4j.helpers.collection.IteratorUtil.asIterable;
-import static org.neo4j.helpers.collection.IteratorUtil.count;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -49,6 +29,19 @@
import org.neo4j.helpers.collection.IteratorUtil;
import org.neo4j.test.TestGraphDatabaseFactory;
+import java.io.IOException;
+import java.util.*;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.matchers.JUnitMatchers.*;
+import static org.neo4j.cypher.javacompat.RegularExpressionMatcher.matchesPattern;
+import static org.neo4j.helpers.collection.IteratorUtil.asIterable;
+import static org.neo4j.helpers.collection.IteratorUtil.count;
+
public class JavaExecutionEngineTests
{
@@ -278,6 +271,28 @@ public void create_node_from_map() throws Exception
assertThat( count( result ), is( 1 ) );
}
+ @Test
+ public void create_multiple_nodes_from_map() throws Exception
+ {
+ // START SNIPPET: create_multiple_nodes_from_map
+ Map<String, Object> n1 = new HashMap<String, Object>();
+ n1.put( "name", "Andres" );
+ n1.put( "position", "Developer" );
+
+ Map<String, Object> n2 = new HashMap<String, Object>();
+ n2.put( "name", "Michael" );
+ n2.put( "position", "Developer" );
+
+ Map<String, Object> params = new HashMap<String, Object>();
+ List<Map<String, Object>> maps = Arrays.asList(n1, n2);
+ params.put( "props", maps);
+ engine.execute("create n = {props} return n", params);
+ // END SNIPPET: create_multiple_nodes_from_map
+
+ ExecutionResult result = engine.execute( "start n=node(*) where n.name in ['Andres', 'Michael'] and n.position = 'Developer' return n" );
+ assertThat( count( result ), is( 2 ) );
+ }
+
private void makeFriends( Node a, Node b )
{
View
39 cypher/src/test/scala/org/neo4j/cypher/MutatingIntegrationTests.scala
@@ -297,4 +297,43 @@ foreach(n in nodes(p) :
assert(result === List(Map("n" -> a)))
}
+ @Test
+ def create_multiple_nodes() {
+ val maps = List(
+ Map("name" -> "Andres", "prefers" -> "Scala"),
+ Map("name" -> "Michael", "prefers" -> "Java"),
+ Map("name" -> "Peter", "prefers" -> "Java"))
+
+ val statistics = parseAndExecute("create n = {params}", "params" -> maps).queryStatistics()
+
+ assert(statistics === stats.copy(
+ nodesCreated = 3,
+ propertiesSet = 6
+ ))
+ }
+
+ @Test
+ def create_multiple_nodes_and_return() {
+ val maps = List(
+ Map("name" -> "Andres"),
+ Map("name" -> "Michael"),
+ Map("name" -> "Peter"))
+
+ val result = parseAndExecute("create n = {params} return n", "params" -> maps).toList
+ assert(result.size === 3)
+ }
+
+ @Test
+ def fail_to_create_from_two_iterables() {
+ val maps1 = List(
+ Map("name" -> "Andres"),
+ Map("name" -> "Michael"),
+ Map("name" -> "Peter"))
+ val maps2 = List(
+ Map("name" -> "Andres"),
+ Map("name" -> "Michael"),
+ Map("name" -> "Peter"))
+
+ intercept[ParameterWrongTypeException](parseAndExecute("create a = {params1}, b = {params2}", "params1" -> maps1, "params2" -> maps2))
+ }
}

0 comments on commit 62f9ce8

Please sign in to comment.
Something went wrong with that request. Please try again.