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);
}