Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion query-by-graph/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ConnectionInterfaceType from "./lib/types/ConnectionInterfaceType.ts";
import ClipboardButton from "./components/ClipboardButton.vue";
import QueryButton from './components/QueryButton.vue';
import WikibaseDataService from './lib/wikidata/WikibaseDataService.ts';
import {selectedDataSource, dataSources, setSelectedDataSource} from './store.ts';
import {selectedDataSource, dataSources, setSelectedDataSource, globalDistinct} from './store.ts';
import {WikibaseDataSource} from "./lib/types/WikibaseDataSource.ts";
import {debounce} from "./lib/utils";
import DataSourcesPopover from "./components/DataSourcesPopover.vue";
Expand Down Expand Up @@ -225,6 +225,19 @@ const deselectAllVariablesForProjection = () => {
}
};

const toggleDistinct = () => {
globalDistinct.value = !globalDistinct.value;
// Sync entity.distinct on every node so the Rust backend export picks it up
if (editor.value) {
editor.value.getNodes().forEach((node: any) => {
if (node.entity) {
node.entity.distinct = globalDistinct.value;
editor.value?.updateNode(node.id);
}
});
}
};


const gotoLink = (url?: string) => {
const link = url || window.location.href;
Expand Down Expand Up @@ -383,6 +396,20 @@ const gotoLink = (url?: string) => {
</p>
</div>

<div class="flex-col flex gap-2">
<h4 class="font-semibold">Distinct Query</h4>
<div class="flex flex-col gap-2">
<Button class="grow" :class="{'highlighted': globalDistinct}" @click="toggleDistinct">
{{ globalDistinct ? 'Distinct: ON' : 'Distinct: OFF' }}
</Button>
</div>
<p class="text-gray-600 text-sm hover:text-gray-900 transition-all">
<em>Hint:</em>
When Distinct is enabled, the <code>SELECT DISTINCT</code> modifier is applied globally to the entire query.
Toggling this on any node's "Distinct?" checkbox has the same effect.
</p>
</div>

<div class="flex-col flex gap-2">
<h4 class="font-semibold">Arrangement</h4>
<div class="flex gap-4">
Expand Down
33 changes: 33 additions & 0 deletions query-by-graph/src/components/EntitySelectorInputControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,22 @@
container-classes="px-2"
label-classes="text-white"
/>
<ProjectionCheckbox
v-if="isVariable"
:id="`distinct-${data.id}`"
v-model="includeAsDistinct"
:is-variable="isVariable"
label="Distinct?"
container-classes="px-2"
label-classes="text-white"
/>
</div>
</template>

<script>
import EntitySelector from "./EntitySelector.vue";
import ProjectionCheckbox from "./ProjectionCheckbox.vue";
import {globalDistinct} from "../store.ts";

export default {
components: {EntitySelector, ProjectionCheckbox},
Expand Down Expand Up @@ -50,6 +60,26 @@ export default {
}
}
}
},
includeAsDistinct: {
get() {
// Mirror the global flag onto the entity so the backend export picks it up
if (this.data.value) {
this.data.value.distinct = globalDistinct.value;
}
return globalDistinct.value;
},
set(value) {
// Writing to any node's checkbox updates the global setting
globalDistinct.value = value;
// Also keep every node's entity in sync immediately via the change callback
if (this.data.value) {
this.data.value.distinct = value;
if (this.data.options?.change) {
this.data.options.change(this.data.value);
}
}
}
}
},
methods: {
Expand All @@ -72,6 +102,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;
}
}
},
}
Expand Down
6 changes: 5 additions & 1 deletion query-by-graph/src/components/ProjectionCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
<label :for="id" :class="['ml-2 text-sm font-medium cursor-pointer', labelClasses]">
Select?
{{ label }}
</label>
</div>
</template>
Expand All @@ -27,6 +27,10 @@ defineProps({
type: Boolean,
default: false
},
label: {
type: String,
default: 'Select?'
},
containerClasses: {
type: String,
default: ''
Expand Down
39 changes: 32 additions & 7 deletions query-by-graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -167,6 +169,11 @@ fn vqg_to_query(
})
.collect::<HashSet<_>>();

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 {
Expand All @@ -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| {
Expand Down Expand Up @@ -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
)
}
}
Expand Down Expand Up @@ -399,15 +408,24 @@ fn query_to_vqg(query: &str) -> Vec<Connection> {
// 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::<HashSet<String>>()),
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::<HashSet<String>>())
Some(v.iter().map(|var| format!("?{}", var.as_str())).collect::<HashSet<String>>()),
false,
),
_ => (match_bgp_or_path_to_vqg(p), None),
_ => (match_bgp_or_path_to_vqg(p), None, false),
};

let mut connections = connections;
Expand All @@ -417,10 +435,14 @@ fn query_to_vqg(query: &str) -> Vec<Connection> {
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('?') {
Expand Down Expand Up @@ -472,6 +494,7 @@ fn match_bgp_or_path_to_vqg(p: GraphPattern) -> Vec<Connection> {
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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down
3 changes: 2 additions & 1 deletion query-by-graph/src/lib/rete/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const variableEntity: EntityType = { // EntityType
abbreviation: "",
},
dataSource: noDataSource,
selectedForProjection: true
selectedForProjection: true,
distinct: false
};

const variableEntityConstructor = (name: string): EntityType => {
Expand Down
1 change: 1 addition & 0 deletions query-by-graph/src/lib/types/EntityType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface EntityType {
prefix: PrefixType,
dataSource: WikibaseDataSource;
selectedForProjection?: boolean;
distinct?: boolean;
isLiteral?: boolean;
}

Expand Down
3 changes: 3 additions & 0 deletions query-by-graph/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {Ref, ref} from 'vue';
import {WikibaseDataSource} from "./lib/types/WikibaseDataSource.ts";
import {factGridDataSource, mimoDataSource, wikiDataDataSource} from "./lib/constants";

// Global DISTINCT setting – when true every variable node is marked as distinct
export const globalDistinct = ref<boolean>(false);

export const defaultDataSources = [
wikiDataDataSource,
factGridDataSource,
Expand Down
70 changes: 70 additions & 0 deletions query-by-graph/tests/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
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: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
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);
}
Loading