diff --git a/docs/03-apis/api-v2/query-language.md b/docs/03-apis/api-v2/query-language.md index 304f330b5d..fde98eb70d 100644 --- a/docs/03-apis/api-v2/query-language.md +++ b/docs/03-apis/api-v2/query-language.md @@ -649,6 +649,48 @@ except that the first argument is a variable representing a resource: FILTER knora-api:matchLabel(?book, "Zeitglöcklein") ``` +### Filtering on Resource IRIs + +A `FILTER` can compare a variable with another variable or IRI +representing a resource. For example, to find a letter whose +author and recipient are different persons: + +``` +PREFIX beol: +PREFIX knora-api: + +CONSTRUCT { + ?letter knora-api:isMainResource true . + ?letter beol:hasAuthor ?person1 . + ?letter beol:hasRecipient ?person2 . +} WHERE { + ?letter a beol:letter . + ?letter beol:hasAuthor ?person1 . + ?letter beol:hasRecipient ?person2 . + FILTER(?person1 != ?person2) . +} +OFFSET 0 +``` + +To find a letter whose author is not a person with a specified IRI: + +``` +PREFIX beol: +PREFIX knora-api: + +CONSTRUCT { + ?letter knora-api:isMainResource true . + ?letter beol:hasAuthor ?person1 . + ?letter beol:hasRecipient ?person2 . +} WHERE { + ?letter a beol:letter . + ?letter beol:hasAuthor ?person1 . + ?letter beol:hasRecipient ?person2 . + FILTER(?person1 != ) . +} +OFFSET 0 +``` + ### CONSTRUCT Clause In the `CONSTRUCT` clause of a Gravsearch query, the variable representing the diff --git a/docs/05-internals/design/api-v2/gravsearch.md b/docs/05-internals/design/api-v2/gravsearch.md index b26c073e5f..044bfef74f 100644 --- a/docs/05-internals/design/api-v2/gravsearch.md +++ b/docs/05-internals/design/api-v2/gravsearch.md @@ -153,8 +153,8 @@ The classes involved in generating prequeries can be found in `org.knora.webapi. If the client submits a count query, the prequery returns the overall number of hits, but not the results themselves. In a first step, before transforming the WHERE clause, query patterns must be further optimised by removing -the `rdfs:type` statement for entities whose type could be inferred from a property since there would be no need -for explicit `rdfs:type` statements for them (unless the property from which the type of an entity must be inferred from +the `rdfs:type` statement for entities whose type could be inferred from their use with a property IRI, since there would be no need +for explicit `rdfs:type` statements for them (unless the property IRI from which the type of an entity must be inferred from is wrapped in an `OPTIONAL` block). This optimisation takes the Gravsearch query as input (rather than the generated SPARQL), because it uses type information that refers to entities in the Gravsearch query, and the generated SPARQL might have different entities. diff --git a/test_data/all_data/beol-data.ttl b/test_data/all_data/beol-data.ttl index 2bc557ca4d..f09a6dbdef 100644 --- a/test_data/all_data/beol-data.ttl +++ b/test_data/all_data/beol-data.ttl @@ -65,6 +65,34 @@ knora-base:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser"; knora-base:attachedToUser . + a beol:person; + rdfs:label "Testperson3"; + knora-base:isDeleted false; + knora-base:attachedToProject ; + knora-base:creationDate "2020-09-21T07:14:09.621165Z"^^xsd:dateTime; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser ; + beol:hasGivenName ; + beol:hasFamilyName . + + a knora-base:TextValue; + knora-base:valueHasUUID "Eaj-gUutRpONYPetDp-SyQ"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-09-21T07:14:09.621165Z"^^xsd:dateTime; + knora-base:valueHasOrder 0; + knora-base:valueHasString "Hummel"; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser . + + a knora-base:TextValue; + knora-base:valueHasUUID "R0yqYbumTD-R7wA3U57ndQ"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-09-21T07:14:09.621165Z"^^xsd:dateTime; + knora-base:valueHasOrder 0; + knora-base:valueHasString "Johann Nepomuk"; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser . + a beol:letter; rdfs:label "Testbrief1"; knora-base:isDeleted false; @@ -168,6 +196,80 @@ knora-base:hasPermissions "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser"; knora-base:attachedToUser . + a beol:letter; + rdfs:label "letter to self"; + knora-base:isDeleted false; + knora-base:attachedToProject ; + knora-base:creationDate "2020-09-18T10:46:06.442289Z"^^xsd:dateTime; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser ; + beol:title ; + beol:creationDate ; + beol:hasAuthor ; + beol:hasAuthorValue ; + beol:hasRecipient ; + beol:hasRecipientValue ; + beol:hasText . + + a knora-base:TextValue; + knora-base:valueHasUUID "xFaiZnw4RH-ES-eovy_FgQ"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueHasMaxStandoffStartIndex "1"^^xsd:nonNegativeInteger; + knora-base:valueCreationDate "2020-09-18T10:46:06.442289Z"^^xsd:dateTime; + knora-base:valueHasOrder 0; + knora-base:valueHasString "I am writing this to myself."; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser . + + a knora-base:LinkValue; + knora-base:valueHasUUID "Cvp07eTqQQSnYdcvqlWW_g"^^xsd:string; + knora-base:isDeleted false; + rdf:subject ; + rdf:predicate beol:hasAuthor; + rdf:object ; + knora-base:valueCreationDate "2020-09-18T10:46:06.442289Z"^^xsd:dateTime; + knora-base:valueHasOrder 0; + knora-base:valueHasRefCount 1; + knora-base:valueHasString "http://rdfh.ch/0801/VvYVIy-FSbOJBsh2d9ZFJw"; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser . + + a knora-base:LinkValue; + knora-base:valueHasUUID "HqYA8dx1QzGzb9hqZNLDzA"^^xsd:string; + knora-base:isDeleted false; + rdf:subject ; + rdf:predicate beol:hasRecipient; + rdf:object ; + knora-base:valueCreationDate "2020-09-18T10:46:06.442289Z"^^xsd:dateTime; + knora-base:valueHasOrder 0; + knora-base:valueHasRefCount 1; + knora-base:valueHasString "http://rdfh.ch/0801/VvYVIy-FSbOJBsh2d9ZFJw"; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser . + + a knora-base:DateValue; + knora-base:valueHasUUID "5_JuhLykRBmhJs_b9X0qhg"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-09-18T10:46:06.442289Z"^^xsd:dateTime; + knora-base:valueHasCalendar "GREGORIAN"; + knora-base:valueHasEndJDN 2458274; + knora-base:valueHasEndPrecision "DAY"; + knora-base:valueHasOrder 0; + knora-base:valueHasStartJDN 2458274; + knora-base:valueHasStartPrecision "DAY"; + knora-base:valueHasString "2018-06-04 CE"; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser . + + a knora-base:TextValue; + knora-base:valueHasUUID "jGtT_87FS2qTkUEigdXMPg"^^xsd:string; + knora-base:isDeleted false; + knora-base:valueCreationDate "2020-09-18T10:46:06.442289Z"^^xsd:dateTime; + knora-base:valueHasOrder 0; + knora-base:valueHasString "letter to self"; + knora-base:hasPermissions "CR knora-admin:Creator|V knora-admin:UnknownUser"; + knora-base:attachedToUser . + a beol:letter; rdfs:label "Test letter"; knora-base:isDeleted false; diff --git a/test_data/searchR2RV2/LetterNotToSelf.jsonld b/test_data/searchR2RV2/LetterNotToSelf.jsonld new file mode 100644 index 0000000000..4faa8856b5 --- /dev/null +++ b/test_data/searchR2RV2/LetterNotToSelf.jsonld @@ -0,0 +1,126 @@ +{ + "@id" : "http://rdfh.ch/0801/_B3lQa6tSymIq7_7SowBsA", + "@type" : "beol:letter", + "beol:hasAuthorValue" : { + "@id" : "http://rdfh.ch/0801/_B3lQa6tSymIq7_7SowBsA/values/Cvp07eTqQQSnYdcvqlWW_g", + "@type" : "knora-api:LinkValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/_B3lQa6tSymIq7_7SowBsAD/Cvp07eTqQQSnYdcvqlWW_gI" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/PSGbemdjZi4kQ6GHJVkLGF" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:linkValueHasTarget" : { + "@id" : "http://rdfh.ch/0801/VvYVIy-FSbOJBsh2d9ZFJw", + "@type" : "beol:person", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/VvYVIy=FSbOJBsh2d9ZFJwi" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/PSGbemdjZi4kQ6GHJVkLGF" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-06-04T08:56:22.513Z" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:userHasPermission" : "RV", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/VvYVIy=FSbOJBsh2d9ZFJwi.20180604T085622513Z" + }, + "rdfs:label" : "Testperson2" + }, + "knora-api:userHasPermission" : "RV", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-06-04T08:56:33.879Z" + }, + "knora-api:valueHasUUID" : "Cvp07eTqQQSnYdcvqlWW_g", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/_B3lQa6tSymIq7_7SowBsAD/Cvp07eTqQQSnYdcvqlWW_gI.20180604T085633879Z" + } + }, + "beol:hasRecipientValue" : { + "@id" : "http://rdfh.ch/0801/_B3lQa6tSymIq7_7SowBsA/values/DVqPKuBBSIauVqjUC7bNvA", + "@type" : "knora-api:LinkValue", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/_B3lQa6tSymIq7_7SowBsAD/DVqPKuBBSIauVqjUC7bNvAe" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/PSGbemdjZi4kQ6GHJVkLGF" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:linkValueHasTarget" : { + "@id" : "http://rdfh.ch/0801/H7s3FmuWTkaCXa54eFANOA", + "@type" : "beol:person", + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/H7s3FmuWTkaCXa54eFANOAd" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/PSGbemdjZi4kQ6GHJVkLGF" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-06-04T08:55:34.086Z" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:userHasPermission" : "RV", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/H7s3FmuWTkaCXa54eFANOAd.20180604T085534086Z" + }, + "rdfs:label" : "Testperson1" + }, + "knora-api:userHasPermission" : "RV", + "knora-api:valueCreationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-06-04T08:56:33.879Z" + }, + "knora-api:valueHasUUID" : "DVqPKuBBSIauVqjUC7bNvA", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/_B3lQa6tSymIq7_7SowBsAD/DVqPKuBBSIauVqjUC7bNvAe.20180604T085633879Z" + } + }, + "knora-api:arkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/_B3lQa6tSymIq7_7SowBsAD" + }, + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/yTerZGyxjZVqFMNNKXCDPF" + }, + "knora-api:attachedToUser" : { + "@id" : "http://rdfh.ch/users/PSGbemdjZi4kQ6GHJVkLGF" + }, + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2018-06-04T08:56:33.879Z" + }, + "knora-api:hasPermissions" : "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser", + "knora-api:userHasPermission" : "RV", + "knora-api:versionArkUrl" : { + "@type" : "xsd:anyURI", + "@value" : "http://0.0.0.0:3336/ark:/72163/1/0801/_B3lQa6tSymIq7_7SowBsAD.20180604T085633879Z" + }, + "rdfs:label" : "Testbrief1", + "@context" : { + "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + "beol" : "http://0.0.0.0:3333/ontology/0801/beol/v2#", + "xsd" : "http://www.w3.org/2001/XMLSchema#" + } +} \ No newline at end of file diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala index 9b39568007..16f4ccf417 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala @@ -868,16 +868,12 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, typeInspectionResult.getTypeOfEntity(queryVar) match { case Some(typeInfo) => - // check if queryVar represents a property or a value + // Does queryVar represent a property? typeInfo match { - case propInfo: PropertyTypeInfo => - - // left arg queryVar is a variable representing a property - // therefore the right argument must be an IRI restricting the property variable to a certain property + // Yes. The right argument must be an IRI restricting the property variable to a certain property. compareExpression.rightArg match { case iriRef: IriRef => - handlePropertyIriQueryVar( queryVar = queryVar, comparisonOperator = compareExpression.operator, @@ -889,16 +885,21 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, } case nonPropInfo: NonPropertyTypeInfo => - - // Is the query using the API v2 simple schema? - if (querySchema == ApiV2Simple) { + // queryVar doesn't represent a property. Does it represent a resource? + if (nonPropInfo.isResourceType) { + // Yes. If the right argument is a variable or IRI, keep the expression as is. We know that the types of the + // arguments are consistent, because this already been checked during type inspection. + compareExpression.rightArg match { + case _: QueryVariable | _: IriRef => TransformedFilterPattern(Some(compareExpression)) + case other => throw GravsearchException(s"Invalid right argument ${other.toSparql} in comparison (expected a variable or IRI representing a resource)") + } + } else if (querySchema == ApiV2Simple) { // The left operand doesn't represent a resource. Is the query using the API v2 simple schema? // Yes. Depending on the value type, transform the given Filter pattern. // Add an extra level by getting the value literal from the value object. // If queryVar refers to a value object as a literal, for the value literal an extra variable has to be created, taking its type into account. nonPropInfo.typeIri.toString match { case OntologyConstants.Xsd.Integer => - handleLiteralQueryVar( queryVar = queryVar, comparisonOperator = compareExpression.operator, @@ -908,7 +909,6 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, ) case OntologyConstants.Xsd.Decimal => - handleLiteralQueryVar( queryVar = queryVar, comparisonOperator = compareExpression.operator, @@ -918,7 +918,6 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, ) case OntologyConstants.Xsd.DateTimeStamp => - handleLiteralQueryVar( queryVar = queryVar, comparisonOperator = compareExpression.operator, @@ -928,7 +927,6 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, ) case OntologyConstants.Xsd.Boolean => - handleLiteralQueryVar( queryVar = queryVar, comparisonOperator = compareExpression.operator, @@ -939,7 +937,6 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, ) case OntologyConstants.Xsd.String => - handleLiteralQueryVar( queryVar = queryVar, comparisonOperator = compareExpression.operator, @@ -950,7 +947,6 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, ) case OntologyConstants.Xsd.Uri => - handleLiteralQueryVar( queryVar = queryVar, comparisonOperator = compareExpression.operator, @@ -961,22 +957,17 @@ abstract class AbstractPrequeryGenerator(constructClause: ConstructClause, ) case OntologyConstants.KnoraApiV2Simple.Date => - handleDateQueryVar(queryVar = queryVar, comparisonOperator = compareExpression.operator, dateValueExpression = compareExpression.rightArg) case OntologyConstants.KnoraApiV2Simple.ListNode => - handleListQueryVar(queryVar = queryVar, comparisonOperator = compareExpression.operator, literalValueExpression = compareExpression.rightArg) case other => throw NotImplementedException(s"Value type $other not supported in FilterExpression") - } - } else { // The query is using the complex schema. Keep the expression as it is. TransformedFilterPattern(Some(compareExpression)) } - } case None => diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala index bb2213281f..151c9cc370 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala @@ -107,7 +107,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe /** * Infers the type of an entity if there is an `rdf:type` statement about it. */ - private class RdfTypeRule(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + private class InferTypeOfSubjectOfRdfTypePredicate(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { override def infer(entityToType: TypeableEntity, intermediateResult: IntermediateTypeInspectionResult, entityInfo: EntityInfoGetResponseV2, @@ -132,12 +132,12 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe if (classDef.isResourceClass) { // Yes. Infer rdf:type knora-api:Resource. val inferredType = NonPropertyTypeInfo(classDef.entityInfoContent.classIri, isResourceType = classDef.isResourceClass, isValueType = classDef.isValueClass) - log.debug("RdfTypeRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfSubjectOfRdfTypePredicate: {} {} .", entityToType, inferredType) Some(inferredType) } else if (classDef.isStandoffClass) { // It's not a resource class, it's a standoff class. Infer rdf:type knora-api:StandoffTag. val inferredType = NonPropertyTypeInfo(OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri) - log.debug("RdfTypeRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfSubjectOfRdfTypePredicate: {} {} .", entityToType, inferredType) Some(inferredType) } else { @@ -145,7 +145,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe if (GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(rdfType.toString)) { // Yes. Return it. val inferredType = NonPropertyTypeInfo(rdfType, isValueType = true) - log.debug("RdfTypeRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfSubjectOfRdfTypePredicate: {} {} .", entityToType, inferredType) Some(inferredType) } else { // No. This must mean it's not allowed in Gravsearch queries. @@ -162,7 +162,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // This isn't a Knora entity. If it's valid in a type inspection result, return it. val inferredType = NonPropertyTypeInfo(rdfType, isValueType = true) - log.debug("RdfTypeRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfSubjectOfRdfTypePredicate: {} {} .", entityToType, inferredType) Some(inferredType) } else { None @@ -186,7 +186,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe /** * Infers the `knora-api:objectType` of a property if the property's IRI is used as a predicate. */ - private class KnoraObjectTypeFromPropertyIriRule(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + private class InferTypeOfPropertyFromItsIri(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { override def infer(entityToType: TypeableEntity, intermediateResult: IntermediateTypeInspectionResult, entityInfo: EntityInfoGetResponseV2, @@ -205,7 +205,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe case Some(objectTypeIri: SmartIri) => val isValue = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(objectTypeIri.toString) val inferredType = PropertyTypeInfo(objectTypeIri = objectTypeIri, objectIsResourceType = readPropertyInfo.isLinkProp, objectIsValueType = isValue) - log.debug("KnoraObjectTypeFromPropertyIriRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfPropertyFromItsIri: {} {} .", entityToType, inferredType) Set(inferredType) case None => @@ -241,70 +241,110 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe * Infers an entity's type if the entity is used as the object of a statement and the predicate's * `knora-api:objectType` is known. */ - private class TypeOfObjectFromPropertyRule(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + private class InferTypeOfObjectFromPredicate(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { override def infer(entityToType: TypeableEntity, intermediateResult: IntermediateTypeInspectionResult, entityInfo: EntityInfoGetResponseV2, usageIndex: UsageIndex): IntermediateTypeInspectionResult = { - // for standoff links it is necessary to refine the determined types first. + // for standoff links it is necessary to refine the determined types first. TODO: why in this rule and not in all rules? val updatedIntermediateResult: IntermediateTypeInspectionResult = refineDeterminedTypes( intermediateResult = intermediateResult, entityInfo = entityInfo ) + /** + * Performs the inference for this rule on a set of statements. + */ + def inferFromStatements(statements: Set[StatementPattern]): Set[GravsearchEntityTypeInfo] = { + statements.flatMap { + statement => + // Is the predicate typeable? + GravsearchTypeInspectionUtil.maybeTypeableEntity(statement.pred) match { + case Some(typeablePred: TypeableEntity) => + // Yes. Do we have its types? + updatedIntermediateResult.entities.get(typeablePred) match { + case Some(entityTypes: Set[GravsearchEntityTypeInfo]) => + // Yes. Use the knora-api:objectType of each PropertyTypeInfo. + entityTypes.flatMap { + case propertyTypeInfo: PropertyTypeInfo => + val inferredType: GravsearchEntityTypeInfo = NonPropertyTypeInfo(propertyTypeInfo.objectTypeIri, isResourceType = propertyTypeInfo.objectIsResourceType, isValueType = propertyTypeInfo.objectIsValueType) + log.debug("InferTypeOfObjectFromPredicate: {} {} .", entityToType, inferredType) + Some(inferredType) + case _ => + None + } + + case _ => + // We don't have the predicate's type. + Set.empty[GravsearchEntityTypeInfo] + } + case None => + // The predicate isn't typeable. + Set.empty[GravsearchEntityTypeInfo] + } + } + } + // Has this entity been used as the object of one or more statements? - val inferredTypes: Set[GravsearchEntityTypeInfo] = usageIndex.objectIndex.get(entityToType) match { - case Some(statements) => + usageIndex.objectIndex.get(entityToType) match { + case Some(statements: Set[StatementPattern]) => // Yes. Try to infer type information from the predicate of each of those statements. - statements.flatMap { + // To keep track of which types are inferred from IRIs representing properties, and which ones + // are inferred from variables representing properties, partition the statements into ones whose + // predicates are IRIs and ones whose predicates are variables. + + val (statementsWithPropertyIris, statementsWithVariablesAsPredicates) = statements.partition { statement => + statement.pred match { + case _: IriRef => true + case _ => false + } + } - // Is the predicate typeable? - GravsearchTypeInspectionUtil.maybeTypeableEntity(statement.pred) match { - case Some(typeablePred: TypeableEntity) => - // Yes. Do we have its types? - updatedIntermediateResult.entities.get(typeablePred) match { - case Some(entityTypes: Set[GravsearchEntityTypeInfo]) => - // Yes. Use the knora-api:objectType of each PropertyTypeInfo. - entityTypes.flatMap { - case propertyTypeInfo: PropertyTypeInfo => - val inferredType: GravsearchEntityTypeInfo = NonPropertyTypeInfo(propertyTypeInfo.objectTypeIri, isResourceType = propertyTypeInfo.objectIsResourceType, isValueType = propertyTypeInfo.objectIsValueType) - log.debug("TypeOfObjectFromPropertyRule: {} {} .", entityToType, inferredType) - Some(inferredType) - case _ => - None - } + // Separately infer types from statements whose predicates are IRIs and statements whose + // predicates are variables. - case _ => - // We don't have the predicate's type. - Set.empty[GravsearchEntityTypeInfo] - } - case None => - // The predicate isn't typeable. - Set.empty[GravsearchEntityTypeInfo] - } + val typesInferredFromPropertyIris: Set[GravsearchEntityTypeInfo] = inferFromStatements(statementsWithPropertyIris) + val typesInferredFromVariablesAsPredicates: Set[GravsearchEntityTypeInfo] = inferFromStatements(statementsWithVariablesAsPredicates) + + // If any types were inferred from statements whose predicates are IRIs, update the + // intermediate type inspection result with that information. + + val intermediateResultWithTypesInferredFromPropertyIris = if (typesInferredFromPropertyIris.nonEmpty) { + updatedIntermediateResult.addTypes(entityToType, typesInferredFromPropertyIris, inferredFromPropertyIri = true) + } else { + updatedIntermediateResult } + val intermediateResultWithTypesInferredFromVariablesAsPredicates = intermediateResultWithTypesInferredFromPropertyIris.addTypes( + entityToType, + typesInferredFromVariablesAsPredicates + ) + + runNextRule( + entityToType = entityToType, + intermediateResult = intermediateResultWithTypesInferredFromVariablesAsPredicates, + entityInfo = entityInfo, + usageIndex = usageIndex + ) + case None => // This entity hasn't been used as a statement object, so this rule isn't relevant. - Set.empty[GravsearchEntityTypeInfo] + runNextRule( + entityToType = entityToType, + intermediateResult = updatedIntermediateResult, + entityInfo = entityInfo, + usageIndex = usageIndex + ) } - - runNextRule( - entityToType = entityToType, - intermediateResult = updatedIntermediateResult.addTypes(entityToType, inferredTypes, inferredFromProperty = true), - entityInfo = entityInfo, - usageIndex = usageIndex - ) } } - /** - * Infers an entity's type if the entity is used as the subject of a statement and - * the predicate's `knora-api:subjectType` is known. + * Infers an entity's type if the entity is used as the subject of a statement in which the predicate is a + * property IRI whose `knora-api:subjectType` is known. */ - private class TypeOfSubjectFromPropertyRule(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + private class InferTypeOfSubjectFromPredicateIri(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { override def infer(entityToType: TypeableEntity, intermediateResult: IntermediateTypeInspectionResult, entityInfo: EntityInfoGetResponseV2, @@ -328,7 +368,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // Yes. Use that type. val isValue = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(subjectTypeIri.toString) val inferredType = NonPropertyTypeInfo(subjectTypeIri, isResourceType = readPropertyInfo.isResourceProp, isValueType = isValue) - log.debug("TypeOfSubjectFromPropertyRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfSubjectFromPredicateIri: {} {} .", entityToType, inferredType) Some(inferredType) case None => @@ -355,7 +395,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe runNextRule( entityToType = entityToType, - intermediateResult = intermediateResult.addTypes(entityToType, inferredTypes, inferredFromProperty = true), + intermediateResult = intermediateResult.addTypes(entityToType, inferredTypes, inferredFromPropertyIri = true), entityInfo = entityInfo, usageIndex = usageIndex ) @@ -365,12 +405,12 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe /** * Infers the `knora-api:objectType` of a property variable or IRI if it's used with an object whose type is known. */ - private class KnoraObjectTypeFromObjectRule(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + private class InferTypeOfPredicateFromObject(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { override def infer(entityToType: TypeableEntity, intermediateResult: IntermediateTypeInspectionResult, entityInfo: EntityInfoGetResponseV2, usageIndex: UsageIndex): IntermediateTypeInspectionResult = { - // for standoff links it is necessary to refine the types first. + // for standoff links it is necessary to refine the types first. TODO: why in this rule and not in all rules? val updatedIntermediateResult: IntermediateTypeInspectionResult = refineDeterminedTypes( intermediateResult = intermediateResult, entityInfo = entityInfo @@ -392,7 +432,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe entityTypes.flatMap { case nonPropertyTypeInfo: NonPropertyTypeInfo => val inferredType: GravsearchEntityTypeInfo = PropertyTypeInfo(objectTypeIri = nonPropertyTypeInfo.typeIri, objectIsResourceType = nonPropertyTypeInfo.isResourceType, nonPropertyTypeInfo.isValueType) - log.debug("KnoraObjectTypeFromObjectRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfPredicateFromObject: {} {} .", entityToType, inferredType) Some(inferredType) case _ => @@ -424,14 +464,14 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe } /** - * Infers the types of entities from their use in FILTER expressions. + * Infers the types of entities if their type was already determined by examining a FILTER expression when + * constructing the usage index. */ - private class EntityTypeFromFilterRule(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + private class InferTypeOfEntityFromKnownTypeInFilter(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { override def infer(entityToType: TypeableEntity, intermediateResult: IntermediateTypeInspectionResult, entityInfo: EntityInfoGetResponseV2, usageIndex: UsageIndex): IntermediateTypeInspectionResult = { - // Do we have one or more types for this entity from a FILTER? val typesFromFilters: Set[GravsearchEntityTypeInfo] = usageIndex.typedEntitiesInFilters.get(entityToType) match { case Some(typesFromFilters: Set[SmartIri]) => @@ -440,7 +480,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe typeFromFilter => val isValue = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(typeFromFilter.toString) val inferredType = NonPropertyTypeInfo(typeFromFilter, isResourceType = !isValue, isValueType = isValue) - log.debug("EntityTypeFromFilterRule: {} {} .", entityToType, inferredType) + log.debug("InferTypeOfEntityFromKnownTypeInFilter: {} {} .", entityToType, inferredType) inferredType } @@ -449,8 +489,25 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe Set.empty[GravsearchEntityTypeInfo] } - // Is this entity a variable? - val typesFromPropertyIriComparisons: Set[GravsearchEntityTypeInfo] = entityToType match { + runNextRule( + entityToType = entityToType, + intermediateResult = intermediateResult.addTypes(entityToType, typesFromFilters), + entityInfo = entityInfo, + usageIndex = usageIndex + ) + } + } + + /** + * Infers a variable's type if it has been compared with a property IRI in a FILTER expression. + */ + private class InferTypeOfVariableFromComparisonWithPropertyIriInFilter(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + override def infer(entityToType: TypeableEntity, + intermediateResult: IntermediateTypeInspectionResult, + entityInfo: EntityInfoGetResponseV2, usageIndex: UsageIndex): IntermediateTypeInspectionResult = { + + val typesFromComparisons: Set[GravsearchEntityTypeInfo] = entityToType match { + // Is this entity a variable? case variableToType: TypeableVariable => // Yes. Has it been used as a predicate? usageIndex.predicateIndex.get(entityToType) match { @@ -470,7 +527,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // Yes. Use that type. val isValue = GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(objectTypeIri.toString) val inferredType = PropertyTypeInfo(objectTypeIri = objectTypeIri, objectIsResourceType = readPropertyInfo.isLinkProp, objectIsValueType = isValue) - log.debug("EntityTypeFromFilterRule: {} {} .", variableToType, inferredType) + log.debug("InferTypeOfEntityFromKnownTypeInFilter: {} {} .", variableToType, inferredType) Some(inferredType) case None => @@ -500,11 +557,43 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe Set.empty[GravsearchEntityTypeInfo] } - val inferredTypes: Set[GravsearchEntityTypeInfo] = typesFromFilters ++ typesFromPropertyIriComparisons + runNextRule( + entityToType = entityToType, + intermediateResult = intermediateResult.addTypes(entityToType, typesFromComparisons), + entityInfo = entityInfo, + usageIndex = usageIndex + ) + } + } + + /** + * Infers the type of a variable or IRI that has been compared with another variable or IRI in a FILTER expression. + */ + private class InferTypeOfEntityFromComparisonWithOtherEntityInFilter(nextRule: Option[InferenceRule]) extends InferenceRule(nextRule = nextRule) { + override def infer(entityToType: TypeableEntity, + intermediateResult: IntermediateTypeInspectionResult, + entityInfo: EntityInfoGetResponseV2, + usageIndex: UsageIndex): IntermediateTypeInspectionResult = { + // Has this entity been compared with one or more other entities in a FILTER? + val typesFromComparisons: Set[GravsearchEntityTypeInfo] = usageIndex.entitiesComparedInFilters.get(entityToType) match { + case Some(comparedEntities: Set[TypeableEntity]) => + // Yes. Get the types that have been inferred for those entities, if any. + val inferredTypes = comparedEntities.flatMap(comparedEntity => intermediateResult.entities(comparedEntity)) + + if (inferredTypes.nonEmpty) { + log.debug("InferTypeOfEntityFromComparisonWithOtherEntityInFilter: {} {} .", entityToType, inferredTypes) + } + + inferredTypes + + case None => + // This entity hasn't been compared with other entities in a FILTER. + Set.empty[GravsearchEntityTypeInfo] + } runNextRule( entityToType = entityToType, - intermediateResult = intermediateResult.addTypes(entityToType, inferredTypes), + intermediateResult = intermediateResult.addTypes(entityToType, typesFromComparisons), entityInfo = entityInfo, usageIndex = usageIndex ) @@ -547,24 +636,24 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe * @return the IRI of the inferred `knora-api:subjectType` of the property, or `None` if it could not inferred. */ def readPropertyInfoToSubjectType(readPropertyInfo: ReadPropertyInfoV2, entityInfo: EntityInfoGetResponseV2, querySchema: ApiV2Schema): Option[SmartIri] = { - - // It's not a resource property. Get the knora-api:subjectType that the ontology responder provided. + // Get the knora-api:subjectType that the ontology responder provided. readPropertyInfo.entityInfoContent.getPredicateIriObject(OntologyConstants.KnoraApiV2Simple.SubjectType.toSmartIri). orElse(readPropertyInfo.entityInfoContent.getPredicateIriObject(OntologyConstants.KnoraApiV2Complex.SubjectType.toSmartIri)) match { case Some(subjectType: SmartIri) => val subjectTypeStr = subjectType.toString - // Is it knora-api:Value or one of the knora-api:ValueBase classes? - if (subjectTypeStr == OntologyConstants.KnoraApiV2Complex.Value || OntologyConstants.KnoraApiV2Complex.ValueBaseClasses.contains(subjectTypeStr)) { - // Yes. Don't use it. - None - } else if (readPropertyInfo.isResourceProp) { + // Is it a resource class? + if (readPropertyInfo.isResourceProp) { + // Yes. Use it. Some(subjectType) + } else if (subjectTypeStr == OntologyConstants.KnoraApiV2Complex.Value || OntologyConstants.KnoraApiV2Complex.ValueBaseClasses.contains(subjectTypeStr)) { + // If it's knora-api:Value or one of the knora-api:ValueBase classes, don't use it. + None } else if (OntologyConstants.KnoraApiV2Complex.FileValueClasses.contains(subjectTypeStr)) { - // No. If it's a file value class, return the representation of file values in the specified schema. + // If it's a file value class, return the representation of file values in the specified schema. Some(getFileTypeForSchema(querySchema)) } else { - // It's not a file value class, either. Is it a standoff class? + // It's not any of those types. Is it a standoff class? val isStandoffClass: Boolean = entityInfo.classInfoMap.get(subjectType) match { case Some(classDef) => classDef.isStandoffClass case None => false @@ -574,7 +663,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // Yes. Infer knora-api:subjectType knora-api:StandoffTag. Some(OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri) } else if (GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(subjectTypeStr)) { - // It's not any of those. If it's valid in a type inspection result, return it. + // It's not any of those. If it's a value type, return it. Some(subjectType) } else { // It's not valid in a type inspection result. This must mean it's not allowed in Gravsearch queries. @@ -628,7 +717,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // Yes. return the object type resource class. Some(objectType) } else if (GravsearchTypeInspectionUtil.GravsearchValueTypeIris.contains(objectTypeStr)) { - // It's not any of those. If it's valid in a type inspection result, return it. + // It's not any of those. If it's a value type, return it. Some(objectType) } else { // No. This must mean it's not allowed in Gravsearch queries. @@ -644,17 +733,19 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // The inference rule pipeline for the first iteration. Includes rules that cannot return additional // information if they are run more than once. - private val firstIterationRulePipeline = new RdfTypeRule( - Some(new KnoraObjectTypeFromPropertyIriRule( - Some(new TypeOfSubjectFromPropertyRule( - Some(new EntityTypeFromFilterRule( - Some(new TypeOfObjectFromPropertyRule( - Some(new KnoraObjectTypeFromObjectRule(None))))))))))) + private val firstIterationRulePipeline = new InferTypeOfSubjectOfRdfTypePredicate( + Some(new InferTypeOfPropertyFromItsIri( + Some(new InferTypeOfSubjectFromPredicateIri( + Some(new InferTypeOfEntityFromKnownTypeInFilter( + Some(new InferTypeOfVariableFromComparisonWithPropertyIriInFilter( + Some(new InferTypeOfObjectFromPredicate( + Some(new InferTypeOfPredicateFromObject(None))))))))))))) // The inference rule pipeline for subsequent iterations. Excludes rules that cannot return additional // information if they are run more than once. - private val subsequentIterationRulePipeline = new TypeOfObjectFromPropertyRule( - Some(new KnoraObjectTypeFromObjectRule(None))) + private val subsequentIterationRulePipeline = new InferTypeOfObjectFromPredicate( + Some(new InferTypeOfPredicateFromObject( + Some(new InferTypeOfEntityFromComparisonWithOtherEntityInFilter(None))))) /** * An index of entity usage in a Gravsearch query. @@ -667,14 +758,16 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe * @param knoraPropertyVariablesInFilters a map of query variables to Knora property IRIs that they are compared to in * FILTER expressions. * @param typedEntitiesInFilters a map of entities to types found for them in FILTER expressions. + * @param entitiesComparedInFilters variables or IRIs that are compared to other variables or IRIs in FILTER expressions. */ - case class UsageIndex(knoraClassIris: Set[SmartIri] = Set.empty[SmartIri], - knoraPropertyIris: Set[SmartIri] = Set.empty[SmartIri], - subjectIndex: Map[TypeableEntity, Set[StatementPattern]] = Map.empty[TypeableEntity, Set[StatementPattern]], - predicateIndex: Map[TypeableEntity, Set[StatementPattern]] = Map.empty[TypeableEntity, Set[StatementPattern]], - objectIndex: Map[TypeableEntity, Set[StatementPattern]] = Map.empty[TypeableEntity, Set[StatementPattern]], - knoraPropertyVariablesInFilters: Map[TypeableVariable, Set[SmartIri]] = Map.empty[TypeableVariable, Set[SmartIri]], - typedEntitiesInFilters: Map[TypeableEntity, Set[SmartIri]] = Map.empty[TypeableEntity, Set[SmartIri]], + case class UsageIndex(knoraClassIris: Set[SmartIri] = Set.empty, + knoraPropertyIris: Set[SmartIri] = Set.empty, + subjectIndex: Map[TypeableEntity, Set[StatementPattern]] = Map.empty, + predicateIndex: Map[TypeableEntity, Set[StatementPattern]] = Map.empty, + objectIndex: Map[TypeableEntity, Set[StatementPattern]] = Map.empty, + knoraPropertyVariablesInFilters: Map[TypeableVariable, Set[SmartIri]] = Map.empty, + typedEntitiesInFilters: Map[TypeableEntity, Set[SmartIri]] = Map.empty, + entitiesComparedInFilters: Map[TypeableEntity, Set[TypeableEntity]] = Map.empty, querySchema: ApiV2Schema) override def inspectTypes(previousResult: IntermediateTypeInspectionResult, @@ -880,8 +973,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe */ def findCommonBaseResourceClass(typesToBeChecked: Set[GravsearchEntityTypeInfo]): SmartIri = { val baseClassesOfFirstType: Seq[SmartIri] = entityInfo.classInfoMap.get(iriOfGravsearchTypeInfo(typesToBeChecked.head)) match { - case Some(classDef: ReadClassInfoV2) => - classDef.allBaseClasses + case Some(classDef: ReadClassInfoV2) => classDef.allBaseClasses case _ => Seq.empty[SmartIri] } @@ -890,8 +982,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe (acc, aType) => // get class info of the type Iri val baseClassesOfType: Seq[SmartIri] = entityInfo.classInfoMap.get(iriOfGravsearchTypeInfo(aType)) match { - case Some(classDef: ReadClassInfoV2) => - classDef.allBaseClasses + case Some(classDef: ReadClassInfoV2) => classDef.allBaseClasses case _ => Seq.empty[SmartIri] } @@ -928,7 +1019,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe inconsistentEntities.keySet.foldLeft(lastResults) { (acc, typedEntity) => // all inconsistent types - val typesToBeChecked: Set[GravsearchEntityTypeInfo] = inconsistentEntities.getOrElse(typedEntity, Set.empty[GravsearchEntityTypeInfo]) + val typesToBeChecked: Set[GravsearchEntityTypeInfo] = inconsistentEntities.getOrElse(typedEntity, Set.empty) val commonBaseClassIri: SmartIri = findCommonBaseResourceClass(typesToBeChecked) // Are all inconsistent types NonPropertyTypeInfo and resourceType? @@ -982,7 +1073,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // iterate over all typeable entities, refine determined types for it by keeping only the specific types. intermediateResult.entities.keySet.foldLeft(intermediateResult) { (acc: IntermediateTypeInspectionResult, typedEntity: TypeableEntity) => - val types: Set[GravsearchEntityTypeInfo] = intermediateResult.entities.getOrElse(typedEntity, Set.empty[GravsearchEntityTypeInfo]) + val types: Set[GravsearchEntityTypeInfo] = intermediateResult.entities.getOrElse(typedEntity, Set.empty) types.foldLeft(acc) { (refinedResults: IntermediateTypeInspectionResult, currType: GravsearchEntityTypeInfo) => @@ -1055,34 +1146,34 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe * Collects information for the usage index from statements. * * @param statementPattern the pattern to be visited. - * @param acc the accumulator. - * @return the accumulator. + * @param usageIndex the usage index being constructed. + * @return an updated usage index. */ - override def visitStatementInWhere(statementPattern: StatementPattern, acc: UsageIndex): UsageIndex = { + override def visitStatementInWhere(statementPattern: StatementPattern, usageIndex: UsageIndex): UsageIndex = { // Index the statement by subject. - val subjectIndex: Map[TypeableEntity, Set[StatementPattern]] = addIndexEntry( + val subjectIndex: Map[TypeableEntity, Set[StatementPattern]] = addStatementIndexEntry( statementEntity = statementPattern.subj, statementPattern = statementPattern, - statementIndex = acc.subjectIndex + statementIndex = usageIndex.subjectIndex ) // Index the statement by predicate. - val predicateIndex: Map[TypeableEntity, Set[StatementPattern]] = addIndexEntry( + val predicateIndex: Map[TypeableEntity, Set[StatementPattern]] = addStatementIndexEntry( statementEntity = statementPattern.pred, statementPattern = statementPattern, - statementIndex = acc.predicateIndex + statementIndex = usageIndex.predicateIndex ) // Index the statement by object. - val objectIndex: Map[TypeableEntity, Set[StatementPattern]] = addIndexEntry( + val objectIndex: Map[TypeableEntity, Set[StatementPattern]] = addStatementIndexEntry( statementEntity = statementPattern.obj, statementPattern = statementPattern, - statementIndex = acc.objectIndex + statementIndex = usageIndex.objectIndex ) // If the statement's predicate is rdf:type, and its object is a Knora entity, add it to the // set of Knora class IRIs. - val knoraClassIris: Set[SmartIri] = acc.knoraClassIris ++ (statementPattern.pred match { + val knoraClassIris: Set[SmartIri] = usageIndex.knoraClassIris ++ (statementPattern.pred match { case IriRef(predIri, _) if predIri.toString == OntologyConstants.Rdf.Type => statementPattern.obj match { case IriRef(objIri, _) if objIri.isKnoraEntityIri => @@ -1096,7 +1187,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe // If the statement's predicate is a Knora property, and isn't a type annotation predicate or a Gravsearch option predicate, // add it to the set of Knora property IRIs. - val knoraPropertyIris: Set[SmartIri] = acc.knoraPropertyIris ++ (statementPattern.pred match { + val knoraPropertyIris: Set[SmartIri] = usageIndex.knoraPropertyIris ++ (statementPattern.pred match { case IriRef(predIri, _) if predIri.isKnoraEntityIri && !(GravsearchTypeInspectionUtil.TypeAnnotationProperties.allTypeAnnotationIris.contains(predIri.toString) || GravsearchTypeInspectionUtil.GravsearchOptionIris.contains(predIri.toString)) => @@ -1105,7 +1196,7 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe case _ => None }) - acc.copy( + usageIndex.copy( knoraClassIris = knoraClassIris, knoraPropertyIris = knoraPropertyIris, subjectIndex = subjectIndex, @@ -1121,14 +1212,14 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe * @param statementEntity the entity (subject, predicate, or object). * @param statementPattern the statement pattern. * @param statementIndex an accumulator for a statement index. - * @return an updated accumulator. + * @return an updated index entry. */ - private def addIndexEntry(statementEntity: Entity, - statementPattern: StatementPattern, - statementIndex: Map[TypeableEntity, Set[StatementPattern]]): Map[TypeableEntity, Set[StatementPattern]] = { + private def addStatementIndexEntry(statementEntity: Entity, + statementPattern: StatementPattern, + statementIndex: Map[TypeableEntity, Set[StatementPattern]]): Map[TypeableEntity, Set[StatementPattern]] = { GravsearchTypeInspectionUtil.maybeTypeableEntity(statementEntity) match { case Some(typeableEntity) => - val currentPatterns = statementIndex.getOrElse(typeableEntity, Set.empty[StatementPattern]) + val currentPatterns: Set[StatementPattern] = statementIndex.getOrElse(typeableEntity, Set.empty) statementIndex + (typeableEntity -> (currentPatterns + statementPattern)) case None => statementIndex @@ -1139,149 +1230,222 @@ class InferringGravsearchTypeInspector(nextInspector: Option[GravsearchTypeInspe * Collects information for the usage index from filters. * * @param filterPattern the pattern to be visited. - * @param acc the accumulator. - * @return the accumulator. + * @param usageIndex the usage index being constructed. + * @return an updated usage index. */ - override def visitFilter(filterPattern: FilterPattern, acc: UsageIndex): UsageIndex = { - visitFilterExpression(filterPattern.expression, acc) + override def visitFilter(filterPattern: FilterPattern, usageIndex: UsageIndex): UsageIndex = { + visitFilterExpression(filterPattern.expression, usageIndex) } /** - * Collects information for the usage index from filter expressions. + * Indexes two entities that are compared in a FILTER expression. * - * @param filterExpression the filter expression to be visited. - * @param acc the accumulator. - * @return the accumulator. + * @param leftQueryVariable the query variable that is the left argument of the comparison. + * @param rightEntity the query variable or IRI that is the right argument of the comparison. + * @param usageIndex the usage index being constructed. + * @return an an updated usage index. */ - private def visitFilterExpression(filterExpression: Expression, acc: UsageIndex): UsageIndex = { - filterExpression match { - case compareExpression: CompareExpression => - compareExpression match { - case CompareExpression(queryVariable: QueryVariable, operator: CompareExpressionOperator.Value, iriRef: IriRef) - if operator == CompareExpressionOperator.EQUALS && iriRef.iri.isKnoraEntityIri => - // A variable is compared to a Knora entity IRI, which must be a property IRI. - // Index the property IRI. - - val typeableVariable = TypeableVariable(queryVariable.variableName) - val currentIris: Set[SmartIri] = acc.knoraPropertyVariablesInFilters.getOrElse(typeableVariable, Set.empty[SmartIri]) - - acc.copy( - knoraPropertyIris = acc.knoraPropertyIris + iriRef.iri, - knoraPropertyVariablesInFilters = acc.knoraPropertyVariablesInFilters + (typeableVariable -> (currentIris + iriRef.iri)) - ) + private def addEntityComparisonIndexEntry(leftQueryVariable: QueryVariable, rightEntity: Entity, usageIndex: UsageIndex): UsageIndex = { + val leftTypeableVariable = TypeableVariable(leftQueryVariable.variableName) + + val rightTypeableEntity: TypeableEntity = GravsearchTypeInspectionUtil.maybeTypeableEntity(rightEntity) match { + case Some(typeableEntity) => typeableEntity + case None => throw GravsearchException(s"Entity ${rightEntity.toSparql} is not valid in a comparison expression") + } + + val currentComparisonsForLeftVariable: Set[TypeableEntity] = usageIndex.entitiesComparedInFilters.getOrElse(leftTypeableVariable, Set.empty) + val currentComparisonsForRightEntity: Set[TypeableEntity] = usageIndex.entitiesComparedInFilters.getOrElse(rightTypeableEntity, Set.empty) - case CompareExpression(queryVariable: QueryVariable, _, xsdLiteral: XsdLiteral) => + usageIndex.copy( + entitiesComparedInFilters = usageIndex.entitiesComparedInFilters + + (leftTypeableVariable -> (currentComparisonsForLeftVariable + rightTypeableEntity)) + + (rightTypeableEntity -> (currentComparisonsForRightEntity + leftTypeableVariable)) + ) + } + + /** + * Visits a [[CompareExpression]] in a [[FilterPattern]]. + * + * @param compareExpression the comparison expression to be visited. + * @param usageIndex the usage index being constructed. + * @return an updated usage index. + */ + private def visitCompareExpression(compareExpression: CompareExpression, usageIndex: UsageIndex): UsageIndex = { + compareExpression match { + case CompareExpression(leftQueryVariable: QueryVariable, operator: CompareExpressionOperator.Value, rightEntity: Entity) => + rightEntity match { + case xsdLiteral: XsdLiteral => // A variable is compared to an XSD literal. Index the variable and the literal's type. - val typeableVariable = TypeableVariable(queryVariable.variableName) - val currentVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(typeableVariable, Set.empty[SmartIri]) + val typeableVariable = TypeableVariable(leftQueryVariable.variableName) + val currentVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(typeableVariable, Set.empty) - acc.copy( - typedEntitiesInFilters = acc.typedEntitiesInFilters + (typeableVariable -> (currentVarTypesFromFilters + xsdLiteral.datatype)) + usageIndex.copy( + typedEntitiesInFilters = usageIndex.typedEntitiesInFilters + (typeableVariable -> (currentVarTypesFromFilters + xsdLiteral.datatype)) ) - case _ => - val accFromLeft = visitFilterExpression(compareExpression.leftArg, acc) - visitFilterExpression(compareExpression.rightArg, accFromLeft) - } + case rightIriRef: IriRef if rightIriRef.iri.isKnoraEntityIri => + // A variable is compared to a Knora ontology entity IRI, which must be a property IRI. + // Index the property IRI in usageIndex.knoraPropertyVariablesInFilters. - case functionCallExpression: FunctionCallExpression => - // One or more variables are used in functions. Index them and their types, if those can be determined from - // the function. - - functionCallExpression.functionIri.iri.toString match { - case OntologyConstants.KnoraApiV2Simple.MatchTextFunction => - // The first argument is a variable representing a string. - val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) - val currentTextVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(textVar, Set.empty[SmartIri]) - - acc.copy( - typedEntitiesInFilters = acc.typedEntitiesInFilters + - (textVar -> (currentTextVarTypesFromFilters + OntologyConstants.Xsd.String.toSmartIri)) - ) + if (operator != CompareExpressionOperator.EQUALS) { + throw GravsearchException(s"A Knora property IRI can be compared only with the equals operator") + } - case OntologyConstants.KnoraApiV2Complex.MatchTextFunction => - // The first argument is a variable representing a text value. - val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) - val currentTextVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(textVar, Set.empty[SmartIri]) + val typeableVariable = TypeableVariable(leftQueryVariable.variableName) + val currentIris: Set[SmartIri] = usageIndex.knoraPropertyVariablesInFilters.getOrElse(typeableVariable, Set.empty) - acc.copy( - typedEntitiesInFilters = acc.typedEntitiesInFilters + - (textVar -> (currentTextVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri)) + usageIndex.copy( + knoraPropertyIris = usageIndex.knoraPropertyIris + rightIriRef.iri, + knoraPropertyVariablesInFilters = usageIndex.knoraPropertyVariablesInFilters + (typeableVariable -> (currentIris + rightIriRef.iri)) ) - case OntologyConstants.KnoraApiV2Simple.MatchLabelFunction => - // The first argument is a variable representing a resource. - val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) - val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri]) - - acc.copy( - typedEntitiesInFilters = acc.typedEntitiesInFilters + - (resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Simple.Resource.toSmartIri)) + case rightQueryVariable: QueryVariable => + // Two variables are compared. Index them both in usageIndex.entitiesComparedInFilters. + addEntityComparisonIndexEntry( + leftQueryVariable = leftQueryVariable, + rightEntity = rightQueryVariable, + usageIndex = usageIndex ) - case OntologyConstants.KnoraApiV2Complex.MatchLabelFunction => - // The first argument is a variable representing a resource. - val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) - val currentResourceVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty[SmartIri]) + case rightIriRef: IriRef => + // A variable is compared with an IRI, which must be a resource IRI. + // Index them both in usageIndex.entitiesComparedInFilters. + + if (!rightIriRef.iri.isKnoraResourceIri) { + throw GravsearchException(s"IRI ${rightIriRef.toSparql}, used in a comparison, is not a Knora resource IRI") + } - acc.copy( - typedEntitiesInFilters = acc.typedEntitiesInFilters + - (resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri)) + addEntityComparisonIndexEntry( + leftQueryVariable = leftQueryVariable, + rightEntity = rightIriRef, + usageIndex = usageIndex ) + } - case OntologyConstants.KnoraApiV2Complex.MatchTextInStandoffFunction => - // The first argument is a variable representing a text value. - val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) - val currentTextVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(textVar, Set.empty[SmartIri]) + case _ => + val usageIndexFromLeft = visitFilterExpression(compareExpression.leftArg, usageIndex) + visitFilterExpression(compareExpression.rightArg, usageIndexFromLeft) + } + } - // The second argument is a variable representing a standoff tag. - val standoffTagVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(1).variableName) - val currentStandoffVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(standoffTagVar, Set.empty[SmartIri]) + /** + * Visits a [[FunctionCallExpression]] in a [[FilterPattern]]. + * + * @param functionCallExpression the function call to be visited. + * @param usageIndex the usage index being constructed. + * @return an updated usage index. + */ + private def visitFunctionCallExpression(functionCallExpression: FunctionCallExpression, usageIndex: UsageIndex): UsageIndex = { + functionCallExpression.functionIri.iri.toString match { + case OntologyConstants.KnoraApiV2Simple.MatchTextFunction => + // The first argument is a variable representing a string. + val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) + val currentTextVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(textVar, Set.empty) + + usageIndex.copy( + typedEntitiesInFilters = usageIndex.typedEntitiesInFilters + + (textVar -> (currentTextVarTypesFromFilters + OntologyConstants.Xsd.String.toSmartIri)) + ) - acc.copy( - typedEntitiesInFilters = acc.typedEntitiesInFilters + - (textVar -> (currentTextVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri)) + - (standoffTagVar -> (currentStandoffVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri)) - ) + case OntologyConstants.KnoraApiV2Complex.MatchTextFunction => + // The first argument is a variable representing a text value. + val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) + val currentTextVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(textVar, Set.empty) - case OntologyConstants.KnoraApiV2Complex.StandoffLinkFunction => - if (functionCallExpression.args.size != 3) throw GravsearchException(s"Three arguments are expected for ${functionCallExpression.functionIri.toSparql}") + usageIndex.copy( + typedEntitiesInFilters = usageIndex.typedEntitiesInFilters + + (textVar -> (currentTextVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri)) + ) - // The first and third arguments are variables or IRIs representing resources. - val resourceEntitiesAndTypes: Seq[(TypeableEntity, Set[SmartIri])] = Seq(functionCallExpression.args.head, functionCallExpression.args(2)).flatMap { - entity => GravsearchTypeInspectionUtil.maybeTypeableEntity(entity) - }.map { - typeableEntity => - val currentVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(typeableEntity, Set.empty[SmartIri]) - typeableEntity -> (currentVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri) - } + case OntologyConstants.KnoraApiV2Simple.MatchLabelFunction => + // The first argument is a variable representing a resource. + val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) + val currentResourceVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty) - // The second argument is a variable representing a standoff tag. - val standoffTagVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(1).variableName) - val currentStandoffVarTypesFromFilters: Set[SmartIri] = acc.typedEntitiesInFilters.getOrElse(standoffTagVar, Set.empty[SmartIri]) + usageIndex.copy( + typedEntitiesInFilters = usageIndex.typedEntitiesInFilters + + (resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Simple.Resource.toSmartIri)) + ) - acc.copy( - typedEntitiesInFilters = acc.typedEntitiesInFilters ++ resourceEntitiesAndTypes + - (standoffTagVar -> (currentStandoffVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri)) - ) + case OntologyConstants.KnoraApiV2Complex.MatchLabelFunction => + // The first argument is a variable representing a resource. + val resourceVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) + val currentResourceVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(resourceVar, Set.empty) + + usageIndex.copy( + typedEntitiesInFilters = usageIndex.typedEntitiesInFilters + + (resourceVar -> (currentResourceVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri)) + ) + + case OntologyConstants.KnoraApiV2Complex.MatchTextInStandoffFunction => + // The first argument is a variable representing a text value. + val textVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(0).variableName) + val currentTextVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(textVar, Set.empty) + + // The second argument is a variable representing a standoff tag. + val standoffTagVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(1).variableName) + val currentStandoffVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(standoffTagVar, Set.empty) + + usageIndex.copy( + typedEntitiesInFilters = usageIndex.typedEntitiesInFilters + + (textVar -> (currentTextVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri)) + + (standoffTagVar -> (currentStandoffVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri)) + ) - case OntologyConstants.KnoraApiV2Complex.ToSimpleDateFunction => - // The function knora-api:toSimpleDate can take either a knora-api:DateValue or a knora-api:StandoffTag, - // so we don't infer the type of its argument. - acc + case OntologyConstants.KnoraApiV2Complex.StandoffLinkFunction => + if (functionCallExpression.args.size != 3) throw GravsearchException(s"Three arguments are expected for ${functionCallExpression.functionIri.toSparql}") - case _ => throw GravsearchException(s"Unrecognised function: ${functionCallExpression.functionIri.toSparql}") + // The first and third arguments are variables or IRIs representing resources. + val resourceEntitiesAndTypes: Seq[(TypeableEntity, Set[SmartIri])] = Seq(functionCallExpression.args.head, functionCallExpression.args(2)).flatMap { + entity => GravsearchTypeInspectionUtil.maybeTypeableEntity(entity) + }.map { + typeableEntity => + val currentVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(typeableEntity, Set.empty) + typeableEntity -> (currentVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri) } + // The second argument is a variable representing a standoff tag. + val standoffTagVar = TypeableVariable(functionCallExpression.getArgAsQueryVar(1).variableName) + val currentStandoffVarTypesFromFilters: Set[SmartIri] = usageIndex.typedEntitiesInFilters.getOrElse(standoffTagVar, Set.empty) + + usageIndex.copy( + typedEntitiesInFilters = usageIndex.typedEntitiesInFilters ++ resourceEntitiesAndTypes + + (standoffTagVar -> (currentStandoffVarTypesFromFilters + OntologyConstants.KnoraApiV2Complex.StandoffTag.toSmartIri)) + ) + + case OntologyConstants.KnoraApiV2Complex.ToSimpleDateFunction => + // The function knora-api:toSimpleDate can take either a knora-api:DateValue or a knora-api:StandoffTag, + // so we don't infer the type of its argument. + usageIndex + + case _ => throw GravsearchException(s"Unrecognised function: ${functionCallExpression.functionIri.toSparql}") + } + } + + /** + * Collects information for the usage index from filter expressions. + * + * @param filterExpression the filter expression to be visited. + * @param usageIndex the usage index being constructed. + * @return an updated usage index. + */ + private def visitFilterExpression(filterExpression: Expression, usageIndex: UsageIndex): UsageIndex = { + filterExpression match { + case compareExpression: CompareExpression => + visitCompareExpression(compareExpression = compareExpression, usageIndex = usageIndex) + + case functionCallExpression: FunctionCallExpression => + visitFunctionCallExpression(functionCallExpression = functionCallExpression, usageIndex = usageIndex) + case andExpression: AndExpression => - val accFromLeft = visitFilterExpression(andExpression.leftArg, acc) - visitFilterExpression(andExpression.rightArg, accFromLeft) + val usageIndexFromLeft = visitFilterExpression(andExpression.leftArg, usageIndex) + visitFilterExpression(filterExpression = andExpression.rightArg, usageIndex = usageIndexFromLeft) case orExpression: OrExpression => - val accFromLeft = visitFilterExpression(orExpression.leftArg, acc) - visitFilterExpression(orExpression.rightArg, accFromLeft) + val usageIndexFromLeft = visitFilterExpression(orExpression.leftArg, usageIndex) + visitFilterExpression(filterExpression = orExpression.rightArg, usageIndex = usageIndexFromLeft) - case _ => acc + case _ => usageIndex } } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/IntermediateTypeInspectionResult.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/IntermediateTypeInspectionResult.scala index 3d1ec26085..6f3ca56d00 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/IntermediateTypeInspectionResult.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/IntermediateTypeInspectionResult.scala @@ -30,31 +30,36 @@ import org.knora.webapi.messages.{OntologyConstants, StringFormatter} * * @param entities a map of Gravsearch entities to the types that were determined for them. If an entity * has more than one type, this means that it has been used with inconsistent types. + * @param entitiesInferredFromPropertyIris entities whose types were inferred from their use with a property IRI. */ case class IntermediateTypeInspectionResult(entities: Map[TypeableEntity, Set[GravsearchEntityTypeInfo]], - entitiesInferredFromProperties: Map[TypeableEntity, Set[GravsearchEntityTypeInfo]] = Map.empty) { + entitiesInferredFromPropertyIris: Map[TypeableEntity, Set[GravsearchEntityTypeInfo]] = Map.empty) { /** * Adds types for an entity. * * @param entity the entity for which types have been found. * @param entityTypes the types to be added. - * @param inferredFromProperty `true` if any of the types of this entity were inferred from its use with a property. + * @param inferredFromPropertyIri `true` if any of the types of this entity were inferred from its use with a property IRI. * @return a new [[IntermediateTypeInspectionResult]] containing the additional type information. */ - def addTypes(entity: TypeableEntity, entityTypes: Set[GravsearchEntityTypeInfo], inferredFromProperty: Boolean = false): IntermediateTypeInspectionResult = { - val newTypes = entities.getOrElse(entity, Set.empty[GravsearchEntityTypeInfo]) ++ entityTypes + def addTypes(entity: TypeableEntity, entityTypes: Set[GravsearchEntityTypeInfo], inferredFromPropertyIri: Boolean = false): IntermediateTypeInspectionResult = { + if (entityTypes.nonEmpty) { + val newTypes = entities.getOrElse(entity, Set.empty[GravsearchEntityTypeInfo]) ++ entityTypes - val newEntitiesInferredFromProperties = if (inferredFromProperty && entityTypes.nonEmpty) { - val newTypesInferredFromProperty = entitiesInferredFromProperties.getOrElse(entity, Set.empty[GravsearchEntityTypeInfo]) ++ entityTypes - entitiesInferredFromProperties + (entity -> newTypesInferredFromProperty) + val newEntitiesInferredFromPropertyIris = if (inferredFromPropertyIri && entityTypes.nonEmpty) { + val newTypesInferredFromPropertyIris = entitiesInferredFromPropertyIris.getOrElse(entity, Set.empty[GravsearchEntityTypeInfo]) ++ entityTypes + entitiesInferredFromPropertyIris + (entity -> newTypesInferredFromPropertyIris) + } else { + entitiesInferredFromPropertyIris + } + + IntermediateTypeInspectionResult( + entities = entities + (entity -> newTypes), + entitiesInferredFromPropertyIris = newEntitiesInferredFromPropertyIris + ) } else { - entitiesInferredFromProperties + this } - - IntermediateTypeInspectionResult( - entities = entities + (entity -> newTypes), - entitiesInferredFromProperties = newEntitiesInferredFromProperties - ) } /** @@ -67,21 +72,21 @@ case class IntermediateTypeInspectionResult(entities: Map[TypeableEntity, Set[Gr def removeType(entity: TypeableEntity, typeToRemove: GravsearchEntityTypeInfo): IntermediateTypeInspectionResult = { val remainingTypes = entities.getOrElse(entity, Set.empty[GravsearchEntityTypeInfo]) - typeToRemove - val updatedEntitiesInferredFromProperties = if (entitiesInferredFromProperties.exists(aType => aType._1 == entity && aType._2.contains(typeToRemove))) { - val remainingTypesInferredFromProperty: Set[GravsearchEntityTypeInfo] = entitiesInferredFromProperties.getOrElse(entity, Set.empty[GravsearchEntityTypeInfo]) - typeToRemove + val updatedEntitiesInferredFromProperties = if (entitiesInferredFromPropertyIris.exists(aType => aType._1 == entity && aType._2.contains(typeToRemove))) { + val remainingTypesInferredFromProperty: Set[GravsearchEntityTypeInfo] = entitiesInferredFromPropertyIris.getOrElse(entity, Set.empty[GravsearchEntityTypeInfo]) - typeToRemove if (remainingTypesInferredFromProperty.nonEmpty) { - entitiesInferredFromProperties + (entity -> remainingTypesInferredFromProperty) + entitiesInferredFromPropertyIris + (entity -> remainingTypesInferredFromProperty) } else { - entitiesInferredFromProperties - entity + entitiesInferredFromPropertyIris - entity } } else { - entitiesInferredFromProperties + entitiesInferredFromPropertyIris } IntermediateTypeInspectionResult( entities = entities + (entity -> remainingTypes), - entitiesInferredFromProperties = updatedEntitiesInferredFromProperties + entitiesInferredFromPropertyIris = updatedEntitiesInferredFromProperties ) } @@ -117,7 +122,7 @@ case class IntermediateTypeInspectionResult(entities: Map[TypeableEntity, Set[Gr throw AssertionException(s"Cannot generate final type inspection result because of inconsistent types") } }, - entitiesInferredFromProperties = entitiesInferredFromProperties + entitiesInferredFromProperties = entitiesInferredFromPropertyIris ) } } diff --git a/webapi/src/test/resources/logback-test.xml b/webapi/src/test/resources/logback-test.xml index 33164ba88c..715feaeb27 100644 --- a/webapi/src/test/resources/logback-test.xml +++ b/webapi/src/test/resources/logback-test.xml @@ -88,7 +88,8 @@ - + + diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index 7fa8c04f32..ebb3582eee 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -2499,68 +2499,6 @@ class SearchRouteV2R2RSpec extends R2RSpec { } } - "do a Gravsearch query for a letter that links to a person with a specified name (optional)" in { - - val gravsearchQuery = - """ - |PREFIX beol: - |PREFIX knora-api: - |PREFIX xsd: - | - | CONSTRUCT { - | ?letter knora-api:isMainResource true . - | - | ?letter beol:creationDate ?date . - | - | ?letter ?linkingProp1 ?person1 . - | - | ?person1 beol:hasFamilyName ?name . - | - | } WHERE { - | ?letter a knora-api:Resource . - | ?letter a beol:letter . - | - | ?letter beol:creationDate ?date . - | - | beol:creationDate knora-api:objectType knora-api:Date . - | ?date a knora-api:Date . - | - | ?letter ?linkingProp1 ?person1 . - | - | ?person1 a knora-api:Resource . - | - | ?linkingProp1 knora-api:objectType knora-api:Resource . - | FILTER(?linkingProp1 = beol:hasAuthor || ?linkingProp1 = beol:hasRecipient) - | - | beol:hasAuthor knora-api:objectType knora-api:Resource . - | beol:hasRecipient knora-api:objectType knora-api:Resource . - | - | OPTIONAL { - | ?person1 beol:hasFamilyName ?name . - | - | beol:hasFamilyName knora-api:objectType xsd:string . - | ?name a xsd:string . - | - | FILTER(?name = "Meier") - | } - | - | } ORDER BY ?date - """.stripMargin - - - Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) ~> searchPath ~> check { - - assert(status == StatusCodes.OK, response.toString) - - val expectedAnswerJSONLD = readOrWriteTextFile(responseAs[String], new File("test_data/searchR2RV2/letterWithPersonWithNameOptional.jsonld"), writeTestDataFiles) - - compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAs[String]) - - checkSearchResponseNumberOfResults(responseAs[String], 1) - - } - } - "do a Gravsearch query for a letter that links to another person with a specified name" in { val gravsearchQuery = @@ -4889,55 +4827,6 @@ class SearchRouteV2R2RSpec extends R2RSpec { } } - "do a Gravsearch query for a letter that links to a person with a specified name (optional) (with type inference)" in { - - val gravsearchQuery = - """ - |PREFIX beol: - |PREFIX knora-api: - |PREFIX xsd: - | - | CONSTRUCT { - | ?letter knora-api:isMainResource true . - | - | ?letter beol:creationDate ?date . - | - | ?letter ?linkingProp1 ?person1 . - | - | ?person1 beol:hasFamilyName ?name . - | - | } WHERE { - | ?letter a beol:letter . - | - | ?letter beol:creationDate ?date . - | - | ?letter ?linkingProp1 ?person1 . - | - | FILTER(?linkingProp1 = beol:hasAuthor || ?linkingProp1 = beol:hasRecipient) - | - | OPTIONAL { - | ?person1 beol:hasFamilyName ?name . - | - | FILTER(?name = "Meier") - | } - | - | } ORDER BY ?date - """.stripMargin - - - Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) ~> searchPath ~> check { - - assert(status == StatusCodes.OK, response.toString) - - val expectedAnswerJSONLD = readOrWriteTextFile(responseAs[String], new File("test_data/searchR2RV2/letterWithPersonWithNameOptional.jsonld"), writeTestDataFiles) - - compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAs[String]) - - checkSearchResponseNumberOfResults(responseAs[String], 1) - - } - } - "do a Gravsearch query for a letter that links to another person with a specified name (with type inference)" in { val gravsearchQuery = @@ -6945,55 +6834,6 @@ class SearchRouteV2R2RSpec extends R2RSpec { } } - "do a Gravsearch query for a letter that links to a person with a specified name (optional) (submitting the complex schema)" in { - - val gravsearchQuery = - """ - |PREFIX beol: - |PREFIX knora-api: - |PREFIX xsd: - | - | CONSTRUCT { - | ?letter knora-api:isMainResource true . - | - | ?letter beol:creationDate ?date . - | - | ?letter ?linkingProp1 ?person1 . - | - | ?person1 beol:hasFamilyName ?name . - | - | } WHERE { - | ?letter a beol:letter . - | - | ?letter beol:creationDate ?date . - | - | ?letter ?linkingProp1 ?person1 . - | - | FILTER(?linkingProp1 = beol:hasAuthor || ?linkingProp1 = beol:hasRecipient) - | - | OPTIONAL { - | ?person1 beol:hasFamilyName ?name . - | - | ?name knora-api:valueAsString "Meier" . - | } - | - | } ORDER BY ?date - """.stripMargin - - - Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) ~> searchPath ~> check { - - assert(status == StatusCodes.OK, response.toString) - - val expectedAnswerJSONLD = readOrWriteTextFile(responseAs[String], new File("test_data/searchR2RV2/letterWithPersonWithNameOptional.jsonld"), writeTestDataFiles) - - compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAs[String]) - - checkSearchResponseNumberOfResults(responseAs[String], 1) - - } - } - "do a Gravsearch query for a letter that links to another person with a specified name (submitting the complex schema)" in { val gravsearchQuery = @@ -8374,5 +8214,113 @@ class SearchRouteV2R2RSpec extends R2RSpec { compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) } } + + "perform a search that compares two variables representing resources (in the simple schema)" in { + val gravsearchQuery: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + |} WHERE { + | ?letter a beol:letter . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + | FILTER(?person1 != ?person2) . + |} + |OFFSET 0""".stripMargin + + // We should get one result, not including ("letter to self"). + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("test_data/searchR2RV2/LetterNotToSelf.jsonld"), writeTestDataFiles) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "perform a search that compares two variables representing resources (in the complex schema)" in { + val gravsearchQuery: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + |} WHERE { + | ?letter a beol:letter . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + | FILTER(?person1 != ?person2) . + |} + |OFFSET 0""".stripMargin + + // We should get one result, not including ("letter to self"). + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("test_data/searchR2RV2/LetterNotToSelf.jsonld")) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "perform a search that compares a variable with a resource IRI (in the simple schema)" in { + val gravsearchQuery: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + |} WHERE { + | ?letter a beol:letter . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + | FILTER(?person1 != ) . + |} + |OFFSET 0""".stripMargin + + // We should get one result, not including ("letter to self"). + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("test_data/searchR2RV2/LetterNotToSelf.jsonld")) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } + + "perform a search that compares a variable with a resource IRI (in the complex schema)" in { + val gravsearchQuery: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + |} WHERE { + | ?letter a beol:letter . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + | FILTER(?person1 != ) . + |} + |OFFSET 0""".stripMargin + + // We should get one result, not including ("letter to self"). + + Post("/v2/searchextended", HttpEntity(SparqlQueryConstants.`application/sparql-query`, gravsearchQuery)) ~> searchPath ~> check { + val searchResponseStr = responseAs[String] + assert(status == StatusCodes.OK, searchResponseStr) + val expectedAnswerJSONLD = readOrWriteTextFile(searchResponseStr, new File("test_data/searchR2RV2/LetterNotToSelf.jsonld")) + compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = searchResponseStr) + } + } } } diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala index 3b8e807f39..e3aa7aaa8c 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala @@ -396,6 +396,285 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { |} """.stripMargin + val QueryComparingResourcesInSimpleSchema: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + |} WHERE { + | ?letter a beol:letter . + | ?letter beol:hasAuthor ?person1 . + | ?letter ?prop ?person2 . + | FILTER(?person1 != ?person2) . + |} + |OFFSET 0""".stripMargin + + val QueryComparingResourcesInSimpleSchemaResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "person2") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "person1") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "letter") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "prop") -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ) + ), + entitiesInferredFromProperties = Map(TypeableVariable(variableName = "person1") -> Set(NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ))) + ) + + val QueryComparingResourcesInComplexSchema: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + |} WHERE { + | ?letter a beol:letter . + | ?letter ?prop ?person1 . + | ?letter beol:hasRecipient ?person2 . + | FILTER(?person2 != ?person1) . + |} + |OFFSET 0""".stripMargin + + val QueryComparingResourcesInComplexSchemaResult: GravsearchTypeInspectionResult = GravsearchTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "person2") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "person1") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/v2#hasRecipient".toSmartIri) -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ), + TypeableVariable(variableName = "letter") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#letter".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "prop") -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ) + ), + entitiesInferredFromProperties = Map(TypeableVariable(variableName = "person2") -> Set(NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ))) + ) + + val QueryComparingResourceIriInSimpleSchema: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + |} WHERE { + | ?letter a beol:letter . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + | FILTER(?person1 != ) . + |} + |OFFSET 0""".stripMargin + + val QueryComparingResourceIriInSimpleSchemaResult: GravsearchTypeInspectionResult = + GravsearchTypeInspectionResult( + entities = Map( + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasRecipient".toSmartIri) -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ), + TypeableVariable(variableName = "person2") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "person1") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableIri(iri = "http://rdfh.ch/0801/F4n1xKa3TCiR4llJeElAGA".toSmartIri) -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "letter") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ) + ), + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "person2") -> Set(NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + )), + TypeableVariable(variableName = "person1") -> Set(NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + )) + ) + ) + + val QueryComparingResourceIriInComplexSchema: String = + """PREFIX beol: + |PREFIX knora-api: + | + |CONSTRUCT { + | ?letter knora-api:isMainResource true . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + |} WHERE { + | ?letter a beol:letter . + | ?letter beol:hasAuthor ?person1 . + | ?letter beol:hasRecipient ?person2 . + | FILTER(?person1 != ) . + |} + |OFFSET 0""".stripMargin + + val QueryComparingResourceIriInComplexSchemaResult: GravsearchTypeInspectionResult = + GravsearchTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "person2") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ), + TypeableVariable(variableName = "person1") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/v2#hasRecipient".toSmartIri) -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ), + TypeableIri(iri = "http://rdfh.ch/0801/F4n1xKa3TCiR4llJeElAGA".toSmartIri) -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "letter") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#letter".toSmartIri, + isResourceType = true, + isValueType = false + ) + ), + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "person1") -> Set(NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + )), + TypeableVariable(variableName = "person2") -> Set(NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + )) + ) + ) + + val QueryWithFilterComparison: String = + """PREFIX knora-api: + |PREFIX beol: + | + |CONSTRUCT { + | ?person knora-api:isMainResource true . + | ?document beol:hasAuthor ?person . + |} WHERE { + | ?person a beol:person . + | ?document beol:hasAuthor ?person . + | FILTER(?document != ) + |}""".stripMargin + + val QueryWithFilterComparisonResult: GravsearchTypeInspectionResult = + GravsearchTypeInspectionResult( + entities = Map( + TypeableVariable(variableName = "person") -> NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableVariable(variableName = "document") -> NonPropertyTypeInfo( + typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, + isResourceType = true, + isValueType = false + ), + TypeableIri(iri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#hasAuthor".toSmartIri) -> PropertyTypeInfo( + objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + objectIsResourceType = true, + objectIsValueType = false + ), + TypeableIri(iri = "http://rdfh.ch/0801/XNn6wanrTHWShGTjoULm5g".toSmartIri) -> NonPropertyTypeInfo( + typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, + isResourceType = true, + isValueType = false + ) + ), + entitiesInferredFromProperties = Map( + TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo( + typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, + isResourceType = true, + isValueType = false + )), + TypeableVariable(variableName = "person") -> Set(NonPropertyTypeInfo( + typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, + isResourceType = true, + isValueType = false + )) + ) + ) + val PathologicalQuery: String = """ |PREFIX incunabula: @@ -680,7 +959,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "mainRes") -> Set( + entitiesInferredFromPropertyIris = Map(TypeableVariable(variableName = "mainRes") -> Set( NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true), NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) ) @@ -697,7 +976,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { )) // Is it removed from entitiesInferredFromProperties? - intermediateTypesWithoutResource.entitiesInferredFromProperties should contain(TypeableVariable(variableName = "mainRes") -> Set( + intermediateTypesWithoutResource.entitiesInferredFromPropertyIris should contain(TypeableVariable(variableName = "mainRes") -> Set( NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) } @@ -713,7 +992,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true) ) ), - entitiesInferredFromProperties = Map( + entitiesInferredFromPropertyIris = Map( TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#basicLetter".toSmartIri, isResourceType = true)) ) ) @@ -726,7 +1005,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { assert(refinedIntermediateResults.entities.size == 1) refinedIntermediateResults.entities should contain(TypeableVariable(variableName = "letter") -> Set( NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) - assert(refinedIntermediateResults.entitiesInferredFromProperties.isEmpty) + assert(refinedIntermediateResults.entitiesInferredFromPropertyIris.isEmpty) } "sanitize inconsistent resource types that only have knora-base:Resource as base class in common" in { @@ -744,7 +1023,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { PropertyTypeInfo(objectTypeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#person".toSmartIri, objectIsResourceType = true) ) ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) + entitiesInferredFromPropertyIris = Map(TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) ) val refinedIntermediateResults = typeInspectionRunner.refineDeterminedTypes( @@ -761,7 +1040,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, isResourceType = true)), TypeableVariable(variableName = "linkingProp1") -> Set(PropertyTypeInfo(objectTypeIri = "http://api.knora.org/ontology/knora-api/simple/v2#Resource".toSmartIri, objectIsResourceType = true)) ), - entitiesInferredFromProperties = Map( + entitiesInferredFromPropertyIris = Map( TypeableVariable(variableName = "letter") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true)) ) ) @@ -780,7 +1059,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#manuscript".toSmartIri, isResourceType = true) ) ), - entitiesInferredFromProperties = Map(TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) + entitiesInferredFromPropertyIris = Map(TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true))) ) val refinedIntermediateResults = typeInspectionRunner.refineDeterminedTypes( @@ -796,7 +1075,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { entities = Map( TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#writtenSource".toSmartIri, isResourceType = true)) ), - entitiesInferredFromProperties = Map( + entitiesInferredFromPropertyIris = Map( TypeableVariable(variableName = "document") -> Set(NonPropertyTypeInfo(typeIri = "http://0.0.0.0:3333/ontology/0801/beol/simple/v2#letter".toSmartIri, isResourceType = true)) ) ) @@ -992,6 +1271,46 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { assert(result.entities == RdfsLabelWithVariableResult.entities) } + "infer the type of a variable when it is compared with another variable in a FILTER (in the simple schema)" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourcesInSimpleSchema) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result.entities == QueryComparingResourcesInSimpleSchemaResult.entities) + } + + "infer the type of a variable when it is compared with another variable in a FILTER (in the complex schema)" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourcesInComplexSchema) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result.entities == QueryComparingResourcesInComplexSchemaResult.entities) + } + + "infer the type of a resource IRI when it is compared with a variable in a FILTER (in the simple schema)" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourceIriInSimpleSchema) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result.entities == QueryComparingResourceIriInSimpleSchemaResult.entities) + } + + "infer the type of a resource IRI when it is compared with a variable in a FILTER (in the complex schema)" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourceIriInComplexSchema) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result.entities == QueryComparingResourceIriInComplexSchemaResult.entities) + } + + "infer knora-api:Resource as the subject type of a subproperty of knora-api:hasLinkTo" in { + val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) + val parsedQuery = GravsearchParser.parseQuery(QueryWithFilterComparison) + val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) + val result = Await.result(resultFuture, timeout) + assert(result.entities == QueryWithFilterComparisonResult.entities) + } + "reject a query with a non-Knora property whose type cannot be inferred" in { val typeInspectionRunner = new GravsearchTypeInspectionRunner(responderData = responderData, inferTypes = true) val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithoutAnnotation)