From 0033cb61534ec874e1e8299fbc64fa6f14692b95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:37:55 +0000 Subject: [PATCH 1/3] Initial plan From 6c40df16aa4a56e53efb7eed48dbd34a9487c216 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:44:19 +0000 Subject: [PATCH 2/3] Add Distinct checkbox for EntityNode variables with SPARQL SELECT DISTINCT support Co-authored-by: HerrMotz <20333692+HerrMotz@users.noreply.github.com> --- .../components/EntitySelectorInputControl.vue | 25 +++++++ .../src/components/ProjectionCheckbox.vue | 6 +- query-by-graph/src/lib.rs | 39 +++++++++-- query-by-graph/src/lib/rete/constants.ts | 3 +- query-by-graph/src/lib/types/EntityType.ts | 1 + query-by-graph/tests/logic.rs | 70 +++++++++++++++++++ 6 files changed, 135 insertions(+), 9 deletions(-) diff --git a/query-by-graph/src/components/EntitySelectorInputControl.vue b/query-by-graph/src/components/EntitySelectorInputControl.vue index 6969d96..a6df931 100644 --- a/query-by-graph/src/components/EntitySelectorInputControl.vue +++ b/query-by-graph/src/components/EntitySelectorInputControl.vue @@ -16,6 +16,15 @@ container-classes="px-2" label-classes="text-white" /> + @@ -50,6 +59,19 @@ export default { } } } + }, + includeAsDistinct: { + get() { + return this.data.value?.distinct === true; + }, + set(value) { + if (this.data.value) { + this.data.value.distinct = value; + if (this.data.options?.change) { + this.data.options.change(this.data.value); + } + } + } } }, methods: { @@ -72,6 +94,9 @@ export default { if (this.data.value.selectedForProjection === undefined) { this.data.value.selectedForProjection = true; } + if (this.data.value.distinct === undefined) { + this.data.value.distinct = false; + } } }, } diff --git a/query-by-graph/src/components/ProjectionCheckbox.vue b/query-by-graph/src/components/ProjectionCheckbox.vue index e3ff476..f7d1997 100644 --- a/query-by-graph/src/components/ProjectionCheckbox.vue +++ b/query-by-graph/src/components/ProjectionCheckbox.vue @@ -8,7 +8,7 @@ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2" /> @@ -27,6 +27,10 @@ defineProps({ type: Boolean, default: false }, + label: { + type: String, + default: 'Select?' + }, containerClasses: { type: String, default: '' diff --git a/query-by-graph/src/lib.rs b/query-by-graph/src/lib.rs index c104646..b60afec 100644 --- a/query-by-graph/src/lib.rs +++ b/query-by-graph/src/lib.rs @@ -21,6 +21,8 @@ pub struct Entity { pub prefix: Prefix, #[serde(default = "default_selected_for_projection")] pub selected_for_projection: bool, + #[serde(default)] + pub distinct: bool, } #[derive(Serialize, Deserialize, Clone)] @@ -167,6 +169,11 @@ fn vqg_to_query( }) .collect::>(); + let has_distinct = connections.iter().any(|connection| { + (connection.source.id.starts_with('?') && connection.source.selected_for_projection && connection.source.distinct) + || (connection.target.id.starts_with('?') && connection.target.selected_for_projection && connection.target.distinct) + }); + let projection_list = if projection_set.is_empty() { String::from("*") } else { @@ -175,6 +182,8 @@ fn vqg_to_query( sorted_projection_set.join(" ") }; + let distinct_keyword = if has_distinct { "DISTINCT " } else { "" }; + let prefix_set = connections .iter() .flat_map(|connection| { @@ -248,13 +257,13 @@ fn vqg_to_query( if add_label_service_prefixes { format!( - "{}\n{}\n{}{}SELECT {} WHERE {{\n{}{}}}", - BD_PREFIX, WIKIBASE_PREFIX, xsd_prefix, prefix_list, projection_list, where_clause, service + "{}\n{}\n{}{}SELECT {}{} WHERE {{\n{}{}}}", + BD_PREFIX, WIKIBASE_PREFIX, xsd_prefix, prefix_list, distinct_keyword, projection_list, where_clause, service ) } else { format!( - "{}{}SELECT {} WHERE {{\n{}{}}}", - xsd_prefix, prefix_list, projection_list, where_clause, service + "{}{}SELECT {}{} WHERE {{\n{}{}}}", + xsd_prefix, prefix_list, distinct_keyword, projection_list, where_clause, service ) } } @@ -399,15 +408,24 @@ fn query_to_vqg(query: &str) -> Vec { // Match on the query type. match parsed_query { Ok(Query::Select { pattern: p, .. }) => { - let (connections, projection_vars) = match p { + let (connections, projection_vars, is_distinct) = match p { + GraphPattern::Distinct { inner } => match *inner { + GraphPattern::Project { variables: v, inner: i } => ( + match_bgp_or_path_to_vqg(*i), + Some(v.iter().map(|var| format!("?{}", var.as_str())).collect::>()), + true, + ), + other => (match_bgp_or_path_to_vqg(other), None, true), + }, GraphPattern::Project { variables: v, inner: i, } => ( match_bgp_or_path_to_vqg(*i), - Some(v.iter().map(|var| format!("?{}", var.as_str())).collect::>()) + Some(v.iter().map(|var| format!("?{}", var.as_str())).collect::>()), + false, ), - _ => (match_bgp_or_path_to_vqg(p), None), + _ => (match_bgp_or_path_to_vqg(p), None, false), }; let mut connections = connections; @@ -417,10 +435,14 @@ fn query_to_vqg(query: &str) -> Vec { if connection.source.id.starts_with('?') { connection.source.selected_for_projection = vars.contains(&connection.source.id); + connection.source.distinct = + is_distinct && vars.contains(&connection.source.id); } if connection.target.id.starts_with('?') { connection.target.selected_for_projection = vars.contains(&connection.target.id); + connection.target.distinct = + is_distinct && vars.contains(&connection.target.id); } for property in &mut connection.properties { if property.id.starts_with('?') { @@ -472,6 +494,7 @@ fn match_bgp_or_path_to_vqg(p: GraphPattern) -> Vec { let r_parsed = match_bgp_or_path_to_vqg(*r); l_parsed.into_iter().chain(r_parsed).collect() } + GraphPattern::Distinct { inner } => match_bgp_or_path_to_vqg(*inner), GraphPattern::Path { subject: s, path: p, @@ -503,6 +526,7 @@ fn connection_constructor( abbreviation: "".to_string(), }, selected_for_projection: true, // Default to true + distinct: false, }, target: Entity { id: object_name.clone(), @@ -512,6 +536,7 @@ fn connection_constructor( abbreviation: "".to_string(), }, selected_for_projection: true, // Default to true + distinct: false, }, properties: vec![Property { id: predicate_name.clone(), diff --git a/query-by-graph/src/lib/rete/constants.ts b/query-by-graph/src/lib/rete/constants.ts index ab07407..6772b7d 100644 --- a/query-by-graph/src/lib/rete/constants.ts +++ b/query-by-graph/src/lib/rete/constants.ts @@ -11,7 +11,8 @@ const variableEntity: EntityType = { // EntityType abbreviation: "", }, dataSource: noDataSource, - selectedForProjection: true + selectedForProjection: true, + distinct: false }; const variableEntityConstructor = (name: string): EntityType => { diff --git a/query-by-graph/src/lib/types/EntityType.ts b/query-by-graph/src/lib/types/EntityType.ts index 11865d5..34e0bdd 100644 --- a/query-by-graph/src/lib/types/EntityType.ts +++ b/query-by-graph/src/lib/types/EntityType.ts @@ -7,6 +7,7 @@ export interface EntityType { prefix: PrefixType, dataSource: WikibaseDataSource; selectedForProjection?: boolean; + distinct?: boolean; isLiteral?: boolean; } diff --git a/query-by-graph/tests/logic.rs b/query-by-graph/tests/logic.rs index 70e940b..707dae6 100644 --- a/query-by-graph/tests/logic.rs +++ b/query-by-graph/tests/logic.rs @@ -324,3 +324,73 @@ SELECT ?o ?s WHERE { }"###; assert_sparql_equivalent(&result, expected); } + +#[test] +fn test_distinct_variable_generates_select_distinct() { + let graph = r###"[{"properties":[{"id":"P69","label":"educated at","prefix":{"iri":"http://www.wikidata.org/prop/direct/","abbreviation":"wdt"},"selectedForProjection":false}],"source":{"id":"Q5879","label":"Johann Wolfgang von Goethe","prefix":{"iri":"http://www.wikidata.org/entity/","abbreviation":"wd"},"selectedForProjection":false,"distinct":false},"target":{"id":"?university","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":true,"distinct":true}}]"###; + + let result = vqg_to_query_wasm(graph, false, false); + let select = select_line(&result); + + assert!(select.contains("DISTINCT"), "Expected SELECT DISTINCT but got: {}", select); + assert!(select.contains("?university")); +} + +#[test] +fn test_no_distinct_generates_plain_select() { + let graph = r###"[{"properties":[{"id":"P69","label":"educated at","prefix":{"iri":"http://www.wikidata.org/prop/direct/","abbreviation":"wdt"},"selectedForProjection":false}],"source":{"id":"Q5879","label":"Johann Wolfgang von Goethe","prefix":{"iri":"http://www.wikidata.org/entity/","abbreviation":"wd"},"selectedForProjection":false,"distinct":false},"target":{"id":"?university","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":true,"distinct":false}}]"###; + + let result = vqg_to_query_wasm(graph, false, false); + let select = select_line(&result); + + assert!(!select.contains("DISTINCT"), "Expected plain SELECT but got: {}", select); + assert!(select.contains("?university")); +} + +#[test] +fn test_parse_select_distinct_query_sets_distinct_flag() { + let query = r###"PREFIX wd: +PREFIX wdt: +SELECT DISTINCT ?university WHERE { + wd:Q5879 wdt:P69 ?university . +}"###; + + let result = query_to_vqg_wasm(query); + let connections = parse_connections_json(&result); + + assert_eq!(connections.len(), 1); + let c = &connections[0]; + + assert_eq!(c["target"]["id"], Value::String("?university".to_string())); + assert_eq!(c["target"]["selectedForProjection"], Value::Bool(true)); + assert_eq!(c["target"]["distinct"], Value::Bool(true)); +} + +#[test] +fn test_parse_non_distinct_query_does_not_set_distinct_flag() { + let query = r###"PREFIX wd: +PREFIX wdt: +SELECT ?university WHERE { + wd:Q5879 wdt:P69 ?university . +}"###; + + let result = query_to_vqg_wasm(query); + let connections = parse_connections_json(&result); + + assert_eq!(connections.len(), 1); + let c = &connections[0]; + + assert_eq!(c["target"]["id"], Value::String("?university".to_string())); + assert_eq!(c["target"]["distinct"], Value::Bool(false)); +} + +#[test] +fn test_distinct_only_applies_when_variable_is_selected_for_projection() { + // distinct=true but selectedForProjection=false → should NOT generate SELECT DISTINCT + let graph = r###"[{"properties":[{"id":"P69","label":"educated at","prefix":{"iri":"http://www.wikidata.org/prop/direct/","abbreviation":"wdt"},"selectedForProjection":false}],"source":{"id":"Q5879","label":"Goethe","prefix":{"iri":"http://www.wikidata.org/entity/","abbreviation":"wd"},"selectedForProjection":false,"distinct":false},"target":{"id":"?university","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":false,"distinct":true}}]"###; + + let result = vqg_to_query_wasm(graph, false, false); + let select = select_line(&result); + + assert!(!select.contains("DISTINCT"), "DISTINCT should not appear when variable is not selected for projection: {}", select); +} From c06a4d576bf8c6dcaffe264e4dc50a5bcf20bd33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:34:00 +0000 Subject: [PATCH 3/3] Fix DISTINCT to use per-variable DISTINCT(?var) syntax instead of SELECT DISTINCT Co-authored-by: HerrMotz <20333692+HerrMotz@users.noreply.github.com> --- query-by-graph/src/lib.rs | 76 ++++++++++++++++++++--------------- query-by-graph/tests/logic.rs | 44 ++++++++++++++++---- 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/query-by-graph/src/lib.rs b/query-by-graph/src/lib.rs index b60afec..f38459d 100644 --- a/query-by-graph/src/lib.rs +++ b/query-by-graph/src/lib.rs @@ -6,7 +6,7 @@ use serde_json::{from_str, to_string}; use spargebra::algebra::{GraphPattern, PropertyPathExpression}; use spargebra::term::{TriplePattern, TermPattern, NamedNodePattern}; use spargebra::{Query, SparqlSyntaxError}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use wasm_bindgen::prelude::*; const INDENTATION_COUNT: usize = 4; @@ -124,20 +124,22 @@ fn vqg_to_query( if connections.is_empty() { String::from("") } else { - fn collect_vars(id: &str, selected: bool, add_service_statement: bool) -> Vec { + fn collect_vars(id: &str, selected: bool, distinct: bool, add_service_statement: bool) -> Vec<(String, bool)> { let mut vars = Vec::new(); if id.starts_with('?') && selected { let var = id.to_string(); - vars.push(var.clone()); + vars.push((var.clone(), distinct)); if add_service_statement { - vars.push(format!("?{}Label", var.trim_start_matches('?'))); + // Label variables are never marked distinct + vars.push((format!("?{}Label", var.trim_start_matches('?')), false)); } } vars } - fn collect_vars_from_property(property: &Property, add_service_statement: bool) -> Vec { - let mut vars = collect_vars(&property.id, property.selected_for_projection, add_service_statement); + fn collect_vars_from_property(property: &Property, add_service_statement: bool) -> Vec<(String, bool)> { + // Properties never carry the distinct flag + let mut vars = collect_vars(&property.id, property.selected_for_projection, false, add_service_statement); for p in &property.properties { vars.extend(collect_vars_from_property(p, add_service_statement)); } @@ -155,35 +157,47 @@ fn vqg_to_query( prefixes } - let projection_set = connections + // Collect (variable, is_distinct) pairs; deduplicate with OR on the distinct flag + let projection_raw: Vec<(String, bool)> = connections .iter() .flat_map(|connection| { - let mut vars = Vec::new(); + let mut vars: Vec<(String, bool)> = Vec::new(); for entity in &[&connection.source, &connection.target] { - vars.extend(collect_vars(&entity.id, entity.selected_for_projection, add_service_statement)); + vars.extend(collect_vars(&entity.id, entity.selected_for_projection, entity.distinct, add_service_statement)); } for property in &connection.properties { vars.extend(collect_vars_from_property(property, add_service_statement)); } vars }) - .collect::>(); + .collect(); - let has_distinct = connections.iter().any(|connection| { - (connection.source.id.starts_with('?') && connection.source.selected_for_projection && connection.source.distinct) - || (connection.target.id.starts_with('?') && connection.target.selected_for_projection && connection.target.distinct) - }); + let mut projection_map: HashMap = HashMap::new(); + for (var, is_distinct) in projection_raw { + projection_map + .entry(var) + .and_modify(|d| *d = *d || is_distinct) + .or_insert(is_distinct); + } - let projection_list = if projection_set.is_empty() { + let projection_list = if projection_map.is_empty() { String::from("*") } else { - let mut sorted_projection_set: Vec<_> = projection_set.into_iter().collect(); - sorted_projection_set.sort(); // Sort the collection - sorted_projection_set.join(" ") + let mut sorted: Vec<(String, bool)> = projection_map.into_iter().collect(); + sorted.sort_by_key(|(var, _)| var.clone()); + sorted + .iter() + .map(|(var, is_distinct)| { + if *is_distinct { + format!("DISTINCT({})", var) + } else { + var.clone() + } + }) + .collect::>() + .join(" ") }; - let distinct_keyword = if has_distinct { "DISTINCT " } else { "" }; - let prefix_set = connections .iter() .flat_map(|connection| { @@ -257,13 +271,13 @@ fn vqg_to_query( if add_label_service_prefixes { format!( - "{}\n{}\n{}{}SELECT {}{} WHERE {{\n{}{}}}", - BD_PREFIX, WIKIBASE_PREFIX, xsd_prefix, prefix_list, distinct_keyword, projection_list, where_clause, service + "{}\n{}\n{}{}SELECT {} WHERE {{\n{}{}}}", + BD_PREFIX, WIKIBASE_PREFIX, xsd_prefix, prefix_list, projection_list, where_clause, service ) } else { format!( - "{}{}SELECT {}{} WHERE {{\n{}{}}}", - xsd_prefix, prefix_list, distinct_keyword, projection_list, where_clause, service + "{}{}SELECT {} WHERE {{\n{}{}}}", + xsd_prefix, prefix_list, projection_list, where_clause, service ) } } @@ -408,14 +422,15 @@ fn query_to_vqg(query: &str) -> Vec { // Match on the query type. match parsed_query { Ok(Query::Select { pattern: p, .. }) => { - let (connections, projection_vars, is_distinct) = match p { + let (connections, projection_vars) = match p { + // SELECT DISTINCT ... is treated as SELECT ... for import purposes; + // per-variable distinct is set by the user via the UI checkbox. GraphPattern::Distinct { inner } => match *inner { GraphPattern::Project { variables: v, inner: i } => ( match_bgp_or_path_to_vqg(*i), Some(v.iter().map(|var| format!("?{}", var.as_str())).collect::>()), - true, ), - other => (match_bgp_or_path_to_vqg(other), None, true), + other => (match_bgp_or_path_to_vqg(other), None), }, GraphPattern::Project { variables: v, @@ -423,9 +438,8 @@ fn query_to_vqg(query: &str) -> Vec { } => ( match_bgp_or_path_to_vqg(*i), Some(v.iter().map(|var| format!("?{}", var.as_str())).collect::>()), - false, ), - _ => (match_bgp_or_path_to_vqg(p), None, false), + _ => (match_bgp_or_path_to_vqg(p), None), }; let mut connections = connections; @@ -435,14 +449,10 @@ fn query_to_vqg(query: &str) -> Vec { if connection.source.id.starts_with('?') { connection.source.selected_for_projection = vars.contains(&connection.source.id); - connection.source.distinct = - is_distinct && vars.contains(&connection.source.id); } if connection.target.id.starts_with('?') { connection.target.selected_for_projection = vars.contains(&connection.target.id); - connection.target.distinct = - is_distinct && vars.contains(&connection.target.id); } for property in &mut connection.properties { if property.id.starts_with('?') { diff --git a/query-by-graph/tests/logic.rs b/query-by-graph/tests/logic.rs index 707dae6..6d1dc9b 100644 --- a/query-by-graph/tests/logic.rs +++ b/query-by-graph/tests/logic.rs @@ -326,14 +326,16 @@ SELECT ?o ?s WHERE { } #[test] -fn test_distinct_variable_generates_select_distinct() { +fn test_distinct_variable_generates_per_variable_distinct() { let graph = r###"[{"properties":[{"id":"P69","label":"educated at","prefix":{"iri":"http://www.wikidata.org/prop/direct/","abbreviation":"wdt"},"selectedForProjection":false}],"source":{"id":"Q5879","label":"Johann Wolfgang von Goethe","prefix":{"iri":"http://www.wikidata.org/entity/","abbreviation":"wd"},"selectedForProjection":false,"distinct":false},"target":{"id":"?university","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":true,"distinct":true}}]"###; let result = vqg_to_query_wasm(graph, false, false); let select = select_line(&result); - assert!(select.contains("DISTINCT"), "Expected SELECT DISTINCT but got: {}", select); - assert!(select.contains("?university")); + // Per-variable DISTINCT: SELECT DISTINCT(?university) WHERE { ... } + assert!(select.contains("DISTINCT(?university)"), "Expected DISTINCT(?university) in: {}", select); + // SELECT keyword itself should not be followed by the global DISTINCT keyword + assert!(!select.starts_with("SELECT DISTINCT "), "Should not use global SELECT DISTINCT, got: {}", select); } #[test] @@ -348,7 +350,9 @@ fn test_no_distinct_generates_plain_select() { } #[test] -fn test_parse_select_distinct_query_sets_distinct_flag() { +fn test_parse_select_distinct_query_does_not_set_per_variable_distinct_flag() { + // Importing SELECT DISTINCT ?university does NOT set distinct=true on individual variables. + // Per-variable distinct is only set via the UI checkbox; the global DISTINCT keyword is ignored on import. let query = r###"PREFIX wd: PREFIX wdt: SELECT DISTINCT ?university WHERE { @@ -363,7 +367,7 @@ SELECT DISTINCT ?university WHERE { assert_eq!(c["target"]["id"], Value::String("?university".to_string())); assert_eq!(c["target"]["selectedForProjection"], Value::Bool(true)); - assert_eq!(c["target"]["distinct"], Value::Bool(true)); + assert_eq!(c["target"]["distinct"], Value::Bool(false)); } #[test] @@ -386,11 +390,37 @@ SELECT ?university WHERE { #[test] fn test_distinct_only_applies_when_variable_is_selected_for_projection() { - // distinct=true but selectedForProjection=false → should NOT generate SELECT DISTINCT + // distinct=true but selectedForProjection=false → DISTINCT(?var) must NOT appear let graph = r###"[{"properties":[{"id":"P69","label":"educated at","prefix":{"iri":"http://www.wikidata.org/prop/direct/","abbreviation":"wdt"},"selectedForProjection":false}],"source":{"id":"Q5879","label":"Goethe","prefix":{"iri":"http://www.wikidata.org/entity/","abbreviation":"wd"},"selectedForProjection":false,"distinct":false},"target":{"id":"?university","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":false,"distinct":true}}]"###; let result = vqg_to_query_wasm(graph, false, false); + + assert!(!result.contains("DISTINCT"), "DISTINCT should not appear when variable is not selected for projection: {}", result); +} + +#[test] +fn test_mixed_distinct_and_non_distinct_variables() { + // ?university is distinct, ?person is not → only ?university gets DISTINCT(...) + let graph = r###"[{"properties":[{"id":"P69","label":"educated at","prefix":{"iri":"http://www.wikidata.org/prop/direct/","abbreviation":"wdt"},"selectedForProjection":false}],"source":{"id":"?person","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":true,"distinct":false},"target":{"id":"?university","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":true,"distinct":true}}]"###; + + let result = vqg_to_query_wasm(graph, false, false); + let select = select_line(&result); + + assert!(select.contains("DISTINCT(?university)"), "Expected DISTINCT(?university) in: {}", select); + // ?person should appear as a plain variable (no DISTINCT wrapper) + assert!(select.contains("?person"), "Expected ?person in: {}", select); + assert!(!select.contains("DISTINCT(?person)"), "?person should not be wrapped in DISTINCT: {}", select); +} + +#[test] +fn test_label_vars_not_distinct_even_when_base_var_is_distinct() { + // With label service, ?universityLabel must NOT be wrapped in DISTINCT + let graph = r###"[{"properties":[{"id":"P69","label":"educated at","prefix":{"iri":"http://www.wikidata.org/prop/direct/","abbreviation":"wdt"},"selectedForProjection":false}],"source":{"id":"Q5879","label":"Goethe","prefix":{"iri":"http://www.wikidata.org/entity/","abbreviation":"wd"},"selectedForProjection":false,"distinct":false},"target":{"id":"?university","label":"Variable","prefix":{"iri":"","abbreviation":""},"selectedForProjection":true,"distinct":true}}]"###; + + let result = vqg_to_query_wasm(graph, true, true); let select = select_line(&result); - assert!(!select.contains("DISTINCT"), "DISTINCT should not appear when variable is not selected for projection: {}", select); + assert!(select.contains("DISTINCT(?university)"), "Expected DISTINCT(?university) in: {}", select); + assert!(!select.contains("DISTINCT(?universityLabel)"), "Label variable must not be wrapped in DISTINCT: {}", select); + assert!(select.contains("?universityLabel"), "Label variable should still appear: {}", select); }