Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to the Reactodia will be documented in this document.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
#### 馃悰 Fixed
- Fix `SparqlDataProvider` queries for connected elements hanging on some endpoints (e.g. Virtuoso chooses a catastrophic query plan): filter out blank nodes on the incoming-link patterns with `FILTER(!isBlank(...))` instead of `FILTER(isIri(...))`, which is equivalent in the subject position where a literal is not possible.
- Fix link type statistics query in `OwlRdfsSettings`/`OwlStatsSettings` being rejected by the Virtuoso cost estimator ("The estimated execution time ... exceeds the limit"): count incoming and outgoing links via `UNION` with an outer `sum()` instead of joining two aggregate sub-queries; counts stay exact, and the previous `LIMIT 101` is dropped as it applied to a single-row aggregate result, i.e. never.
- Fix error on a link statistics response with unbound counts, which some endpoints return when aggregating over an empty solution group: treat a missing count as 0 (with `COALESCE` in the default query as well).

## [0.35.2] - 2026-08-08
#### 馃悰 Fixed
Expand Down
7 changes: 6 additions & 1 deletion src/data/sparql/responseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,12 @@ export function appendProperty(
values.push(propValue);
}

function parseCount(countLiteral: Rdf.Literal): number {
function parseCount(countLiteral: Rdf.Literal | undefined): number {
// Count binding may be missing e.g. when aggregating over an empty
// solution group (some endpoints return an unbound value instead of 0)
if (!countLiteral) {
return 0;
}
const numericCount = +countLiteral.value;
return Number.isFinite(numericCount) ? numericCount : 0;
}
Expand Down
21 changes: 15 additions & 6 deletions src/data/sparql/sparqlDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,9 +665,12 @@ export class SparqlDataProvider implements DataProvider {
const navigateElementFilterOut = this.acceptBlankNodes
? 'FILTER (IsIri(?outObject) || IsBlank(?outObject))'
: 'FILTER IsIri(?outObject)';
// ?inObject is in the subject position where only blank nodes are possible
// besides IRIs, and isIri() there causes some endpoints (e.g. Virtuoso)
// to choose a catastrophic query plan scanning the whole graph
const navigateElementFilterIn = this.acceptBlankNodes
? 'FILTER (IsIri(?inObject) || IsBlank(?inObject))'
: 'FILTER IsIri(?inObject)';
? ''
: 'FILTER(!isBlank(?inObject))';

const foundLinkStats: DataProviderLinkCount[] = [];
await Promise.all(connectedLinkTypes.map(async ({linkType, hasInLink, hasOutLink}) => {
Expand Down Expand Up @@ -887,16 +890,22 @@ export class SparqlDataProvider implements DataProvider {

const linkPattern = refLinkType || '?link';
const bindType = refLinkType ? `BIND(${refLinkType} as ?link)` : '';
// FILTER(IsIri()) is used to prevent blank nodes appearing in results
const blankFilter = this.acceptBlankNodes
// Filters prevent blank nodes and literals appearing in results;
// in the subject position only blank nodes are possible, and isIri()
// there is avoided because it causes some endpoints (e.g. Virtuoso)
// to choose a catastrophic query plan scanning the whole graph
const outFilter = this.acceptBlankNodes
? 'FILTER(isIri(?inst) || isBlank(?inst))'
: 'FILTER(isIri(?inst))';
const inFilter = this.acceptBlankNodes
? ''
: 'FILTER(!isBlank(?inst))';

if (!direction || direction === 'out') {
unionParts.push(`{ ${refElementIRI} ${linkPattern} ?inst BIND("out" as ?direction) ${bindType} ${blankFilter} }`);
unionParts.push(`{ ${refElementIRI} ${linkPattern} ?inst BIND("out" as ?direction) ${bindType} ${outFilter} }`);
}
if (!direction || direction === 'in') {
unionParts.push(`{ ?inst ${linkPattern} ${refElementIRI} BIND("in" as ?direction) ${bindType} ${blankFilter} }`);
unionParts.push(`{ ?inst ${linkPattern} ${refElementIRI} BIND("in" as ?direction) ${bindType} ${inFilter} }`);
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/data/sparql/sparqlDataProviderSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,18 +722,18 @@ const OwlRdfsSettingsOverride: Partial<SparqlDataProviderSettings> = {
}
`,
linkTypesStatisticsQuery: `
SELECT ?link ?outCount ?inCount
SELECT (\${linkId} as ?link) (COALESCE(sum(?__out), 0) as ?outCount) (COALESCE(sum(?__in), 0) as ?inCount)
WHERE {
{
SELECT (\${linkId} as ?link) (count(?outObject) as ?outCount) WHERE {
SELECT (1 as ?__out) (0 as ?__in) WHERE {
\${linkConfigurationOut}
\${navigateElementFilterOut}
} LIMIT 101
} {
SELECT (\${linkId} as ?link) (count(?inObject) as ?inCount) WHERE {
}
} UNION {
SELECT (0 as ?__out) (1 as ?__in) WHERE {
\${linkConfigurationIn}
\${navigateElementFilterIn}
} LIMIT 101
}
}
}
`,
Expand Down
6 changes: 4 additions & 2 deletions src/data/sparql/sparqlModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ export interface LinkBinding {

export interface LinkCountBinding {
link: Rdf.NamedNode | Rdf.BlankNode;
inCount: Rdf.Literal;
outCount: Rdf.Literal;
/** May be unbound e.g. when an endpoint aggregates over an empty solution group. */
inCount?: Rdf.Literal;
/** May be unbound e.g. when an endpoint aggregates over an empty solution group. */
outCount?: Rdf.Literal;
}

export interface ConnectedLinkTypeBinding {
Expand Down
17 changes: 16 additions & 1 deletion test/data/sparql/sparqlProviderBasic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
ElementIri, ElementModel, ElementTypeIri, ElementTypeModel, LinkTypeIri, LinkTypeModel,
LinkModel, PropertyTypeIri, PropertyTypeModel,
} from '../../../src/data/model';
import type { DataProviderLookupItem } from '../../../src/data/dataProvider';
import type { DataProviderLinkCount, DataProviderLookupItem } from '../../../src/data/dataProvider';
import { type MemoryDataset } from '../../../src/data/rdf/memoryDataset';
import * as Rdf from '../../../src/data/rdf/rdfModel';
import { rdf, owl } from '../../../src/data/rdf/vocabulary';
Expand Down Expand Up @@ -93,6 +93,21 @@ describe('SparqlDataProvider', () => {
);
});

it('provides connectedLinkStats() with exact counts', async () => {
const provider = await makeSparqlDataProvider(
{},
{...OwlStatsSettings, filterOnlyLanguages: ['en']},
);
const stats = await provider.connectedLinkStats({
elementId: org.Organization,
});
expect(stats.find(s => s.id === rdfs.subClassOf)).toEqual({
id: rdfs.subClassOf,
inCount: 3,
outCount: 1,
} satisfies DataProviderLinkCount);
});

it('provides propertyTypes()', async () => {
const provider = await makeSparqlDataProvider(
{},
Expand Down