+ sparql MINUS
authorEric Prud'hommeaux <eric@w3.org>
Sat, 30 Jan 2010 13:43:11 -0500
changeset 144 43bbad68f4fb
parent 143 f1283558478f
child 145 5fa8b27f1391
+ sparql MINUS
src/main/scala/RDB2RDFMain.scala
src/main/scala/SPARQL.scala
src/test/scala/RDB2RDFTest.scala
src/test/scala/SparqlTest.scala
--- a/src/main/scala/RDB2RDFMain.scala	Fri Jan 29 16:09:58 2010 -0500
+++ b/src/main/scala/RDB2RDFMain.scala	Sat Jan 30 13:43:11 2010 -0500
@@ -435,6 +435,79 @@
     }
   }
 
+  def synthesizeOuterJoin(initState:R2RState, gp:sparql.GraphPattern, negate:Boolean, db:sql.DatabaseDesc, enforceForeignKeys:Boolean):R2RState = {
+    /* SPARQL OPTIONALs are treated as SQL subselects.
+     * Set up initial state for this subselect.
+     */
+    val leftJoinAlias = sql.RelAlias(sql.Name("G_opt" + initState.joins.size))
+    val initDisjoints:Set[sql.Select] = Set()
+    val emptyState = R2RState(
+      util.AddOrderedSet[sql.Join](), 
+      Map[sparql.Var, SQL2RDFValueMapper](), 
+      Set[sql.Expression]()
+    )
+
+    /* Create the select for the nested graph pattern.
+     */
+    val optionalState = mapGraphPattern(db, emptyState, gp, enforceForeignKeys)
+    val optionalVars = findVars(gp)
+    val disjointNo = sql.NamedAttribute(sql.PrimaryExpressionTyped(sql.Datatype.INTEGER,sql.Name("" + initState.joins.size)),
+					sql.AttrAlias(sql.Name("_DISJOINT_")))
+    val leftJoinVars = findVars(gp).toList
+    val attrlist:Set[sql.NamedAttribute] = leftJoinVars.foldLeft(Set(disjointNo))((attrs, v) =>
+      attrs ++ Set(sql.NamedAttribute(varToAttribute(optionalState.varmap, v), sql.AttrAlias(attrAliasNameFromVar(v))))
+      										)
+    val subselect = sql.Select(
+      sql.AttributeList(attrlist),
+      sql.TableList(optionalState.joins),
+      optionalState.exprs.size match {
+      	case 0 => None
+      	case 1 => Some(optionalState.exprs.toList(0))
+      	case _ => Some(sql.ExprConjunction(optionalState.exprs))
+      }
+    )
+
+    /* Create a condition to test if this OPTIONAL was matched (called
+     * _DISJOINT_ as OPTIONAL behaves pretty much like a disjunction).
+     */
+    val optionalCond = sql.RelationalExpressionNull(sql.PrimaryExpressionAttr(
+      sql.RelAliasAttribute(leftJoinAlias, sql.Attribute(sql.Name("_DISJOINT_")))))
+
+    /* Bind variables to the attributes projected from the subselect; handle
+     * corefs (equivalence with earlier bindings).
+     */
+    val outerState2 =
+      optionalVars.foldLeft(
+	R2RState(initState.joins,
+		 initState.varmap,
+		 Set[sql.Expression]()))((myState, v) => 
+		   subselectVars(myState, v, leftJoinAlias, optionalCond,
+				 initState.varmap, optionalState.varmap, true))
+
+    /* The final state includes the subselect as a join, the variables bound
+     * to subselect projection, and no new expresssions. The expressions
+     * derived from corefs are conditions for the LEFT OUTER JOIN.
+     */
+    val join = sql.LeftOuterJoin(sql.AliasedResource(sql.Subselect(subselect), leftJoinAlias), 
+	 outerState2.exprs.size match {
+	   case 0 =>
+	     sql.RelationalExpressionEq(sql.PrimaryExpressionTyped(sql.Datatype.INTEGER,sql.Name("1")),
+					sql.PrimaryExpressionTyped(sql.Datatype.INTEGER,sql.Name("1")))
+	     // /* Require corefs unless we have a leading OPTIONAL. */
+	     // if (...)
+	     // else
+	     //   error ("Nested GP has no variables shared with its context; cowaredly refusing to join ON 1.")
+	   case 1 => outerState2.exprs.toList(0)
+	   case _ => sql.ExprConjunction(outerState2.exprs)
+	 }
+       )
+    val exprs =
+      if (negate) {
+	initState.exprs + sql.RelationalExpressionNull(sql.PrimaryExpressionAttr(sql.RelAliasAttribute(leftJoinAlias, sql.Attribute(sql.Name("_DISJOINT_")))))
+      } else initState.exprs
+    R2RState(initState.joins + join, outerState2.varmap, exprs)
+  }
+
   def mapGraphPattern(db:sql.DatabaseDesc, state:R2RState, gp:sparql.GraphPattern, enforceForeignKeys:Boolean):R2RState = {
     gp match {
       case sparql.TableFilter(gp2:sparql.GraphPattern, expr:sparql.Expression) => {
@@ -538,73 +611,11 @@
 	      )), sql.RelAlias(sql.Name("_EMPTY_"))), None), state.varmap, state.exprs)
 	  else
 	    state
