From 5de22cf30e6ce3a62800c6723501571a606c8179 Mon Sep 17 00:00:00 2001 From: Andres Taylor Date: Wed, 30 Nov 2011 08:28:20 +0100 Subject: [PATCH] Column aliasing is now supported --- cypher/src/docs/dev/ql/return/index.txt | 1 + .../org/neo4j/cypher/commands/Query.scala | 14 ++++---- .../neo4j/cypher/commands/ReturnItem.scala | 26 ++++++++++++-- .../neo4j/cypher/parser/ReturnClause.scala | 35 +++++++++++++------ .../javacompat/JavaExecutionEngineTests.java | 2 +- .../org/neo4j/cypher/CypherParserTest.scala | 13 +++++-- .../neo4j/cypher/ExecutionEngineTest.scala | 22 ++++++++++++ .../org/neo4j/cypher/docgen/ReturnTest.scala | 9 +++++ 8 files changed, 98 insertions(+), 24 deletions(-) diff --git a/cypher/src/docs/dev/ql/return/index.txt b/cypher/src/docs/dev/ql/return/index.txt index 78152347e..c18725380 100644 --- a/cypher/src/docs/dev/ql/return/index.txt +++ b/cypher/src/docs/dev/ql/return/index.txt @@ -11,6 +11,7 @@ include::return-nodes.txt[] include::return-relationships.txt[] include::return-property.txt[] include::identifier-with-uncommon-characters.txt[] +include::column-alias.txt[] include::optional-properties.txt[] include::unique-results.txt[] diff --git a/cypher/src/main/scala/org/neo4j/cypher/commands/Query.scala b/cypher/src/main/scala/org/neo4j/cypher/commands/Query.scala index e5af8e36a..957ada88e 100644 --- a/cypher/src/main/scala/org/neo4j/cypher/commands/Query.scala +++ b/cypher/src/main/scala/org/neo4j/cypher/commands/Query.scala @@ -24,18 +24,18 @@ object Query { def start(startItems: StartItem*) = new QueryBuilder(startItems) } -case class Query(returns: Return, start: Start, matching: Option[ Match ], where: Option[ Clause ], - aggregation: Option[ Aggregation ], - sort: Option[ Sort ], slice: Option[ Slice ], namedPaths: Option[ NamedPaths ]) +case class Query(returns: Return, start: Start, matching: Option[Match], where: Option[Clause], + aggregation: Option[Aggregation], + sort: Option[Sort], slice: Option[Slice], namedPaths: Option[NamedPaths]) -case class Return(columns: List[ String ], returnItems: ReturnItem*) +case class Return(columns: List[String], returnItems: ReturnItem*) case class Start(startItems: StartItem*) case class Match(patterns: Pattern*) -case class NamedPaths(paths: NamedPath*) extends Traversable[ Pattern ] { - def foreach[ U ](f: ( Pattern ) => U) { +case class NamedPaths(paths: NamedPath*) extends Traversable[Pattern] { + def foreach[U](f: (Pattern) => U) { paths.flatten.foreach(f) } } @@ -44,4 +44,4 @@ case class Aggregation(aggregationItems: AggregationItem*) case class Sort(sortItems: SortItem*) -case class Slice(from: Option[ Value ], limit: Option[ Value ]) \ No newline at end of file +case class Slice(from: Option[Value], limit: Option[Value]) \ No newline at end of file diff --git a/cypher/src/main/scala/org/neo4j/cypher/commands/ReturnItem.scala b/cypher/src/main/scala/org/neo4j/cypher/commands/ReturnItem.scala index 1c4220107..f9f384c37 100644 --- a/cypher/src/main/scala/org/neo4j/cypher/commands/ReturnItem.scala +++ b/cypher/src/main/scala/org/neo4j/cypher/commands/ReturnItem.scala @@ -29,6 +29,8 @@ abstract sealed class ReturnItem(val identifier: Identifier) extends (Map[String def columnName = identifier.name def concreteReturnItem = this + + override def toString() = identifier.name } case class ValueReturnItem(value: Value) extends ReturnItem(value.identifier) { @@ -39,22 +41,42 @@ case class ValueReturnItem(value: Value) extends ReturnItem(value.identifier) { } } +case class AliasReturnItem(inner: ReturnItem, newName:String) extends ReturnItem(Identifier(newName, inner.identifier.typ)) { + def apply(m: Map[String, Any]): Any = inner.apply(m) + + def assertDependencies(source: Pipe) { + inner.assertDependencies(source) + } + + override def toString() = inner.toString() + " AS " + newName +} + case class ValueAggregationItem(value: AggregationValue) extends AggregationItem(value.identifier) { def assertDependencies(source: Pipe) { value.checkAvailable(source.symbols) } - def createAggregationFunction: AggregationFunction = value.createAggregationFunction + + def createAggregationFunction: AggregationFunction = value.createAggregationFunction } -abstract sealed class AggregationItem(identifier:Identifier) extends ReturnItem(identifier) { +abstract sealed class AggregationItem(identifier: Identifier) extends ReturnItem(identifier) { def apply(m: Map[String, Any]): Map[String, Any] = m def createAggregationFunction: AggregationFunction + override def toString() = identifier.name } +case class AliasAggregationItem(inner:AggregationItem, newName:String) extends AggregationItem(Identifier(newName, inner.identifier.typ)) { + def assertDependencies(source: Pipe) { + inner.assertDependencies(source) + } + + def createAggregationFunction: AggregationFunction = inner.createAggregationFunction +} + case class CountStar() extends AggregationItem(Identifier("count(*)", IntegerType())) { def createAggregationFunction: AggregationFunction = new CountStarFunction diff --git a/cypher/src/main/scala/org/neo4j/cypher/parser/ReturnClause.scala b/cypher/src/main/scala/org/neo4j/cypher/parser/ReturnClause.scala index f15929317..e93d3467c 100644 --- a/cypher/src/main/scala/org/neo4j/cypher/parser/ReturnClause.scala +++ b/cypher/src/main/scala/org/neo4j/cypher/parser/ReturnClause.scala @@ -19,28 +19,41 @@ package org.neo4j.cypher.parser * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + import org.neo4j.cypher.commands._ import scala.util.parsing.combinator._ + trait ReturnClause extends JavaTokenParsers with Tokens with ReturnItems { + def column = (aggregate | returnItem) ~ opt(ignoreCase("AS") ~> identity) ^^ { + case returnItem ~ alias => alias match { + case None => returnItem + case Some(newColumnName) => returnItem match { + case x: AggregationItem => AliasAggregationItem(x, newColumnName) + case x => AliasReturnItem(x, newColumnName) + } + } + } - def returns: Parser[(Return, Option[Aggregation])] = ignoreCase("return") ~> opt("distinct") ~ rep1sep((aggregate | returnItem), ",") ^^ - { case distinct ~ items => { - val list = items.filter(_.isInstanceOf[AggregationItem]).map(_.asInstanceOf[AggregationItem]) + def returns: Parser[(Return, Option[Aggregation])] = ignoreCase("return") ~> opt("distinct") ~ rep1sep(column, ",") ^^ { + case distinct ~ items => { + val aggregationItems = items.filter(_.isInstanceOf[AggregationItem]).map(_.asInstanceOf[AggregationItem]) val none: Option[Aggregation] = distinct match { case Some(x) => Some(Aggregation()) case None => None } - ( - Return(items.map(_.columnName).toList, items.filter(!_.isInstanceOf[AggregationItem]): _*), - list match { - case List() => none - case _ => Some(Aggregation(list : _*)) - } - ) - }} + val aggregation = aggregationItems match { + case List() => none + case _ => Some(Aggregation(aggregationItems: _*)) + } + + val returnItems = Return(items.map(_.columnName).toList, items.filter(!_.isInstanceOf[AggregationItem]): _*) + + (returnItems, aggregation) + } + } } diff --git a/cypher/src/test/java/org/neo4j/cypher/javacompat/JavaExecutionEngineTests.java b/cypher/src/test/java/org/neo4j/cypher/javacompat/JavaExecutionEngineTests.java index ba3f38cb0..1007dd373 100644 --- a/cypher/src/test/java/org/neo4j/cypher/javacompat/JavaExecutionEngineTests.java +++ b/cypher/src/test/java/org/neo4j/cypher/javacompat/JavaExecutionEngineTests.java @@ -133,7 +133,7 @@ private void createTenNodes() { public void exampleConsole() throws Exception { Query query = CypherParser.parseConsole( //START SNIPPET: Identifier - "start n=node(0) return n.NOT_EXISTING" + "start n=node(0) return n.NOT_EXISTING, n.`property with spaces in it`" //END SNIPPET: Identifier ); diff --git a/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala b/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala index 66160498f..8f005e1b6 100644 --- a/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala +++ b/cypher/src/test/scala/org/neo4j/cypher/CypherParserTest.scala @@ -30,10 +30,17 @@ import org.scalatest.Assertions class CypherParserTest extends JUnitSuite with Assertions { @Test def shouldParseEasiestPossibleQuery() { - val q = Query. + testQuery("start s = NODE(1) return s", + Query. + start(NodeById("s", 1)). + returns(ValueReturnItem(EntityValue("s")))) + } + + @Test def shouldHandleAliasingOfColumnNames() { + testQuery("start s = NODE(1) return s as somethingElse", + Query. start(NodeById("s", 1)). - returns(ValueReturnItem(EntityValue("s"))) - testQuery("start s = NODE(1) return s", q) + returns(AliasReturnItem(ValueReturnItem(EntityValue("s")), "somethingElse"))) } @Test def sourceIsAnIndex() { diff --git a/cypher/src/test/scala/org/neo4j/cypher/ExecutionEngineTest.scala b/cypher/src/test/scala/org/neo4j/cypher/ExecutionEngineTest.scala index cdc8dc93f..4ee1c7bfb 100644 --- a/cypher/src/test/scala/org/neo4j/cypher/ExecutionEngineTest.scala +++ b/cypher/src/test/scala/org/neo4j/cypher/ExecutionEngineTest.scala @@ -1239,6 +1239,28 @@ return a assert(List(a) === result.columnAs[Node]("a").toList) } + @Test def shouldSupportColumnRenaming() { + val a = createNode(Map("name" -> "Andreas")) + + val result = parseAndExecute(""" +start a = node(1) +return a as OneLove +""") + + assert(List(a) === result.columnAs[Node]("OneLove").toList) + } + + @Test def shouldSupportColumnRenamingForAggregatesAsWell() { + val a = createNode(Map("name" -> "Andreas")) + + val result = parseAndExecute(""" +start a = node(1) +return count(*) as OneLove +""") + + assert(List(1) === result.columnAs[Node]("OneLove").toList) + } + @Ignore("Should be supported, but doesn't work") @Test def shouldBeAbleToQueryNumericIndexes() { val a = createNode("x" -> 5) diff --git a/cypher/src/test/scala/org/neo4j/cypher/docgen/ReturnTest.scala b/cypher/src/test/scala/org/neo4j/cypher/docgen/ReturnTest.scala index 6a2bc9ed8..a95095ec3 100644 --- a/cypher/src/test/scala/org/neo4j/cypher/docgen/ReturnTest.scala +++ b/cypher/src/test/scala/org/neo4j/cypher/docgen/ReturnTest.scala @@ -87,4 +87,13 @@ like this:""", returns = """The node named B, but only once.""", (p) => assertEquals(List(node("B")), p.columnAs[Node]("b").toList)) } + + @Test def column_aliasing() { + testQuery( + title = "Column alias", + text = """If the name of the column should be different from the expression used, you can rename it by using AS .""", + queryText = """start a=node(%A%) return a.age AS SomethingTotallyDifferent""", + returns = """Returns the age property of a node, but renames the column.""", + (p) => assertEquals(List(55), p.columnAs[Node]("SomethingTotallyDifferent").toList)) + } } \ No newline at end of file