Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEO-9 Relationships and patterns can go live. #4

Merged
merged 1 commit into from
Nov 30, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,45 +87,48 @@ class PersonRelation extends Relationship[PersonRelation, Person] {

In this example all nodes of Person type are returned.
```
scala> val personNodes = Person.returns(p => p).execute
scala> val personNodes = Person().returns(case p ~~ _ => p).execute
personNodes: Future[Seq[Person]]
```

The strange construct in the returns function is execution of extractor in the pattern. Pattern defines set of objects
that participate in the query. The objects are nodes and relationships.

You can also query for specific attributes of a node.
```
scala> val personNames = Person.returns(p => p.name).execute
scala> val personNames = Person().returns(case p ~~ _ => p.name).execute
personNames: Future[Seq[String]]
```

A query that involves attributes matching.
```
scala> val personNodes = Person( p => p.name := "Tom" ).returns(p => p).execute
scala> val personNodes = Person(_.name := "Tom").returns(case p ~~ _ => p).execute
personNodes: Future[Seq[Person]]
```

Query for a person that has a relationship to another person
```
scala> val personNodes = Person.relatedTo[Person].returns(p => p).execute
scala> val personNodes = (Person() :->: Person()).returns(case p1 ~~ _ => p).execute
personNodes: Future[Seq[Person]]
```

Query for a person that has a relationship to another person with given name
```
scala> val personNodes = Person.relatedTo(Person(p => p.name := "James").returns(p => p).execute
scala> val personNodes = (Person() :->: Person(_.name := "James")).returns(case p ~~ _ => p).execute
personNodes: Future[Seq[Person]]
```


Query for a person that has a relationship to another person
```
scala> val personNodes = Person.relatedTo(WorkRelationship :-> Person).returns((p1,r,p2) => p1).execute
scala> val personNodes = (Person() :<-: WorkRelationship() :->: Person()).returns(case p1 ~~ r ~~ p2 ~~ _ => p1).execute
personNodes: Future[Seq[Person]]
```


Query for a person that has a relationship to another person with given name
```
scala> val personNodes = Person.relatedTo(WorkRelationship(r => r.company := "ABC") :-> Person(p => p.name := "John"))
returns((p1,r,p2) => p1).execute
scala> val personNodes = (Person() :-: WorkRelationship(_.company := "ABC") :->: Person(_.name := "John"))
.returns(case p1 ~~ _ => p1).execute
personNodes: Future[Seq[Person]]
```
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ abstract class JsonParser[R] extends ResultParser[R] {

private[this] def singleErrorMessage(error: (JsPath, scala.Seq[ValidationError])) = {
val (path: JsPath, errors: Seq[ValidationError]) = error
val message = errors.foldLeft(errors.head.message)((acc, err) => s"$acc,${err.message}")
val message = errors.foldLeft(errors.head.message)((acc,err) => s"$acc,${err.message}")
s"Errors at $path: $message"
}

private[client] def buildErrorMessage(error: JsError) = {
error.errors.tail.foldLeft(singleErrorMessage(error.errors.head))((acc, err) => s"acc,${singleErrorMessage(err)}")
error.errors.tail.foldLeft(singleErrorMessage(error.errors.head))((acc,err) => s"acc,${singleErrorMessage(err)}")
}

override def parseResult(response: HttpResponse): Try[R] = {
Expand All @@ -69,4 +69,4 @@ abstract class JsonParser[R] extends ResultParser[R] {
*/
class JsonValidationException(msg: String) extends Exception

class InvalidResponseException(msg: String) extends Exception
class InvalidResponseException(msg: String) extends Exception
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ object ServerCall {
def apply[RT](endpoint: RestEndpoint, returnExpression: ReturnExpression[RT])(implicit client: RestClient) = {
new ServerCall[RT](endpoint, None, returnExpression)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.websudos.reactiveneo.query.DefaultFormatters.{BooleanFormatter, Doubl
/**
* Implicit definitions used in DSL.
*/
trait DefaultImports extends MatchQueryImplicits with ImplicitConversions {
trait DefaultImports {

implicit val stringFormatter = new StringFormatter
implicit val intFormatter = new IntegerFormatter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ private[reactiveneo] abstract class GraphObject[Owner <: GraphObject[Owner, Reco
* Constructs a pattern for the class related to this accompanying object.
*/
def apply(predBuilder: (Owner => Predicate[_])*)
(implicit m: Manifest[Owner]): Pattern[Owner] = {
(implicit m: Manifest[Owner]) = {
val obj = m.runtimeClass.newInstance().asInstanceOf[Owner]
val pattern = Pattern(obj, nodeAliases.head, predBuilder.map(pred => pred(obj)): _*)
val pattern = GraphObjectSelection(obj, predBuilder.map(pred => pred(obj)): _*)
pattern
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2014 websudos ltd.
* 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.
*/
package com.websudos.reactiveneo.dsl

import com.websudos.reactiveneo.attribute.AbstractAttribute
import com.websudos.reactiveneo.query.{ValueFormatter, CypherOperators, BuiltQuery}

/**
* Criteria applied to a graph object. Cypher representation of a pattern is ```(n: Label { name = "Mark" })```
*/
private[reactiveneo] case class GraphObjectSelection[Owner <: GraphObject[Owner, _]](
owner: Owner,
predicates: Predicate[_]*) {

@inline
private def predicatesQuery: Option[BuiltQuery] = {
if(predicates.nonEmpty) {
Some(predicates.tail.foldLeft(predicates.head.clause)((agg, next) => agg.append(",").append(next.clause)))
} else {
None
}
}

/**
* Builds a query string of alias, object name and criteria if some.
*/
def queryClause(context: QueryBuilderContext): BuiltQuery = {
val alias = context.resolve(owner)
val (open:String, close:String) = this.owner match {
case _:Relationship[_,_] => "[" -> "]"
case _ => "(" -> ")"
}
BuiltQuery(s"$alias:${owner.objectName}")
.append(predicatesQuery.map(" {" + _.queryString + "}").getOrElse(""))
.wrapped(open, close)
}

override def toString: String = {
owner.objectName + predicatesQuery.map(" {" + _.queryString + "}")
}
}

/**
* Predicate filtering nodes by attribute value.
* @param attribute Attribute to filter.
* @param value Lookup value
*/
private[reactiveneo] case class Predicate[T](
attribute: AbstractAttribute[T], value: T)(implicit formatter: ValueFormatter[T]) {

val clause: BuiltQuery = {
if(value == null)
throw new IllegalArgumentException("NULL value is not allowed in predicate.")
new BuiltQuery(attribute.name).append(CypherOperators.COLON).append(value)
}

}

trait PredicateOps {

implicit class PredicateFunctions[V](attr: AbstractAttribute[V])(implicit formatter: ValueFormatter[V]) {

implicit def :=(value: V): Predicate[V] = {
new Predicate[V](attr, value)
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,48 @@ import com.websudos.reactiveneo.query.BuiltQuery
*/
trait ImplicitConversions {

/**
* Wraps a string with a BuiltQuery object.
* @param str String to wrap
* @return Returns query object.
*/
implicit def stringToQuery(str: String): BuiltQuery = BuiltQuery(str)

/**
* Conversion that simplifies query building. It allows to build the query directly from a pattern.
* ```
* PersonNode(p=>p.name := "Mark").returns(p=>p)
* ```
* @param p Predicate that forms initial node for the query
* @tparam P Pattern type.
* @return Returns query object.
*/
implicit def patternToQuery[P <: Pattern](p: P): MatchQuery[P, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, _] = {
MatchQuery.createRootQuery(p, new QueryBuilderContext)
}


/**
* Convert single node selection to the [[com.websudos.reactiveneo.dsl.Pattern]] object
* @param sel Graph node selection
* @tparam N Type of node
* @return Returns Pattern with given [[com.websudos.reactiveneo.dsl.GraphObjectSelection]] as root.
*/
implicit def selectionToPattern[N <: Node[N,_]](sel: GraphObjectSelection[N]): PatternLink[N, PNil] = {
val pattern = new PatternLink[N, PNil](Start, sel)
pattern
}

/**
* Convert single node selection to the [[com.websudos.reactiveneo.dsl.MatchQuery]] object
* @param sel Graph node selection
* @tparam N Type of node
* @return Returns Query object.
*/
implicit def selectionToQuery[N <: Node[N,_]](sel: GraphObjectSelection[N]):
MatchQuery[PatternLink[N,PNil], WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, _] = {
val pattern = new PatternLink[N, PNil](Start, sel)
val query = MatchQuery.createRootQuery(pattern, new QueryBuilderContext)
query
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,11 @@
package com.websudos.reactiveneo.dsl

import com.websudos.reactiveneo.client.{ServerCall, SingleTransaction, RestClient}
import com.websudos.reactiveneo.query.{CypherOperators, BuiltQuery, CypherKeywords, CypherQueryBuilder}
import com.websudos.reactiveneo.query.{BuiltQuery, CypherKeywords, CypherQueryBuilder}

import scala.annotation.implicitNotFound
import scala.concurrent.Future

sealed trait RelationshipDirection

private[reactiveneo] abstract class Any extends RelationshipDirection

private[reactiveneo] abstract class Left extends RelationshipDirection

private[reactiveneo] abstract class Right extends RelationshipDirection

sealed trait RelationshipBind

private[reactiveneo] abstract class RelationshipBound extends RelationshipBind
Expand Down Expand Up @@ -60,46 +52,40 @@ private[reactiveneo] abstract class LimitUnbound extends LimitBind

/**
* Query builder is responsible for encapsulating nodes information and selection criteria.
* @param node Initial node to query against.
* @param pattern Pattern this query is build against.
* @param builtQuery Current query string.
* @param aliases Map of added node types to corresponding alias value used in RETURN clause.
* @param context Map of added node types to corresponding alias value used in RETURN clause.
*/
private[reactiveneo] class MatchQuery[
GO <: Node[GO, _],
P <: Pattern,
WB <: WhereBind,
RB <: ReturnBind,
OB <: OrderBind,
LB <: LimitBind,
RT](node: GO,
RT](pattern: P,
builtQuery: BuiltQuery,
aliases: Map[GraphObject[_, _], String],
context: QueryBuilderContext,
ret: Option[ReturnExpression[RT]] = None) extends CypherQueryBuilder {


def query: String = builtQuery.queryString


@implicitNotFound("You cannot use two where clauses on a single query")
final def relatesTo(go: GraphObject[GO, _])(implicit ev: WB =:= WhereUnbound): MatchQuery[GO, WB, RB, OB, LB, _] = {
new MatchQuery[GO, WB, RB, OB, LB, Any] (
node,
builtQuery,
aliases)
}

@implicitNotFound("You cannot use two where clauses on a single query")
final def where(condition: GO => Criteria[GO])(implicit ev: WB =:= WhereUnbound): MatchQuery[GO, WhereBound, RB, OB, LB, _] = {
new MatchQuery[GO, WhereBound, RB, OB, LB, Any] (
node,
builtQuery.appendSpaced(CypherKeywords.RETURN).appendSpaced(CypherOperators.WILDCARD),
aliases)
final def where(condition: P => Criteria[_])
(implicit ev: WB =:= WhereUnbound): MatchQuery[P, WhereBound, RB, OB, LB, _] = {
new MatchQuery[P, WhereBound, RB, OB, LB, Any] (
pattern,
where(builtQuery, condition(pattern).clause),
context)
}

final def returns[URT](ret: GO => ReturnExpression[URT]): MatchQuery[GO, WB, ReturnBound, OB, LB, URT] = {
new MatchQuery[GO, WB, ReturnBound, OB, LB, URT] (
node,
builtQuery.appendSpaced(CypherKeywords.RETURN).appendSpaced(aliases.values.mkString(",")),
aliases)
final def returns[URT](ret: P => ReturnExpression[URT]): MatchQuery[P, WB, ReturnBound, OB, LB, URT] = {
new MatchQuery[P, WB, ReturnBound, OB, LB, URT] (
pattern,
builtQuery.appendSpaced(CypherKeywords.RETURN).appendSpaced(ret(pattern).query(context)),
context,
Some(ret(pattern)))
}

@implicitNotFound("You need to add return clause to capture the type of result")
Expand All @@ -113,11 +99,15 @@ private[reactiveneo] class MatchQuery[
private[reactiveneo] object MatchQuery {


def createRootQuery[GO <: Node[GO, _]](pattern: Pattern[GO], aliases: IndexedSeq[String]): MatchQuery[GO, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, _] = {
val query = new BuiltQuery(CypherKeywords.MATCH).appendSpaced(pattern.clause)
new MatchQuery[GO, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, Any](
pattern.owner,
def createRootQuery[P <: Pattern](
pattern: P,
context: QueryBuilderContext):
MatchQuery[P, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, _] = {
pattern.foreach(context.nextLabel(_))
val query = new BuiltQuery(CypherKeywords.MATCH).appendSpaced(pattern.queryClause(context))
new MatchQuery[P, WhereUnbound, ReturnUnbound, OrderUnbound, LimitUnbound, Any](
pattern,
query,
Map(pattern.owner -> pattern.alias))
context)
}
}

This file was deleted.