-
-	/* SPARQL OPTIONALs are treated as SQL subselects.
-	 * Set up initial state for this subselect.
-	 */
-      	val leftJoinAlias = sql.RelAlias(sql.Name("G_opt" + state_postLeadingTable.joins.size))
-      	val initDisjoints:Set[sql.Select] = Set()
-      	val emptyState = R2RState(
-      	  util.AddOrderedSet[sql.Join](), 
-      	  Map[sparql.Var, SQL2RDFValueMapper](), 
-      	  Set[sql.Expression]()
-      	)
-
-	/* Create the select for the nested graph pattern.
-	 */
-      	val optionalState = mapGraphPattern(db, emptyState, gp, enforceForeignKeys)
-      	val optionalVars = findVars(gp)
-	val disjointNo = sql.NamedAttribute(sql.PrimaryExpressionTyped(sql.Datatype.INTEGER,sql.Name("" + state_postLeadingTable.joins.size)),
-					    sql.AttrAlias(sql.Name("_DISJOINT_")))
-      	val leftJoinVars = findVars(gp).toList
-      	val attrlist:Set[sql.NamedAttribute] = leftJoinVars.foldLeft(Set(disjointNo))((attrs, v) =>
-      	  attrs ++ Set(sql.NamedAttribute(varToAttribute(optionalState.varmap, v), sql.AttrAlias(attrAliasNameFromVar(v))))
-      	)
-      	val subselect = sql.Select(
-      	  sql.AttributeList(attrlist),
-      	  sql.TableList(optionalState.joins),
-      	  optionalState.exprs.size match {
-      	    case 0 => None
-      	    case 1 => Some(optionalState.exprs.toList(0))
-      	    case _ => Some(sql.ExprConjunction(optionalState.exprs))
-      	  }
-      	)
-
-	/* Create a condition to test if this OPTIONAL was matched (called
-	 * _DISJOINT_ as OPTIONAL behaves pretty much like a disjunction).
-	 */
-	val optionalCond = sql.RelationalExpressionNull(sql.PrimaryExpressionAttr(
-	  sql.RelAliasAttribute(leftJoinAlias, sql.Attribute(sql.Name("_DISJOINT_")))))
-
-	/* Bind variables to the attributes projected from the subselect; handle
-	 * corefs (equivalence with earlier bindings).
-	 */
-	val outerState2 =
-	  optionalVars.foldLeft(
-	    R2RState(state_postLeadingTable.joins,
-		     state_postLeadingTable.varmap,
-		     Set[sql.Expression]()))((myState, v) => 
-		       subselectVars(myState, v, leftJoinAlias, optionalCond,
-				     state_postLeadingTable.varmap, optionalState.varmap, true))
-
-	/* The final state includes the subselect as a join, the variables bound
-	 * to subselect projection, and no new expresssions. The expressions
-	 * derived from corefs are conditions for the LEFT OUTER JOIN.
-	 */
-      	val join = sql.LeftOuterJoin(sql.AliasedResource(sql.Subselect(subselect), leftJoinAlias), 
-      	  outerState2.exprs.size match {
-      	    case 0 =>
-	      /* Require corefs unless we have a leading OPTIONAL. */
-	      if (state.joins.size == 0)
-		sql.RelationalExpressionEq(sql.PrimaryExpressionTyped(sql.Datatype.INTEGER,sql.Name("1")),
-					   sql.PrimaryExpressionTyped(sql.Datatype.INTEGER,sql.Name("1")))
-	      else
-		error ("Nested GP has no variables shared with its context; cowaredly refusing to join ON 1.")
-      	    case 1 => outerState2.exprs.toList(0)
-      	    case _ => sql.ExprConjunction(outerState2.exprs)
-      	  }
-			       )
-      	R2RState(state_postLeadingTable.joins + join, outerState2.varmap, state.exprs)
+	synthesizeOuterJoin(state_postLeadingTable, gp, false, db, enforceForeignKeys)
+      }
+      case sparql.MinusGraphPattern(gp) => {
+	if (state.joins.size == 0) state
+	else synthesizeOuterJoin(state, gp, true, db, enforceForeignKeys)
       }
       case sparql.GraphGraphPattern(gp) => error("no code to handle GraphGraphPatterns (" + gp + ")")
     }
