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

Implement EXTENDS syntax for Graph DDL #787

Merged
merged 13 commits into from
Feb 6, 2019
Merged
140 changes: 91 additions & 49 deletions graph-ddl/src/main/scala/org/opencypher/graphddl/GraphDdl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,22 @@ object GraphDdl {

val graphTypes = ddlParts.graphTypes
.keyBy(_.name)
.mapValues { graphType => tryWithGraphType(graphType.name) {
global.push(graphType.statements)
}}
.mapValues { graphType =>
tryWithGraphType(graphType.name) {
global.push(graphType.statements)
}
}
.view.force

val graphs = ddlParts.graphs
.map { graph => tryWithGraph(graph.definition.name) {
val graphType = graph.definition.maybeGraphTypeName
.map(name => graphTypes.getOrFail(name, "Unresolved graph type"))
.getOrElse(global)
toGraph(graphType, graph)
}}
.map { graph =>
tryWithGraph(graph.definition.name) {
val graphType = graph.definition.maybeGraphTypeName
.map(name => graphTypes.getOrFail(name, "Unresolved graph type"))
.getOrElse(global)
toGraph(graphType, graph)
}
}
.keyBy(_.name)

GraphDdl(
Expand All @@ -76,10 +80,10 @@ object GraphDdl {

def apply(statements: List[DdlStatement]): DdlParts = {
val result = statements.foldLeft(DdlParts.empty) {
case (parts, s: SetSchemaDefinition) => parts.copy(maybeSetSchema = Some(s))
case (parts, s: SetSchemaDefinition) => parts.copy(maybeSetSchema = Some(s))
case (parts, s: ElementTypeDefinition) => parts.copy(elementTypes = parts.elementTypes :+ s)
case (parts, s: GraphTypeDefinition) => parts.copy(graphTypes = parts.graphTypes :+ s)
case (parts, s: GraphDefinition) => parts.copy(graphs = parts.graphs :+ GraphDefinitionWithContext(s, parts.maybeSetSchema))
case (parts, s: GraphTypeDefinition) => parts.copy(graphTypes = parts.graphTypes :+ s)
case (parts, s: GraphDefinition) => parts.copy(graphs = parts.graphs :+ GraphDefinitionWithContext(s, parts.maybeSetSchema))
}
result.elementTypes.validateDistinctBy(_.name, "Duplicate element type")
result.graphTypes.validateDistinctBy(_.name, "Duplicate graph type")
Expand All @@ -101,8 +105,8 @@ object GraphDdl {

def apply(statements: List[GraphTypeStatement]): GraphTypeParts = {
val result = statements.foldLeft(GraphTypeParts.empty) {
case (parts, s: ElementTypeDefinition) => parts.copy(elementTypes = parts.elementTypes :+ s)
case (parts, s: NodeTypeDefinition) => parts.copy(nodeTypes = parts.nodeTypes :+ s)
case (parts, s: ElementTypeDefinition) => parts.copy(elementTypes = parts.elementTypes :+ s)
case (parts, s: NodeTypeDefinition) => parts.copy(nodeTypes = parts.nodeTypes :+ s)
case (parts, s: RelationshipTypeDefinition) => parts.copy(relTypes = parts.relTypes :+ s)
}
result.elementTypes.validateDistinctBy(_.name, "Duplicate element type")
Expand All @@ -124,8 +128,8 @@ object GraphDdl {

def apply(mappings: List[GraphStatement]): GraphParts =
mappings.foldLeft(GraphParts.empty) {
case (parts, s: GraphTypeStatement) => parts.copy(graphTypeStatements = parts.graphTypeStatements :+ s)
case (parts, s: NodeMappingDefinition) => parts.copy(nodeMappings = parts.nodeMappings :+ s)
case (parts, s: GraphTypeStatement) => parts.copy(graphTypeStatements = parts.graphTypeStatements :+ s)
case (parts, s: NodeMappingDefinition) => parts.copy(nodeMappings = parts.nodeMappings :+ s)
case (parts, s: RelationshipMappingDefinition) => parts.copy(relMappings = parts.relMappings :+ s)
}
}
Expand Down Expand Up @@ -168,13 +172,13 @@ object GraphDdl {
.foldLeftOver(allNodeTypes) { case (schema, (labels, properties)) =>
schema.withNodePropertyKeys(labels, properties)
}
.foldLeftOver(allNodeTypes.keySet.flatten.map(resolveElementType)) { case (schema, eType) =>
.foldLeftOver(allNodeTypes.keySet.flatten.flatMap(resolveElementTypes)) { case (schema, eType) =>
eType.maybeKey.fold(schema)(key => schema.withNodeKey(eType.name, key._2))
}
.foldLeftOver(allEdgeTypes) { case (schema, (label, properties)) =>
schema.withRelationshipPropertyKeys(label, properties)
}
.foldLeftOver(allEdgeTypes.keySet.map(resolveElementType)) { case (schema, eType) =>
.foldLeftOver(allEdgeTypes.keySet.flatMap(resolveElementTypes)) { case (schema, eType) =>
eType.maybeKey.fold(schema)(key => schema.withNodeKey(eType.name, key._2))
}
.withSchemaPatterns(allPatterns.toSeq: _*)
Expand All @@ -191,40 +195,83 @@ object GraphDdl {
elementTypes = local.elementTypes,

nodeTypes = Set(
parts.nodeTypes.map(_.elementTypes),
parts.relTypes.map(_.sourceNodeType.elementTypes),
parts.relTypes.map(_.targetNodeType.elementTypes)
parts.nodeTypes.map(local.resolveNodeLabels),
parts.relTypes.map(relType => local.resolveNodeLabels(relType.sourceNodeType)),
parts.relTypes.map(relType => local.resolveNodeLabels(relType.targetNodeType))
).flatten.map(labels => labels -> tryWithNode(labels)(
mergeProperties(labels.map(local.resolveElementType))
mergeProperties(labels.flatMap(local.resolveElementTypes))
)).toMap,

edgeTypes = Set(
parts.relTypes.map(_.elementType)
parts.relTypes.map(local.resolveRelType)
).flatten.map(label => label -> tryWithRel(label)(
mergeProperties(Set(local.resolveElementType(label)))
mergeProperties(local.resolveElementTypes(label))
)).toMap,

patterns = parts.relTypes.map(relType => SchemaPattern(
relType.sourceNodeType.elementTypes,
relType.elementType,
relType.targetNodeType.elementTypes
local.resolveNodeLabels(relType.sourceNodeType),
local.resolveRelType(relType),
local.resolveNodeLabels(relType.targetNodeType)
)).toSet
)
}

def toNodeType(nodeTypeDefinition: NodeTypeDefinition): NodeType =
NodeType(resolveNodeLabels(nodeTypeDefinition))

def toRelType(relationshipTypeDefinition: RelationshipTypeDefinition): RelationshipType =
RelationshipType(
startNodeType = toNodeType(relationshipTypeDefinition.sourceNodeType),
elementType = resolveRelType(relationshipTypeDefinition),
endNodeType = toNodeType(relationshipTypeDefinition.targetNodeType))

private def resolveNodeLabels(nodeType: NodeTypeDefinition): Set[String] =
tryWithNode(nodeType.elementTypes)(nodeType.elementTypes.flatMap(resolveElementTypes).map(_.name))

private def resolveRelType(relType: RelationshipTypeDefinition): String = {
val resolved = tryWithRel(relType.elementType)(resolveElementTypes(relType.elementType))

if (resolved.size > 1) {
illegalInheritance("Inheritance not allowed for relationship types ", relType.elementType)
}
resolved.head.name
}

private def mergeProperties(elementTypes: Set[ElementTypeDefinition]): PropertyKeys = {
elementTypes
.flatMap(_.properties)
.foldLeft(PropertyKeys.empty) { case (props, (name, cypherType)) =>
props.get(name).filter(_ != cypherType) match {
case Some(t) => incompatibleTypes(name, cypherType, t)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note:
Why do we throw an error here? Shouldn't Cypher generally support this. It is more a spark-cypher restriction

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! I thought the same and will change it in another PR and do the error handling in SQL PGDS (just didn't want to touch it in this PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case None => props.updated(name, cypherType)
case None => props.updated(name, cypherType)
}
}
}

private def resolveElementTypes(name: String): Set[ElementTypeDefinition] = {
val elementType = resolveElementType(name)
detectCircularDependency(elementType)
resolveParents(elementType)
}

private def resolveElementType(name: String): ElementTypeDefinition =
allElementTypes.getOrElse(name, unresolved(s"Unresolved element type", name))

private def resolveParents(node: ElementTypeDefinition): Set[ElementTypeDefinition] =
node.parents.map(resolveElementType).flatMap(resolveParents) + node

private def detectCircularDependency(node: ElementTypeDefinition): Unit = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 😉

def traverse(node: ElementTypeDefinition, path: List[ElementTypeDefinition]): Unit = {
node.parents.foreach { p =>
val parentElementType = allElementTypes.getOrElse(p, unresolved(s"Unresolved element type", p))
if (path.contains(parentElementType)) {
illegalInheritance("Circular dependency detected", (path.map(_.name) :+ p).mkString(" -> "))
}
traverse(parentElementType, path :+ parentElementType)
}
}
traverse(node, List(node))
}
}

private def toGraph(
Expand All @@ -236,36 +283,39 @@ object GraphDdl {
.push(parts.graphTypeStatements)
.push(parts.nodeMappings.map(_.nodeType) ++ parts.relMappings.map(_.relType))

val okapiSchema = graphType.asOkapiSchema

Graph(
name = GraphName(graph.definition.name),
graphType = graphType.asOkapiSchema,
graphType = okapiSchema,
nodeToViewMappings = parts.nodeMappings
.flatMap(nm => toNodeToViewMappings(graphType.asOkapiSchema, graph.maybeSetSchema, nm))
.flatMap(nmd => toNodeToViewMappings(graphType.toNodeType(nmd.nodeType), okapiSchema, graph.maybeSetSchema, nmd))
.validateDistinctBy(_.key, "Duplicate node mapping")
.keyBy(_.key),
edgeToViewMappings = parts.relMappings
.flatMap(em => toEdgeToViewMappings(graphType.asOkapiSchema, graph.maybeSetSchema, em))
.flatMap(rmd => toEdgeToViewMappings(graphType.toRelType(rmd.relType), okapiSchema, graph.maybeSetSchema, rmd))
.validateDistinctBy(_.key, "Duplicate relationship mapping")
)
}

private def toNodeToViewMappings(
graphType: Schema,
nodeType: NodeType,
okapiSchema: Schema,
maybeSetSchema: Option[SetSchemaDefinition],
nmd: NodeMappingDefinition
): Seq[NodeToViewMapping] = {
nmd.nodeToView.map { nvd =>
tryWithContext(s"Error in node mapping for: ${nmd.nodeType.elementTypes.mkString(",")}") {

val nodeKey = NodeViewKey(toNodeType(nmd.nodeType), toViewId(maybeSetSchema, nvd.viewId))
val nodeKey = NodeViewKey(nodeType, toViewId(maybeSetSchema, nvd.viewId))

tryWithContext(s"Error in node mapping for: $nodeKey") {
NodeToViewMapping(
nodeType = nodeKey.nodeType,
view = toViewId(maybeSetSchema, nvd.viewId),
propertyMappings = toPropertyMappings(
elementTypes = nodeKey.nodeType.elementTypes,
graphTypePropertyKeys = graphType.nodePropertyKeys(nodeKey.nodeType.elementTypes).keySet,
graphTypePropertyKeys = okapiSchema.nodePropertyKeys(nodeKey.nodeType.elementTypes).keySet,
maybePropertyMapping = nvd.maybePropertyMapping
)
)
Expand All @@ -275,14 +325,15 @@ object GraphDdl {
}

private def toEdgeToViewMappings(
graphType: Schema,
relType: RelationshipType,
okapiSchema: Schema,
maybeSetSchema: Option[SetSchemaDefinition],
rmd: RelationshipMappingDefinition
): Seq[EdgeToViewMapping] = {
rmd.relTypeToView.map { rvd =>
tryWithContext(s"Error in relationship mapping for: ${rmd.relType}") {

val edgeKey = EdgeViewKey(toRelType(rmd.relType), toViewId(maybeSetSchema, rvd.viewDef.viewId))
val edgeKey = EdgeViewKey(relType, toViewId(maybeSetSchema, rvd.viewDef.viewId))

tryWithContext(s"Error in relationship mapping for: $edgeKey") {
EdgeToViewMapping(
Expand Down Expand Up @@ -310,7 +361,7 @@ object GraphDdl {
),
propertyMappings = toPropertyMappings(
elementTypes = Set(rmd.relType.elementType),
graphTypePropertyKeys = graphType.relationshipPropertyKeys(rmd.relType.elementType).keySet,
graphTypePropertyKeys = okapiSchema.relationshipPropertyKeys(rmd.relType.elementType).keySet,
maybePropertyMapping = rvd.maybePropertyMapping
)
)
Expand Down Expand Up @@ -339,15 +390,6 @@ object GraphDdl {
}
}

private def toNodeType(nodeTypeDefinition: NodeTypeDefinition): NodeType =
NodeType(nodeTypeDefinition.elementTypes)

private def toRelType(relTypeDefinition: RelationshipTypeDefinition): RelationshipType =
RelationshipType(
startNodeType = toNodeType(relTypeDefinition.sourceNodeType),
elementType = relTypeDefinition.elementType,
endNodeType = toNodeType(relTypeDefinition.targetNodeType))

private def toPropertyMappings(
elementTypes: Set[String],
graphTypePropertyKeys: Set[String],
Expand Down Expand Up @@ -384,7 +426,7 @@ object GraphDdl {
def validateDistinctBy[K](key: T => K, msg: String): C[T] = {
elems.groupBy(key).foreach {
case (k, values) if values.size > 1 => duplicate(msg, k)
case _ =>
case _ =>
}
elems
}
Expand Down Expand Up @@ -467,7 +509,7 @@ case class Join(
)

object NodeType {
def apply(elementTypes: String*): NodeType = NodeType(elementTypes.toSet)
def apply(elementTypeLabels: String*): NodeType = NodeType(elementTypeLabels.toSet)
}

case class NodeType(elementTypes: Set[String]) {
Expand All @@ -481,11 +523,11 @@ object RelationshipType {

case class RelationshipType(startNodeType: NodeType, elementType: String, endNodeType: NodeType) {
override def toString: String = s"$startNodeType-[$elementType]->$endNodeType"

}

trait ElementViewKey {
def elementType: Set[String]

def viewId: ViewId
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ case class SetSchemaDefinition(

case class ElementTypeDefinition(
name: String,
parents: Set[String] = Set.empty,
properties: Map[String, CypherType] = Map.empty,
maybeKey: Option[KeyDefinition] = None
) extends GraphDdlAst with DdlStatement with GraphTypeStatement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ abstract class GraphDdlException(msg: String, cause: Option[Exception] = None) e
private[graphddl] object GraphDdlException {

def unresolved(desc: String, reference: Any): Nothing = throw UnresolvedReferenceException(
s"""$desc: $reference"""
s"$desc: $reference"
)

def unresolved(desc: String, reference: Any, available: Traversable[Any]): Nothing = throw UnresolvedReferenceException(
Expand All @@ -45,7 +45,11 @@ private[graphddl] object GraphDdlException {
)

def duplicate(desc: String, definition: Any): Nothing = throw DuplicateDefinitionException(
s"""$desc: $definition"""
s"$desc: $definition"
)

def illegalInheritance(desc: String, reference: Any): Nothing = throw IllegalInheritanceException(
s"$desc: $reference"
)

def incompatibleTypes(msg: String): Nothing =
Expand Down Expand Up @@ -73,6 +77,8 @@ case class UnresolvedReferenceException(msg: String, cause: Option[Exception] =

case class DuplicateDefinitionException(msg: String, cause: Option[Exception] = None) extends GraphDdlException(msg, cause)

case class IllegalInheritanceException(msg: String, cause: Option[Exception] = None) extends GraphDdlException(msg, cause)

case class TypeException(msg: String, cause: Option[Exception] = None) extends GraphDdlException(msg, cause)

case class MalformedIdentifier(msg: String, cause: Option[Exception] = None) extends GraphDdlException(msg, cause)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ object GraphDdlParser {

private def CREATE[_: P]: P[Unit] = keyword("CREATE")
private def ELEMENT[_: P]: P[Unit] = keyword("ELEMENT")
private def EXTENDS[_: P]: P[Unit] = keyword("EXTENDS")
private def KEY[_: P]: P[Unit] = keyword("KEY")
private def GRAPH[_: P]: P[Unit] = keyword("GRAPH")
private def TYPE[_: P]: P[Unit] = keyword("TYPE")
Expand All @@ -87,22 +88,23 @@ object GraphDdlParser {
P(identifier.! ~/ CypherTypeParser.cypherType)

private def properties[_: P]: P[Map[String, CypherType]] =
P("(" ~/ property.rep(min = 1, sep = ",").map(_.toMap) ~/ ")")
P("(" ~/ property.rep(min = 0, sep = ",").map(_.toMap) ~/ ")")

private def keyDefinition[_: P]: P[(String, Set[String])] =
P(KEY ~/ identifier.! ~/ "(" ~/ identifier.!.rep(min = 1, sep = ",").map(_.toSet) ~/ ")")

def elementTypeDefinition[_: P]: P[ElementTypeDefinition] = {
P(identifier.! ~/ properties.? ~/ keyDefinition.?).map {
case (id, None, maybeKey) => ElementTypeDefinition(id, maybeKey = maybeKey)
case (id, Some(props), maybeKey) => ElementTypeDefinition(id, props, maybeKey)
private def extendsDefinition[_: P]: P[Set[String]] =
P(EXTENDS ~/ identifier.!.rep(min = 1, sep = ",").map(_.toSet))

def elementTypeDefinition[_: P]: P[ElementTypeDefinition] =
P(identifier.! ~/ extendsDefinition.? ~/ properties.? ~/ keyDefinition.?).map {
case (id, maybeParents, maybeProps, maybeKey) =>
ElementTypeDefinition(id, maybeParents.getOrElse(Set.empty), maybeProps.getOrElse(Map.empty), maybeKey)
}
}

def globalElementTypeDefinition[_: P]: P[ElementTypeDefinition] =
P(CREATE ~ ELEMENT ~/ TYPE ~/ elementTypeDefinition)


// ==== Schema ====

def elementType[_: P]: P[String] =
Expand Down
Loading