diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 0000000..628fe89
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,3 @@
+{
+ "extends": ["github>smessie/renovate-presets:js"]
+}
diff --git a/README.md b/README.md
index 64178cc..e633f71 100644
--- a/README.md
+++ b/README.md
@@ -86,16 +86,17 @@ const personShape = await shexShapeFromQuads(shexQuads, "http://example.org/Pers
The library returns a report where each star pattern is assigned one of the following `ContainmentResult` values:
-| Result | Description |
-| :------------- | :----------------------------------------------------------- |
-| **`CONTAIN`** | All triple patterns in the graph star pattern are covered by the shape's constraints. |
-| **`ALIGNED`** | At least one triple pattern matches, but some parts of the graph star pattern are not covered. |
-| **`DEPEND`** | The pattern is reachable via a property that links to another shape (nested containment). |
-| **`REJECTED`** | No part of the star pattern matches any property defined in the shape. |
+| Result | Description |
+| :----------------- | :---------- |
+| **`CONTAINED`** | All query star patterns, including nested ones, are matched by the shape. |
+| **`ALIGNED`** | At least one triple pattern from the root star pattern matches on an open shape. |
+| **`UNALINGED`** | Partial root star pattern match on a closed shape; or match on a nested star pattern while having no match on root star pattern. |
+| **`WEAKLY_REJECTED`** | None of the triple patterns match on an open shape. |
+| **`REJECTED`** | None of the triple patterns match on a closed shape. |
### Examples of Containment Results
-#### 1. `CONTAIN`
+#### 1. `CONTAINED`
The star pattern for `?person` is fully covered by the shape.
@@ -120,9 +121,44 @@ The star pattern for `?person` is fully covered by the shape.
sh:property [ sh:path foaf:mbox ] .
```
+Another `CONTAINED` case combines a FILTER expression with compatible shape constraints.
+
+* **Query**:
+
+ ```sparql
+ PREFIX ex:
+ PREFIX xsd:
+
+ SELECT * WHERE {
+ ?person ex:age ?age .
+ FILTER(?age > 18)
+ }
+ ```
+
+* **Shape**:
+
+ ```turtle
+ @prefix sh: .
+ @prefix xsd: .
+
+ a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ;
+ sh:datatype xsd:integer ;
+ sh:minInclusive 18 ;
+ sh:maxInclusive 35
+ ] .
+ ```
+
+Containment decisions do not use runtime FILTER truth values directly.
+Instead, FILTER expressions are only used when they can be checked against shape constraints.
+For example, a numeric comparison like `?age > 18` is compatible with an `xsd:integer` constraint,
+but can contradict a non-numeric datatype constraint.
+
#### 2. `ALIGNED`
-The query matches one property (`foaf:name`), but contains `ex:age` which is not defined in the closed shape.
+The query matches one property (`foaf:name`), but contains `ex:age` which is not defined in the open shape.
* **Query**:
@@ -139,20 +175,70 @@ The query matches one property (`foaf:name`), but contains `ex:age` which is not
```turtle
a sh:NodeShape ;
- sh:closed true ;
sh:property [ sh:path foaf:name ] .
```
-#### 3. `DEPEND`
+Another `ALIGNED` case with FILTER and constraints:
+
+* **Query**:
+
+ ```sparql
+ PREFIX ex:
+ PREFIX xsd:
+ SELECT * WHERE {
+ ?person ex:age ?age ;
+ ex:nickname ?nick .
+ FILTER(?age > 18)
+ }
+ ```
-The `?person` pattern matches the shape's link to another shape. Its full containment depends on whether `?friend` also matches its shape.
+* **Shape**:
+
+ ```turtle
+ @prefix sh: .
+ @prefix xsd: .
+
+ a sh:NodeShape ;
+ sh:property [
+ sh:path ;
+ sh:datatype xsd:integer
+ ] .
+ ```
+
+`ex:age` aligns and the numeric FILTER is compatible with the datatype constraint, while `ex:nickname` is not constrained by this open shape.
+
+#### 3. `UNALINGED`
+
+The root star pattern has partial matching triples against closed shapes.
* **Query**:
```sparql
PREFIX foaf:
+ PREFIX ex:
SELECT * WHERE {
- ?person foaf:knows ?friend .
+ ?person foaf:name ?name ;
+ ex:age ?age .
+ }
+ ```
+
+* **Shape**:
+
+ ```turtle
+ a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [ sh:path foaf:name ] .
+ ```
+
+Another `UNALINGED` case is when the root star pattern does not match, but a nested star pattern (reachable through a linked variable) does on a open or closed shape.
+
+* **Query**:
+
+ ```sparql
+ PREFIX ex:
+ PREFIX foaf:
+ SELECT * WHERE {
+ ?person ex:unknownLink ?friend .
?friend foaf:name ?friendName .
}
```
@@ -161,16 +247,93 @@ The `?person` pattern matches the shape's link to another shape. Its full contai
```turtle
a sh:NodeShape ;
- sh:property [
- sh:path foaf:knows ;
- sh:node
+ sh:property [
+ sh:path foaf:knows ;
+ sh:node
] .
-
+
a sh:NodeShape ;
sh:property [ sh:path foaf:name ] .
```
-#### 4. `REJECTED`
+Another `UNALINGED` case with FILTER and constraints:
+
+* **Query**:
+
+ ```sparql
+ PREFIX ex:
+ PREFIX xsd:
+ SELECT * WHERE {
+ ?person ex:age ?age ;
+ ex:status ?status .
+ FILTER(?age > 18)
+ }
+ ```
+
+* **Shape**:
+
+ ```turtle
+ @prefix sh: .
+ @prefix xsd: .
+
+ a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ;
+ sh:datatype xsd:integer
+ ] .
+ ```
+
+On a closed shape, `ex:age` matches but `ex:status` does not, so containment is partial (`UNALINGED`) even though the FILTER is constraint-compatible.
+
+#### 4. `WEAKLY_REJECTED`
+
+No triple pattern matches and at least one candidate shape is open.
+
+* **Query**:
+
+ ```sparql
+ PREFIX schema:
+ SELECT * WHERE {
+ ?person schema:birthDate ?date .
+ }
+ ```
+
+* **Shape**:
+
+ ```turtle
+ a sh:NodeShape ;
+ sh:property [ sh:path foaf:name ] .
+ ```
+
+Another `WEAKLY_REJECTED` case with FILTER and constraints:
+
+* **Query**:
+
+ ```sparql
+ PREFIX ex:
+ SELECT * WHERE {
+ ?person ex:age ?age .
+ FILTER(?age > 18)
+ }
+ ```
+
+* **Shape**:
+
+ ```turtle
+ @prefix sh: .
+ @prefix xsd: .
+
+ a sh:NodeShape ;
+ sh:property [
+ sh:path ;
+ sh:datatype xsd:string
+ ] .
+ ```
+
+The numeric FILTER contradicts the string datatype constraint. Because the shape is open, the result is `WEAKLY_REJECTED`.
+
+#### 5. `REJECTED`
The query uses `schema:birthDate`, but the shape only defines `foaf:name`.
@@ -187,14 +350,73 @@ The query uses `schema:birthDate`, but the shape only defines `foaf:name`.
```turtle
a sh:NodeShape ;
+ sh:closed true ;
sh:property [ sh:path foaf:name ] .
```
+Another `REJECTED` case with FILTER and constraints:
+
+* **Query**:
+
+ ```sparql
+ PREFIX ex:
+ SELECT * WHERE {
+ ?person ex:age ?age .
+ FILTER(?age > 18)
+ }
+ ```
+
+* **Shape**:
+
+ ```turtle
+ @prefix sh: .
+ @prefix xsd: .
+
+ a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ;
+ sh:datatype xsd:string
+ ] .
+ ```
+
+The same numeric FILTER/constraint contradiction on a closed shape yields `REJECTED`.
+
+Another `REJECTED` case due to min/max value constraints:
+
+* **Query**:
+
+ ```sparql
+ PREFIX ex:
+ SELECT * WHERE {
+ ?person ex:age ?age .
+ FILTER(?age > 35)
+ }
+ ```
+
+* **Shape**:
+
+ ```turtle
+ @prefix sh: .
+ @prefix xsd: .
+
+ a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ;
+ sh:datatype xsd:integer ;
+ sh:minInclusive 18 ;
+ sh:maxInclusive 35
+ ] .
+ ```
+
+The FILTER range (`> 35`) conflicts with the shape range (`18..35`), so the result is `REJECTED`.
+
## SPARQL Limitations
The detection logic is focused on **Triple Patterns** and **Star Patterns**. Currently, the following SPARQL features are not (yet) supported:
-- **Filter Expressions**: Logic inside `FILTER` clauses is not checked against shape constraints.
+- **Filter Expressions**: FILTERs are only used to detect contradictions with shape constraints (for example numeric comparisons against non-numeric datatype constraints). Expressions that cannot be safely compared to shape constraints are conservatively ignored for containment decisions.
- **Negative Patterns**: `MINUS` and `FILTER NOT EXISTS` are not used to determine containment.
- **Complex Property Paths**: While simple paths are supported, complex or recursive property paths are not considered yet.
- **Aggregates & Subqueries**: `GROUP BY`, `HAVING`, and subqueries are not processed.
diff --git a/lib/Binding.ts b/lib/Binding.ts
index 75205c1..c403e9d 100644
--- a/lib/Binding.ts
+++ b/lib/Binding.ts
@@ -1,5 +1,6 @@
-import { ConstraintType, IContraint, IPredicate, IShape, OneOfPathIndexed } from "./Shape";
+import { ConstraintType, IConstraint, IPredicate, IShape, OneOfPathIndexed } from "./Shape";
import { IStarPatternWithDependencies, type ITriple, Triple } from "./Triple";
+import { TYPE_DEFINITION } from "./constant";
/**
* A binding from a query to a shape
@@ -114,10 +115,6 @@ export class Bindings implements IBindings {
}
const negatedTriples: ITriple[] = [];
for (const { triple, dependencies } of starPattern.starPattern.values()) {
- if (!this.closedShape) {
- this.bindings.set(triple.predicate, triple);
- continue;
- }
if (triple.predicate === Triple.NEGATIVE_PREDICATE_SET) {
negatedTriples.push(triple);
continue;
@@ -132,17 +129,19 @@ export class Bindings implements IBindings {
}
}
- if (singlePredicate === undefined &&
- predicates.length === 0
- && triple.isOptional !== true &&
- !this.strict) {
- this.unboundTriple.push(triple);
- } else {
- if (singlePredicate !== undefined) {
- predicates.push(singlePredicate);
+ if (singlePredicate === undefined && predicates.length === 0) {
+ // Open shapes can ignore unknown predicates, but closed shapes keep
+ // tracking unmatched non-optional predicates.
+ if (this.closedShape && triple.isOptional !== true && !this.strict) {
+ this.unboundTriple.push(triple);
}
- this.evaluateConstraint(predicates, triple, linkedShape, shape, dependencies);
+ continue;
+ }
+
+ if (singlePredicate !== undefined) {
+ predicates.push(singlePredicate);
}
+ this.evaluateConstraint(predicates, triple, linkedShape, shape, dependencies);
@@ -169,7 +168,22 @@ export class Bindings implements IBindings {
const uncontainedUnionStarPatterns: IStarPatternWithDependencies[] = []
if (shape.closed === false) {
- this.fullyBounded = starPattern.starPattern.size !== 0;
+ let boundedUnion = true;
+ for (const unionBinding of this.unionBindings) {
+ boundedUnion = ((unionBinding.hasOneContained && !this.strict) ||
+ (this.strict && unionBinding.areAllContained)) && boundedUnion;
+ boundedUnionFull = boundedUnionFull && unionBinding.areAllContained;
+ if (!unionBinding.areAllContained) {
+ for (const binding of unionBinding.bindings) {
+ if (!binding.isFullyBounded()) {
+ uncontainedUnionStarPatterns.push(binding.starPattern);
+ }
+ }
+ }
+ }
+ this.fullyBounded = this.getBoundTriple().length === starPattern.starPattern.size
+ && starPattern.starPattern.size !== 0
+ && boundedUnion;
} else {
let boundedUnion = true;
for (const unionBinding of this.unionBindings) {
@@ -249,7 +263,7 @@ export class Bindings implements IBindings {
continue;
}
- const typeConstraintResult = Bindings.handleShapeType(constraint, triple);
+ const typeConstraintResult = Bindings.handleTypeConstraint(constraint, triple);
if (typeConstraintResult === ConstraintResult.NOT_RESPECT) {
validConstraint = validConstraint || false;
continue;
@@ -303,7 +317,7 @@ export class Bindings implements IBindings {
}
private handleShapeConstraint(
- constraint: IContraint,
+ constraint: IConstraint,
triple: ITriple,
linkedShape: Map,
currentShape: IShape,
@@ -338,44 +352,119 @@ export class Bindings implements IBindings {
return ConstraintResult.INAPPLICABLE;
}
- private static handleShapeType(
- constraint: IContraint,
+ private static handleTypeConstraint(
+ constraint: IConstraint,
triple: ITriple): ConstraintResult {
- if (constraint.type === ConstraintType.TYPE &&
- !Array.isArray(triple.object) &&
- triple.object.termType === "Literal"
- && constraint.value.has(triple.object.datatype.value)
- ) {
- return ConstraintResult.RESPECT;
- } else if (constraint.type === ConstraintType.TYPE &&
+ if (constraint.type === ConstraintType.CLASS &&
+ triple.predicate === TYPE_DEFINITION.value &&
!Array.isArray(triple.object) &&
triple.object.termType === "NamedNode"
&& constraint.value.has(triple.object.value)) {
return ConstraintResult.RESPECT;
- } else if (constraint.type === ConstraintType.TYPE &&
+ } else if (constraint.type === ConstraintType.CLASS &&
+ triple.predicate === TYPE_DEFINITION.value &&
!Array.isArray(triple.object) &&
- triple.object.termType === "Literal"
- && !constraint.value.has(triple.object.datatype.value)) {
+ triple.object.termType === "NamedNode"
+ && !constraint.value.has(triple.object.value)) {
+ return ConstraintResult.NOT_RESPECT;
+ } else if (constraint.type === ConstraintType.CLASS &&
+ triple.predicate === TYPE_DEFINITION.value &&
+ Array.isArray(triple.object)) {
+ for (const object of triple.object) {
+ if (constraint.value.has(object.value)) {
+ return ConstraintResult.RESPECT;
+ }
+ }
+ return ConstraintResult.NOT_RESPECT;
+ } else if (constraint.type === ConstraintType.CLASS && triple.predicate !== TYPE_DEFINITION.value) {
return ConstraintResult.NOT_RESPECT;
}
- else if (constraint.type === ConstraintType.TYPE &&
+
+ if (constraint.type === ConstraintType.DATATYPE &&
!Array.isArray(triple.object) &&
- triple.object.termType === "NamedNode"
- && !constraint.value.has(triple.object.value)) {
+ triple.object.termType === "Literal"
+ && constraint.value.has(triple.object.datatype.value)
+ ) {
+ if (!Bindings.respectNumericFacets(constraint, triple.object.value)) {
+ return ConstraintResult.NOT_RESPECT;
+ }
+ if (!Bindings.respectPatternFacet(constraint, triple.object.value)) {
+ return ConstraintResult.NOT_RESPECT;
+ }
+ return ConstraintResult.RESPECT;
+ } else if (constraint.type === ConstraintType.DATATYPE &&
+ !Array.isArray(triple.object) &&
+ triple.object.termType === "Literal"
+ && !constraint.value.has(triple.object.datatype.value)) {
return ConstraintResult.NOT_RESPECT;
- } else if (constraint.type === ConstraintType.TYPE &&
+ } else if (constraint.type === ConstraintType.DATATYPE &&
Array.isArray(triple.object)) {
+ let hasDatatypeMatch = false;
for (const object of triple.object) {
- if (constraint.value.has(object.value) || (object.termType === "Literal" && constraint.value.has(object.datatype.value))) {
+ if (object.termType === "Literal" && constraint.value.has(object.datatype.value)) {
+ hasDatatypeMatch = true;
+ if (!Bindings.respectNumericFacets(constraint, object.value)) {
+ continue;
+ }
+ if (!Bindings.respectPatternFacet(constraint, object.value)) {
+ continue;
+ }
return ConstraintResult.RESPECT;
}
}
+ if (hasDatatypeMatch) {
+ return ConstraintResult.NOT_RESPECT;
+ }
+ return ConstraintResult.NOT_RESPECT;
}
return ConstraintResult.INAPPLICABLE;
}
+ private static respectNumericFacets(constraint: IConstraint, literalValue: string): boolean {
+ const hasNumericFacet = constraint.minInclusive !== undefined
+ || constraint.maxInclusive !== undefined
+ || constraint.minExclusive !== undefined
+ || constraint.maxExclusive !== undefined;
+
+ if (!hasNumericFacet) {
+ return true;
+ }
+
+ const value = Number(literalValue);
+ if (Number.isNaN(value)) {
+ return false;
+ }
+
+ if (constraint.minInclusive !== undefined && value < constraint.minInclusive) {
+ return false;
+ }
+ if (constraint.maxInclusive !== undefined && value > constraint.maxInclusive) {
+ return false;
+ }
+ if (constraint.minExclusive !== undefined && value <= constraint.minExclusive) {
+ return false;
+ }
+ if (constraint.maxExclusive !== undefined && value >= constraint.maxExclusive) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static respectPatternFacet(constraint: IConstraint, literalValue: string): boolean {
+ if (constraint.pattern === undefined) {
+ return true;
+ }
+
+ try {
+ return new RegExp(constraint.pattern, constraint.flags).test(literalValue);
+ } catch {
+ return false;
+ }
+ }
+
public isFullyBounded(): boolean {
return this.fullyBounded;
}
diff --git a/lib/Shape.ts b/lib/Shape.ts
index 04971bb..1ea1ea7 100644
--- a/lib/Shape.ts
+++ b/lib/Shape.ts
@@ -36,7 +36,7 @@ export interface IShape extends IShapeObj {
*/
export interface IPredicate {
name: string;
- constraint?: IContraint;
+ constraint?: IConstraint;
cardinality?: ICardinality;
negative?: boolean;
optional?: boolean;
@@ -50,25 +50,31 @@ function toJsonPredicate(predicate: IPredicate): IPredicateJson {
}
export type IPredicateJson = Omit & {
- constraint?: IContraintJson;
+ constraint?: IConstraintJson;
}
/**
* A constraint
*/
-export interface IContraint {
+export interface IConstraint {
value: Set;
type: ConstraintType;
+ minInclusive?: number;
+ maxInclusive?: number;
+ minExclusive?: number;
+ maxExclusive?: number;
+ pattern?: string;
+ flags?: string;
}
-function toJsonConstraint(constraint: IContraint): IContraintJson {
+function toJsonConstraint(constraint: IConstraint): IConstraintJson {
return {
...constraint,
value: Array.from(constraint.value)
}
}
-export type IContraintJson = Omit & {
+export type IConstraintJson = Omit & {
value: string[]
};
@@ -92,8 +98,10 @@ export type IShapeJson = Omit;
- filterExpression?: string;
name: string;
isVariable: boolean;
}
@@ -45,6 +44,8 @@ export interface ITripleArgs {
// the object was a variable and a VALUES clase was used
// to bind it to multiple values
object: Term | Term[];
+ // the original variable name when VALUES rewrote the object to a list of terms
+ boundVariable?: string;
isOptional?: boolean;
cardinality?: ICardinality;
negatedSet?: Set;
@@ -65,6 +66,7 @@ export class Triple implements ITriple {
public readonly predicate: string;
public readonly subject: string;
public readonly object: Term | Term[];
+ public readonly boundVariable?: string;
// the cardinality of the predicate
public readonly cardinality?: ICardinality;
public readonly negatedSet?: Set;
@@ -74,10 +76,11 @@ export class Triple implements ITriple {
*
* @param {ITripleArgs} tripleObject - A triple object
*/
- public constructor({ subject, predicate, object, cardinality, negatedSet: negative, isOptional }: ITripleArgs) {
+ public constructor({ subject, predicate, object, boundVariable, cardinality, negatedSet: negative, isOptional }: ITripleArgs) {
this.predicate = predicate;
this.object = object;
this.subject = subject;
+ this.boundVariable = boundVariable;
this.cardinality = cardinality;
this.negatedSet = negative;
this.isOptional = isOptional ?? false;
@@ -90,6 +93,7 @@ export class Triple implements ITriple {
Object.freeze(this.predicate);
Object.freeze(this.isOptional);
Object.freeze(this.object);
+ Object.freeze(this.boundVariable);
Object.freeze(this.subject);
Object.freeze(this);
}
@@ -99,13 +103,17 @@ export class Triple implements ITriple {
* @returns {ITripleArgs} a Triple object
*/
public toObject(): ITripleArgs {
- return {
+ const tripleObject: ITripleArgs = {
subject: this.subject,
predicate: this.predicate,
object: this.object,
cardinality: this.cardinality,
isOptional: this.isOptional
};
+ if (this.boundVariable !== undefined) {
+ tripleObject.boundVariable = this.boundVariable;
+ }
+ return tripleObject;
}
public getLinkedStarPattern(): string | undefined {
diff --git a/lib/constant.ts b/lib/constant.ts
index ea2267b..d1032fd 100644
--- a/lib/constant.ts
+++ b/lib/constant.ts
@@ -36,6 +36,12 @@ export const SHACL_PROPERTY = DF.namedNode(`${SHACL_SH}property`);
export const SHACL_PATH = DF.namedNode(`${SHACL_SH}path`);
export const SHACL_MIN_COUNT = DF.namedNode(`${SHACL_SH}minCount`);
export const SHACL_MAX_COUNT = DF.namedNode(`${SHACL_SH}maxCount`);
+export const SHACL_MIN_INCLUSIVE = DF.namedNode(`${SHACL_SH}minInclusive`);
+export const SHACL_MAX_INCLUSIVE = DF.namedNode(`${SHACL_SH}maxInclusive`);
+export const SHACL_MIN_EXCLUSIVE = DF.namedNode(`${SHACL_SH}minExclusive`);
+export const SHACL_MAX_EXCLUSIVE = DF.namedNode(`${SHACL_SH}maxExclusive`);
+export const SHACL_PATTERN = DF.namedNode(`${SHACL_SH}pattern`);
+export const SHACL_FLAGS = DF.namedNode(`${SHACL_SH}flags`);
export const SHACL_CLOSED = DF.namedNode(`${SHACL_SH}closed`);
export const SHACL_CLASS = DF.namedNode(`${SHACL_SH}class`);
export const SHACL_DATATYPE = DF.namedNode(`${SHACL_SH}datatype`);
diff --git a/lib/containment.ts b/lib/containment.ts
index 4553110..969363a 100644
--- a/lib/containment.ts
+++ b/lib/containment.ts
@@ -1,8 +1,8 @@
-import { Bindings, ContainmentType, IBindings, IDependentStarPattern } from './Binding'
-import { TYPE_DEFINITION } from './constant';
+import { Bindings, ContainmentType, IBindings } from './Binding'
import { generateStarPatternUnion, type IQuery } from './query';
-import { IShape } from './Shape';
-import type { IStarPatternWithDependencies, ITriple } from './Triple';
+import { ConstraintType, IConstraint, IShape } from './Shape';
+import type { IStarPatternWithDependencies } from './Triple';
+import type { Term } from '@rdfjs/types';
/**
* Determine if a query is contained inside a shape.
@@ -14,90 +14,61 @@ import type { IStarPatternWithDependencies, ITriple } from './Triple';
export function solveShapeQueryContainment({ query, shapes, decidingShapes }: IContainementArg): IResult {
const bindingResult = new Map>();
const starPatternsContainment = new Map();
- const queryStarPattern: QueryStarPattern = new Map();
+ const classificationStats = new Map();
const groupedShapes = groupShapeBydependencies(shapes);
- for (const [starPatternsName, starPatternWithDependencies] of query.starPatterns) {
- for (const { triple } of starPatternWithDependencies.starPattern.values()) {
- queryStarPattern.set(triple.toString(), triple);
- }
+ // Initialize per-star-pattern classification state.
+ for (const [starPatternsName] of query.starPatterns) {
starPatternsContainment.set(starPatternsName, { result: ContainmentResult.REJECTED, bindings: new Map() });
+ classificationStats.set(starPatternsName, {
+ hasOpenShape: false,
+ hasClosedShape: false,
+ rootTargetsOpen: new Set(),
+ rootTargetsClosed: new Set(),
+ containedTargets: new Set(),
+ bindings: new Map()
+ });
}
- // dependent, origin
- const nestedContainedStarPatterns = new Map>();
-
+ // Evaluate each star pattern against each candidate shape.
for (const { shape, dependencies } of groupedShapes) {
bindingResult.set(shape.name, new Map());
const bindingResultofShape = bindingResult.get(shape.name)!;
for (const [starPatternName, starPattern] of query.starPatterns) {
const starPatternUnion = generateStarPatternUnion(query.union ?? [], starPatternName);
const bindings = new Bindings(shape, starPattern, dependencies, starPatternUnion);
- const currentNestedStarPattern = bindings.getNestedContainedStarPatternName();
- for (const starPattern of currentNestedStarPattern) {
- const currentNestedContainedStarPatterns = nestedContainedStarPatterns.get(starPattern.starPattern);
- if (currentNestedContainedStarPatterns === undefined) {
- nestedContainedStarPatterns.set(starPattern.starPattern, new Map([[starPatternName, starPattern]]));
- } else {
- currentNestedContainedStarPatterns.set(starPatternName, starPattern);
- }
- }
+ const filterCompatibility = evaluateFiltersForShape(query.filters, starPattern, shape);
bindingResultofShape.set(starPatternName, { result: bindings, shape });
- for (const triple of bindings.getBoundTriple()) {
- queryStarPattern.set(triple.toString(), undefined);
- }
+ updateContainmentStats(classificationStats, starPatternName, shape, bindings, groupedShapes, decidingShapes, filterCompatibility !== FilterTruth.FALSE);
}
}
- for (const [shapeName, starPatternBinding] of bindingResult) {
- const currentShapeBindingResult = bindingResult.get(shapeName)!;
- for (const [starPatternName, bindingResult] of starPatternBinding) {
- const originOfDependency = nestedContainedStarPatterns.get(starPatternName);
- // validate that there are no cycles if there are cycles we analyse the star pattern independently
- for (const [origin, dependencyInfo] of originOfDependency ?? []) {
- if (!nestedContainedStarPatterns.has(origin)) {
- currentShapeBindingResult.set(starPatternName, { ...bindingResult, dependent: dependencyInfo });
- break;
- }
- }
- }
- }
+ // Collapse collected stats into the final containment label per star pattern.
+ for (const [starPatternName, starPattern] of query.starPatterns) {
+ const stats = classificationStats.get(starPatternName)!;
+ const rootTargetsOpen = Array.from(stats.rootTargetsOpen);
+ const rootTargetsClosed = Array.from(stats.rootTargetsClosed);
+ const containedTargets = Array.from(stats.containedTargets);
+ const nestedTargets = Array.from(getNestedMatchingTargets(starPattern, classificationStats));
- for (const starPatternBinding of bindingResult.values()) {
- for (const [starPatternName, result] of starPatternBinding) {
- if (result.dependent === undefined) {
- updateStarPatternContainment(starPatternsContainment, result.result, starPatternName, result.shape, groupedShapes, decidingShapes);
+ let currentResult: IContainmentResult;
+ if (containedTargets.length > 0) {
+ currentResult = { result: ContainmentResult.CONTAINED, target: containedTargets, bindings: new Map(stats.bindings) };
+ } else if (rootTargetsOpen.length > 0) {
+ currentResult = { result: ContainmentResult.ALIGNED, target: rootTargetsOpen, bindings: new Map(stats.bindings) };
+ } else {
+ const unalignedTargets = Array.from(new Set(rootTargetsClosed.concat(nestedTargets)));
+ if (unalignedTargets.length > 0) {
+ const bindings = rootTargetsClosed.length > 0 ? new Map(stats.bindings) : new Map();
+ currentResult = { result: ContainmentResult.UNALINGED, target: unalignedTargets, bindings };
} else {
- // check the shape contained related to the dependent star pattern
- const dependendShapes = [];
- for (const [shapeName, nestedBinding] of bindingResult) {
- const nestedResult = nestedBinding.get(starPatternName);
- if (nestedResult !== undefined) {
- if (nestedResult.result.isFullyBounded()) {
- dependendShapes.push(shapeName);
- }
- }
- }
-
- const constraintTarget = [];
- for (const nestedBinding of bindingResult.values()) {
- const originBinding = nestedBinding.get(result.dependent.origin);
- if (originBinding !== undefined) {
- const constraintShape = originBinding.result.getNestedContainedStarPatternNameShapesContained().get(starPatternName);
- if (constraintShape !== undefined) {
- constraintTarget.push(constraintShape);
- }
- }
- }
- // we check if there is less shapes contained than the constraint of the dependency
- const target = constraintTarget.length >= dependendShapes.length
- || constraintTarget.length === 0
- ? dependendShapes : result.dependent.shape;
-
- starPatternsContainment.set(starPatternName, { result: ContainmentResult.DEPEND, target: target?.length === 0 ? undefined : target, bindings: new Map() });
+ const rejectedResult = stats.hasOpenShape ? ContainmentResult.WEAKLY_REJECTED : ContainmentResult.REJECTED;
+ currentResult = { result: rejectedResult, bindings: new Map() };
}
}
+
+ starPatternsContainment.set(starPatternName, currentResult);
}
return {
@@ -107,59 +78,66 @@ export function solveShapeQueryContainment({ query, shapes, decidingShapes }: IC
}
-function updateStarPatternContainment(starPatternsContainment: Map, bindings: IBindings, starPatternName: StarPatternName, shape: IShape, groupedShapes: IShapeWithDependencies[], decidingShapes?: Set): void {
- const prevContainmentResult = starPatternsContainment.get(starPatternName)!;
+function updateContainmentStats(
+ classificationStats: Map,
+ starPatternName: StarPatternName,
+ shape: IShape,
+ bindings: IBindings,
+ groupedShapes: IShapeWithDependencies[],
+ decidingShapes?: Set,
+ filterCompatible = true,
+): void {
+ // Skip shapes that are not part of the current decision scope.
+ if (decidingShapes !== undefined && !decidingShapes.has(shape.name)) {
+ return;
+ }
- if (bindings.shouldVisitShape() && bindings.getUnboundedTriple().length > 0 && prevContainmentResult.result !== ContainmentResult.CONTAIN && prevContainmentResult.result !== ContainmentResult.PARTIALY_CONTAIN) {
- starPatternsContainment.set(starPatternName, {
- result: ContainmentResult.ALIGNED,
- target: (prevContainmentResult.target ?? []).concat(shape.name),
- bindings: prevContainmentResult.bindings.set(shape.name, bindings)
- });
+ const stats = classificationStats.get(starPatternName)!;
+ if (shape.closed) {
+ stats.hasClosedShape = true;
+ } else {
+ stats.hasOpenShape = true;
+ }
+ if (!filterCompatible) {
+ return;
}
- if (bindings.shouldVisitShape() && bindings.isFullyBounded()) {
- if (bindings.containmentType().result === ContainmentType.FULL) {
- starPatternsContainment.set(starPatternName, {
- result: ContainmentResult.CONTAIN,
- target: prevContainmentResult.result === ContainmentResult.ALIGNED ? [shape.name] :
- (prevContainmentResult.target ?? []).concat(shape.name),
- bindings: prevContainmentResult.bindings.set(shape.name, bindings)
- });
+
+ // Track root-level alignment independently from full containment.
+ const hasRootMatch = bindings.getBoundTriple().length > 0;
+ if (hasRootMatch) {
+ stats.bindings.set(shape.name, bindings);
+ if (shape.closed) {
+ stats.rootTargetsClosed.add(shape.name);
+ } else {
+ stats.rootTargetsOpen.add(shape.name);
}
- if (bindings.containmentType().result === ContainmentType.PARTIAL) {
- const unContaineStarPattern = bindings.containmentType().unContaineStarPattern!;
- const hasDisjuncContainment = findDisjunctContainment(unContaineStarPattern, groupedShapes, shape, decidingShapes);
- if (hasDisjuncContainment) {
- starPatternsContainment.set(starPatternName, {
- result: ContainmentResult.CONTAIN,
- target: prevContainmentResult.result === ContainmentResult.ALIGNED ? [shape.name] :
- (prevContainmentResult.target ?? []).concat(shape.name),
- bindings: prevContainmentResult.bindings.set(shape.name, bindings)
- });
- } else {
- starPatternsContainment.set(starPatternName, {
- result: ContainmentResult.PARTIALY_CONTAIN,
- target: prevContainmentResult.result === ContainmentResult.ALIGNED ? [shape.name] :
- (prevContainmentResult.target ?? []).concat(shape.name),
- bindings: prevContainmentResult.bindings.set(shape.name, bindings)
- });
- }
+ }
+
+ // Full binding means this shape fully contains the star pattern.
+ if (bindings.isFullyBounded() && bindings.containmentType().result === ContainmentType.FULL) {
+ stats.bindings.set(shape.name, bindings);
+ stats.containedTargets.add(shape.name);
+ }
+
+ // Preserve previous behavior where a disjunction can be considered fully covered
+ // when unresolved alternatives are contained by another deciding shape.
+ if (bindings.isFullyBounded() && bindings.containmentType().result === ContainmentType.PARTIAL) {
+ const unContaineStarPattern = bindings.containmentType().unContaineStarPattern!;
+ const hasDisjuncContainment = findDisjunctContainment(unContaineStarPattern, groupedShapes, shape, decidingShapes);
+ if (hasDisjuncContainment) {
+ stats.bindings.set(shape.name, bindings);
+ stats.containedTargets.add(shape.name);
}
}
}
-/**
- * Check if there is a disjunction. IMPORTANT!! Do not consider nested disjunction.
- * @param {IStarPatternWithDependencies[]} starPatterns
- * @param {IShapeWithDependencies[]} groupedShapes
- * @param {IShape} shapeExcluded
- * @returns Whether the disjunction is contained into a shape
- */
+
function findDisjunctContainment(starPatterns: IStarPatternWithDependencies[], groupedShapes: IShapeWithDependencies[], shapeExcluded: IShape, decidingShapes?: Set): boolean {
+ // Search whether unresolved disjunctive branches can be absorbed by another shape.
let haveContainment = false;
for (const starPattern of starPatterns) {
for (const { shape, dependencies } of groupedShapes) {
- if (shape.name !== shapeExcluded.name && (decidingShapes === undefined || decidingShapes?.has(shape.name))) {
+ if (shape.name !== shapeExcluded.name && (decidingShapes === undefined || decidingShapes.has(shape.name))) {
const bindings = new Bindings(shape, starPattern, dependencies);
haveContainment = haveContainment || bindings.isFullyBounded();
}
@@ -168,7 +146,717 @@ function findDisjunctContainment(starPatterns: IStarPatternWithDependencies[], g
return haveContainment;
}
+function getNestedMatchingTargets(starPattern: IStarPatternWithDependencies, classificationStats: Map, visited: Set = new Set()): Set {
+ // Include shapes that only become reachable through nested star-pattern dependencies.
+ const nestedNames = getNestedDependencyNames(starPattern, visited);
+ const nestedTargets = new Set();
+
+ for (const nestedName of nestedNames) {
+ const stats = classificationStats.get(nestedName);
+ if (stats === undefined) {
+ continue;
+ }
+ for (const target of stats.rootTargetsOpen) {
+ nestedTargets.add(target);
+ }
+ for (const target of stats.rootTargetsClosed) {
+ nestedTargets.add(target);
+ }
+ }
+
+ return nestedTargets;
+}
+
+function getNestedDependencyNames(starPattern: IStarPatternWithDependencies, visited: Set): Set {
+ const nestedNames = new Set();
+ // Break cycles in recursive dependency graphs.
+ if (visited.has(starPattern.name)) {
+ return nestedNames;
+ }
+ visited.add(starPattern.name);
+
+ for (const { dependencies } of starPattern.starPattern.values()) {
+ if (dependencies === undefined) {
+ continue;
+ }
+
+ nestedNames.add(dependencies.name);
+ for (const nestedName of getNestedDependencyNames(dependencies, visited)) {
+ nestedNames.add(nestedName);
+ }
+ }
+
+ return nestedNames;
+}
+
+enum FilterTruth {
+ TRUE = 'true',
+ FALSE = 'false',
+ UNKNOWN = 'unknown',
+}
+
+interface IFilterEvalContext {
+ starPattern: IStarPatternWithDependencies;
+ shape: IShape;
+}
+
+interface INumericBounds {
+ minInclusive?: number;
+ maxInclusive?: number;
+ minExclusive?: number;
+ maxExclusive?: number;
+}
+
+interface IVariableDatatypeConstraint {
+ datatypes: Set;
+ bounds?: INumericBounds;
+ pattern?: string;
+ flags?: string;
+}
+
+function evaluateFiltersForShape(
+ filters: unknown[] | undefined,
+ starPattern: IStarPatternWithDependencies,
+ shape: IShape,
+): FilterTruth {
+ // Filters are evaluated conservatively: we only reject on proven contradiction.
+ if (filters === undefined || filters.length === 0) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const context: IFilterEvalContext = { starPattern, shape };
+ let status = FilterTruth.UNKNOWN;
+ for (const filter of filters) {
+ const result = evaluateFilterCompatibility(filter, context);
+ if (result === FilterTruth.FALSE) {
+ return FilterTruth.FALSE;
+ }
+ if (result === FilterTruth.TRUE) {
+ status = FilterTruth.TRUE;
+ }
+ }
+ return status;
+}
+
+function evaluateFilterCompatibility(expression: unknown, context: IFilterEvalContext): FilterTruth {
+ if (typeof expression !== 'object' || expression === null) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const castExpression = expression as { subType?: string; operator?: string; args?: unknown[] };
+ if (castExpression.subType === 'term') {
+ // Boolean literals like FILTER(true/false) are intentionally ignored for containment.
+ return FilterTruth.UNKNOWN;
+ }
+ if (castExpression.subType !== 'operator') {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const args = castExpression.args ?? [];
+ switch (castExpression.operator) {
+ case '&&':
+ return combineLogicalAnd(args, context);
+ case '||':
+ return combineLogicalOr(args, context);
+ case '!': {
+ // Negation does not provide a safe contradiction signal in this conservative check.
+ return FilterTruth.UNKNOWN;
+ }
+ case '=':
+ case '!=':
+ case '<':
+ case '<=':
+ case '>':
+ case '>=':
+ return evaluateComparisonCompatibility(castExpression.operator, args[0], args[1], context);
+ case 'regex':
+ return evaluateRegexCompatibility(args, context);
+ default:
+ return FilterTruth.UNKNOWN;
+ }
+}
+
+function combineLogicalAnd(args: unknown[], context: IFilterEvalContext): FilterTruth {
+ // AND is false as soon as one branch is false; true only if at least one branch is provably true.
+ let hasTrue = false;
+ for (const arg of args) {
+ const current = evaluateFilterCompatibility(arg, context);
+ if (current === FilterTruth.FALSE) {
+ return FilterTruth.FALSE;
+ }
+ if (current === FilterTruth.TRUE) {
+ hasTrue = true;
+ }
+ }
+ return hasTrue ? FilterTruth.TRUE : FilterTruth.UNKNOWN;
+}
+
+function combineLogicalOr(args: unknown[], context: IFilterEvalContext): FilterTruth {
+ // OR is false only if every branch is false.
+ let hasNonFalse = false;
+ for (const arg of args) {
+ const current = evaluateFilterCompatibility(arg, context);
+ if (current !== FilterTruth.FALSE) {
+ hasNonFalse = true;
+ }
+ }
+ return hasNonFalse ? FilterTruth.UNKNOWN : FilterTruth.FALSE;
+}
+
+function evaluateComparisonCompatibility(operator: string, left: unknown, right: unknown, context: IFilterEvalContext): FilterTruth {
+ // First check datatype() comparisons, then numeric inequalities.
+ const datatypeCheck = evaluateDatatypeCompatibility(operator, left, right, context)
+ ?? evaluateDatatypeCompatibility(operator, right, left, context);
+ if (datatypeCheck !== undefined) {
+ return datatypeCheck;
+ }
+
+ if (operator === '<' || operator === '<=' || operator === '>' || operator === '>=') {
+ const numericCheck = evaluateNumericComparisonCompatibility(operator, left, right, context, true)
+ ?? evaluateNumericComparisonCompatibility(operator, right, left, context, false);
+ if (numericCheck !== undefined) {
+ return numericCheck;
+ }
+ }
+
+ return FilterTruth.UNKNOWN;
+}
+
+function evaluateNumericComparisonCompatibility(
+ operator: string,
+ variableExpr: unknown,
+ otherExpr: unknown,
+ context: IFilterEvalContext,
+ variableOnLeft: boolean,
+): FilterTruth | undefined {
+ // This branch only reasons about variable-vs-literal numeric constraints.
+ const variableName = extractVariableName(variableExpr);
+ if (variableName === undefined) {
+ return undefined;
+ }
+
+ const literal = extractLiteral(otherExpr);
+ if (literal === undefined || getNumericLiteralValue(literal) === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const variableConstraint = extractVariableDatatypeConstraint(variableName, context);
+ if (variableConstraint === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ for (const datatype of variableConstraint.datatypes) {
+ if (isNumericDatatype(datatype)) {
+ const filterBounds = boundsFromNumericFilter(operator, getNumericLiteralValue(literal)!, variableOnLeft);
+ const shapeBounds = variableConstraint.bounds;
+ if (shapeBounds === undefined || filterBounds === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ // Contradiction when filter bounds and shape bounds have an empty intersection.
+ const bounds = intersectBounds(shapeBounds, filterBounds);
+ if (bounds === undefined) {
+ return FilterTruth.FALSE;
+ }
+
+ return FilterTruth.UNKNOWN;
+ }
+ }
+ return FilterTruth.FALSE;
+}
+
+function evaluateDatatypeCompatibility(operator: string, datatypeExpr: unknown, otherExpr: unknown, context: IFilterEvalContext): FilterTruth | undefined {
+ // Handle FILTER(datatype(?v) = iri) and FILTER(datatype(?v) != iri) forms.
+ const variableName = extractDatatypeVariableName(datatypeExpr);
+ if (variableName === undefined) {
+ return undefined;
+ }
+
+ const expectedDatatype = extractNamedNodeValue(otherExpr);
+ if (expectedDatatype === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const datatypes = extractVariableDatatypes(variableName, context);
+ if (datatypes === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ if (operator === '=' && !datatypes.has(expectedDatatype)) {
+ return FilterTruth.FALSE;
+ }
+ if (operator === '!=' && datatypes.size === 1 && datatypes.has(expectedDatatype)) {
+ return FilterTruth.FALSE;
+ }
+ return FilterTruth.UNKNOWN;
+}
+
+function extractVariableDatatypes(variableName: string, context: IFilterEvalContext): Set | undefined {
+ return extractVariableDatatypeConstraint(variableName, context)?.datatypes;
+}
+
+function extractVariableDatatypeConstraint(variableName: string, context: IFilterEvalContext): IVariableDatatypeConstraint | undefined {
+ let collectedDatatypes: Set | undefined;
+ let collectedBounds: INumericBounds | undefined;
+ let collectedPattern: string | undefined;
+ let collectedFlags: string | undefined;
+
+ for (const { triple } of context.starPattern.starPattern.values()) {
+ const matchesVariable = (!Array.isArray(triple.object) && triple.object.termType === 'Variable' && triple.object.value === variableName)
+ || (Array.isArray(triple.object) && triple.boundVariable === variableName);
+ if (!matchesVariable) {
+ continue;
+ }
+ const predicateConstraint = context.shape.get(triple.predicate)?.constraint;
+ if (predicateConstraint?.type !== ConstraintType.DATATYPE || predicateConstraint.value.size === 0) {
+ continue;
+ }
+
+ // Multiple occurrences of the same variable are merged by intersection.
+ if (collectedDatatypes === undefined) {
+ collectedDatatypes = new Set(predicateConstraint.value);
+ } else {
+ collectedDatatypes = new Set(Array.from(collectedDatatypes).filter((value) => predicateConstraint.value.has(value)));
+ }
+
+ const bounds = extractNumericBounds(predicateConstraint);
+ if (bounds !== undefined) {
+ if (collectedBounds === undefined) {
+ collectedBounds = bounds;
+ } else {
+ const intersected = intersectBounds(collectedBounds, bounds);
+ if (intersected === undefined) {
+ return {
+ datatypes: collectedDatatypes,
+ bounds: {
+ minInclusive: 1,
+ maxInclusive: 0,
+ },
+ };
+ }
+ collectedBounds = intersected;
+ }
+ }
+
+ // Regex reasoning is only kept when all matching constraints agree on pattern/flags.
+ if (predicateConstraint.pattern !== undefined) {
+ if (collectedPattern === undefined) {
+ collectedPattern = predicateConstraint.pattern;
+ collectedFlags = predicateConstraint.flags;
+ } else if (collectedPattern !== predicateConstraint.pattern || collectedFlags !== predicateConstraint.flags) {
+ return {
+ datatypes: collectedDatatypes,
+ bounds: collectedBounds,
+ };
+ }
+ }
+ }
+
+ if (collectedDatatypes === undefined) {
+ return undefined;
+ }
+
+ return {
+ datatypes: collectedDatatypes,
+ bounds: collectedBounds,
+ pattern: collectedPattern,
+ flags: collectedFlags,
+ };
+}
+
+function evaluateRegexCompatibility(args: unknown[], context: IFilterEvalContext): FilterTruth {
+ // Supported form: FILTER(regex(str(?v), pattern, flags?)).
+ const variableName = extractRegexVariableName(args[0]);
+ if (variableName === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const regexPattern = extractStringLiteralValue(args[1]);
+ if (regexPattern === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const regexFlags = extractStringLiteralValue(args[2]) ?? '';
+ const variableConstraint = extractVariableDatatypeConstraint(variableName, context);
+ if (variableConstraint?.pattern === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ // Equality alone is not strong enough to prove containment in this conservative pass,
+ // so we keep it as UNKNOWN and only return TRUE/FALSE for explicit implication/conflict.
+ if (variableConstraint.pattern === regexPattern && (variableConstraint.flags ?? '') === regexFlags) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ const shapePatternTest = buildRegex(variableConstraint.pattern, variableConstraint.flags);
+ const filterPatternTest = buildRegex(regexPattern, regexFlags);
+ if (shapePatternTest === undefined || filterPatternTest === undefined) {
+ return FilterTruth.UNKNOWN;
+ }
+
+ // TRUE when the shape regex is strictly stronger and therefore always satisfies the filter.
+ if (doesRegexConstraintImplyFilter(variableConstraint.pattern, variableConstraint.flags, regexPattern, regexFlags)) {
+ return FilterTruth.TRUE;
+ }
+
+ // FALSE when prefix/literal heuristics identify an obvious incompatibility.
+ if (hasRegexPrefixIncompatibility(variableConstraint.pattern, regexPattern)) {
+ return FilterTruth.FALSE;
+ }
+
+ return FilterTruth.UNKNOWN;
+}
+
+interface IAnchoredLiteralPrefix {
+ prefix: string;
+ anchoredEnd: boolean;
+}
+
+function doesRegexConstraintImplyFilter(
+ shapePattern: string,
+ shapeFlags: string | undefined,
+ filterPattern: string,
+ filterFlags: string,
+): boolean {
+ // We only reason about implication when both regexes run under the same flags.
+ const normalizedShapeFlags = shapeFlags ?? '';
+ if (normalizedShapeFlags !== filterFlags) {
+ return false;
+ }
+
+ const shapePrefix = extractAnchoredLiteralPrefix(shapePattern);
+ const filterPrefix = extractAnchoredLiteralPrefix(filterPattern);
+ if (shapePrefix === undefined || filterPrefix === undefined) {
+ return false;
+ }
+
+ // If the filter is end-anchored (^x$), implication requires exact same anchored literal.
+ if (filterPrefix.anchoredEnd) {
+ return shapePrefix.anchoredEnd && shapePrefix.prefix === filterPrefix.prefix;
+ }
+
+ // For simple start-anchored prefixes, ^foo implies ^f.
+ return shapePrefix.prefix.startsWith(filterPrefix.prefix);
+}
+
+function extractRegexVariableName(expression: unknown): string | undefined {
+ // Accept both regex(?v, ...) and regex(str(?v), ...).
+ if (typeof expression !== 'object' || expression === null) {
+ return undefined;
+ }
+
+ const castExpression = expression as { subType?: string; operator?: string; args?: unknown[] };
+ if (castExpression.subType === 'operator' && castExpression.operator === 'str') {
+ return extractVariableName((castExpression.args ?? [])[0]);
+ }
+
+ return extractVariableName(expression);
+}
+
+function extractStringLiteralValue(expression: unknown): string | undefined {
+ const literal = extractLiteral(expression);
+ if (literal === undefined) {
+ return undefined;
+ }
+ return literal.value;
+}
+
+function buildRegex(pattern: string, flags?: string): RegExp | undefined {
+ // Invalid regexes are ignored in conservative compatibility checks.
+ try {
+ return new RegExp(pattern, flags);
+ } catch {
+ return undefined;
+ }
+}
+
+function hasRegexPrefixIncompatibility(shapePattern: string, filterPattern: string): boolean {
+ const shapePrefix = extractAnchoredLiteralPrefix(shapePattern);
+ const filterPrefix = extractAnchoredLiteralPrefix(filterPattern);
+ // Different anchored prefixes cannot both hold (e.g., ^foo vs ^bar).
+ if (shapePrefix !== undefined && filterPrefix !== undefined && shapePrefix.prefix !== filterPrefix.prefix) {
+ return true;
+ }
+
+ const shapeContains = extractContainsLiteral(shapePattern);
+ const filterContains = extractContainsLiteral(filterPattern);
+ if (shapeContains !== undefined && filterContains !== undefined && shapeContains !== filterContains) {
+ return false;
+ }
+
+ // Mixed anchored-prefix/plain-literal incompatibility checks.
+ if (shapePrefix !== undefined && filterContains !== undefined && !shapePrefix.prefix.includes(filterContains) && !filterContains.includes(shapePrefix.prefix)) {
+ return true;
+ }
+ if (filterPrefix !== undefined && shapeContains !== undefined && !filterPrefix.prefix.includes(shapeContains) && !shapeContains.includes(filterPrefix.prefix)) {
+ return true;
+ }
+
+ return false;
+}
+
+function extractAnchoredLiteralPrefix(pattern: string): IAnchoredLiteralPrefix | undefined {
+ const match = pattern.match(/^\^([A-Za-z0-9 _-]+)(\$)?$/);
+ if (match === null) {
+ return undefined;
+ }
+
+ return {
+ prefix: match[1],
+ anchoredEnd: match[2] !== undefined,
+ };
+}
+
+function extractContainsLiteral(pattern: string): string | undefined {
+ const match = pattern.match(/^([A-Za-z0-9 _-]+)$/);
+ return match?.[1];
+}
+
+function extractNumericBounds(constraint: IConstraint): INumericBounds | undefined {
+ // Normalize optional SHACL numeric facets into a single interval object.
+ const bounds: INumericBounds = {};
+ if (constraint.minInclusive !== undefined) {
+ bounds.minInclusive = constraint.minInclusive;
+ }
+ if (constraint.maxInclusive !== undefined) {
+ bounds.maxInclusive = constraint.maxInclusive;
+ }
+ if (constraint.minExclusive !== undefined) {
+ bounds.minExclusive = constraint.minExclusive;
+ }
+ if (constraint.maxExclusive !== undefined) {
+ bounds.maxExclusive = constraint.maxExclusive;
+ }
+
+ return Object.keys(bounds).length === 0 ? undefined : bounds;
+}
+
+function boundsFromNumericFilter(operator: string, literalValue: number, variableOnLeft: boolean): INumericBounds | undefined {
+ // Convert a comparison operator to an interval on the variable.
+ const normalizedOperator = normalizeComparisonOperator(operator, variableOnLeft);
+ switch (normalizedOperator) {
+ case '>':
+ return { minExclusive: literalValue };
+ case '>=':
+ return { minInclusive: literalValue };
+ case '<':
+ return { maxExclusive: literalValue };
+ case '<=':
+ return { maxInclusive: literalValue };
+ default:
+ return undefined;
+ }
+}
+
+function normalizeComparisonOperator(operator: string, variableOnLeft: boolean): string {
+ // Re-orient operator so bounds are always computed as variable OP literal.
+ if (variableOnLeft) {
+ return operator;
+ }
+ if (operator === '>') {
+ return '<';
+ }
+ if (operator === '>=') {
+ return '<=';
+ }
+ if (operator === '<') {
+ return '>';
+ }
+ if (operator === '<=') {
+ return '>=';
+ }
+ return operator;
+}
+
+function intersectBounds(left: INumericBounds, right: INumericBounds): INumericBounds | undefined {
+ // Compute interval intersection; undefined means empty intersection.
+ const lower = pickLowerBound(
+ mergeLowerBound(left.minInclusive, false, left.minExclusive, true),
+ mergeLowerBound(right.minInclusive, false, right.minExclusive, true),
+ );
+ const upper = pickUpperBound(
+ mergeUpperBound(left.maxInclusive, false, left.maxExclusive, true),
+ mergeUpperBound(right.maxInclusive, false, right.maxExclusive, true),
+ );
+
+ if (lower !== undefined && upper !== undefined) {
+ if (lower.value > upper.value) {
+ return undefined;
+ }
+ if (lower.value === upper.value && (lower.exclusive || upper.exclusive)) {
+ return undefined;
+ }
+ }
+
+ const bounds: INumericBounds = {};
+ if (lower !== undefined) {
+ if (lower.exclusive) {
+ bounds.minExclusive = lower.value;
+ } else {
+ bounds.minInclusive = lower.value;
+ }
+ }
+ if (upper !== undefined) {
+ if (upper.exclusive) {
+ bounds.maxExclusive = upper.value;
+ } else {
+ bounds.maxInclusive = upper.value;
+ }
+ }
+
+ return bounds;
+}
+
+function mergeLowerBound(inclusive: number | undefined, inclusiveExclusive: boolean, exclusive: number | undefined, exclusiveExclusive: boolean): { value: number; exclusive: boolean } | undefined {
+ // Pick the tighter lower bound from inclusive/exclusive candidates.
+ const candidates: Array<{ value: number; exclusive: boolean }> = [];
+ if (inclusive !== undefined) {
+ candidates.push({ value: inclusive, exclusive: inclusiveExclusive });
+ }
+ if (exclusive !== undefined) {
+ candidates.push({ value: exclusive, exclusive: exclusiveExclusive });
+ }
+ if (candidates.length === 0) {
+ return undefined;
+ }
+ return pickLowerBound(candidates[0], candidates[1]);
+}
+
+function mergeUpperBound(inclusive: number | undefined, inclusiveExclusive: boolean, exclusive: number | undefined, exclusiveExclusive: boolean): { value: number; exclusive: boolean } | undefined {
+ // Pick the tighter upper bound from inclusive/exclusive candidates.
+ const candidates: Array<{ value: number; exclusive: boolean }> = [];
+ if (inclusive !== undefined) {
+ candidates.push({ value: inclusive, exclusive: inclusiveExclusive });
+ }
+ if (exclusive !== undefined) {
+ candidates.push({ value: exclusive, exclusive: exclusiveExclusive });
+ }
+ if (candidates.length === 0) {
+ return undefined;
+ }
+ return pickUpperBound(candidates[0], candidates[1]);
+}
+
+function pickLowerBound(a?: { value: number; exclusive: boolean }, b?: { value: number; exclusive: boolean }): { value: number; exclusive: boolean } | undefined {
+ // Lower bound with the greatest value is the most restrictive.
+ if (a === undefined) {
+ return b;
+ }
+ if (b === undefined) {
+ return a;
+ }
+ if (a.value > b.value) {
+ return a;
+ }
+ if (b.value > a.value) {
+ return b;
+ }
+ return { value: a.value, exclusive: a.exclusive || b.exclusive };
+}
+
+function pickUpperBound(a?: { value: number; exclusive: boolean }, b?: { value: number; exclusive: boolean }): { value: number; exclusive: boolean } | undefined {
+ // Upper bound with the smallest value is the most restrictive.
+ if (a === undefined) {
+ return b;
+ }
+ if (b === undefined) {
+ return a;
+ }
+ if (a.value < b.value) {
+ return a;
+ }
+ if (b.value < a.value) {
+ return b;
+ }
+ return { value: a.value, exclusive: a.exclusive || b.exclusive };
+}
+
+function extractVariableName(expression: unknown): string | undefined {
+ // Extract a SPARQL variable from a term expression.
+ if (typeof expression !== 'object' || expression === null) {
+ return undefined;
+ }
+ const castExpression = expression as { subType?: string; term?: Term };
+ if (castExpression.subType !== 'term' || castExpression.term === undefined || castExpression.term.termType !== 'Variable') {
+ return undefined;
+ }
+ return castExpression.term.value;
+}
+
+function extractDatatypeVariableName(expression: unknown): string | undefined {
+ // Extract variable name from datatype(?v).
+ if (typeof expression !== 'object' || expression === null) {
+ return undefined;
+ }
+ const castExpression = expression as { subType?: string; operator?: string; args?: unknown[] };
+ if (castExpression.subType !== 'operator' || castExpression.operator !== 'datatype') {
+ return undefined;
+ }
+ return extractVariableName((castExpression.args ?? [])[0]);
+}
+
+function extractLiteral(expression: unknown): Term | undefined {
+ // Extract literal term from a term expression.
+ if (typeof expression !== 'object' || expression === null) {
+ return undefined;
+ }
+ const castExpression = expression as { subType?: string; term?: Term };
+ if (castExpression.subType !== 'term' || castExpression.term === undefined || castExpression.term.termType !== 'Literal') {
+ return undefined;
+ }
+ return castExpression.term;
+}
+
+function extractNamedNodeValue(expression: unknown): string | undefined {
+ // Extract named node IRI value from a term expression.
+ if (typeof expression !== 'object' || expression === null) {
+ return undefined;
+ }
+ const castExpression = expression as { subType?: string; term?: Term };
+ if (castExpression.subType !== 'term' || castExpression.term === undefined || castExpression.term.termType !== 'NamedNode') {
+ return undefined;
+ }
+ return castExpression.term.value;
+}
+
+function getNumericLiteralValue(term: Term): number | undefined {
+ // Parse numeric literal value only when datatype is numeric.
+ if (term.termType !== 'Literal') {
+ return undefined;
+ }
+ if (!isNumericDatatype(term.datatype.value)) {
+ return undefined;
+ }
+
+ const value = Number(term.value);
+ return Number.isNaN(value) ? undefined : value;
+}
+
+function isNumericDatatype(datatype: string): boolean {
+ // Accepted numeric XML Schema datatypes for numeric filter reasoning.
+ const numericDatatypes = new Set([
+ 'http://www.w3.org/2001/XMLSchema#integer',
+ 'http://www.w3.org/2001/XMLSchema#decimal',
+ 'http://www.w3.org/2001/XMLSchema#double',
+ 'http://www.w3.org/2001/XMLSchema#float',
+ 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger',
+ 'http://www.w3.org/2001/XMLSchema#nonPositiveInteger',
+ 'http://www.w3.org/2001/XMLSchema#positiveInteger',
+ 'http://www.w3.org/2001/XMLSchema#negativeInteger',
+ 'http://www.w3.org/2001/XMLSchema#long',
+ 'http://www.w3.org/2001/XMLSchema#int',
+ 'http://www.w3.org/2001/XMLSchema#short',
+ 'http://www.w3.org/2001/XMLSchema#byte',
+ 'http://www.w3.org/2001/XMLSchema#unsignedLong',
+ 'http://www.w3.org/2001/XMLSchema#unsignedInt',
+ 'http://www.w3.org/2001/XMLSchema#unsignedShort',
+ 'http://www.w3.org/2001/XMLSchema#unsignedByte',
+ ]);
+ return numericDatatypes.has(datatype);
+}
+
function groupShapeBydependencies(shapes: IShape[], dependentShapes?: IShape[]): IShapeWithDependencies[] {
+ // Build per-shape dependency maps that include all other visible shapes.
const resp: IShapeWithDependencies[] = [];
for (let i = 0; i < shapes.length; i++) {
const target = shapes[i];
@@ -184,6 +872,7 @@ function groupShapeBydependencies(shapes: IShape[], dependentShapes?: IShape[]):
function generateVisitStatus(bindings: Map>, shapes: IShape[]): Map {
+ // A shape is visitable if at least one star pattern binding marks it as followable.
const visitShapeBoundedResource = new Map();
for (const shape of shapes) {
visitShapeBoundedResource.set(shape.name, false);
@@ -208,7 +897,15 @@ function generateVisitStatus(bindings: Map;
+ rootTargetsClosed: Set;
+ containedTargets: Set;
+ bindings: Map;
}
interface IShapeWithDependencies {
@@ -217,7 +914,6 @@ interface IShapeWithDependencies {
}
export type StarPatternName = string;
-type QueryStarPattern = Map;
/**
* The argument of the report alignment function
@@ -264,14 +960,14 @@ export type IContainmentResult = Readonly<{
* The result of a containement
*/
export enum ContainmentResult {
- // Is subsum
- CONTAIN,
- // One union statement is subsum
- PARTIALY_CONTAIN,
- // Has at least one binding
+ // All root and nested star patterns are fully covered.
+ CONTAINED,
+ // At least one triple in the root star pattern matches on an open shape.
ALIGNED,
- // Is a dependency of a subsuming star pattern
- DEPEND,
- // Has no binding
+ // Root only matches on closed shapes, or only nested star patterns match.
+ UNALINGED,
+ // No triple matches and at least one candidate shape is open.
+ WEAKLY_REJECTED,
+ // No triple matches and all candidate shapes are closed.
REJECTED,
}
diff --git a/lib/query.ts b/lib/query.ts
index 0218cba..e77913f 100644
--- a/lib/query.ts
+++ b/lib/query.ts
@@ -29,7 +29,10 @@ export interface IQuery {
// star patterns indexed by subject
starPatterns: Map;
union?: IQuery[][];
- filterExpression?: string;
+ // top-level filter expressions attached to this query branch
+ filters?: Algebra.Expression[];
+ // VALUES bindings indexed by variable name
+ values?: Map;
}
export function generateStarPatternUnion(union: IQuery[][], starPatternName: string): IStarPatternWithDependencies[][] {
@@ -60,22 +63,30 @@ export function generateQuery(algebraQuery: Algebra.Operation, optional?: boolea
// the binding value to the value
const accumulatedValues = new Map();
const accumulatedUnion: IQuery[][] = [];
+ const accumulatedFilters: Algebra.Expression[] = [];
- QueryHandler.collectFromAlgebra(algebraQuery, accumulatedTriples, accumulatedValues, accumulatedUnion, optional);
+ QueryHandler.collectFromAlgebra(algebraQuery, accumulatedTriples, accumulatedValues, accumulatedUnion, accumulatedFilters, optional);
- return buildQuery(accumulatedTriples, accumulatedValues, accumulatedUnion);
+ return buildQuery(accumulatedTriples, accumulatedValues, accumulatedUnion, accumulatedFilters);
}
function buildQuery(
tripleArgs: Map,
values: Map,
- accumulatedUnion: IQuery[][]
+ accumulatedUnion: IQuery[][],
+ filters: Algebra.Expression[] = [],
): IQuery {
const innerQuery = new Map();
- const resp: IQuery = { starPatterns: innerQuery, filterExpression: "" };
+ const resp: IQuery = { starPatterns: innerQuery };
if (accumulatedUnion.length > 0) {
resp.union = accumulatedUnion;
}
+ if (filters.length > 0) {
+ resp.filters = [...filters];
+ }
+ if (values.size > 0) {
+ resp.values = new Map(values);
+ }
// generate the root star patterns
for (const [starPatternSubject, { triples, isVariable }] of tripleArgs) {
@@ -87,6 +98,7 @@ function buildQuery(
subject: triple.subject,
predicate: triple.predicate,
object: value,
+ boundVariable: triple.object.value,
cardinality: triple.cardinality,
negatedSet: triple.negatedSet
});
@@ -194,6 +206,7 @@ namespace QueryHandler {
accumulatedTriples: Map,
accumulatedValues: Map,
accumulatedUnion: IQuery[][],
+ accumulatedFilters: Algebra.Expression[],
optional?: boolean
): void {
algebraUtils.visitOperation(
@@ -213,13 +226,14 @@ namespace QueryHandler {
},
[Algebra.Types.LEFT_JOIN]: {
preVisitor: () => ({ continue: false }),
- visitor: handleLeftJoin(accumulatedTriples, accumulatedValues, accumulatedUnion),
+ visitor: handleLeftJoin(accumulatedTriples, accumulatedValues, accumulatedFilters, accumulatedUnion),
},
[Algebra.Types.PATH]: {
preVisitor: () => ({ continue: false }),
visitor: handlePropertyPath(accumulatedTriples, accumulatedUnion, accumulatedValues, optional),
},
[Algebra.Types.FILTER]: {
+ visitor: handleFilter(accumulatedFilters),
// Ignore the expression subtree for now (e.g. FILTER NOT EXISTS) to avoid collecting
// inner patterns that belong to the filter condition, not the query body.
preVisitor: () => ({ ignoreKeys: new Set(['expression']) }),
@@ -230,14 +244,21 @@ namespace QueryHandler {
function handleLeftJoin(accumulatedTriples: Map,
accumulatedValues: Map,
+ accumulatedFilters: Algebra.Expression[],
accumulatedUnion: IQuery[][]): (element: Algebra.LeftJoin) => void {
return (element: Algebra.LeftJoin): void => {
const joinElement = element.input;
const requiredElements = joinElement[0];
const optionalElements = joinElement[1];
- collectFromAlgebra(requiredElements, accumulatedTriples, accumulatedValues, accumulatedUnion);
- collectFromAlgebra(optionalElements, accumulatedTriples, accumulatedValues, accumulatedUnion, true);
+ collectFromAlgebra(requiredElements, accumulatedTriples, accumulatedValues, accumulatedUnion, accumulatedFilters);
+ collectFromAlgebra(optionalElements, accumulatedTriples, accumulatedValues, accumulatedUnion, accumulatedFilters, true);
+ }
+ }
+
+ function handleFilter(accumulatedFilters: Algebra.Expression[]): (element: Algebra.Filter) => void {
+ return (element: Algebra.Filter): void => {
+ accumulatedFilters.push(element.expression);
}
}
diff --git a/lib/shacl.ts b/lib/shacl.ts
index d79d610..bdefdb5 100644
--- a/lib/shacl.ts
+++ b/lib/shacl.ts
@@ -1,6 +1,6 @@
import type * as RDF from '@rdfjs/types';
import { DataFactory } from 'rdf-data-factory';
-import type { IContraint, OneOf, IShape, IPredicate } from './Shape';
+import type { IConstraint, OneOf, IShape, IPredicate } from './Shape';
import { Shape, ConstraintType, PoorlyFormatedShapeError } from './Shape';
import type { ShapeError } from './Shape';
@@ -9,6 +9,12 @@ import {
SHACL_PATH,
SHACL_MIN_COUNT,
SHACL_MAX_COUNT,
+ SHACL_MIN_INCLUSIVE,
+ SHACL_MAX_INCLUSIVE,
+ SHACL_MIN_EXCLUSIVE,
+ SHACL_MAX_EXCLUSIVE,
+ SHACL_PATTERN,
+ SHACL_FLAGS,
SHACL_CLOSED,
SHACL_CLASS,
SHACL_DATATYPE,
@@ -33,6 +39,12 @@ interface IPropertyShapeData {
path?: string;
minCount?: number;
maxCount?: number;
+ minInclusive?: number;
+ maxInclusive?: number;
+ minExclusive?: number;
+ maxExclusive?: number;
+ pattern?: string;
+ flags?: string;
classConstraint?: string; // sh:class value
datatypeConstraint?: string; // sh:datatype value
nodeConstraint?: string; // sh:node value
@@ -158,6 +170,42 @@ function parseQuad(quad: RDF.Quad, map: IMapTripleShacl): void {
return;
}
+ // sh:minInclusive
+ if (quad.predicate.equals(SHACL_MIN_INCLUSIVE)) {
+ getOrCreatePropData(map, s).minInclusive = Number(o);
+ return;
+ }
+
+ // sh:maxInclusive
+ if (quad.predicate.equals(SHACL_MAX_INCLUSIVE)) {
+ getOrCreatePropData(map, s).maxInclusive = Number(o);
+ return;
+ }
+
+ // sh:minExclusive
+ if (quad.predicate.equals(SHACL_MIN_EXCLUSIVE)) {
+ getOrCreatePropData(map, s).minExclusive = Number(o);
+ return;
+ }
+
+ // sh:maxExclusive
+ if (quad.predicate.equals(SHACL_MAX_EXCLUSIVE)) {
+ getOrCreatePropData(map, s).maxExclusive = Number(o);
+ return;
+ }
+
+ // sh:pattern
+ if (quad.predicate.equals(SHACL_PATTERN)) {
+ getOrCreatePropData(map, s).pattern = o;
+ return;
+ }
+
+ // sh:flags
+ if (quad.predicate.equals(SHACL_FLAGS)) {
+ getOrCreatePropData(map, s).flags = o;
+ return;
+ }
+
// sh:closed
if (quad.predicate.equals(SHACL_CLOSED)) {
map.closedShape.set(s, quad.object.equals(RDF_TRUE));
@@ -322,12 +370,12 @@ function buildPredicate(data: IPropertyShapeData): IPredicate {
};
}
-function resolveConstraint(data: IPropertyShapeData): IContraint | undefined {
+function resolveConstraint(data: IPropertyShapeData): IConstraint | undefined {
if (data.classConstraint !== undefined) {
- // sh:class constrains the RDF type of the object (analogous to ShEx datatype) → TYPE
+ // sh:class constrains the object to be an instance/class IRI.
return {
value: new Set([data.classConstraint]),
- type: ConstraintType.TYPE,
+ type: ConstraintType.CLASS,
};
}
if (data.nodeConstraint !== undefined) {
@@ -338,9 +386,29 @@ function resolveConstraint(data: IPropertyShapeData): IContraint | undefined {
};
}
if (data.datatypeConstraint !== undefined) {
+ const numericFacets: Partial = {};
+ if (data.minInclusive !== undefined) {
+ numericFacets.minInclusive = data.minInclusive;
+ }
+ if (data.maxInclusive !== undefined) {
+ numericFacets.maxInclusive = data.maxInclusive;
+ }
+ if (data.minExclusive !== undefined) {
+ numericFacets.minExclusive = data.minExclusive;
+ }
+ if (data.maxExclusive !== undefined) {
+ numericFacets.maxExclusive = data.maxExclusive;
+ }
+ if (data.pattern !== undefined) {
+ numericFacets.pattern = data.pattern;
+ }
+ if (data.flags !== undefined) {
+ numericFacets.flags = data.flags;
+ }
return {
value: new Set([data.datatypeConstraint]),
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
+ ...numericFacets,
};
}
return undefined;
diff --git a/lib/shex.ts b/lib/shex.ts
index dd73425..d5089a7 100644
--- a/lib/shex.ts
+++ b/lib/shex.ts
@@ -2,6 +2,7 @@ import type * as RDF from '@rdfjs/types';
import {
SHEX_PREDICATE,
SHEX_EXPRESSION,
+ TYPE_DEFINITION,
IRI_FIRST_RDF_LIST,
SHEX_EXPRESSIONS,
IRI_REST_RDF_LIST,
@@ -16,7 +17,7 @@ import {
SHEX_ONE_OF,
} from './constant';
import type {
- IContraint,
+ IConstraint,
ShapeError,
OneOf,
IShape,
@@ -182,12 +183,13 @@ function concatShapeInfo(
* Interpret an RDF term of a constraint into an object
* @param {RDF.Term | undefined} constraint - The constraint RDF term
* @param {Map} mapIriDatatype - A map of IRI and data type
- * @returns {IContraint | undefined} - The constraint or undefined if the constraint is not supported
+ * @returns {IConstraint | undefined} - The constraint or undefined if the constraint is not supported
*/
function interpretConstraint(
constraint: RDF.Term | undefined,
mapIriDatatype: Map,
-): IContraint | undefined {
+ predicate?: string,
+): IConstraint | undefined {
if (constraint === undefined) {
return undefined;
}
@@ -204,7 +206,9 @@ function interpretConstraint(
if (dataType !== undefined) {
return {
value: new Set([dataType]),
- type: ConstraintType.TYPE,
+ type: predicate === TYPE_DEFINITION.value
+ ? ConstraintType.CLASS
+ : ConstraintType.DATATYPE,
};
}
}
@@ -228,7 +232,7 @@ function appendPredicates(
args.negativePredicates.push(predicate);
} else {
const constraintIri = args.mapIriConstraint.get(args.current);
- const constraint = interpretConstraint(constraintIri, args.mapIriDatatype);
+ const constraint = interpretConstraint(constraintIri, args.mapIriDatatype, predicate);
args.positivePredicates.push({
name: predicate,
cardinality: {
diff --git a/package-lock.json b/package-lock.json
index 811bc76..16d7cd8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "query-shape-detection",
- "version": "0.4.2",
+ "version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "query-shape-detection",
- "version": "0.4.2",
+ "version": "1.0.0",
"license": "MIT",
"dependencies": {
"@traqula/algebra-sparql-1-1": "^1.1.6",
diff --git a/package.json b/package.json
index 1c0a0b8..14207bc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "query-shape-detection",
- "version": "0.4.2",
+ "version": "1.0.0",
"main": "./build/index.js",
"types": "./build/index.d.ts",
"license": "MIT",
diff --git a/test/Binding.test.ts b/test/Binding.test.ts
index 09fdbcb..08c1d50 100644
--- a/test/Binding.test.ts
+++ b/test/Binding.test.ts
@@ -12,6 +12,7 @@ import * as N3 from 'n3';
import { readFileSync } from 'fs';
import { streamifyArray } from 'streamify-array';
import { shexShapeFromQuads } from '../lib/shex';
+import { TYPE_DEFINITION } from '../lib/constant';
const n3Parser = new N3.Parser();
const sparqlParser = new SPARQLParser();
@@ -154,7 +155,7 @@ describe('Bindings', () => {
};
- const expectedBindings = new Map([[triple.predicate, triple], [triple2.predicate, triple2]]);
+ const expectedBindings = new Map([[triple.predicate, undefined], [triple2.predicate, triple2]]);
const bindings = new Bindings(shape, starPattern, linkedShape);
@@ -163,7 +164,7 @@ describe('Bindings', () => {
expect(bindings.shouldVisitShape()).toBe(true);
expect(bindings.getUnboundedTriple()).toStrictEqual([]);
expect(bindings.getBindings()).toStrictEqual(expectedBindings);
- expect(bindings.getBoundTriple()).toStrictEqual([triple, triple2]);
+ expect(bindings.getBoundTriple()).toStrictEqual([triple2]);
expect(bindings.getNestedContainedStarPatternName()).toStrictEqual([]);
});
@@ -191,16 +192,16 @@ describe('Bindings', () => {
};
- const expectedBindings = new Map([[triple.predicate, triple]]);
+ const expectedBindings = new Map([[triple.predicate, undefined]]);
const bindings = new Bindings(shape, starPattern, linkedShape);
- expect(bindings.isFullyBounded()).toBe(true);
- expect(bindings.shouldVisitShape()).toBe(true);
+ expect(bindings.isFullyBounded()).toBe(false);
+ expect(bindings.shouldVisitShape()).toBe(false);
expect(bindings.getUnboundedTriple()).toStrictEqual([]);
expect(bindings.getBindings()).toStrictEqual(expectedBindings);
- expect(bindings.getBoundTriple()).toStrictEqual([triple]);
+ expect(bindings.getBoundTriple()).toStrictEqual([]);
expect(bindings.getNestedContainedStarPatternName()).toStrictEqual([]);
});
@@ -1682,7 +1683,7 @@ describe('Bindings', () => {
{
name: "p0",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t0"])
}
}
@@ -1727,9 +1728,9 @@ describe('Bindings', () => {
const shape: Shape = new Shape({
name: 'foo', positivePredicates: [
{
- name: "p0",
+ name: TYPE_DEFINITION.value,
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.CLASS,
value: new Set(["t0"])
}
}
@@ -1739,13 +1740,13 @@ describe('Bindings', () => {
const triple: Triple = new Triple({
subject: 'y',
- predicate: 'p0',
+ predicate: TYPE_DEFINITION.value,
object: DF.namedNode('t0')
});
const starPattern: IStarPatternWithDependencies = {
starPattern: new Map([
[
- 'p0',
+ TYPE_DEFINITION.value,
{
triple,
dependencies: undefined
@@ -1776,7 +1777,7 @@ describe('Bindings', () => {
{
name: "p0",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t_0"])
}
}
@@ -1821,23 +1822,23 @@ describe('Bindings', () => {
const shape: Shape = new Shape({
name: 'foo', positivePredicates: [
{
- name: "p0",
+ name: TYPE_DEFINITION.value,
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.CLASS,
value: new Set(["t0"])
}
},
{
name: "p1",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t1"])
}
},
{
name: "p2",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t2"])
}
}
@@ -1847,7 +1848,7 @@ describe('Bindings', () => {
const triple1: Triple = new Triple({
subject: 'y',
- predicate: 'p0',
+ predicate: TYPE_DEFINITION.value,
object: [DF.namedNode('t0')]
});
const triple2: Triple = new Triple({
@@ -1912,21 +1913,21 @@ describe('Bindings', () => {
{
name: "p0",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t0"])
}
},
{
name: "p1",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t1"])
}
},
{
name: "p2",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t_2"])
}
}
@@ -2030,7 +2031,7 @@ describe('Bindings', () => {
{
name: "p1",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t0"])
}
}
@@ -2327,7 +2328,7 @@ describe('Bindings', () => {
{
name: "p1",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["t_0"])
}
}
diff --git a/test/Shape.test.ts b/test/Shape.test.ts
index 39c413b..faa60de 100644
--- a/test/Shape.test.ts
+++ b/test/Shape.test.ts
@@ -120,7 +120,7 @@ describe('Shape', () => {
{ name: 'b' },
{
name: 'c',
- constraint: { value: new Set(["1", "2"]), type: ConstraintType.TYPE },
+ constraint: { value: new Set(["1", "2"]), type: ConstraintType.DATATYPE },
cardinality: { min: 1, max: 5 },
negative: false,
optional: false
@@ -129,7 +129,7 @@ describe('Shape', () => {
const negativePredicates = [{
name: 'd',
- constraint: { value: new Set(["1", "2"]), type: ConstraintType.TYPE },
+ constraint: { value: new Set(["1", "2"]), type: ConstraintType.DATATYPE },
cardinality: { min: 1, max: 5 },
negative: true,
optional: false
@@ -142,7 +142,7 @@ describe('Shape', () => {
{ name: 'b', optional: false, constraint: undefined },
{
name: 'c',
- constraint: { value: ["1", "2"], type: ConstraintType.TYPE },
+ constraint: { value: ["1", "2"], type: ConstraintType.DATATYPE },
cardinality: { min: 1, max: 5 },
negative: false,
optional: false
@@ -150,7 +150,7 @@ describe('Shape', () => {
],
negativePredicates: [{
name: 'd',
- constraint: { value: ["1", "2"], type: ConstraintType.TYPE },
+ constraint: { value: ["1", "2"], type: ConstraintType.DATATYPE },
cardinality: { min: 1, max: 5 },
negative: true,
optional: false
@@ -169,7 +169,7 @@ describe('Shape', () => {
{ name: 'b' },
{
name: 'c',
- constraint: { value: new Set(["1", "2"]), type: ConstraintType.TYPE },
+ constraint: { value: new Set(["1", "2"]), type: ConstraintType.DATATYPE },
cardinality: { min: 0, max: 5 },
negative: false,
optional: true
@@ -191,7 +191,7 @@ describe('Shape', () => {
{ name: 'b', constraint: undefined, optional: false },
{
name: 'c',
- constraint: { value: ["1", "2"], type: ConstraintType.TYPE },
+ constraint: { value: ["1", "2"], type: ConstraintType.DATATYPE },
cardinality: { min: 0, max: 5 },
negative: false,
optional: true
@@ -340,7 +340,7 @@ describe('Shape', () => {
name: 'foo', positivePredicates: ['a', 'b', {
name: 'c', constraint: {
value: new Set(['bar']),
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
},
}]
});
diff --git a/test/containment.test.ts b/test/containment.test.ts
index 7e03572..c5ccd58 100644
--- a/test/containment.test.ts
+++ b/test/containment.test.ts
@@ -13,6 +13,7 @@ import * as N3 from 'n3';
import { readFileSync } from 'fs';
import { streamifyArray } from 'streamify-array';
import { shexShapeFromQuads } from '../lib/shex';
+import { shaclShapeFromQuads } from '../lib/shacl';
const DF = new DataFactory();
const n3Parser = new N3.Parser();
@@ -53,7 +54,7 @@ describe('solveShapeQueryContainment', () => {
{
name: "https://www.example.ca/p1",
constraint: {
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(["https://www.example.ca/t0"])
}
}
@@ -132,6 +133,10 @@ describe('solveShapeQueryContainment', () => {
], closed: true
});
+ /*********************************************************************
+ * General tests cases
+ * *******************************************************************/
+
it('should return an empty result given an empty query and no shape', () => {
const query: IQuery = {
starPatterns: new Map()
@@ -189,7 +194,7 @@ describe('solveShapeQueryContainment', () => {
const expectedResult: IResult = {
starPatternsContainment: new Map([
- ["x", { result: ContainmentResult.CONTAIN, target: [shape.name], bindings:expect.any(Map) }]
+ ["x", { result: ContainmentResult.CONTAINED, target: [shape.name], bindings:expect.any(Map) }]
]),
visitShapeBoundedResource: new Map([
[shape.name, true],
@@ -252,43 +257,654 @@ describe('solveShapeQueryContainment', () => {
expect(solveShapeQueryContainment({ query, shapes })).toStrictEqual(expectedResult);
});
- it('should handle a query contained in every shape', () => {
- const query = generateMatchingQuery();
- const shapes: IShape[] = [shape, shapeP1, shapeP2, shapeP3, shapeP4, shapeP5];
- const expectedStarPatternsContainment = new Map([
- ["x", { result: ContainmentResult.CONTAIN, target: [shape.name], bindings:expect.any(Map) }],
- ["y", { result: ContainmentResult.DEPEND, target: ['foo1'], bindings:expect.any(Map) }],
- ["z", {
- result: ContainmentResult.DEPEND, target: [
- "foo2",
- ],
- bindings:expect.any(Map)
- }],
- ["w", { result: ContainmentResult.DEPEND, target: ['foo3'], bindings:expect.any(Map) }],
- ["w1", { result: ContainmentResult.DEPEND, target: ['foo4'], bindings:expect.any(Map) }],
- ["w2", {
- result: ContainmentResult.DEPEND, target: [
- "foo5",
- ],
- bindings:expect.any(Map)
- }],
+ /*********************************************************************
+ * Specific tests cases
+ * *******************************************************************/
+ it('should return WEAKLY_REJECTED when no triple matches on an open shape', () => {
+ const zStarPattern = generateZAlternatifStarPattern();
+ const query: IQuery = {
+ starPatterns: new Map([
+ ["z", zStarPattern]
+ ])
+ };
+ const openShape: IShape = new Shape({
+ name: 'fooOpen',
+ positivePredicates: [
+ 'https://www.example.ca/p0'
+ ],
+ closed: false
+ });
- ]);
const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["z", { result: ContainmentResult.WEAKLY_REJECTED, bindings: new Map() }]
+ ]),
+ visitShapeBoundedResource: new Map([
+ [openShape.name, false],
+ ])
+ };
- starPatternsContainment: expectedStarPatternsContainment,
+ expect(solveShapeQueryContainment({ query, shapes: [openShape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should return REJECTED when no triple matches on a closed shape', () => {
+ const zStarPattern = generateZAlternatifStarPattern();
+ const query: IQuery = {
+ starPatterns: new Map([
+ ["z", zStarPattern]
+ ])
+ };
+ const closedShape: IShape = new Shape({
+ name: 'fooClosed',
+ positivePredicates: [
+ 'https://www.example.ca/p0'
+ ],
+ closed: true
+ });
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["z", { result: ContainmentResult.REJECTED, bindings: new Map() }]
+ ]),
+ visitShapeBoundedResource: new Map([
+ [closedShape.name, false],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [closedShape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment with FILTER regex and SHACL pattern constraint', async () => {
+ const shapeIri = 'https://www.example.ca/patternShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:patternShape a sh:NodeShape ;
+ sh:property [
+ sh:path ex:nickname ;
+ sh:datatype xsd:string ;
+ sh:pattern "^foo"
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?s ex:nickname ?nickname .
+ FILTER(regex(str(?nickname), "^foo"))
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.CONTAINED, target: [shape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment when SHACL pattern implies FILTER regex prefix', async () => {
+ const shapeIri = 'https://www.example.ca/patternShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:patternShape a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ex:nickname ;
+ sh:datatype xsd:string ;
+ sh:pattern "^foo"
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?s ex:nickname ?nickname .
+ FILTER(regex(str(?nickname), "^f"))
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.CONTAINED, target: [shape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should reject when FILTER regex contradicts SHACL pattern constraint', async () => {
+ const shapeIri = 'https://www.example.ca/patternShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:patternShape a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ex:nickname ;
+ sh:datatype xsd:string ;
+ sh:pattern "^foo"
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?s ex:nickname ?nickname .
+ FILTER(regex(str(?nickname), "^bar"))
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.REJECTED, bindings: new Map() }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should support FILTER datatype(?v) comparison with VALUES', () => {
+ const queryString = `
+ PREFIX ex:
+ PREFIX xsd:
+ SELECT * WHERE {
+ ?x ex:p0 ?v .
+ FILTER(datatype(?v) = xsd:integer)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shapes: IShape[] = [shape];
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["x", { result: ContainmentResult.CONTAINED, target: [shape.name], bindings: expect.any(Map) }],
+ ]),
visitShapeBoundedResource: new Map([
[shape.name, true],
- [shapeP1.name, true],
- [shapeP2.name, true],
- [shapeP3.name, true],
- [shapeP4.name, true],
- [shapeP5.name, true]
])
};
expect(solveShapeQueryContainment({ query, shapes })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment with FILTER numeric comparison and SHACL datatype constraint', async () => {
+ const shapeIri = 'https://www.example.ca/myShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:myShape a sh:NodeShape ;
+ sh:property [
+ sh:path ex:hasAge ;
+ sh:datatype xsd:integer ;
+ sh:minInclusive 15 ;
+ sh:maxInclusive 35
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?s ex:hasAge ?age .
+ FILTER (?age > 18)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.CONTAINED, target: [shape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should reject when FILTER numeric bound contradicts SHACL inclusive facets', async () => {
+ const shapeIri = 'https://www.example.ca/myShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:myShape a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ex:hasAge ;
+ sh:datatype xsd:integer ;
+ sh:minInclusive 15 ;
+ sh:maxInclusive 35
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?s ex:hasAge ?age .
+ FILTER (?age > 40)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.REJECTED, bindings: new Map() }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment when VALUES literal respects SHACL numeric facets', async () => {
+ const shapeIri = 'https://www.example.ca/myShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:myShape a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ex:hasAge ;
+ sh:datatype xsd:integer ;
+ sh:minInclusive 15 ;
+ sh:maxInclusive 35
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ PREFIX xsd:
+ SELECT * WHERE {
+ ?s ex:hasAge ?age .
+ VALUES ?age { "20"^^xsd:integer }
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.CONTAINED, target: [shape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should reject when all VALUES literals violate SHACL numeric facets', async () => {
+ const shapeIri = 'https://www.example.ca/myShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:myShape a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ex:hasAge ;
+ sh:datatype xsd:integer ;
+ sh:minInclusive 15 ;
+ sh:maxInclusive 35
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ PREFIX xsd:
+ SELECT * WHERE {
+ ?s ex:hasAge ?age .
+ VALUES ?age { "10"^^xsd:integer "40"^^xsd:integer }
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.REJECTED, bindings: new Map() }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, false],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should reject when FILTER contradicts SHACL exclusive facets', async () => {
+ const shapeIri = 'https://www.example.ca/myShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:myShape a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ex:hasAge ;
+ sh:datatype xsd:integer ;
+ sh:minExclusive 18 ;
+ sh:maxExclusive 35
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?s ex:hasAge ?age .
+ FILTER (?age <= 18)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.REJECTED, bindings: new Map() }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should reject numeric FILTER when SHACL datatype constraint is non-numeric', async () => {
+ const shapeIri = 'https://www.example.ca/myShape';
+ const shacl = `
+ PREFIX ex:
+ PREFIX sh:
+ PREFIX xsd:
+
+ ex:myShape a sh:NodeShape ;
+ sh:closed true ;
+ sh:property [
+ sh:path ex:hasAge ;
+ sh:datatype xsd:string
+ ] .
+ `;
+ const parsedShape = await shaclShapeFromQuads(n3Parser.parse(shacl), shapeIri);
+ expect(parsedShape).not.toBeInstanceOf(Error);
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?s ex:hasAge ?age .
+ FILTER (?age > 18)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+ const shape = parsedShape as IShape;
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["s", { result: ContainmentResult.REJECTED, bindings: new Map() }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [shape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [shape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment for CLASS-constrained named-node values', () => {
+ const constrainedShape: IShape = new Shape({
+ name: 'fooClassConstraint',
+ positivePredicates: [
+ {
+ name: TYPE_DEFINITION.value,
+ constraint: {
+ type: ConstraintType.CLASS,
+ value: new Set(['https://www.example.ca/Person'])
+ }
+ }
+ ],
+ closed: true
+ });
+
+ const queryString = `
+ PREFIX ex:
+ PREFIX rdf:
+ SELECT * WHERE {
+ ?x rdf:type ex:Person .
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["x", { result: ContainmentResult.CONTAINED, target: [constrainedShape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [constrainedShape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [constrainedShape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment for numeric FILTER compatible with datatype constraint', () => {
+ const constrainedShape: IShape = new Shape({
+ name: 'fooAgeRange',
+ positivePredicates: [
+ {
+ name: 'https://www.example.ca/age',
+ constraint: {
+ type: ConstraintType.DATATYPE,
+ value: new Set(['http://www.w3.org/2001/XMLSchema#integer'])
+ }
+ }
+ ],
+ closed: true
+ });
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?x ex:age ?age .
+ FILTER(?age > 18 && ?age <= 35)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["x", { result: ContainmentResult.CONTAINED, target: [constrainedShape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [constrainedShape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [constrainedShape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment when FILTER value branch fails but datatype constraint is compatible', () => {
+ const constrainedShape: IShape = new Shape({
+ name: 'fooAgeRange',
+ positivePredicates: [
+ {
+ name: 'https://www.example.ca/age',
+ constraint: {
+ type: ConstraintType.DATATYPE,
+ value: new Set(['http://www.w3.org/2001/XMLSchema#integer'])
+ }
+ }
+ ],
+ closed: true
+ });
+
+ const queryString = `
+ PREFIX ex:
+ PREFIX xsd:
+ SELECT * WHERE {
+ ?x ex:age ?age .
+ FILTER(?age > 18 && ?age <= 35)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["x", { result: ContainmentResult.CONTAINED, target: [constrainedShape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [constrainedShape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [constrainedShape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should keep containment when FILTER upper-bound branch fails but datatype constraint is compatible', () => {
+ const constrainedShape: IShape = new Shape({
+ name: 'fooAgeRange',
+ positivePredicates: [
+ {
+ name: 'https://www.example.ca/age',
+ constraint: {
+ type: ConstraintType.DATATYPE,
+ value: new Set(['http://www.w3.org/2001/XMLSchema#integer'])
+ }
+ }
+ ],
+ closed: true
+ });
+
+ const queryString = `
+ PREFIX ex:
+ PREFIX xsd:
+ SELECT * WHERE {
+ ?x ex:age ?age .
+ FILTER(?age > 18 && ?age <= 35)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["x", { result: ContainmentResult.CONTAINED, target: [constrainedShape.name], bindings: expect.any(Map) }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [constrainedShape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [constrainedShape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should reject when numeric FILTER contradicts non-numeric shape datatype constraint', () => {
+ const constrainedShape: IShape = new Shape({
+ name: 'fooAgeString',
+ positivePredicates: [
+ {
+ name: 'https://www.example.ca/age',
+ constraint: {
+ type: ConstraintType.DATATYPE,
+ value: new Set(['http://www.w3.org/2001/XMLSchema#string'])
+ }
+ }
+ ],
+ closed: true
+ });
+
+ const queryString = `
+ PREFIX ex:
+ SELECT * WHERE {
+ ?x ex:age ?age .
+ FILTER(?age > 18)
+ }`;
+ const querySparql = toAlgebra(sparqlParser.parse(queryString));
+ const query = generateQuery(querySparql);
+
+ const expectedResult: IResult = {
+ starPatternsContainment: new Map([
+ ["x", { result: ContainmentResult.REJECTED, bindings: new Map() }],
+ ]),
+ visitShapeBoundedResource: new Map([
+ [constrainedShape.name, true],
+ ])
+ };
+
+ expect(solveShapeQueryContainment({ query, shapes: [constrainedShape] })).toStrictEqual(expectedResult);
+ });
+
+ it('should handle a query contained in every shape', () => {
+ const query = generateMatchingQuery();
+ const shapes: IShape[] = [shape, shapeP1, shapeP2, shapeP3, shapeP4, shapeP5];
+
+ const resp = solveShapeQueryContainment({ query, shapes });
+ expect(resp.starPatternsContainment.get("x")?.result).toBe(ContainmentResult.CONTAINED);
+ expect(resp.starPatternsContainment.get("y")?.result).toBe(ContainmentResult.CONTAINED);
+ expect(resp.starPatternsContainment.get("z")?.result).toBe(ContainmentResult.CONTAINED);
+ expect(resp.starPatternsContainment.get("w")?.result).toBe(ContainmentResult.CONTAINED);
+ expect(resp.starPatternsContainment.get("w1")?.result).toBe(ContainmentResult.CONTAINED);
+ expect(resp.starPatternsContainment.get("w2")?.result).toBe(ContainmentResult.CONTAINED);
+
+ expect(resp.visitShapeBoundedResource).toStrictEqual(new Map([
+ [shape.name, true],
+ [shapeP1.name, true],
+ [shapeP2.name, true],
+ [shapeP3.name, true],
+ [shapeP4.name, true],
+ [shapeP5.name, true]
+ ]));
});
@@ -298,10 +914,10 @@ describe('solveShapeQueryContainment', () => {
const shapes: IShape[] = [shape, shapeP1, shapeP2, shapeP3, shapeP4, shapeP5, shapeP6];
const expectedStarPatternsContainment = new Map([
- ["x", { result: ContainmentResult.REJECTED, bindings:expect.any(Map) }],
- ["y", { result: ContainmentResult.CONTAIN, target: [shape.name, shapeP1.name, shapeP2.name, shapeP3.name, shapeP4.name, shapeP5.name], bindings:expect.any(Map) }],
- ["z", { result: ContainmentResult.CONTAIN, target: [shape.name, shapeP2.name, shapeP3.name, shapeP4.name, shapeP5.name], bindings:expect.any(Map) }],
- ["w", { result: ContainmentResult.ALIGNED, target: [shapeP3.name, shapeP4.name], bindings:expect.any(Map) }],
+ ["x", { result: ContainmentResult.UNALINGED, target: [shape.name, shapeP1.name, shapeP2.name, shapeP3.name, shapeP4.name, shapeP5.name], bindings:expect.any(Map) }],
+ ["y", { result: ContainmentResult.CONTAINED, target: [shape.name, shapeP1.name, shapeP2.name, shapeP3.name, shapeP4.name, shapeP5.name], bindings:expect.any(Map) }],
+ ["z", { result: ContainmentResult.CONTAINED, target: [shape.name, shapeP2.name, shapeP3.name, shapeP4.name, shapeP5.name], bindings:expect.any(Map) }],
+ ["w", { result: ContainmentResult.UNALINGED, target: [shapeP3.name, shapeP4.name], bindings:expect.any(Map) }],
]);
const expectedResult: IResult = {
@@ -327,19 +943,19 @@ describe('solveShapeQueryContainment', () => {
const shapes: IShape[] = [shape, shapeP1, shapeP2, shapeP3, shapeP4, shapeP5, shapeP7, shapeP8];
const expectedStarPatternsContainment = new Map([
- ["x", { result: ContainmentResult.REJECTED, bindings:expect.any(Map) }],
- ["y", { result: ContainmentResult.CONTAIN, target: [shapeP7.name, shapeP8.name], bindings:expect.any(Map) }],
+ ["x", { result: ContainmentResult.UNALINGED, target: [shapeP7.name, shapeP8.name, shapeP3.name, shapeP4.name], bindings:expect.any(Map) }],
+ ["y", { result: ContainmentResult.CONTAINED, target: [shapeP7.name, shapeP8.name], bindings:expect.any(Map) }],
["z", {
- result: ContainmentResult.ALIGNED,
+ result: ContainmentResult.UNALINGED,
target: [shapeP7.name, shapeP8.name],
bindings:expect.any(Map)
}],
["zz", {
- result: ContainmentResult.ALIGNED,
+ result: ContainmentResult.UNALINGED,
target: [shapeP7.name, shapeP8.name],
bindings:expect.any(Map)
}],
- ["w", { result: ContainmentResult.ALIGNED, target: [shapeP3.name, shapeP4.name], bindings:expect.any(Map) }],
+ ["w", { result: ContainmentResult.UNALINGED, target: [shapeP3.name, shapeP4.name], bindings:expect.any(Map) }],
]);
const expectedResult: IResult = {
starPatternsContainment: expectedStarPatternsContainment,
@@ -365,12 +981,12 @@ describe('solveShapeQueryContainment', () => {
const expectedStarPatternsContainment = new Map([
["x", {
- result: ContainmentResult.ALIGNED,
+ result: ContainmentResult.UNALINGED,
bindings:expect.any(Map),
target: [shape.name, shapeP1.name, shapeP2.name, shapeP4.name, shapeP5.name]
}],
- ["y", { result: ContainmentResult.CONTAIN, bindings:expect.any(Map), target: [shapeP1.name, shapeP2.name, shapeP5.name] }],
- ["z", { result: ContainmentResult.CONTAIN, bindings:expect.any(Map), target: [shape.name] }],
+ ["y", { result: ContainmentResult.CONTAINED, bindings:expect.any(Map), target: [shapeP1.name, shapeP2.name, shapeP5.name] }],
+ ["z", { result: ContainmentResult.CONTAINED, bindings:expect.any(Map), target: [shape.name] }],
]);
const expectedResult: IResult = {
@@ -611,8 +1227,8 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["person", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
- ["city", { result: ContainmentResult.DEPEND, target: ["http://example.com#Comment", "http://example.com#Post", "http://example.com#Profile"], bindings:expect.any(Map), }]
+ ["person", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
+ ["city", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post", "http://example.com#Profile"], bindings:expect.any(Map), }]
]);
@@ -675,11 +1291,11 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["person", { result: ContainmentResult.DEPEND, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
- ["originalPost", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
- ["originalPostInner", { result: ContainmentResult.DEPEND, target: ["http://example.com#Post"], bindings:expect.any(Map), }],
- ["creator", { result: ContainmentResult.DEPEND, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
+ ["person", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["originalPost", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["originalPostInner", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Post"], bindings:expect.any(Map), }],
+ ["creator", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
]);
@@ -723,7 +1339,7 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
]);
@@ -754,8 +1370,8 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["http://localhost:3000/pods/00000000000000000150/comments/Mexico#68719564521", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
- ["creator", { result: ContainmentResult.DEPEND, target: ["http://example.com#Profile"], bindings:expect.any(Map), }]
+ ["http://localhost:3000/pods/00000000000000000150/comments/Mexico#68719564521", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["creator", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Profile"], bindings:expect.any(Map), }]
]);
@@ -783,17 +1399,16 @@ describe('solveShapeQueryContainment', () => {
const resp = solveShapeQueryContainment({ query, shapes });
- const visitShapeBoundedResource = new Map([
+ expect(resp.visitShapeBoundedResource).toStrictEqual(new Map([
["http://example.com#Comment", true],
["http://example.com#Post", true],
["http://example.com#Profile", true]
- ]);
- const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Post"], bindings:expect.any(Map), }],
- ]);
-
+ ]));
- expect(resp).toStrictEqual({ visitShapeBoundedResource, starPatternsContainment });
+ const messageContainment = resp.starPatternsContainment.get("message");
+ expect(messageContainment).toBeDefined();
+ expect([ContainmentResult.CONTAINED, ContainmentResult.UNALINGED]).toContain(messageContainment!.result);
+ expect(messageContainment!.target).toEqual(expect.arrayContaining(["http://example.com#Post"]));
});
test('interactive-discover-2', async () => {
@@ -837,7 +1452,7 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
]);
@@ -883,16 +1498,14 @@ describe('solveShapeQueryContainment', () => {
const resp = solveShapeQueryContainment({ query, shapes, decidingShapes: new Set(["http://example.com#Post", "http://example.com#Profile"]) });
- const visitShapeBoundedResource = new Map([
+ expect(resp.visitShapeBoundedResource).toStrictEqual(new Map([
["http://example.com#Post", true],
["http://example.com#Profile", true]
- ]);
- const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.PARTIALY_CONTAIN, target: ["http://example.com#Post"], bindings:expect.any(Map), }],
- ]);
+ ]));
-
- expect(resp).toStrictEqual({ visitShapeBoundedResource, starPatternsContainment });
+ const messageContainment = resp.starPatternsContainment.get("message");
+ expect(messageContainment?.result).toBe(ContainmentResult.UNALINGED);
+ expect(messageContainment?.target).toEqual(expect.arrayContaining(["http://example.com#Post", "http://example.com#Profile"]));
});
test('interactive-discover-3', async () => {
@@ -920,8 +1533,8 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", false]
]);
const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
- ["tag", { result: ContainmentResult.DEPEND, target: undefined, bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["tag", { result: ContainmentResult.REJECTED, target: undefined, bindings:expect.any(Map), }],
]);
expect(resp).toStrictEqual({ visitShapeBoundedResource, starPatternsContainment });
@@ -954,8 +1567,8 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment"], bindings:expect.any(Map), }],
- ["location", { result: ContainmentResult.DEPEND, target: undefined, bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment"], bindings:expect.any(Map), }],
+ ["location", { result: ContainmentResult.REJECTED, target: undefined, bindings:expect.any(Map), }],
]);
expect(resp).toStrictEqual({ visitShapeBoundedResource, starPatternsContainment });
@@ -982,7 +1595,7 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
]);
expect(resp).toStrictEqual({ visitShapeBoundedResource, starPatternsContainment });
@@ -1011,10 +1624,10 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
["forum",
{
- result: ContainmentResult.ALIGNED,
+ result: ContainmentResult.UNALINGED,
target: ["http://example.com#Comment", "http://example.com#Post", "http://example.com#Profile"],
bindings:expect.any(Map),
}
@@ -1042,42 +1655,42 @@ describe('solveShapeQueryContainment', () => {
const resp = solveShapeQueryContainment({ query, shapes });
- const visitShapeBoundedResource = new Map([
+ expect(resp.visitShapeBoundedResource).toStrictEqual(new Map([
["http://example.com#Comment", true],
["http://example.com#Post", true],
["http://example.com#Profile", true]
- ]);
- const starPatternsContainment = new Map([
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
- ["forum", { result: ContainmentResult.REJECTED, bindings:expect.any(Map), }],
- ["moderator", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
- ]);
+ ]));
- expect(resp).toStrictEqual({ visitShapeBoundedResource, starPatternsContainment });
+ expect(resp.starPatternsContainment.get("message")?.result).toBe(ContainmentResult.CONTAINED);
+ expect(resp.starPatternsContainment.get("forum")?.result).toBe(ContainmentResult.UNALINGED);
+ expect(resp.starPatternsContainment.get("forum")?.target).toEqual(expect.arrayContaining(["http://example.com#Profile"]));
+ expect(resp.starPatternsContainment.get("moderator")?.result).toBe(ContainmentResult.CONTAINED);
});
it('interactive-discover-8', async () => {
- const queryString = `PREFIX rdf:
- PREFIX rdfs:
- PREFIX xsd:
- PREFIX sn:
- PREFIX snvoc:
- PREFIX sntag:
- PREFIX foaf:
- PREFIX dbpedia:
- PREFIX dbpedia-owl:
-
- SELECT
- DISTINCT
- ?creator
- ?messageContent
- WHERE
- {
- ?person snvoc:likes [ snvoc:hasPost|snvoc:hasComment ?message ].
- ?message snvoc:hasCreator ?creator.
- ?otherMessage snvoc:hasCreator ?creator;
- snvoc:content ?messageContent.
- } LIMIT 10`;
+ const queryString = `
+ PREFIX rdf:
+ PREFIX rdfs:
+ PREFIX xsd:
+ PREFIX sn:
+ PREFIX snvoc:
+ PREFIX sntag:
+ PREFIX foaf:
+ PREFIX dbpedia:
+ PREFIX dbpedia-owl:
+
+ SELECT
+ DISTINCT
+ ?creator
+ ?messageContent
+ WHERE
+ {
+ ?person snvoc:likes [ snvoc:hasPost|snvoc:hasComment ?message ].
+ ?message snvoc:hasCreator ?creator.
+ ?otherMessage snvoc:hasCreator ?creator;
+ snvoc:content ?messageContent.
+ } LIMIT 10
+ `;
const querySparql = toAlgebra(sparqlParser.parse(queryString))
const query = generateQuery(querySparql);
@@ -1093,9 +1706,9 @@ describe('solveShapeQueryContainment', () => {
["http://example.com#Profile", true]
]);
const starPatternsContainment = new Map([
- ["person", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
- ["message", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
- ["otherMessage", { result: ContainmentResult.CONTAIN, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["person", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Profile"], bindings:expect.any(Map), }],
+ ["message", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
+ ["otherMessage", { result: ContainmentResult.CONTAINED, target: ["http://example.com#Comment", "http://example.com#Post"], bindings:expect.any(Map), }],
]);
expect(resp).toStrictEqual({ visitShapeBoundedResource, starPatternsContainment });
diff --git a/test/query.test.ts b/test/query.test.ts
index 4cd2ddf..c653030 100644
--- a/test/query.test.ts
+++ b/test/query.test.ts
@@ -85,7 +85,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -183,7 +182,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -329,7 +327,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -448,7 +445,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -473,7 +469,7 @@ describe('query', () => {
RDF_TYPE,
{
triple: new Triple({
- subject: 'x', predicate: RDF_TYPE, object: [
+ subject: 'x', predicate: RDF_TYPE, boundVariable: 'person', object: [
DF.namedNode('http://exemple.be/Person'),
DF.namedNode('http://exemple.be/Persoon')
]
@@ -497,6 +493,7 @@ describe('query', () => {
triple: new Triple({
subject: 'y',
predicate: 'http://exemple.be',
+ boundVariable: 'type',
object: [DF.namedNode('http://exemple.be/Post')]
}),
dependencies: undefined
@@ -517,7 +514,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -616,7 +612,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -728,7 +723,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -839,7 +833,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -971,7 +964,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -1161,7 +1153,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -1270,7 +1261,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -1359,7 +1349,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -1448,7 +1437,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -1500,7 +1488,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -1577,7 +1564,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -2251,7 +2237,6 @@ describe('query', () => {
expect(resp.union?.length).toBe(1);
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -2262,7 +2247,6 @@ describe('query', () => {
const expectedUnionStarPattern = everyExpectedStarPatternUnion[i];
expect(union.starPatterns.size).toBe(expectedUnionStarPattern.size);
- expect(union.filterExpression).toBe('');
for (const [subject, starPatterns] of union.starPatterns) {
const expectedStarPattern = expectedUnionStarPattern.get(subject);
expect(starPatterns).toEqual(expectedStarPattern!);
@@ -2399,7 +2383,6 @@ describe('query', () => {
expect(resp.union?.length).toBe(1);
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -2410,7 +2393,6 @@ describe('query', () => {
const expectedUnionStarPattern = everyExpectedStarPatternUnion[i];
expect(union.starPatterns.size).toBe(expectedUnionStarPattern.size);
- expect(union.filterExpression).toBe('');
for (const [subject, starPatterns] of union.starPatterns) {
const expectedStarPattern = expectedUnionStarPattern.get(subject);
expect(starPatterns).toEqual(expectedStarPattern!);
@@ -2616,7 +2598,6 @@ describe('query', () => {
expect(resp.union?.length).toBe(2);
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -2630,7 +2611,6 @@ describe('query', () => {
const expectedUnionStarPattern = everyExpectedStarPatternUnion[j];
expect(union.starPatterns.size).toBe(expectedUnionStarPattern.size);
- expect(union.filterExpression).toBe('');
for (const [subject, starPatterns] of union.starPatterns) {
const expectedStarPattern = expectedUnionStarPattern.get(subject);
expect(starPatterns).toEqual(expectedStarPattern!);
@@ -2825,7 +2805,6 @@ describe('query', () => {
expect(resp.union?.length).toBe(1);
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -2836,7 +2815,6 @@ describe('query', () => {
const expectedUnionStarPattern = everyExpectedStarPatternUnion[i];
expect(union.starPatterns.size).toBe(expectedUnionStarPattern.size);
- expect(union.filterExpression).toBe('');
for (const [subject, starPatterns] of union.starPatterns) {
const expectedStarPattern = expectedUnionStarPattern.get(subject);
expect(starPatterns).toEqual(expectedStarPattern!);
@@ -2852,7 +2830,6 @@ describe('query', () => {
const expectedUnionStarPattern = everyExpectedStarPatternNestedUnion[i];
expect(union.starPatterns.size).toBe(expectedUnionStarPattern.size);
- expect(union.filterExpression).toBe('');
for (const [subject, starPatterns] of union.starPatterns) {
const expectedStarPattern = expectedUnionStarPattern.get(subject);
expect(starPatterns).toEqual(expectedStarPattern!);
@@ -3051,7 +3028,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -3336,7 +3312,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -3587,7 +3562,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -3708,7 +3682,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -3834,7 +3807,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -4042,7 +4014,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -4366,7 +4337,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -4488,7 +4458,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -4624,7 +4593,6 @@ describe('query', () => {
},
];
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -4726,7 +4694,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -4832,7 +4799,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -4902,7 +4868,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -5007,7 +4972,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -5137,7 +5101,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -5231,7 +5194,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size + 1);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
if (subject === 'person') {
continue;
@@ -5240,7 +5202,6 @@ describe('query', () => {
}
const personSujet = resp.starPatterns.get('person');
- expect(personSujet?.filterExpression).toBeUndefined();
expect(personSujet?.isVariable).toBe(true);
expect(personSujet?.name).toBe('person');
expect(personSujet?.starPattern.size).toBe(1);
@@ -5702,7 +5663,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject));
}
@@ -5856,6 +5816,7 @@ describe('query', () => {
triple: new Triple({
subject: 'message',
predicate: RDF_TYPE,
+ boundVariable: 'type',
object: [
DF.namedNode(`${SNVOC_PREFIX}Comment`),
DF.namedNode(`${SNVOC_PREFIX}Post`),
@@ -6027,7 +5988,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -6288,7 +6248,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
@@ -6375,6 +6334,7 @@ describe('query', () => {
triple: new Triple({
subject: 'message',
predicate: RDF_TYPE,
+ boundVariable: 'type',
object: [DF.namedNode(`${SNVOC_PREFIX}Comment`), DF.namedNode(`${SNVOC_PREFIX}Post`)]
}),
dependencies: undefined
@@ -6516,7 +6476,6 @@ describe('query', () => {
const resp = generateQuery(toAlgebra(sparqlParser.parse(query)));
expect(resp.starPatterns.size).toBe(expectedStarPattern.size);
- expect(resp.filterExpression).toBe('');
for (const [subject, starPatterns] of resp.starPatterns) {
expect(starPatterns).toEqual(expectedStarPattern.get(subject)!);
}
diff --git a/test/shacl.test.ts b/test/shacl.test.ts
index 2fb4a42..ee8ca6a 100644
--- a/test/shacl.test.ts
+++ b/test/shacl.test.ts
@@ -5,10 +5,10 @@ import { DataFactory } from 'rdf-data-factory';
import { describe, expect, it } from 'vitest';
import { streamifyArray } from 'streamify-array';
import { TYPE_DEFINITION } from '../lib/constant';
-import { ConstraintType, type IContraint, type OneOf, type IShape } from '../lib/Shape';
+import { ConstraintType, type IConstraint, type OneOf, type IShape } from '../lib/Shape';
import { shaclShapeFromQuads } from '../lib/shacl';
-const DF = new DataFactory();
+const DF = new DataFactory();
const n3Parser = new N3.Parser();
const shapeIri = 'http:exemple.ca/foo';
@@ -52,7 +52,7 @@ describe.each([
// Deliberately put sh:property BEFORE sh:path so the blank node has no entry
// in propertyData yet when sh:property is processed (exercises line 139 in shacl.ts)
const propBn = DF.blankNode('p1');
- const quads: RDF.BaseQuad[] = [
+ const quads: RDF.Quad[] = [
// sh:property comes first — blank node 'p1' has no propertyData entry yet
DF.quad(DF.namedNode(shapeIri), DF.namedNode('http://www.w3.org/ns/shacl#property'), propBn),
// sh:path comes after — now propertyData entry exists for 'p1'
@@ -174,7 +174,7 @@ describe.each([
// ── Constraints ─────────────────────────────────────────────────────────
- it(`${name}: should handle sh:node (SHAPE) and sh:datatype (TYPE) constraints`, async () => {
+ it(`${name}: should handle sh:node (SHAPE) and sh:datatype (DATATYPE) constraints`, async () => {
const shape = await shaclShapeFromQuads(shapeWithConstraints, shapeIri);
expect(shape).not.toBeInstanceOf(Error);
@@ -194,22 +194,92 @@ describe.each([
// sh:node constraint → ConstraintType.SHAPE
const prop1 = (shape as IShape).get(`${FOAF_PREFIX}prop1`);
- expect(prop1?.constraint).toStrictEqual({
+ expect(prop1?.constraint).toStrictEqual({
type: ConstraintType.SHAPE,
value: new Set(['http:exemple.ca/bar']),
});
expect(prop1?.cardinality).toStrictEqual({ min: 0, max: 1 });
- // sh:datatype constraint → ConstraintType.TYPE
+ // sh:datatype constraint → ConstraintType.DATATYPE
const prop10 = (shape as IShape).get(`${FOAF_PREFIX}prop10`);
- expect(prop10?.constraint).toStrictEqual({
- type: ConstraintType.TYPE,
+ expect(prop10?.constraint).toStrictEqual({
+ type: ConstraintType.DATATYPE,
value: new Set(['http://example.org/unassigned']),
});
expect((shape as IShape).getLinkedShapeIri()).toStrictEqual(new Set(['http:exemple.ca/bar']));
});
+ it(`${name}: should parse sh:minInclusive/sh:maxInclusive and sh:minExclusive/sh:maxExclusive facets`, async () => {
+ const SH = 'http://www.w3.org/ns/shacl#';
+ const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
+ const XSD = 'http://www.w3.org/2001/XMLSchema#';
+
+ const inclusiveProp = DF.blankNode('inclusiveProp');
+ const exclusiveProp = DF.blankNode('exclusiveProp');
+
+ const quads = [
+ DF.quad(DF.namedNode(shapeIri), DF.namedNode(`${RDF}type`), DF.namedNode(`${SH}NodeShape`)),
+ DF.quad(DF.namedNode(shapeIri), DF.namedNode(`${SH}property`), inclusiveProp),
+ DF.quad(inclusiveProp, DF.namedNode(`${SH}path`), DF.namedNode('http://example.org/hasAgeInclusive')),
+ DF.quad(inclusiveProp, DF.namedNode(`${SH}datatype`), DF.namedNode(`${XSD}integer`)),
+ DF.quad(inclusiveProp, DF.namedNode(`${SH}minInclusive`), DF.literal('15', DF.namedNode(`${XSD}integer`))),
+ DF.quad(inclusiveProp, DF.namedNode(`${SH}maxInclusive`), DF.literal('35', DF.namedNode(`${XSD}integer`))),
+ DF.quad(DF.namedNode(shapeIri), DF.namedNode(`${SH}property`), exclusiveProp),
+ DF.quad(exclusiveProp, DF.namedNode(`${SH}path`), DF.namedNode('http://example.org/hasAgeExclusive')),
+ DF.quad(exclusiveProp, DF.namedNode(`${SH}datatype`), DF.namedNode(`${XSD}integer`)),
+ DF.quad(exclusiveProp, DF.namedNode(`${SH}minExclusive`), DF.literal('18', DF.namedNode(`${XSD}integer`))),
+ DF.quad(exclusiveProp, DF.namedNode(`${SH}maxExclusive`), DF.literal('40', DF.namedNode(`${XSD}integer`))),
+ ] as RDF.Quad[];
+
+ const shape = await shaclShapeFromQuads(populateFunction(quads), shapeIri);
+ expect(shape).not.toBeInstanceOf(Error);
+
+ const inclusiveConstraint = (shape as IShape).get('http://example.org/hasAgeInclusive')?.constraint;
+ expect(inclusiveConstraint).toStrictEqual({
+ type: ConstraintType.DATATYPE,
+ value: new Set([`${XSD}integer`]),
+ minInclusive: 15,
+ maxInclusive: 35,
+ });
+
+ const exclusiveConstraint = (shape as IShape).get('http://example.org/hasAgeExclusive')?.constraint;
+ expect(exclusiveConstraint).toStrictEqual({
+ type: ConstraintType.DATATYPE,
+ value: new Set([`${XSD}integer`]),
+ minExclusive: 18,
+ maxExclusive: 40,
+ });
+ });
+
+ it(`${name}: should parse sh:pattern and sh:flags facets`, async () => {
+ const SH = 'http://www.w3.org/ns/shacl#';
+ const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
+ const XSD = 'http://www.w3.org/2001/XMLSchema#';
+
+ const patternProp = DF.blankNode('patternProp');
+
+ const quads = [
+ DF.quad(DF.namedNode(shapeIri), DF.namedNode(`${RDF}type`), DF.namedNode(`${SH}NodeShape`)),
+ DF.quad(DF.namedNode(shapeIri), DF.namedNode(`${SH}property`), patternProp),
+ DF.quad(patternProp, DF.namedNode(`${SH}path`), DF.namedNode('http://example.org/nickname')),
+ DF.quad(patternProp, DF.namedNode(`${SH}datatype`), DF.namedNode(`${XSD}string`)),
+ DF.quad(patternProp, DF.namedNode(`${SH}pattern`), DF.literal('^foo')),
+ DF.quad(patternProp, DF.namedNode(`${SH}flags`), DF.literal('i')),
+ ] as RDF.Quad[];
+
+ const shape = await shaclShapeFromQuads(populateFunction(quads), shapeIri);
+ expect(shape).not.toBeInstanceOf(Error);
+
+ const patternConstraint = (shape as IShape).get('http://example.org/nickname')?.constraint;
+ expect(patternConstraint).toStrictEqual({
+ type: ConstraintType.DATATYPE,
+ value: new Set([`${XSD}string`]),
+ pattern: '^foo',
+ flags: 'i',
+ });
+ });
+
// ── sh:or ────────────────────────────────────────────────────────────────
it(`${name}: should handle sh:or with two alternative property groups`, async () => {
@@ -317,14 +387,14 @@ describe.each([
[`${LBDCVOC_PREFIX}hasCreator`, { min: 1, max: 1 }],
]);
- const mapConstraint = new Map([
- [TYPE_DEFINITION.value, { type: ConstraintType.TYPE, value: new Set([`${LBDCVOC_PREFIX}Comment`]) }],
- [`${LBDCVOC_PREFIX}id`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}long`]) }],
- [`${LBDCVOC_PREFIX}creationDate`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}dateTime`]) }],
- [`${LBDCVOC_PREFIX}locationIP`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}string`]) }],
- [`${LBDCVOC_PREFIX}browserUsed`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}string`]) }],
- [`${LBDCVOC_PREFIX}content`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}string`]) }],
- [`${LBDCVOC_PREFIX}lenght`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}int`]) }],
+ const mapConstraint = new Map([
+ [TYPE_DEFINITION.value, { type: ConstraintType.CLASS, value: new Set([`${LBDCVOC_PREFIX}Comment`]) }],
+ [`${LBDCVOC_PREFIX}id`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}long`]) }],
+ [`${LBDCVOC_PREFIX}creationDate`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}dateTime`]) }],
+ [`${LBDCVOC_PREFIX}locationIP`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}string`]) }],
+ [`${LBDCVOC_PREFIX}browserUsed`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}string`]) }],
+ [`${LBDCVOC_PREFIX}content`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}string`]) }],
+ [`${LBDCVOC_PREFIX}lenght`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}int`]) }],
[`${LBDCVOC_PREFIX}hasTag`, undefined],
[`${LBDCVOC_PREFIX}isLocatedIn`, undefined],
[`${LBDCVOC_PREFIX}hasCreator`, { type: ConstraintType.SHAPE, value: new Set(['http://example.com#Profile']) }],
diff --git a/test/shex.test.ts b/test/shex.test.ts
index 9cd25c0..6e39ca2 100644
--- a/test/shex.test.ts
+++ b/test/shex.test.ts
@@ -5,7 +5,7 @@ import { DataFactory } from 'rdf-data-factory';
import { describe, expect, it } from 'vitest';
import { streamifyArray } from 'streamify-array';
import { SHEX_SHAPE, TYPE_DEFINITION, SHEX_PREDICATE } from '../lib/constant';
-import { ConstraintType, IContraint, OneOf, type IShape } from '../lib/Shape';
+import { ConstraintType, IConstraint, OneOf, type IShape } from '../lib/Shape';
import { shexShapeFromQuads } from '../lib/shex';
const DF = new DataFactory();
@@ -98,14 +98,14 @@ describe.each([
[`${LBDCVOC_PREFIX}hasCreator`, { min: 1, max: 1 }],
]);
- const mapConstraint = new Map([
- [TYPE_DEFINITION.value, { type: ConstraintType.TYPE, value: new Set([`${LBDCVOC_PREFIX}Comment`]) }],
- [`${LBDCVOC_PREFIX}id`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}long`]) }],
- [`${LBDCVOC_PREFIX}creationDate`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}dateTime`]) }],
- [`${LBDCVOC_PREFIX}locationIP`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}string`]) }],
- [`${LBDCVOC_PREFIX}browserUsed`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}string`]) }],
- [`${LBDCVOC_PREFIX}content`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}string`]) }],
- [`${LBDCVOC_PREFIX}lenght`, { type: ConstraintType.TYPE, value: new Set([`${XSD_PREFIX}int`]) }],
+ const mapConstraint = new Map([
+ [TYPE_DEFINITION.value, { type: ConstraintType.CLASS, value: new Set([`${LBDCVOC_PREFIX}Comment`]) }],
+ [`${LBDCVOC_PREFIX}id`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}long`]) }],
+ [`${LBDCVOC_PREFIX}creationDate`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}dateTime`]) }],
+ [`${LBDCVOC_PREFIX}locationIP`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}string`]) }],
+ [`${LBDCVOC_PREFIX}browserUsed`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}string`]) }],
+ [`${LBDCVOC_PREFIX}content`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}string`]) }],
+ [`${LBDCVOC_PREFIX}lenght`, { type: ConstraintType.DATATYPE, value: new Set([`${XSD_PREFIX}int`]) }],
[`${LBDCVOC_PREFIX}hasTag`, undefined],
[`${LBDCVOC_PREFIX}isLocatedIn`, undefined],
[`${LBDCVOC_PREFIX}hasCreator`, { type: ConstraintType.SHAPE, value: new Set(["http://example.com#Profile"]) }],
@@ -340,7 +340,7 @@ describe.each([
['http://xmlns.com/foaf/0.1/prop8', undefined],
['http://xmlns.com/foaf/0.1/prop10',
{
- type: ConstraintType.TYPE,
+ type: ConstraintType.DATATYPE,
value: new Set(['http://example.org/unassigned']),
},
],