@@ -632,7 +643,7 @@
       ))
 
     /* Construct the generated query as an abstract syntax. */
-    sql.Select(
+    val select = sql.Select(
       sql.AttributeList(attrlist),
       sql.TableList(r2rState.joins),
       r2rState.exprs.size match {
@@ -640,7 +651,10 @@
 	case 1 => Some(r2rState.exprs.toList(0))
 	case _ => Some(sql.ExprConjunction(r2rState.exprs))
       }
-    ).makePretty()
+    )
+    // println("r2rState.varmap: " + r2rState.varmap)
+    // println("select.expression: " + select.expression)
+    select.makePretty()
   }
 }
 
--- a/src/main/scala/SPARQL.scala	Fri Jan 29 16:09:58 2010 -0500
+++ b/src/main/scala/SPARQL.scala	Sat Jan 30 13:43:11 2010 -0500
@@ -24,6 +24,7 @@
 case class TableDisjunction(gps:List[GraphPattern]) extends GraphPattern
 case class TableFilter(gp:GraphPattern, expr:Expression) extends GraphPattern
 case class OptionalGraphPattern(gp:GraphPattern) extends GraphPattern
+case class MinusGraphPattern(gp:GraphPattern) extends GraphPattern
 case class GraphGraphPattern(gp:GraphPattern) extends GraphPattern
 
 case class TriplePattern(s:S, p:P, o:O)
@@ -161,6 +162,7 @@
 
   def graphpatternnottriplesORfilter:Parser[GraphPattern] = (
       "OPTIONAL"~groupgraphpattern ^^ { case "OPTIONAL"~ggp => OptionalGraphPattern(ggp) }
+    | "MINUS"~groupgraphpattern ^^ { case "MINUS"~ggp => MinusGraphPattern(ggp) }
     | rep1sep(groupgraphpattern, "UNION") ^^ { x => if (x.size > 1) TableDisjunction(x) else x(0) }
     | "GRAPH"~uri~groupgraphpattern ^^ { case "GRAPH"~u~ggp => GraphGraphPattern(ggp) }
     | filter ^^ { x => TableFilter(null, x) }
--- a/src/test/scala/RDB2RDFTest.scala	Fri Jan 29 16:09:58 2010 -0500
+++ b/src/test/scala/RDB2RDFTest.scala	Sat Jan 30 13:43:11 2010 -0500
@@ -862,7 +862,7 @@
 			 Attribute("ingredient") -> Value(Datatype.INTEGER)))
       ))
 
-  test("swobjects/tests/healthCare/lists-notBound/db.rq") {
+  test("swobjects/tests/healthCare/lists-notBound/db.rq AS OPTIONAL") {
     val sparqlParser = Sparql()
     val sparqlSelect = sparqlParser.parseAll(sparqlParser.select, """
 PREFIX Person: <http://hospital.example/DB/Person#>
@@ -938,4 +938,81 @@
 """
   }
 
+  test("swobjects/tests/healthCare/lists-notBound/db.rq") {
+    val sparqlParser = Sparql()
+    val sparqlSelect = sparqlParser.parseAll(sparqlParser.select, """
+PREFIX Person: <http://hospital.example/DB/Person#>
+PREFIX Sex_DE: <http://hospital.example/DB/Sex_DE#>
+PREFIX Item_Medication: <http://hospital.example/DB/Item_Medication#>
+PREFIX Medication: <http://hospital.example/DB/Medication#>
+PREFIX Medication_DE: <http://hospital.example/DB/Medication_DE#>
+PREFIX NDCcodes: <http://hospital.example/DB/NDCcodes#>
+PREFIX xsd : <http://www.w3.org/2001/XMLSchema#>
+
+SELECT ?patient
+ WHERE {
+    ?patient Person:MiddleName ?middleName .
+    ?patient Person:DateOfBirth ?dob .
+    ?patient Person:SexDE ?sexEntry .
+    ?sexEntry Sex_DE:EntryName ?sex .
+
+    ?indicItem Item_Medication:PatientID ?patient .
+    ?indicItem Item_Medication:PerformedDTTM ?indicDate .
+    ?indicItem Item_Medication:EntryName ?takes .
+    ?indicMed Medication:ItemID ?indicItem .
+    ?indicMed Medication:MedDictDE ?indicDE .
+    ?indicDE Medication_DE:NDC ?indicNDC .
+    ?indicCode NDCcodes:NDC ?indicNDC .
+    ?indicCode NDCcodes:ingredient "6809"^^xsd:integer
+
+    MINUS {
+        ?disqualItem Item_Medication:PatientID ?patient .
+        ?disqualItem Item_Medication:PerformedDTTM ?disqualDate .
+        ?disqualItem Item_Medication:EntryName ?takes .
+        ?disqualMed Medication:ItemID ?disqualItem .
+        ?disqualMed Medication:MedDictDE ?disqualDE .
+        ?disqualDE Medication_DE:NDC ?disqualNDC .
+        ?disqualCode NDCcodes:NDC ?disqualNDC .
+        ?disqualCode NDCcodes:ingredient "11289"^^xsd:integer
+    }
+      }
+""").get
+    val sqlParser = Sql()
+    val parsed = sqlParser.parseAll(sqlParser.select, """
+SELECT R_patient.ID AS patient
+  FROM Person AS R_patient
+       INNER JOIN Sex_DE AS R_sexEntry ON R_sexEntry.ID=R_patient.SexDE
+       INNER JOIN Item_Medication AS R_indicItem ON R_indicItem.PatientID=R_patient.ID
+       INNER JOIN Medication AS R_indicMed ON R_indicMed.ItemID=R_indicItem.ID
+       INNER JOIN Medication_DE AS R_indicDE ON R_indicDE.ID=R_indicMed.MedDictDE
+       INNER JOIN NDCcodes AS R_indicCode ON R_indicCode.NDC=R_indicDE.NDC
+       LEFT OUTER JOIN (
+   SELECT R_disqualCode.ID AS disqualCode, R_disqualMed.MedDictDE AS disqualDE,
+          R_disqualItem.PerformedDTTM AS disqualDate, R_disqualItem.ID AS disqualItem,
+          R_disqualMed.ID AS disqualMed, R_disqualDE.NDC AS disqualNDC,
+          R_disqualItem.PatientID AS patient, R_disqualItem.EntryName AS takes, 6 AS _DISJOINT_
+     FROM Item_Medication AS R_disqualItem
+          INNER JOIN Medication AS R_disqualMed ON R_disqualMed.ItemID=R_disqualItem.ID
+          INNER JOIN Medication_DE AS R_disqualDE ON R_disqualDE.ID=R_disqualMed.MedDictDE
+          INNER JOIN NDCcodes AS R_disqualCode ON R_disqualCode.NDC=R_disqualDE.NDC
+    WHERE R_disqualCode.ingredient=11289
+      AND R_disqualItem.EntryName IS NOT NULL
+      AND R_disqualItem.PatientID IS NOT NULL
+      AND R_disqualItem.PerformedDTTM IS NOT NULL
+              ) AS G_opt6 ON G_opt6.patient=R_patient.ID
+                         AND G_opt6.takes=R_indicItem.EntryName
+ WHERE G_opt6._DISJOINT_ IS NULL
+   AND R_indicCode.ingredient=6809
+   AND R_indicItem.EntryName IS NOT NULL
+   AND R_indicItem.PerformedDTTM IS NOT NULL
+   AND R_patient.DateOfBirth IS NOT NULL
+   AND R_patient.MiddleName IS NOT NULL
+   AND R_sexEntry.EntryName IS NOT NULL
+""").get
+    val generated = RDB2RDF(hosp1, sparqlSelect, StemURI("http://hospital.example/DB/"), false, false)
+    assert(generated === parsed)
+    val output = """
+"""
+  }
+
 }
--- a/src/test/scala/SparqlTest.scala	Fri Jan 29 16:09:58 2010 -0500
+++ b/src/test/scala/SparqlTest.scala	Sat Jan 30 13:43:11 2010 -0500
@@ -315,6 +315,31 @@
     assert(tps === a.parseAll(a.select, e).get)
   }
 
+  test("parse an minus") {
+    val a = Sparql()
+    val e = """
+SELECT ?x { { ?x <http://hr.example/DB/Employee#manager> ?y} MINUS { ?x <http://hr.example/DB/Employee#manager> ?y} }
+"""
+    val tps =
+      Select(
+	SparqlAttributeList(List(Var("x"))),
+	TableConjunction(List(
+	  TriplesBlock(
+	    List(
+	      TriplePattern(
+		SVar(Var("x")),
+		PUri(Stem("http://hr.example/DB"),Rel("Employee"),Attr("manager")),
+		OVar(Var("y"))))),
+	  MinusGraphPattern(
+	    TriplesBlock(
+	      List(
+		TriplePattern(
+		  SVar(Var("x")),
+		  PUri(Stem("http://hr.example/DB"),Rel("Employee"),Attr("manager")),
+		  OVar(Var("y")))))))))
+    assert(tps === a.parseAll(a.select, e).get)
+  }
+
   test("parse disj1") {
     val a = Sparql()
     val e = """