@@ -796,6 +796,10 @@ impl ShaclValidator {
796796 // and the run came back CLEAN. Same defect in sh:in and sh:not.
797797 let mut or_alternatives: HashMap < String , Vec < String > > = HashMap :: new ( ) ;
798798 let mut or_unsupported: HashSet < String > = HashSet :: new ( ) ;
799+ // Property shape -> a member datatype the store does not preserve.
800+ // The direct sh:datatype path refuses that question; a member
801+ // answering it reported every widened value as matching nothing.
802+ let mut or_unpreserved: HashMap < String , String > = HashMap :: new ( ) ;
799803 let or_rows = query_solutions_bound (
800804 & shapes_store,
801805 & format ! (
@@ -835,7 +839,12 @@ impl ShaclValidator {
835839 continue ;
836840 }
837841 let clause = if let Some ( dt) = row. get ( "datatype" ) {
838- datatype_conforms ( & strip_angle_brackets ( dt) , "val" )
842+ let dt = strip_angle_brackets ( dt) ;
843+ if datatype_is_indistinguishable_in_store ( & dt) {
844+ or_unpreserved. insert ( p, dt) ;
845+ continue ;
846+ }
847+ datatype_conforms ( & dt, "val" )
839848 } else if let Some ( c) = row. get ( "class" ) {
840849 format ! (
841850 "EXISTS {{ ?val <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>/<http://www.w3.org/2000/01/rdf-schema#subClassOf>* <{}> }}" ,
@@ -849,6 +858,13 @@ impl ShaclValidator {
849858 } ;
850859 or_alternatives. entry ( p) . or_default ( ) . push ( clause) ;
851860 }
861+ for ( p, dt) in & or_unpreserved {
862+ or_alternatives. remove ( p) ;
863+ if !or_unsupported. contains ( p) {
864+ let path = or_paths. get ( p) . cloned ( ) . unwrap_or_default ( ) ;
865+ skipped. push ( unpreserved_datatype_skip ( & shape_iri, "sh:or" , & path, dt) ) ;
866+ }
867+ }
852868 for p in & or_unsupported {
853869 or_alternatives. remove ( p) ;
854870 skipped. push ( serde_json:: json!( {
@@ -876,6 +892,10 @@ impl ShaclValidator {
876892 // and 198 real violations were reported as a clean run.
877893 let mut not_clauses: HashMap < String , Vec < String > > = HashMap :: new ( ) ;
878894 let mut not_unsupported: HashSet < String > = HashSet :: new ( ) ;
895+ // As `or_unpreserved`. Here answering anyway was a false clean: a
896+ // value of the forbidden datatype read as xsd:integer, did not
897+ // match, and satisfied the negation.
898+ let mut not_unpreserved: HashMap < String , String > = HashMap :: new ( ) ;
879899 let not_rows = query_solutions_bound (
880900 & shapes_store,
881901 & format ! (
@@ -918,7 +938,12 @@ impl ShaclValidator {
918938 continue ;
919939 }
920940 let clause = if let Some ( dt) = row. get ( "datatype" ) {
921- datatype_conforms ( & strip_angle_brackets ( dt) , "val" )
941+ let dt = strip_angle_brackets ( dt) ;
942+ if datatype_is_indistinguishable_in_store ( & dt) {
943+ not_unpreserved. insert ( p, dt) ;
944+ continue ;
945+ }
946+ datatype_conforms ( & dt, "val" )
922947 } else if let Some ( c) = row. get ( "class" ) {
923948 format ! (
924949 "EXISTS {{ ?val <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>/<http://www.w3.org/2000/01/rdf-schema#subClassOf>* <{}> }}" ,
@@ -939,6 +964,13 @@ impl ShaclValidator {
939964 } ;
940965 not_clauses. entry ( p) . or_default ( ) . push ( clause) ;
941966 }
967+ for ( p, dt) in & not_unpreserved {
968+ not_clauses. remove ( p) ;
969+ if !not_unsupported. contains ( p) {
970+ let path = not_paths. get ( p) . cloned ( ) . unwrap_or_default ( ) ;
971+ skipped. push ( unpreserved_datatype_skip ( & shape_iri, "sh:not" , & path, dt) ) ;
972+ }
973+ }
942974 for p in & not_unsupported {
943975 not_clauses. remove ( p) ;
944976 skipped. push ( serde_json:: json!( {
@@ -1087,20 +1119,30 @@ impl ShaclValidator {
10871119 . collect ( ) ;
10881120 members. sort ( ) ;
10891121 let disjunction = members. join ( " || " ) ;
1090- let node_message_term = query_solutions (
1122+ let node_terms = query_solutions (
10911123 & shapes_store,
10921124 & format ! (
10931125 r#"PREFIX sh: <http://www.w3.org/ns/shacl#>
1094- SELECT ?m WHERE {{ {} sh:message ?m }}"# ,
1095- shape_term
1126+ SELECT ?m ?sev WHERE {{
1127+ OPTIONAL {{ {shape_term} sh:message ?m }}
1128+ OPTIONAL {{ {shape_term} sh:severity ?sev }}
1129+ }}"#
10961130 ) ,
1097- ) ?
1098- . first ( )
1099- . and_then ( |r| r . get ( "m" ) . cloned ( ) ) ;
1131+ ) ?;
1132+ let node_term = | k : & str | node_terms . first ( ) . and_then ( |r| r . get ( k ) . cloned ( ) ) ;
1133+ let node_message_term = node_term ( "m" ) ;
11001134 let node_message =
11011135 node_message_term. as_deref ( ) . map ( strip_quotes) . unwrap_or_default ( ) ;
1136+ // The node shape's sh:severity, as on every other path.
1137+ // This one wrote "Violation" whatever was declared, and
1138+ // the RDF report followed it with sh:Violation.
1139+ let node_severity_term = node_term ( "sev" ) ;
1140+ let node_severity = node_severity_term
1141+ . as_deref ( )
1142+ . map ( severity_name)
1143+ . unwrap_or_else ( || "Violation" . to_string ( ) ) ;
11021144 let node_or_rdf = RdfTerms :: new (
1103- None ,
1145+ node_severity_term ,
11041146 node_message_term. filter ( |_| !node_message. is_empty ( ) ) ,
11051147 ) ;
11061148 let query = format ! (
@@ -1117,7 +1159,7 @@ impl ShaclValidator {
11171159 node_message. clone ( )
11181160 } ;
11191161 violations. push ( attribute ( & shape_iri, & shape_iri, & node_or_rdf, serde_json:: json!( {
1120- "severity" : "Violation" ,
1162+ "severity" : node_severity ,
11211163 "focus_node" : strip_angle_brackets( focus) ,
11221164 "value" : strip_angle_brackets( focus) ,
11231165 "constraint" : "or" ,
@@ -1184,6 +1226,17 @@ impl ShaclValidator {
11841226 row. get ( "message" ) . cloned ( ) . filter ( |_| !q_message. is_empty ( ) ) ,
11851227 ) ;
11861228
1229+ if let Some ( dt) = row. get ( "datatype" ) . map ( |d| strip_angle_brackets ( d) )
1230+ && datatype_is_indistinguishable_in_store ( & dt)
1231+ {
1232+ skipped. push ( unpreserved_datatype_skip (
1233+ & shape_iri,
1234+ "sh:qualifiedValueShape" ,
1235+ & q_path,
1236+ & dt,
1237+ ) ) ;
1238+ continue ;
1239+ }
11871240 let clause = if row. get ( "other" ) . is_some ( ) {
11881241 None
11891242 } else if let Some ( c) = row. get ( "class" ) {
@@ -1460,15 +1513,7 @@ impl ShaclValidator {
14601513 // for every value that satisfied the shape: nine of them in
14611514 // jsonld-escaping-conformance, which is how this was found.
14621515 if datatype_is_indistinguishable_in_store ( & dt) {
1463- skipped. push ( serde_json:: json!( {
1464- "shape" : strip_angle_brackets( & shape_iri) ,
1465- "constraint" : "sh:datatype" ,
1466- "path" : path,
1467- "datatype" : dt,
1468- "reason" : "the store does not preserve this datatype IRI, so a value \
1469- carrying it is indistinguishable from one carrying the type \
1470- it derives from; the constraint was not evaluated",
1471- } ) ) ;
1516+ skipped. push ( unpreserved_datatype_skip ( & shape_iri, "sh:datatype" , & path, & dt) ) ;
14721517 continue ;
14731518 }
14741519 // One evaluator gave two answers to one constraint. SHACL
@@ -2796,7 +2841,18 @@ fn compile_node_shape(
27962841 "EXISTS {{ ?{var} <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>/<http://www.w3.org/2000/01/rdf-schema#subClassOf>* <{}> }}" ,
27972842 strip_angle_brackets( obj)
27982843 ) ) ,
2799- "datatype" => clauses. push ( datatype_conforms ( & strip_angle_brackets ( obj) , var) ) ,
2844+ "datatype" => {
2845+ // Refused for the same reason as on a property inside the
2846+ // shape, below: the store cannot tell the datatype from the
2847+ // one it derives from. Only the property form was guarded, so
2848+ // `sh:node` to a shape declaring `sh:datatype xsd:int` itself
2849+ // reported every value the store had widened to xsd:integer.
2850+ let dt = strip_angle_brackets ( obj) ;
2851+ if datatype_is_indistinguishable_in_store ( & dt) {
2852+ return None ;
2853+ }
2854+ clauses. push ( datatype_conforms ( & dt, var) )
2855+ }
28002856 "nodeKind" => clauses. push ( node_kind_test ( & strip_angle_brackets ( obj) , var) ?) ,
28012857 _ => return None ,
28022858 }
@@ -2978,6 +3034,24 @@ fn datatype_is_indistinguishable_in_store(datatype: &str) -> bool {
29783034}
29793035
29803036
3037+ /// The `skipped_constraints` entry for a constraint that names a datatype
3038+ /// [`datatype_is_indistinguishable_in_store`] says cannot be decided. One
3039+ /// record, so the direct `sh:datatype` constraint and the `sh:or`, `sh:not`
3040+ /// and `sh:qualifiedValueShape` members that name such a datatype refuse in
3041+ /// the same words: the member paths used to answer the question the direct
3042+ /// path refuses, and answered it wrongly.
3043+ fn unpreserved_datatype_skip ( shape : & str , constraint : & str , path : & str , datatype : & str ) -> serde_json:: Value {
3044+ serde_json:: json!( {
3045+ "shape" : strip_angle_brackets( shape) ,
3046+ "constraint" : constraint,
3047+ "path" : path,
3048+ "datatype" : datatype,
3049+ "reason" : "the store does not preserve this datatype IRI, so a value \
3050+ carrying it is indistinguishable from one carrying the type \
3051+ it derives from; the constraint was not evaluated",
3052+ } )
3053+ }
3054+
29813055/// The SPARQL pattern selecting the focus nodes of one target, bound to `var`.
29823056///
29833057/// Note for anyone counting over the result: the class selector binds a focus
0 commit comments