use serde::Serialize;
use crate::card_derive::{
DatasetCard, ExampleQuery, Tier, CARD_TOP_N, GEO_ASWKT, GEO_HASGEOMETRY, O_LITERAL,
};
use crate::RDF_TYPE;
const PREFIXES: &str = "\
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX void: <http://rdfs.org/ns/void#>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
PREFIX wgs: <http://www.w3.org/2003/01/geo/wgs84_pos#>
PREFIX time: <http://www.w3.org/2006/time#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Cap {
TopClass,
TopPred,
LabeledClass,
LabelPred,
ObjectPred,
TimePred,
NumPred,
BaseIri,
ExternalIri,
GeoLatLong,
WktPath,
Bbox,
NamedGraphs,
Link,
HasLiterals,
AnnotatedStatements,
AnnotationPred,
AnnotatedPred,
QuotingPred,
}
const ALL_CAPS: &[Cap] = &[
Cap::TopClass,
Cap::TopPred,
Cap::LabeledClass,
Cap::LabelPred,
Cap::ObjectPred,
Cap::TimePred,
Cap::NumPred,
Cap::BaseIri,
Cap::ExternalIri,
Cap::GeoLatLong,
Cap::WktPath,
Cap::Bbox,
Cap::NamedGraphs,
Cap::Link,
Cap::HasLiterals,
Cap::AnnotatedStatements,
Cap::AnnotationPred,
Cap::AnnotatedPred,
Cap::QuotingPred,
];
impl Cap {
fn key(self) -> &'static str {
match self {
Cap::TopClass => "TOP_CLASS",
Cap::TopPred => "TOP_PRED",
Cap::LabeledClass => "LABELED_CLASS",
Cap::LabelPred => "LABEL_PRED",
Cap::ObjectPred => "OBJECT_PRED",
Cap::TimePred => "TIME_PRED",
Cap::NumPred => "NUM_PRED",
Cap::BaseIri => "BASE_IRI",
Cap::ExternalIri => "EXTERNAL_IRI",
Cap::GeoLatLong => "GEO_LATLONG",
Cap::WktPath => "WKT_PATH",
Cap::Bbox => "BBOX",
Cap::NamedGraphs => "NAMED_GRAPHS",
Cap::Link => "LINK",
Cap::HasLiterals => "HAS_LITERALS",
Cap::AnnotatedStatements => "ANNOTATED_STATEMENTS",
Cap::AnnotationPred => "ANNOTATION_PRED",
Cap::AnnotatedPred => "ANNOTATED_PRED",
Cap::QuotingPred => "QUOTING_PRED",
}
}
fn placeholder(self) -> Option<&'static str> {
Some(match self {
Cap::TopClass => "{{TOP_CLASS}}",
Cap::TopPred => "{{TOP_PRED}}",
Cap::LabeledClass => "{{LABELED_CLASS}}",
Cap::LabelPred => "{{LABEL_PRED}}",
Cap::ObjectPred => "{{OBJECT_PRED}}",
Cap::TimePred => "{{TIME_PRED}}",
Cap::NumPred => "{{NUM_PRED}}",
Cap::BaseIri => "{{BASE_IRI}}",
Cap::WktPath => "{{WKT_PATH}}",
Cap::Bbox => "{{BBOX_POLYGON}}",
Cap::AnnotationPred => "{{ANNOTATION_PRED}}",
Cap::AnnotatedPred => "{{ANNOTATED_PRED}}",
Cap::QuotingPred => "{{QUOTING_PRED}}",
Cap::ExternalIri
| Cap::GeoLatLong
| Cap::NamedGraphs
| Cap::Link
| Cap::HasLiterals
| Cap::AnnotatedStatements => return None,
})
}
fn joint_with(self) -> &'static [Cap] {
match self {
Cap::LabeledClass => &[Cap::LabelPred],
Cap::LabelPred => &[Cap::LabeledClass],
Cap::AnnotatedPred => &[Cap::AnnotationPred],
Cap::AnnotationPred => &[Cap::AnnotatedPred],
_ => &[],
}
}
}
fn class_carries(card: &DatasetCard, class: &str, pred: &str) -> bool {
card.class_links
.iter()
.any(|l| l.s_class == class && l.predicate == pred)
}
fn annotated_statement(card: &DatasetCard) -> Option<(&str, &str)> {
card.signals
.annotated_statement
.as_ref()
.map(|(quoted, about)| (quoted.as_str(), about.as_str()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NonEmpty {
AnyGraph,
Aggregate,
Witnessed,
Undecidable(&'static str),
}
struct Template {
id: &'static str,
title: &'static str,
dimension: &'static str,
question: &'static str,
body: &'static str,
named_body: Option<&'static str>,
mixed_body: Option<&'static str>,
tier: Tier,
requires: &'static [Cap],
nonempty: NonEmpty,
fallback: Option<Fallback>,
provably_empty: Option<fn(&DatasetCard) -> bool>,
hook_is_exact: bool,
}
struct Fallback {
body: &'static str,
requires: &'static [Cap],
nonempty: NonEmpty,
}
const TEMPLATES: &[Template] = &[
Template {
id: "ov-one-row",
title: "One statement, now",
dimension: "overview",
question: "Return exactly one statement — did this file open and answer?",
body: "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 1",
named_body: Some("SELECT ?g ?s ?p ?o WHERE { GRAPH ?g { ?s ?p ?o } } LIMIT 1"),
mixed_body: Some(
"SELECT ?s ?p ?o WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } } LIMIT 1",
),
tier: Tier::Index,
requires: &[],
nonempty: NonEmpty::AnyGraph,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ov-triples",
title: "How many statements?",
dimension: "overview",
question: "How big is this graph — how many triples?",
body: "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }",
named_body: Some("SELECT (COUNT(*) AS ?n) WHERE { GRAPH ?g { ?s ?p ?o } }"),
mixed_body: Some(
"SELECT (COUNT(*) AS ?n) WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }",
),
tier: Tier::Summary,
requires: &[],
nonempty: NonEmpty::Aggregate,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ov-pred-list",
title: "What relationships exist?",
dimension: "overview",
question: "Which predicates (relationships) appear in the data?",
body: "SELECT DISTINCT ?p WHERE { ?s ?p ?o }",
named_body: Some("SELECT DISTINCT ?p WHERE { GRAPH ?g { ?s ?p ?o } }"),
mixed_body: Some(
"SELECT DISTINCT ?p WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } }",
),
tier: Tier::Summary,
requires: &[],
nonempty: NonEmpty::AnyGraph,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ov-pred-hist",
title: "How frequent is each relationship?",
dimension: "overview",
question: "How many statements use each predicate?",
body: "SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o } GROUP BY ?p",
named_body: Some("SELECT ?p (COUNT(*) AS ?n) WHERE { GRAPH ?g { ?s ?p ?o } } GROUP BY ?p"),
mixed_body: Some(
"SELECT ?p (COUNT(*) AS ?n) WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } } GROUP BY ?p",
),
tier: Tier::Summary,
requires: &[],
nonempty: NonEmpty::AnyGraph,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ov-ask-pred",
title: "Does the top predicate occur?",
dimension: "overview",
question: "Is the most common predicate present at all?",
body: "ASK { ?s {{TOP_PRED}} ?o }",
named_body: Some("ASK { GRAPH ?g { ?s {{TOP_PRED}} ?o } }"),
mixed_body: None, tier: Tier::Summary,
requires: &[Cap::TopPred],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "id-sample",
title: "A real entity to start from",
dimension: "identity",
question: "Give me one concrete entity of the most common type.",
body: "SELECT ?s WHERE { ?s a {{TOP_CLASS}} } LIMIT 1",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::TopClass],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "lb-labels",
title: "Readable names for entities",
dimension: "labels",
question: "What are the human-readable names of some entities?",
body: "SELECT ?s ?label WHERE { ?s a {{LABELED_CLASS}} ; {{LABEL_PRED}} ?label } LIMIT 50",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::LabeledClass, Cap::LabelPred],
nonempty: NonEmpty::Witnessed,
fallback: Some(Fallback {
body: "SELECT ?s ?label WHERE { ?s {{LABEL_PRED}} ?label } LIMIT 50",
requires: &[Cap::LabelPred],
nonempty: NonEmpty::Witnessed,
}),
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ty-class-hist",
title: "What kinds of things, how many?",
dimension: "types",
question: "How many entities of each class are there?",
body: "SELECT ?c (COUNT(?s) AS ?n) WHERE { ?s a ?c } GROUP BY ?c ORDER BY DESC(?n) LIMIT 50",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::TopClass],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ty-entities",
title: "How many distinct typed entities?",
dimension: "types",
question: "How many distinct entities carry a type?",
body: "SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE { ?s a [] }",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::TopClass],
nonempty: NonEmpty::Aggregate,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ty-class-shape",
title: "What does the top class look like?",
dimension: "types",
question: "Which predicates describe the most common class?",
body: "SELECT ?p (COUNT(*) AS ?n) WHERE { ?s a {{TOP_CLASS}} ; ?p ?o } GROUP BY ?p ORDER BY DESC(?n)",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::TopClass],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "top-class-graph",
title: "Which classes link to which?",
dimension: "topology",
question: "What is the class-to-class schema graph?",
body: "SELECT ?sC ?p ?oC (COUNT(*) AS ?n) WHERE { ?s a ?sC ; ?p ?o OPTIONAL { ?o a ?oC } } GROUP BY ?sC ?p ?oC ORDER BY DESC(?n)",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::TopClass],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "top-out-hubs",
title: "Most connected sources",
dimension: "topology",
question: "Which subjects make the most statements?",
body: "SELECT ?s (COUNT(*) AS ?d) WHERE { ?s ?p ?o } GROUP BY ?s ORDER BY DESC(?d) LIMIT 25",
named_body: Some(
"SELECT ?s (COUNT(*) AS ?d) WHERE { GRAPH ?g { ?s ?p ?o } } GROUP BY ?s ORDER BY DESC(?d) LIMIT 25",
),
mixed_body: Some(
"SELECT ?s (COUNT(*) AS ?d) WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } } GROUP BY ?s ORDER BY DESC(?d) LIMIT 25",
),
tier: Tier::Index,
requires: &[],
nonempty: NonEmpty::AnyGraph,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "top-in-hubs",
title: "Most referenced entities",
dimension: "topology",
question: "Which resources are referenced most often?",
body: "SELECT ?o (COUNT(*) AS ?d) WHERE { ?s ?p ?o FILTER(!isLiteral(?o)) } GROUP BY ?o ORDER BY DESC(?d) LIMIT 25",
named_body: Some(
"SELECT ?o (COUNT(*) AS ?d) WHERE { GRAPH ?g { ?s ?p ?o FILTER(!isLiteral(?o)) } } GROUP BY ?o ORDER BY DESC(?d) LIMIT 25",
),
mixed_body: Some(
"SELECT ?o (COUNT(*) AS ?d) WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } FILTER(!isLiteral(?o)) } GROUP BY ?o ORDER BY DESC(?d) LIMIT 25",
),
tier: Tier::Index,
requires: &[],
nonempty: NonEmpty::Undecidable(
"no non-literal object exists; decidable (and refused) in default-graph scope only, \
since the card profiles the default graph and this body may be GRAPH-scoped",
),
fallback: None,
provably_empty: Some(|card| card.in_hubs.is_empty()),
hook_is_exact: true,
},
Template {
id: "top-dangling",
title: "Referenced but undescribed IRIs",
dimension: "topology",
question: "Which IRIs are referenced as objects but never described?",
body: "SELECT ?o WHERE { ?s ?p ?o FILTER(isIRI(?o)) FILTER NOT EXISTS { ?o ?p2 ?o2 } } LIMIT 100",
named_body: Some(
"SELECT ?o WHERE { GRAPH ?g { ?s ?p ?o FILTER(isIRI(?o)) } FILTER NOT EXISTS { GRAPH ?h { ?o ?p2 ?o2 } } } LIMIT 100",
),
mixed_body: Some(
"SELECT ?o WHERE { { ?s ?p ?o } UNION { GRAPH ?g { ?s ?p ?o } } FILTER(isIRI(?o)) FILTER NOT EXISTS { ?o ?p2 ?o2 } FILTER NOT EXISTS { GRAPH ?h { ?o ?p3 ?o3 } } } LIMIT 100",
),
tier: Tier::Index,
requires: &[],
nonempty: NonEmpty::Undecidable(
"a fully-described graph has no dangling IRI — emptiness here is the answer, \
not a failure, and the card does not record which objects are also subjects",
),
fallback: None,
provably_empty: Some(|card| card.in_hubs.is_empty()),
hook_is_exact: false,
},
Template {
id: "top-reach",
title: "What's reachable by following one relation?",
dimension: "connectivity",
question: "Following the most common entity-to-entity relation, what is reachable from a node?",
body: "SELECT ?x ?y WHERE { { SELECT ?x WHERE { ?x {{OBJECT_PRED}} ?o } LIMIT 1 } ?x {{OBJECT_PRED}}+ ?y } LIMIT 100",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::ObjectPred],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "cmp-coverage",
title: "Do all entities of the labelled class have a label?",
dimension: "connectivity",
question: "How complete is labelling on the class that carries labels?",
body: "SELECT (COUNT(?s) AS ?total) (COUNT(?l) AS ?have) WHERE { ?s a {{LABELED_CLASS}} OPTIONAL { ?s {{LABEL_PRED}} ?l } }",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::LabeledClass, Cap::LabelPred],
nonempty: NonEmpty::Aggregate,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "lk-sameas",
title: "Aligned to which external datasets?",
dimension: "links",
question: "Which entity-alignment predicates are used, and how often?",
body: "SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o VALUES ?p { owl:sameAs skos:exactMatch skos:closeMatch rdfs:seeAlso } } GROUP BY ?p",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::Link],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "lk-external",
title: "Which predicates link out?",
dimension: "links",
question: "Which predicates point to IRIs outside this dataset?",
body: "SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o FILTER(isIRI(?o) && !STRSTARTS(STR(?o), \"{{BASE_IRI}}\")) } GROUP BY ?p ORDER BY DESC(?n) LIMIT 50",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::BaseIri, Cap::ExternalIri],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "lt-datatypes",
title: "What value types appear?",
dimension: "literals",
question: "Which literal datatypes are used, and how often?",
body: "SELECT ?dt (COUNT(*) AS ?n) WHERE { ?s ?p ?o FILTER(isLiteral(?o)) } GROUP BY (DATATYPE(?o) AS ?dt) ORDER BY DESC(?n)",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::HasLiterals],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "lt-langs",
title: "What languages?",
dimension: "literals",
question: "Which language tags appear on literals, and how often?",
body: "SELECT ?l (COUNT(*) AS ?n) WHERE { ?s ?p ?o FILTER(isLiteral(?o)) } GROUP BY (LANG(?o) AS ?l) ORDER BY DESC(?n)",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::HasLiterals],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "lt-numrange",
title: "Range of a numeric property",
dimension: "literals",
question: "What is the min / average / max of the top numeric property?",
body: "SELECT (MIN(?v) AS ?lo) (AVG(?v) AS ?avg) (MAX(?v) AS ?hi) WHERE { ?s {{NUM_PRED}} ?o BIND(xsd:double(?o) AS ?v) }",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::NumPred],
nonempty: NonEmpty::Aggregate,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ti-extent",
title: "What period does it span?",
dimension: "time",
question: "What is the earliest and latest value of the top time predicate?",
body: "SELECT (MIN(?d) AS ?from) (MAX(?d) AS ?to) WHERE { ?s {{TIME_PRED}} ?d }",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::TimePred],
nonempty: NonEmpty::Aggregate,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ti-histogram",
title: "Distribution over time",
dimension: "time",
question: "How are entities distributed by year?",
body: "SELECT ?yr (COUNT(*) AS ?n) WHERE { ?s {{TIME_PRED}} ?d BIND(SUBSTR(STR(?d),1,4) AS ?yr) } GROUP BY ?yr ORDER BY ?yr",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::TimePred],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "sp-bbox",
title: "Geographic extent",
dimension: "space",
question: "What is the bounding box of the wgs84 coordinates? (lon/lat)",
body: "SELECT (MIN(?lon) AS ?minLon) (MIN(?lat) AS ?minLat) (MAX(?lon) AS ?maxLon) (MAX(?lat) AS ?maxLat) WHERE { ?s wgs:long ?lon ; wgs:lat ?lat }",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::GeoLatLong],
nonempty: NonEmpty::Aggregate,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "sp-within",
title: "Features inside a box",
dimension: "space",
question: "Which features fall inside the dataset's bounding box? (WKT is lon/lat)",
body: "SELECT ?s WHERE { ?s {{WKT_PATH}} ?w FILTER(geof:sfWithin(?w, \"{{BBOX_POLYGON}}\"^^geo:wktLiteral)) } LIMIT 100",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::WktPath, Cap::Bbox],
nonempty: NonEmpty::Undecidable(
"the box is derived from wgs84 lat/long literals, not from the WKT geometries this \
query reads, and the card records no coordinate extent for them",
),
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ng-list",
title: "What graphs partition this?",
dimension: "graphs",
question: "Which named graphs does the dataset contain?",
body: "SELECT DISTINCT ?g WHERE { GRAPH ?g { ?s ?p ?o } }",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::NamedGraphs],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ng-sizes",
title: "Which graphs are biggest?",
dimension: "graphs",
question: "Which named graphs hold the most statements?",
body: "SELECT ?g (COUNT(*) AS ?n) WHERE { GRAPH ?g { ?s ?p ?o } } GROUP BY ?g ORDER BY DESC(?n) LIMIT 25",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::NamedGraphs],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "ng-sample",
title: "A peek inside the graphs",
dimension: "graphs",
question: "What do statements in the named graphs look like?",
body: "SELECT ?g ?s ?p ?o WHERE { GRAPH ?g { ?s ?p ?o } } LIMIT 10",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::NamedGraphs],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "qt-sample",
title: "A statement someone made a statement about",
dimension: "statements",
question: "Which statements does this dataset say something about, and what does it say?",
body: "SELECT ?s ?p ?o ?value WHERE { << ?s ?p ?o >> {{ANNOTATION_PRED}} ?value } LIMIT 25",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::AnnotationPred],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "qt-about",
title: "What gets said about statements?",
dimension: "statements",
question: "Which predicates qualify a statement rather than an entity, and how often?",
body: "SELECT ?about (COUNT(*) AS ?n) WHERE { << ?s ?p ?o >> ?about ?value } GROUP BY ?about ORDER BY DESC(?n) LIMIT 50",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::AnnotatedStatements],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "qt-qualifiers",
title: "Who (or what) qualifies the assertions?",
dimension: "statements",
question: "How many assertions does each distinct qualifier value account for?",
body: "SELECT ?value (COUNT(*) AS ?n) WHERE { << ?s ?p ?o >> {{ANNOTATION_PRED}} ?value } GROUP BY ?value ORDER BY DESC(?n) LIMIT 50",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::AnnotationPred],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "qt-coverage",
title: "How much of the data is qualified?",
dimension: "statements",
question: "Of the statements that get annotated most, how many actually carry an annotation?",
body: "SELECT (COUNT(*) AS ?asserted) (COUNT(?value) AS ?qualified) WHERE { ?s {{ANNOTATED_PRED}} ?o OPTIONAL { << ?s {{ANNOTATED_PRED}} ?o >> {{ANNOTATION_PRED}} ?value } }",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::AnnotatedPred, Cap::AnnotationPred],
nonempty: NonEmpty::Aggregate,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
Template {
id: "qt-quoted-object",
title: "Statements held as values",
dimension: "statements",
question: "Which statements are referenced as the value of another statement?",
body: "SELECT ?s ?qs ?qp ?qo WHERE { ?s {{QUOTING_PRED}} << ?qs ?qp ?qo >> } LIMIT 25",
named_body: None,
mixed_body: None,
tier: Tier::Index,
requires: &[Cap::QuotingPred],
nonempty: NonEmpty::Witnessed,
fallback: None,
provably_empty: None,
hook_is_exact: false,
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Claim {
CannotBeEmpty,
Undecidable,
Unknown,
}
pub fn claim_of(id: &str) -> Claim {
match TEMPLATES.iter().find(|t| t.id == id) {
Some(t) if matches!(t.nonempty, NonEmpty::Undecidable(_)) => Claim::Undecidable,
Some(_) => Claim::CannotBeEmpty,
None => Claim::Unknown,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GraphScope {
DefaultOnly,
NamedOnly,
Mixed,
}
impl GraphScope {
fn of(card: &DatasetCard) -> Self {
if card.named_graph_count == 0 {
GraphScope::DefaultOnly
} else if card.triple_count == 0 {
GraphScope::NamedOnly
} else {
GraphScope::Mixed
}
}
}
struct Caps {
top_class: Option<String>,
top_pred: Option<String>,
labeled_class: Option<String>,
label_pred: Option<String>,
object_pred: Option<String>,
time_pred: Option<String>,
num_pred: Option<String>,
base_iri: Option<String>,
external_iri: bool,
wkt_path: Option<String>,
bbox_polygon: Option<String>,
geo_latlong: bool,
named_graphs: bool,
link: bool,
has_literals: bool,
annotated_statements: bool,
annotation_pred: Option<String>,
annotated_pred: Option<String>,
quoting_pred: Option<String>,
}
impl Caps {
fn from_card(card: &DatasetCard) -> Self {
let s = &card.signals;
let top_pred = card
.predicates
.iter()
.map(|(p, _)| p)
.find(|p| p.as_str() != RDF_TYPE)
.or_else(|| card.predicates.first().map(|(p, _)| p))
.cloned();
let label_pred = s.label_predicate.clone();
let labeled_class = label_pred.as_ref().and_then(|lp| {
card.classes
.iter()
.map(|(c, _)| c)
.find(|c| class_carries(card, c, lp))
.cloned()
});
let object_pred = card
.class_links
.iter()
.find(|l| l.o_class != O_LITERAL && l.predicate != RDF_TYPE)
.map(|l| l.predicate.clone());
let has_pred = |iri: &str| card.predicates.iter().any(|(p, _)| p == iri);
let wkt_path = has_pred(GEO_ASWKT).then(|| {
if has_pred(GEO_HASGEOMETRY) {
"geo:hasGeometry?/geo:asWKT".to_string()
} else {
"geo:asWKT".to_string()
}
});
let bbox_polygon = wkt_path.is_some().then(|| bbox_polygon(s.spatial_bbox));
let external_iri = s.base_iri.as_deref().is_some_and(|base| {
card.classes
.iter()
.chain(card.in_hubs.iter())
.any(|(term, _)| {
let iri = term.trim_start_matches('<').trim_end_matches('>');
!term.starts_with("_:") && !iri.starts_with(base)
})
});
let witness = annotated_statement(card);
let annotated_pred = witness.map(|(quoted, _)| quoted.to_string());
let annotation_pred = witness
.map(|(_, about)| about.to_string())
.or_else(|| s.annotation_predicates.first().cloned());
Caps {
top_class: card.classes.first().map(|(c, _)| c.clone()),
top_pred,
labeled_class,
label_pred,
object_pred,
time_pred: s.time_predicates.first().cloned(),
num_pred: s.numeric_predicates.first().cloned(),
base_iri: s.base_iri.clone(),
external_iri,
wkt_path,
bbox_polygon,
geo_latlong: s.geo_latlong,
named_graphs: card.named_graph_count > 0,
link: !s.link_predicates.is_empty(),
has_literals: !card.datatypes.is_empty(),
annotated_statements: !s.annotation_predicates.is_empty(),
annotation_pred,
annotated_pred,
quoting_pred: s.quoting_predicates.first().cloned(),
}
}
fn value(&self, cap: Cap) -> Option<&str> {
match cap {
Cap::TopClass => self.top_class.as_deref(),
Cap::TopPred => self.top_pred.as_deref(),
Cap::LabeledClass => self.labeled_class.as_deref(),
Cap::LabelPred => self.label_pred.as_deref(),
Cap::ObjectPred => self.object_pred.as_deref(),
Cap::TimePred => self.time_pred.as_deref(),
Cap::NumPred => self.num_pred.as_deref(),
Cap::BaseIri => self.base_iri.as_deref(),
Cap::WktPath => self.wkt_path.as_deref(),
Cap::Bbox => self.bbox_polygon.as_deref(),
Cap::AnnotationPred => self.annotation_pred.as_deref(),
Cap::AnnotatedPred => self.annotated_pred.as_deref(),
Cap::QuotingPred => self.quoting_pred.as_deref(),
Cap::ExternalIri
| Cap::GeoLatLong
| Cap::NamedGraphs
| Cap::Link
| Cap::HasLiterals
| Cap::AnnotatedStatements => None,
}
}
fn available(&self, cap: Cap) -> bool {
match cap {
Cap::ExternalIri => self.external_iri,
Cap::GeoLatLong => self.geo_latlong,
Cap::NamedGraphs => self.named_graphs,
Cap::Link => self.link,
Cap::HasLiterals => self.has_literals,
Cap::AnnotatedStatements => self.annotated_statements,
other => self.value(other).is_some(),
}
}
fn substitute(&self, body: &str) -> String {
let mut out = body.to_string();
for &cap in ALL_CAPS {
if let (Some(ph), Some(v)) = (cap.placeholder(), self.value(cap)) {
out = out.replace(ph, v);
}
}
out
}
}
fn check_body(id: &str, body: &str, requires: &[Cap], nonempty: NonEmpty) -> Result<(), String> {
let used: Vec<Cap> = ALL_CAPS
.iter()
.copied()
.filter(|c| c.placeholder().is_some_and(|ph| body.contains(ph)))
.collect();
for cap in &used {
if !requires.contains(cap) {
return Err(format!(
"{id}: body uses {} but does not require {}",
cap.placeholder().unwrap_or_default(),
cap.key()
));
}
}
for chunk in body.split("{{").skip(1) {
let name = chunk.split("}}").next().unwrap_or(chunk);
let ph = format!("{{{{{name}}}}}");
if !ALL_CAPS.iter().any(|c| c.placeholder() == Some(&ph[..])) {
return Err(format!("{id}: body has an unknown placeholder {ph}"));
}
}
if matches!(nonempty, NonEmpty::Undecidable(_)) {
return Ok(());
}
for (i, a) in used.iter().enumerate() {
for b in &used[i + 1..] {
if !a.joint_with().contains(b) {
return Err(format!(
"{id}: conjoins {} with {}, which are chosen independently — \
make one jointly derived (Cap::joint_with) or declare NonEmpty::Undecidable",
a.key(),
b.key()
));
}
}
}
Ok(())
}
fn bbox_polygon(bbox: Option<[f64; 4]>) -> String {
let [min_lon, min_lat, max_lon, max_lat] = bbox.unwrap_or([-180.0, -90.0, 180.0, 90.0]);
format!(
"POLYGON(({min_lon} {min_lat}, {max_lon} {min_lat}, {max_lon} {max_lat}, {min_lon} {max_lat}, {min_lon} {min_lat}))"
)
}
pub fn generate(card: &DatasetCard) -> Vec<ExampleQuery> {
let caps = Caps::from_card(card);
let scope = GraphScope::of(card);
let mut out = Vec::new();
for t in TEMPLATES {
let all = |rs: &[Cap]| rs.iter().all(|&c| caps.available(c));
let (base, named, mixed, requires, nonempty) = if all(t.requires) {
(t.body, t.named_body, t.mixed_body, t.requires, t.nonempty)
} else if let Some(f) = t.fallback.as_ref().filter(|f| all(f.requires)) {
(f.body, None, None, f.requires, f.nonempty)
} else {
continue;
};
let (body, tier) = match scope {
GraphScope::DefaultOnly => (base, t.tier),
GraphScope::NamedOnly => match named {
Some(b) => (b, Tier::Index),
None if requires.contains(&Cap::NamedGraphs) => (base, t.tier),
None => continue,
},
GraphScope::Mixed => match mixed {
Some(b) => (b, Tier::Index),
None => (base, t.tier),
},
};
if scope == GraphScope::DefaultOnly && t.provably_empty.is_some_and(|f| f(card)) {
continue;
}
debug_assert!(
check_body(t.id, body, requires, nonempty).is_ok(),
"{}",
check_body(t.id, body, requires, nonempty).unwrap_err()
);
let body = caps.substitute(body);
debug_assert!(
!body.contains("{{"),
"template {} left an unsubstituted placeholder",
t.id
);
out.push(ExampleQuery {
id: t.id.to_string(),
title: t.title.to_string(),
dimension: t.dimension.to_string(),
question: t.question.to_string(),
sparql: format!("{PREFIXES}\n{body}"),
tier,
requires: requires.iter().map(|c| c.key().to_string()).collect(),
});
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Verdict {
Answers,
Empty,
Vacuous,
Suspect,
Undecidable,
Unrecognized,
}
impl Verdict {
pub fn as_str(self) -> &'static str {
match self {
Verdict::Answers => "answers",
Verdict::Empty => "empty",
Verdict::Vacuous => "vacuous",
Verdict::Suspect => "suspect",
Verdict::Undecidable => "undecidable",
Verdict::Unrecognized => "unrecognized",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Finding {
pub id: String,
pub verdict: Verdict,
pub why: String,
pub revision: &'static str,
pub substitutions: Vec<Substitution>,
pub conjoined: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed: Option<Observation>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Observation {
pub outcome: &'static str,
pub rows: u64,
pub bytes: u64,
pub requests: u64,
pub debug_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recorded: Option<Recorded>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Recorded {
pub bytes: u64,
pub requests: u64,
pub rows: u64,
pub agrees: bool,
}
impl Observation {
pub fn contradicts(&self, verdict: Verdict) -> bool {
match verdict {
Verdict::Answers => self.outcome != "answers",
Verdict::Empty => self.outcome == "answers",
Verdict::Vacuous => self.outcome == "answers",
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Substitution {
pub placeholder: String,
pub value: String,
pub origin: Vec<String>,
}
struct Legacy {
id: &'static str,
body: &'static str,
}
const LEGACY_BODIES: &[Legacy] = &[
Legacy {
id: "top-reach",
body: "SELECT ?y WHERE { {{HUB_IRI}} {{TOP_PRED}}+ ?y } LIMIT 100",
},
Legacy {
id: "lk-sameas",
body: "SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o VALUES ?p { owl:sameAs skos:exactMatch rdfs:seeAlso } } GROUP BY ?p",
},
];
fn cap_of(card: &DatasetCard) -> usize {
if card.top_n > 0 {
card.top_n as usize
} else {
CARD_TOP_N
}
}
fn complete<T>(card: &DatasetCard, list: &[T]) -> bool {
list.len() < cap_of(card)
}
enum Refuted {
Proved,
AtMost { budget: u64, instances: u64 },
Shadowed { described: bool, twin: bool },
Unknown,
}
fn refute_class_pred(card: &DatasetCard, class: &str, pred: &str) -> Refuted {
let instances = card
.classes
.iter()
.find(|(c, _)| c == class)
.map(|(_, n)| *n)
.unwrap_or(0);
let accounted: u64 = card
.class_links
.iter()
.filter(|l| l.predicate == pred)
.map(|l| l.count)
.sum();
let Some((_, total)) = card.predicates.iter().find(|(p, _)| p == pred) else {
return Refuted::Unknown;
};
let accounted_for_all = accounted >= *total || complete(card, &card.class_links);
if !accounted_for_all {
return Refuted::AtMost {
budget: total - accounted,
instances,
};
}
let described = card.class_links.iter().any(|l| l.s_class == class);
let twin = card
.classes
.iter()
.any(|(c, n)| c != class && *n == instances);
if described && !twin {
Refuted::Proved
} else {
Refuted::Shadowed { described, twin }
}
}
fn origins(card: &DatasetCard, v: &str) -> Vec<String> {
let mut o = Vec::new();
if let Some(i) = card.classes.iter().position(|(c, _)| c == v) {
o.push(format!("classes[{i}]"));
}
if let Some(i) = card.predicates.iter().position(|(p, _)| p == v) {
o.push(format!("predicates[{i}]"));
}
if let Some(i) = card.top_hubs.iter().position(|(h, _)| h == v) {
o.push(format!("top_hubs[{i}]"));
}
let s = &card.signals;
if s.label_predicate.as_deref() == Some(v) {
o.push("signals.label_predicate".to_string());
}
if let Some(i) = s.time_predicates.iter().position(|p| p == v) {
o.push(format!("signals.time_predicates[{i}]"));
}
if let Some(i) = s.numeric_predicates.iter().position(|p| p == v) {
o.push(format!("signals.numeric_predicates[{i}]"));
}
if s.base_iri.as_deref() == Some(v) {
o.push("signals.base_iri".to_string());
}
if let Some(i) = s.annotation_predicates.iter().position(|p| p == v) {
o.push(format!("signals.annotation_predicates[{i}]"));
}
if let Some(i) = s.quoting_predicates.iter().position(|p| p == v) {
o.push(format!("signals.quoting_predicates[{i}]"));
}
if let Some((quoted, about)) = annotated_statement(card) {
if v == quoted {
o.push("signals.annotated_statement.0".to_string());
}
if v == about {
o.push("signals.annotated_statement.1".to_string());
}
}
o
}
type Binds = Vec<(String, String)>;
fn distinct_ranked<'a>(card: &DatasetCard, binds: &'a Binds) -> Vec<&'a str> {
let mut names: Vec<&str> = binds
.iter()
.filter(|(_, v)| !origins(card, v).is_empty())
.map(|(n, _)| n.as_str())
.collect();
names.sort_unstable();
names.dedup();
names
}
fn bound<'a>(binds: &'a Binds, name: &str) -> Option<&'a str> {
binds
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| v.as_str())
}
fn match_pattern(pattern: &str, body: &str) -> Option<Binds> {
let mut binds: Binds = Vec::new();
let mut pat = pattern;
let mut rest = body;
loop {
let Some(i) = pat.find("{{") else {
return (pat == rest).then_some(binds);
};
let (lit, tail) = pat.split_at(i);
rest = rest.strip_prefix(lit)?;
let end = tail.find("}}")?;
let name = &tail[2..end];
pat = &tail[end + 2..];
let next_lit = &pat[..pat.find("{{").unwrap_or(pat.len())];
let cut = if next_lit.is_empty() {
rest.len()
} else {
rest.find(next_lit)?
};
if cut == 0 {
return None; }
binds.push((name.to_string(), rest[..cut].to_string()));
rest = &rest[cut..];
}
}
fn class_pred_conjunction(pattern: &str) -> Option<(&str, &str)> {
let after = pattern.split("?s a {{").nth(1)?;
let class = after.split("}}").next()?;
let tail = after.split_once("}}")?.1;
for sep in ["; {{", "?s {{"] {
if let Some(x) = tail.split(sep).nth(1) {
return Some((class, x.split("}}").next()?));
}
}
None
}
fn values_predicates(body: &str) -> Option<Vec<String>> {
let inner = body.split("VALUES ?p {").nth(1)?.split('}').next()?;
Some(
inner
.split_whitespace()
.filter_map(|t| {
let (p, local) = t.split_once(':')?;
let ns = match p {
"owl" => "http://www.w3.org/2002/07/owl#",
"skos" => "http://www.w3.org/2004/02/skos/core#",
"rdfs" => "http://www.w3.org/2000/01/rdf-schema#",
_ => return None,
};
Some(format!("<{ns}{local}>"))
})
.collect(),
)
}
fn required_path_steps(path: &str) -> Vec<String> {
path.split('/')
.filter(|step| !step.ends_with('?') && !step.ends_with('*'))
.filter_map(|step| match step.trim_end_matches('+') {
"geo:asWKT" => Some(GEO_ASWKT.to_string()),
"geo:hasGeometry" => Some(GEO_HASGEOMETRY.to_string()),
_ => None,
})
.collect()
}
pub fn audit(card: &DatasetCard) -> Vec<Finding> {
let caps = Caps::from_card(card);
let scope = GraphScope::of(card);
let fresh = generate(card);
card.queries
.iter()
.map(|q| {
let revision = match fresh.iter().find(|r| r.id == q.id) {
Some(r) if r.sparql == q.sparql => "current",
Some(_) => "superseded",
None => "dropped",
};
finding(card, &caps, scope, q, revision)
})
.collect()
}
fn finding(
card: &DatasetCard,
caps: &Caps,
scope: GraphScope,
q: &ExampleQuery,
revision: &'static str,
) -> Finding {
let body = q
.sparql
.lines()
.filter(|l| !l.starts_with("PREFIX"))
.collect::<Vec<_>>()
.join("\n");
let body = body.trim();
let mk = |verdict, why: String, revision, binds: &Binds| {
let substitutions: Vec<Substitution> = binds
.iter()
.map(|(n, v)| Substitution {
placeholder: n.clone(),
value: v.clone(),
origin: origins(card, v),
})
.collect();
Finding {
id: q.id.clone(),
verdict,
why,
revision,
conjoined: distinct_ranked(card, binds).len() > 1,
substitutions,
observed: None,
}
};
let Some(t) = TEMPLATES.iter().find(|t| t.id == q.id) else {
return mk(
Verdict::Unrecognized,
"no template of this id is in the library any more".to_string(),
"retired",
&Vec::new(),
);
};
let patterns = [
Some(t.body),
t.named_body,
t.mixed_body,
t.fallback.as_ref().map(|f| f.body),
]
.into_iter()
.flatten()
.chain(
LEGACY_BODIES
.iter()
.filter(|l| l.id == q.id)
.map(|l| l.body),
);
let Some((pattern, binds)) = patterns.into_iter().find_map(|p| {
match_pattern(p, body).map(|b| (p, b))
}) else {
return mk(
Verdict::Unrecognized,
"the body matches no known revision of this template".to_string(),
"unknown",
&Vec::new(),
);
};
if !body.contains("GRAPH") && card.triple_count == 0 && card.named_graph_count > 0 {
return mk(
Verdict::Empty,
format!(
"the body scans the default graph, which the card records as empty \
({} named graph(s) hold all {} statements)",
card.named_graph_count, card.quad_count
),
revision,
&binds,
);
}
if scope == GraphScope::DefaultOnly {
if let Some(hook) = t.provably_empty {
if hook(card) {
return mk(
Verdict::Empty,
"the card's own precompute for this query is empty (the generator \
refuses to emit it today)"
.to_string(),
revision,
&binds,
);
}
if t.hook_is_exact {
return mk(
Verdict::Answers,
"the card precomputes this query's own result, and it is not empty".to_string(),
revision,
&binds,
);
}
}
}
if let Some((cn, pn)) = class_pred_conjunction(pattern) {
if let (Some(class), Some(pred)) = (bound(&binds, cn), bound(&binds, pn)) {
let aggregate = matches!(t.nonempty, NonEmpty::Aggregate);
if class_carries(card, class, pred) {
return mk(
Verdict::Answers,
format!("class_links witnesses {class} carrying {pred}"),
revision,
&binds,
);
}
return match refute_class_pred(card, class, pred) {
Refuted::Proved => mk(
if aggregate {
Verdict::Vacuous
} else {
Verdict::Empty
},
format!(
"the class_links quotient accounts for every use of {pred} and \
none falls on {class}{}",
if aggregate {
" — the aggregate returns one row whose count is 0"
} else {
""
}
),
revision,
&binds,
),
Refuted::AtMost { budget, instances } => mk(
Verdict::Suspect,
format!(
"no class_links row witnesses {class} carrying {pred}; the quotient \
is truncated, so at most {budget} of the class's {instances} \
instances could be joined"
),
revision,
&binds,
),
Refuted::Shadowed { described, twin } => mk(
Verdict::Suspect,
format!(
"the quotient accounts for every use of {pred} and none falls on \
{class}, but {} — the quotient files a multi-typed subject under \
one class only, so its silence is not evidence",
match (described, twin) {
(false, true) =>
"the class has no class_links row of its own AND another class \
has exactly its instance count",
(false, false) => "the class has no class_links row of its own",
_ => "another class has exactly its instance count",
}
),
revision,
&binds,
),
Refuted::Unknown => mk(
Verdict::Undecidable,
format!("{pred} is not in the card's (capped) predicate list"),
revision,
&binds,
),
};
}
}
if let Some(path) = bound(&binds, "WKT_PATH") {
let missing: Vec<String> = required_path_steps(path)
.into_iter()
.filter(|iri| !card.predicates.iter().any(|(p, _)| p == iri))
.collect();
if !missing.is_empty() && complete(card, &card.predicates) {
return mk(
Verdict::Empty,
format!(
"the path {path} requires {} , which the card's complete predicate \
list does not contain",
missing.join(", ")
),
revision,
&binds,
);
}
}
if let Some(listed) = values_predicates(body) {
let present: Vec<&String> = card
.signals
.link_predicates
.iter()
.filter(|p| listed.contains(p))
.collect();
if present.is_empty() {
return mk(
Verdict::Empty,
format!(
"the body pins VALUES ?p to {} , none of which this dataset uses \
(it links with {})",
listed.join(" "),
card.signals.link_predicates.join(" ")
),
revision,
&binds,
);
}
}
if q.id == "lk-external" && !caps.external_iri {
let why = "no IRI the card records lies outside the dataset's base IRI".to_string();
return if complete(card, &card.in_hubs) && complete(card, &card.classes) {
mk(
Verdict::Empty,
format!("{why}, and both lists are complete — the FILTER keeps nothing"),
revision,
&binds,
)
} else {
mk(
Verdict::Suspect,
format!("{why}, but the lists are truncated so one could have fallen off"),
revision,
&binds,
)
};
}
if let (Some(quoted), Some(about)) = (
bound(&binds, "ANNOTATED_PRED"),
bound(&binds, "ANNOTATION_PRED"),
) {
return match annotated_statement(card) {
Some((q, a)) if q == quoted && a == about => mk(
Verdict::Answers,
format!(
"signals.annotated_statement witnesses a {quoted} statement this graph \
both asserts and annotates with {about}"
),
revision,
&binds,
),
Some((q, a)) => mk(
Verdict::Suspect,
format!(
"the body pins {quoted} / {about}, but the card's witnessed pair is \
{q} / {a} — nothing here proves these two meet on one statement"
),
revision,
&binds,
),
None => mk(
Verdict::Undecidable,
"the card records no annotated-statement witness, so it cannot say whether \
the quoted predicate and the annotating one ever meet"
.to_string(),
revision,
&binds,
),
};
}
let ranked = distinct_ranked(card, &binds);
if ranked.len() > 1 {
return mk(
Verdict::Undecidable,
format!(
"conjoins {} , drawn from different rankings — the card relates no \
specific subject to a specific predicate",
ranked.join(" and ")
),
revision,
&binds,
);
}
if matches!(t.nonempty, NonEmpty::Aggregate)
&& body.contains(" ; ")
&& !body.contains("GROUP BY")
{
return mk(
Verdict::Undecidable,
"an un-grouped aggregate always returns one row, but its body conjoins two \
predicates the card only counted separately — the row may be vacuous"
.to_string(),
revision,
&binds,
);
}
match t.nonempty {
NonEmpty::Undecidable(reason) => {
mk(Verdict::Undecidable, reason.to_string(), revision, &binds)
}
NonEmpty::AnyGraph | NonEmpty::Aggregate | NonEmpty::Witnessed => mk(
Verdict::Answers,
match t.nonempty {
NonEmpty::AnyGraph => "any non-empty graph answers this",
NonEmpty::Aggregate => "an un-grouped aggregate always returns one row",
_ => "every substituted term came from a count the card records as > 0",
}
.to_string(),
revision,
&binds,
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::card_derive::{derive_card, CardInput};
use crate::{eval_query, ingest, summary_query_shape, QueryOutput, Rete};
const TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
fn q(s: &str, p: &str, o: &str) -> (String, String, String, Option<String>) {
(s.into(), p.into(), o.into(), None)
}
fn may_be_empty(id: &str) -> bool {
claim_of(id) == Claim::Undecidable
}
fn assert_every_query_returns_rows(card: &DatasetCard, rete: &Rete) {
assert!(!card.queries.is_empty(), "card generated no queries at all");
for eq in &card.queries {
let allow_empty = may_be_empty(&eq.id);
match eval_query(rete, &eq.sparql) {
Ok(QueryOutput::Select(_, rows)) => assert!(
allow_empty || !rows.is_empty(),
"{}: returned zero rows:\n{}",
eq.id,
eq.sparql
),
Ok(QueryOutput::Ask(b)) => assert!(allow_empty || b, "{}: ASK false", eq.id),
Ok(QueryOutput::Construct(ts)) => {
assert!(
allow_empty || !ts.is_empty(),
"{}: constructed nothing",
eq.id
)
}
Err(e) => panic!("{}: failed to run: {e:?}", eq.id),
}
}
}
fn card_and_graph(
quads: Vec<(String, String, String, Option<String>)>,
term_count: u64,
named_graphs: u64,
) -> (DatasetCard, Rete) {
let card = derive_card(&quads, term_count, named_graphs, CardInput::default());
let (bytes, _) =
ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
(card, Rete::open(&bytes).unwrap())
}
fn sparql_of<'a>(card: &'a DatasetCard, id: &str) -> Option<&'a str> {
card.queries
.iter()
.find(|q| q.id == id)
.map(|q| q.sparql.as_str())
}
fn count_value(rete: &Rete, sparql: &str, var: &str) -> u64 {
match eval_query(rete, sparql).unwrap() {
QueryOutput::Select(_, rows) => {
assert_eq!(rows.len(), 1, "expected one aggregate row");
let lit = rows[0].get(var).expect("aggregate variable bound");
lit.trim_start_matches('"')
.split('"')
.next()
.unwrap()
.parse()
.expect("count literal parses")
}
other => panic!("expected a SELECT result, got {other:?}"),
}
}
fn rich_quads() -> Vec<(String, String, String, Option<String>)> {
let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
let sameas = "<http://www.w3.org/2002/07/owl#sameAs>";
let lat = "<http://www.w3.org/2003/01/geo/wgs84_pos#lat>";
let long = "<http://www.w3.org/2003/01/geo/wgs84_pos#long>";
let aswkt = "<http://www.opengis.net/ont/geosparql#asWKT>";
let hasgeom = "<http://www.opengis.net/ont/geosparql#hasGeometry>";
let gyear = "http://www.w3.org/2001/XMLSchema#gYear";
let xint = "http://www.w3.org/2001/XMLSchema#integer";
let wkt = "http://www.opengis.net/ont/geosparql#wktLiteral";
let dbl = "http://www.w3.org/2001/XMLSchema#double";
let mut v = Vec::new();
for i in 0..6 {
let s = format!("<http://ex/person/{i}>");
v.push(q(&s, TYPE, "<http://ex/Person>"));
v.push(q(&s, label, &format!("\"Person {i}\"@en")));
v.push(q(&s, label, &format!("\"Personne {i}\"@fr")));
v.push(q(
&s,
"<http://ex/birthYear>",
&format!("\"{}\"^^<{gyear}>", 1800 + i),
));
v.push(q(
&s,
"<http://ex/friendCount>",
&format!("\"{}\"^^<{xint}>", i * 3),
));
v.push(q(
&s,
"<http://ex/knows>",
&format!("<http://ex/person/{}>", (i + 1) % 6),
));
v.push(q(&s, "<http://ex/bornIn>", "<http://ex/place/0>"));
if i % 2 == 0 {
v.push(q(
&s,
sameas,
&format!("<http://www.wikidata.org/entity/Q{}>", 100 + i),
));
}
}
v.push(q("<http://ex/place/0>", TYPE, "<http://ex/Place>"));
v.push(q("<http://ex/place/0>", label, "\"Rome\"@en"));
v.push(q("<http://ex/place/0>", lat, &format!("\"41.9\"^^<{dbl}>")));
v.push(q(
"<http://ex/place/0>",
long,
&format!("\"12.5\"^^<{dbl}>"),
));
v.push(q(
"<http://ex/place/0>",
hasgeom,
"<http://ex/place/0/geom>",
));
v.push(q(
"<http://ex/place/0/geom>",
aswkt,
&format!("\"POINT(12.5 41.9)\"^^<{wkt}>"),
));
v
}
#[test]
fn generated_queries_parse_run_and_are_tiered() {
let quads = rich_quads();
let card = derive_card(&quads, 50, 0, CardInput::default());
assert!(
card.queries.len() >= 12,
"expected >= 12 starter queries, got {}",
card.queries.len()
);
let (bytes, _) =
ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
let rete = Rete::open(&bytes).unwrap();
for eq in &card.queries {
assert!(
eq.sparql.starts_with("PREFIX"),
"{}: no PREFIX block",
eq.id
);
assert!(
!eq.sparql.contains("{{"),
"{}: unsubstituted placeholder",
eq.id
);
assert!(!eq.dimension.is_empty(), "{}: missing dimension", eq.id);
let result = eval_query(&rete, &eq.sparql);
assert!(
result.is_ok(),
"{}: failed to run: {:?}",
eq.id,
result.err()
);
if eq.tier == Tier::Summary {
let shape = summary_query_shape(&eq.sparql);
assert!(
matches!(shape, Ok(Some(_))),
"{}: tagged Summary but not summary-shaped: {:?}",
eq.id,
shape
);
}
}
assert_every_query_returns_rows(&card, &rete);
}
fn named_only_quads() -> Vec<(String, String, String, Option<String>)> {
let mut v = Vec::new();
for gi in 0..3 {
let g = format!("<http://ex/graph/{gi}>");
for i in 0..4 {
let s = format!("<http://ex/item/{gi}-{i}>");
v.push((
s.clone(),
TYPE.to_string(),
"<http://ex/Item>".to_string(),
Some(g.clone()),
));
v.push((
s.clone(),
"<http://ex/rel>".to_string(),
format!("<http://ex/item/{gi}-{}>", (i + 1) % 4),
Some(g.clone()),
));
v.push((
s,
"<http://ex/ref>".to_string(),
"<http://other/never-described>".to_string(),
Some(g.clone()),
));
}
}
v
}
#[test]
fn named_graph_only_queries_are_scoped_and_return_rows() {
let quads = named_only_quads();
let total = quads.len() as u64;
let card = derive_card(&quads, 20, 3, CardInput::default());
assert_eq!(card.triple_count, 0);
assert_eq!(card.named_graph_count, 3);
for eq in &card.queries {
assert!(
eq.sparql.contains("GRAPH"),
"{}: default-graph-only query on a named-graph-only file:\n{}",
eq.id,
eq.sparql
);
assert_ne!(eq.tier, Tier::Summary, "{}: untruthful Summary tier", eq.id);
}
let (bytes, _) =
ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
let rete = Rete::open(&bytes).unwrap();
assert_every_query_returns_rows(&card, &rete);
let ov = card.queries.iter().find(|q| q.id == "ov-triples").unwrap();
assert_eq!(count_value(&rete, &ov.sparql, "n"), total);
}
#[test]
fn mixed_graph_queries_cover_both_sides_and_return_rows() {
let mut quads = Vec::new();
let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
for i in 0..4 {
let s = format!("<http://ex/doc/{i}>");
quads.push(q(&s, TYPE, "<http://ex/Doc>"));
quads.push(q(&s, label, &format!("\"Doc {i}\"@en")));
quads.push(q(
&s,
"<http://ex/rel>",
&format!("<http://ex/doc/{}>", (i + 1) % 4),
));
quads.push(q(&s, "<http://ex/rel>", "<http://other/never-described>"));
}
let default_count = quads.len() as u64;
let g = "<http://ex/graph/annotations>".to_string();
for i in 0..5 {
quads.push((
format!("<http://ex/note/{i}>"),
"<http://ex/about>".to_string(),
format!("<http://ex/doc/{}>", i % 4),
Some(g.clone()),
));
}
let total = quads.len() as u64;
let card = derive_card(&quads, 30, 1, CardInput::default());
assert_eq!(card.triple_count, default_count);
assert_eq!(card.quad_count, total);
assert_eq!(card.named_graph_count, 1);
let (bytes, _) =
ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
let rete = Rete::open(&bytes).unwrap();
assert_every_query_returns_rows(&card, &rete);
let ov = card.queries.iter().find(|q| q.id == "ov-triples").unwrap();
assert_eq!(count_value(&rete, &ov.sparql, "n"), total);
let ids: Vec<&str> = card.queries.iter().map(|q| q.id.as_str()).collect();
for id in ["ng-list", "ng-sizes", "ng-sample", "id-sample", "lb-labels"] {
assert!(ids.contains(&id), "{id} missing from a mixed-graph card");
}
}
#[test]
fn default_graph_bodies_are_unchanged() {
let quads = rich_quads();
let card = derive_card(&quads, 50, 0, CardInput::default());
let ov = card.queries.iter().find(|q| q.id == "ov-triples").unwrap();
assert!(ov
.sparql
.ends_with("SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }"));
assert_eq!(ov.tier, Tier::Summary);
for eq in &card.queries {
assert!(
!eq.sparql.contains("GRAPH ?g") && !eq.sparql.contains("UNION"),
"{}: graph-scoped body leaked into a default-graph card",
eq.id
);
}
}
#[test]
fn one_row_smoke_query_returns_exactly_one_row_in_every_scope() {
type Quads = Vec<(String, String, String, Option<String>)>;
let cases: Vec<(Quads, u64)> = vec![
(rich_quads(), 0), (named_only_quads(), 3), ];
for (quads, ng) in cases {
let card = derive_card(&quads, 50, ng, CardInput::default());
let smoke = card
.queries
.iter()
.find(|q| q.id == "ov-one-row")
.expect("ov-one-row always emits");
let (bytes, _) =
ingest::assemble_dataset_with_opts(quads, true, false, None, |_, _| Vec::new());
let rete = Rete::open(&bytes).unwrap();
match eval_query(&rete, &smoke.sparql).unwrap() {
QueryOutput::Select(_, rows) => {
assert_eq!(rows.len(), 1, "ov-one-row must return exactly one row")
}
other => panic!("unexpected result form: {other:?}"),
}
}
}
#[test]
fn families_are_gated_by_signal() {
let quads = vec![
q("<http://ex/a>", TYPE, "<http://ex/T>"),
q("<http://ex/a>", "<http://ex/rel>", "<http://ex/b>"),
];
let card = derive_card(&quads, 4, 0, CardInput::default());
let ids: Vec<&str> = card.queries.iter().map(|q| q.id.as_str()).collect();
for absent in [
"sp-bbox",
"sp-within",
"ti-extent",
"ti-histogram",
"lb-labels",
"ng-list",
"ng-sample",
] {
assert!(!ids.contains(&absent), "{absent} should be gated out");
}
for present in ["ov-triples", "ov-pred-list", "ov-pred-hist"] {
assert!(ids.contains(&present), "{present} should always emit");
}
assert!(
!ids.contains(&"lk-external"),
"lk-external has no external IRI"
);
}
fn mtg_shaped_quads() -> Vec<(String, String, String, Option<String>)> {
let name = "<http://schema.org/name>";
let mut v = Vec::new();
for i in 0..30 {
let s = format!("<http://ex/ruling/{i}>");
v.push(q(&s, TYPE, "<http://ex/Ruling>"));
v.push(q(&s, "<http://ex/text>", &format!("\"Ruling text {i}\"")));
v.push(q(
&s,
"<http://ex/about>",
&format!("<http://ex/card/{}>", i % 5),
));
}
for i in 0..5 {
let s = format!("<http://ex/card/{i}>");
v.push(q(&s, TYPE, "<http://ex/Card>"));
v.push(q(&s, name, &format!("\"Card {i}\"")));
v.push(q(&s, "<http://ex/legalIn>", "<http://ex/format/standard>"));
}
v
}
#[test]
fn top_class_without_the_label_predicate_still_answers() {
let (card, rete) = card_and_graph(mtg_shaped_quads(), 60, 0);
assert_eq!(card.classes[0].0, "<http://ex/Ruling>");
assert_eq!(
card.signals.label_predicate.as_deref(),
Some("<http://schema.org/name>")
);
assert!(
!card
.class_links
.iter()
.any(|l| l.s_class == "<http://ex/Ruling>"
&& l.predicate == "<http://schema.org/name>"),
"fixture precondition: the top class carries no label"
);
let lb = sparql_of(&card, "lb-labels").expect("lb-labels emitted");
assert!(
lb.contains("?s a <http://ex/Card> ; <http://schema.org/name> ?label"),
"lb-labels must join the LABELLED class:\n{lb}"
);
assert!(
!lb.contains("Ruling"),
"the unlabelled top class leaked in:\n{lb}"
);
let cov = sparql_of(&card, "cmp-coverage").expect("cmp-coverage emitted");
assert!(
cov.contains("<http://ex/Card>"),
"cmp-coverage on the wrong class"
);
assert_every_query_returns_rows(&card, &rete);
}
#[test]
fn labels_off_every_known_class_fall_back_to_the_class_free_query() {
let label = "<http://www.w3.org/2000/01/rdf-schema#label>";
let mut quads = Vec::new();
for i in 0..8 {
let s = format!("<http://ex/model/{i}>");
quads.push(q(&s, TYPE, "<http://ex/Model>"));
quads.push(q(&s, "<http://ex/downloads>", &format!("\"{i}\"")));
}
for i in 0..3 {
quads.push(q(
&format!("<http://ex/term/{i}>"),
label,
&format!("\"Term {i}\"@en"),
));
}
let (card, rete) = card_and_graph(quads, 30, 0);
assert_eq!(card.signals.label_predicate.as_deref(), Some(label));
let lb = sparql_of(&card, "lb-labels").expect("lb-labels still emitted");
assert!(
lb.ends_with("SELECT ?s ?label WHERE { ?s <http://www.w3.org/2000/01/rdf-schema#label> ?label } LIMIT 50"),
"expected the class-free fallback:\n{lb}"
);
let req = &card
.queries
.iter()
.find(|x| x.id == "lb-labels")
.unwrap()
.requires;
assert_eq!(req, &["LABEL_PRED".to_string()]);
assert!(sparql_of(&card, "cmp-coverage").is_none());
assert_every_query_returns_rows(&card, &rete);
}
#[test]
fn geometry_hung_directly_off_the_feature_is_reachable() {
let aswkt = "<http://www.opengis.net/ont/geosparql#asWKT>";
let wkt = "http://www.opengis.net/ont/geosparql#wktLiteral";
let mut quads = Vec::new();
for i in 0..5 {
let s = format!("<http://ex/district/{i}>");
quads.push(q(&s, TYPE, "<http://ex/District>"));
quads.push(q(&s, aswkt, &format!("\"POINT({} 4{i})\"^^<{wkt}>", i + 7)));
}
let (card, rete) = card_and_graph(quads, 20, 0);
let sp = sparql_of(&card, "sp-within").expect("sp-within emitted");
assert!(
sp.contains("?s geo:asWKT ?w"),
"expected the direct geometry path:\n{sp}"
);
assert!(
!sp.contains("hasGeometry"),
"a hasGeometry step the data does not have:\n{sp}"
);
match eval_query(&rete, sp).unwrap() {
QueryOutput::Select(_, rows) => {
assert_eq!(rows.len(), 5, "every feature is in the box")
}
other => panic!("unexpected result form: {other:?}"),
}
assert_every_query_returns_rows(&card, &rete);
}
#[test]
fn both_geometry_layouts_are_covered_by_one_path() {
let (card, rete) = card_and_graph(rich_quads(), 50, 0);
let sp = sparql_of(&card, "sp-within").expect("sp-within emitted");
assert!(
sp.contains("?s geo:hasGeometry?/geo:asWKT ?w"),
"expected the optional-step path:\n{sp}"
);
match eval_query(&rete, sp).unwrap() {
QueryOutput::Select(_, rows) => assert!(!rows.is_empty()),
other => panic!("unexpected result form: {other:?}"),
}
}
#[test]
fn a_literal_only_graph_gets_no_object_queries() {
let quads: Vec<_> = (0..6)
.map(|i| {
q(
&format!("<http://ex/row/{i}>"),
"<http://ex/value>",
&format!("\"v{i}\""),
)
})
.collect();
let (card, rete) = card_and_graph(quads, 12, 0);
assert!(
card.in_hubs.is_empty(),
"fixture precondition: no IRI objects"
);
let ids: Vec<&str> = card.queries.iter().map(|x| x.id.as_str()).collect();
for absent in ["top-in-hubs", "top-dangling"] {
assert!(!ids.contains(&absent), "{absent} would return zero rows");
}
assert_every_query_returns_rows(&card, &rete);
}
const RECORDED_BY: &str = "<http://ex/recordedBy>";
const CONFIDENCE: &str = "<http://ex/confidence>";
const STATES: &str = "<http://ex/states>";
const COUNT_OF: &str = "<http://ex/count>";
fn annotated_quads() -> Vec<(String, String, String, Option<String>)> {
let mut v = Vec::new();
for i in 0..4 {
let s = format!("<http://ex/occ/{i}>");
let species = if i < 2 {
"<http://ex/Swallow>"
} else {
"<http://ex/Robin>"
};
v.push(q(&s, TYPE, species));
v.push(q(&s, COUNT_OF, &format!("\"{}\"", i + 1)));
if i < 3 {
let quoted = format!("<<{s} {TYPE} {species}>>");
v.push(q(
"ed,
RECORDED_BY,
if i == 0 {
"<http://ex/jsmith>"
} else {
"<http://ex/adoe>"
},
));
if i == 0 {
v.push(q("ed, CONFIDENCE, "\"0.9\""));
}
}
if i < 2 {
v.push(q(
&format!("<http://ex/claim/{i}>"),
STATES,
&format!("<<{s} {COUNT_OF} \"{}\">>", i + 1),
));
}
}
v
}
#[test]
fn statement_queries_are_generated_and_return_rows() {
let quads = annotated_quads();
let (card, rete) = card_and_graph(quads, 40, 0);
let s = &card.signals;
assert_eq!(s.annotation_predicates, vec![RECORDED_BY, CONFIDENCE]);
assert_eq!(s.quoting_predicates, vec![STATES]);
assert_eq!(
s.annotated_statement,
Some((TYPE.to_string(), RECORDED_BY.to_string())),
"the witness must be a statement the graph BOTH asserts and annotates"
);
let ids: Vec<&str> = card.queries.iter().map(|x| x.id.as_str()).collect();
for want in [
"qt-sample",
"qt-about",
"qt-qualifiers",
"qt-coverage",
"qt-quoted-object",
] {
assert!(ids.contains(&want), "{want} should be emitted: {ids:?}");
}
for eq in card.queries.iter().filter(|x| x.id.starts_with("qt-")) {
assert_eq!(eq.dimension, "statements");
assert!(
!eq.sparql.contains("<<("),
"{}: RDF 1.2 triple-term syntax in a QUERY",
eq.id
);
}
assert_every_query_returns_rows(&card, &rete);
let cov = sparql_of(&card, "qt-coverage").unwrap();
assert_eq!(count_value(&rete, cov, "asserted"), 4);
assert_eq!(count_value(&rete, cov, "qualified"), 3);
}
#[test]
fn a_graph_without_quoted_triples_gains_nothing() {
let quads = rich_quads();
let card = derive_card(&quads, 50, 0, CardInput::default());
let s = &card.signals;
assert!(s.annotation_predicates.is_empty());
assert!(s.quoting_predicates.is_empty());
assert!(s.annotated_statement.is_none());
assert!(
!card.queries.iter().any(|x| x.id.starts_with("qt-")),
"a graph with no quoted triple must gain no statement query"
);
let json = String::from_utf8(card.to_json_bytes()).unwrap();
for key in [
"annotation_predicates",
"quoting_predicates",
"annotated_statement",
] {
assert!(!json.contains(key), "{key} reached a card that has none");
}
}
#[test]
fn a_statement_held_only_as_a_value_gets_only_the_object_position_query() {
let quads = vec![
q("<http://ex/a>", TYPE, "<http://ex/T>"),
q("<http://ex/a>", "<http://ex/p>", "<http://ex/b>"),
q(
"<http://ex/claim>",
STATES,
"<<<http://ex/a> <http://ex/p> <http://ex/b>>>",
),
];
let (card, rete) = card_and_graph(quads, 12, 0);
assert!(card.signals.annotation_predicates.is_empty());
assert_eq!(card.signals.quoting_predicates, vec![STATES]);
let ids: Vec<&str> = card.queries.iter().map(|x| x.id.as_str()).collect();
assert!(ids.contains(&"qt-quoted-object"));
for absent in ["qt-sample", "qt-about", "qt-qualifiers", "qt-coverage"] {
assert!(
!ids.contains(&absent),
"{absent} has no subject-position statement to match"
);
}
assert_every_query_returns_rows(&card, &rete);
}
#[test]
fn an_annotation_of_an_unasserted_statement_witnesses_no_coverage() {
let quads = vec![
q("<http://ex/a>", TYPE, "<http://ex/T>"),
q(
"<<<http://ex/a> <http://ex/p> <http://ex/b>>>",
RECORDED_BY,
"<http://ex/jsmith>",
),
];
let (card, rete) = card_and_graph(quads, 10, 0);
assert_eq!(card.signals.annotation_predicates, vec![RECORDED_BY]);
assert!(
card.signals.annotated_statement.is_none(),
"nothing asserts <http://ex/a> <http://ex/p> <http://ex/b>"
);
let ids: Vec<&str> = card.queries.iter().map(|x| x.id.as_str()).collect();
assert!(
!ids.contains(&"qt-coverage"),
"no witness, no coverage query"
);
for want in ["qt-sample", "qt-about", "qt-qualifiers"] {
assert!(ids.contains(&want), "{want} needs only the star layer");
}
assert_every_query_returns_rows(&card, &rete);
}
#[test]
fn the_audit_decides_the_star_conjunction_from_the_witness() {
let mut card = derive_card(&annotated_quads(), 40, 0, CardInput::default());
let findings = audit(&card);
assert_eq!(verdict_of(&findings, "qt-coverage"), Verdict::Answers);
card.signals.annotated_statement = Some((COUNT_OF.to_string(), CONFIDENCE.to_string()));
let findings = audit(&card);
assert_eq!(verdict_of(&findings, "qt-coverage"), Verdict::Suspect);
}
#[test]
fn every_template_body_is_witnessed() {
let mut problems = Vec::new();
for t in TEMPLATES {
for body in [Some(t.body), t.named_body, t.mixed_body]
.into_iter()
.flatten()
{
if let Err(e) = check_body(t.id, body, t.requires, t.nonempty) {
problems.push(e);
}
}
if let Some(f) = &t.fallback {
if let Err(e) = check_body(t.id, f.body, f.requires, f.nonempty) {
problems.push(e);
}
assert!(
f.requires.len() < t.requires.len(),
"{}: a fallback must need LESS than the template it stands in for",
t.id
);
}
}
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
fn ship_instead(card: &mut DatasetCard, id: &str, body: &str) {
let q = card
.queries
.iter_mut()
.find(|q| q.id == id)
.unwrap_or_else(|| panic!("{id} is not in this card"));
q.sparql = format!("{PREFIXES}\n{body}");
}
fn verdict_of(findings: &[Finding], id: &str) -> Verdict {
findings
.iter()
.find(|f| f.id == id)
.unwrap_or_else(|| panic!("{id} was not audited"))
.verdict
}
fn rows_of(rete: &Rete, sparql: &str) -> usize {
match eval_query(rete, sparql).unwrap() {
QueryOutput::Select(_, rows) => rows.len(),
QueryOutput::Ask(b) => usize::from(b),
other => panic!("unexpected result form: {other:?}"),
}
}
#[test]
fn a_freshly_generated_card_audits_clean() {
for (quads, ng) in [(rich_quads(), 0), (named_only_quads(), 3)] {
let card = derive_card(&quads, 50, ng, CardInput::default());
for f in audit(&card) {
assert_eq!(
f.revision, "current",
"{}: the generator would not write this body",
f.id
);
assert!(
matches!(f.verdict, Verdict::Answers | Verdict::Undecidable),
"{}: {} — {}",
f.id,
f.verdict.as_str(),
f.why
);
if f.verdict == Verdict::Undecidable {
assert!(
may_be_empty(&f.id) || f.id == "sp-bbox",
"{}: undecidable but the table claims otherwise — {}",
f.id,
f.why
);
}
}
}
}
#[test]
fn the_audit_refutes_a_label_query_on_an_unlabelled_top_class() {
let quads = mtg_shaped_quads();
let (mut card, rete) = card_and_graph(quads, 60, 0);
let top = card.classes[0].0.clone();
ship_instead(
&mut card,
"lb-labels",
&format!("SELECT ?s ?label WHERE {{ ?s a {top} ; <http://schema.org/name> ?label }} LIMIT 50"),
);
ship_instead(
&mut card,
"cmp-coverage",
&format!(
"SELECT (COUNT(?s) AS ?total) (COUNT(?l) AS ?have) WHERE {{ ?s a {top} \
OPTIONAL {{ ?s <http://schema.org/name> ?l }} }}"
),
);
let findings = audit(&card);
assert_eq!(verdict_of(&findings, "lb-labels"), Verdict::Empty);
assert_eq!(verdict_of(&findings, "cmp-coverage"), Verdict::Vacuous);
assert_eq!(rows_of(&rete, sparql_of(&card, "lb-labels").unwrap()), 0);
assert_eq!(rows_of(&rete, sparql_of(&card, "cmp-coverage").unwrap()), 1);
}
#[test]
fn the_audit_will_not_refute_a_dually_typed_class() {
let name = "<http://schema.org/name>";
let component = "<http://ex/ArchiveComponent>";
let unit = "<http://ex/Unit>";
let mut quads = Vec::new();
for i in 0..8 {
let s = format!("<http://ex/unit/{i}>");
quads.push(q(&s, TYPE, component));
quads.push(q(&s, TYPE, unit));
quads.push(q(&s, name, &format!("\"Unit {i}\"")));
}
let (mut card, rete) = card_and_graph(quads, 30, 0);
assert_eq!(
card.classes[0].0, component,
"fixture: the twin ranks first"
);
assert!(
!card.class_links.iter().any(|l| l.s_class == component),
"fixture: the quotient never mentions the top class"
);
ship_instead(
&mut card,
"lb-labels",
&format!("SELECT ?s ?label WHERE {{ ?s a {component} ; {name} ?label }} LIMIT 50"),
);
assert_eq!(verdict_of(&audit(&card), "lb-labels"), Verdict::Suspect);
assert_eq!(rows_of(&rete, sparql_of(&card, "lb-labels").unwrap()), 8);
}
#[test]
fn the_audit_refutes_a_path_through_a_predicate_that_is_not_there() {
let aswkt = "<http://www.opengis.net/ont/geosparql#asWKT>";
let wkt = "http://www.opengis.net/ont/geosparql#wktLiteral";
let mut quads = Vec::new();
for i in 0..5 {
let s = format!("<http://ex/district/{i}>");
quads.push(q(&s, TYPE, "<http://ex/District>"));
quads.push(q(&s, aswkt, &format!("\"POINT({} 4{i})\"^^<{wkt}>", i + 7)));
}
let (mut card, rete) = card_and_graph(quads, 20, 0);
ship_instead(
&mut card,
"sp-within",
"SELECT ?s WHERE { ?s geo:hasGeometry/geo:asWKT ?w FILTER(geof:sfWithin(?w, \
\"POLYGON((-180 -90, 180 -90, 180 90, -180 90, -180 -90))\"^^geo:wktLiteral)) } LIMIT 100",
);
assert_eq!(verdict_of(&audit(&card), "sp-within"), Verdict::Empty);
assert_eq!(rows_of(&rete, sparql_of(&card, "sp-within").unwrap()), 0);
}
#[test]
fn the_audit_refutes_a_values_list_that_misses_the_only_link_predicate() {
let close = "<http://www.w3.org/2004/02/skos/core#closeMatch>";
let mut quads = vec![q("<http://ex/a>", TYPE, "<http://ex/T>")];
for i in 0..4 {
quads.push(q(
&format!("<http://ex/a{i}>"),
close,
&format!("<http://other/x{i}>"),
));
}
let (mut card, rete) = card_and_graph(quads, 20, 0);
assert_eq!(card.signals.link_predicates, vec![close.to_string()]);
ship_instead(
&mut card,
"lk-sameas",
"SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o VALUES ?p { owl:sameAs \
skos:exactMatch rdfs:seeAlso } } GROUP BY ?p",
);
assert_eq!(verdict_of(&audit(&card), "lk-sameas"), Verdict::Empty);
assert_eq!(rows_of(&rete, sparql_of(&card, "lk-sameas").unwrap()), 0);
}
#[test]
fn the_audit_recognizes_the_legacy_reach_query_and_declines_to_decide() {
let (mut card, _) = card_and_graph(rich_quads(), 50, 0);
let hub = card.top_hubs[0].0.clone();
let pred = card.predicates[0].0.clone();
ship_instead(
&mut card,
"top-reach",
&format!("SELECT ?y WHERE {{ {hub} {pred}+ ?y }} LIMIT 100"),
);
let f = audit(&card);
let r = f.iter().find(|f| f.id == "top-reach").unwrap();
assert_eq!(r.verdict, Verdict::Undecidable);
assert_eq!(r.revision, "superseded");
assert!(r.conjoined, "two independently-ranked terms in one body");
assert_eq!(r.substitutions.len(), 2);
}
#[test]
fn an_unknown_body_is_not_force_fitted() {
let (mut card, _) = card_and_graph(rich_quads(), 50, 0);
ship_instead(
&mut card,
"lb-labels",
"SELECT ?x WHERE { ?x ?y ?z } LIMIT 3",
);
assert_eq!(
verdict_of(&audit(&card), "lb-labels"),
Verdict::Unrecognized
);
}
#[test]
fn the_table_is_well_formed() {
let mut ids: Vec<&str> = TEMPLATES.iter().map(|t| t.id).collect();
let n = ids.len();
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), n, "duplicate template id");
for &cap in ALL_CAPS {
assert!(
TEMPLATES.iter().any(|t| t.requires.contains(&cap)
|| t.fallback
.as_ref()
.is_some_and(|f| f.requires.contains(&cap))),
"{} is required by no template",
cap.key()
);
}
for t in TEMPLATES {
assert!(
!t.hook_is_exact || t.provably_empty.is_some(),
"{}: hook_is_exact without a hook to be exact about",
t.id
);
}
for l in LEGACY_BODIES {
let t = TEMPLATES
.iter()
.find(|t| t.id == l.id)
.unwrap_or_else(|| panic!("legacy body for unknown template {}", l.id));
assert!(
![Some(t.body), t.named_body, t.mixed_body]
.into_iter()
.flatten()
.any(|b| b == l.body),
"{}: legacy body is still a current one",
l.id
);
}
}
}